Compare commits

...
Author SHA1 Message Date
uvos 338fce6511 HIP/CUDA: remove pragma unroll from loops in conv2d where itteration count cant be infered at compile time (#31255) 2026-10-27 19:42:04 +01:00
Tarek DakhranandXuan Son Nguyen 19ff817fb2 model : add LiquidAI/LFM3-Audio-2B and LiquidAI/LFM3-Omni-2B (#31257)
* model : add LiquidAI/LFM3-Audio-2B and LiquidAI/LFM3-Omni-2B

Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
Assisted-by: Claude Opus 5.5

* convert : read the LFM3-Audio config.json as released

A transformers that knows lfm3_audio rewrites the config into another layout (no lfm/encoder keys).

Assisted-by: Claude Opus 5.5

* mtmd : disable flash attention for the LFM3-Audio depthformer

Assisted-by: Claude Opus 5.5

* tts : add a voice option for LFM3-Audio and LFM3-Omni

The model card offers a female and a male voice; --tts-voice for llama-tts, "voice" for the server /tts.

Assisted-by: Claude Opus 5.5

* clean up template handling

* convert : detect LFM3-Audio and LFM3-Omni by model_type

Assisted-by: Claude Opus 5.5

* move banned tokens --> logits bias --> mtmd_helper

* leave mtmd-cli unchanged

* no more template_asr_system

* left over

* llama : keep the LFM3-Audio encoder convs and codec unquantized

Assisted-by: Claude Opus 5.5

* common, mtmd : shorten comments

Assisted-by: Claude Opus 5.5

---------

Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
2026-10-27 18:47:14 +01:00
Tarek DakhranandAldehir Rojas eb7818c0b8 common : support deferred tools and tool_reference content parts (#31256)
* common : support deferred tools and tool_reference content parts

Assisted-by: Claude Opus 5.5

* common : credit the deferred tools co-author

Co-authored-by: Aldehir Rojas <765227+aldehir@users.noreply.github.com>

Assisted-by: Claude Opus 5.5

* tests : fix the type check of the deferred tools test

Assisted-by: Claude Opus 5.5
2026-10-27 18:39:11 +01:00
Mengsheng Wu 6c44591049 snapdragon: switch QDC artifact upload and log download to v2 API (#31240)
Signed-off-by: Mengsheng Wu <mengshengwu@outlook.com>
2026-10-27 18:06:41 +01:00
19cce69199 model : add LiquidAI/LFM3-1.7B and LiquidAI/LFM3-VL-2B (#31252)
* model : add LiquidAI/LFM3-1.7B and LiquidAI/LFM3-VL-2B

Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co>
Co-authored-by: Xuan-Son Nguyen <son@huggingface.co>
Assisted-by: Claude Opus 5.5

* tests : set the SWA rope base of the LFM3 test model

Assisted-by: Claude Opus 5.5

* llama : enable recurrent state rollback for LFM3

Assisted-by: Claude Opus 5.5

* Apply batched suggestions from code review

Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co>
Co-authored-by: Tarek Dakhran <github@dakhran.com>

* gguf-py : align the lfm3vl mapping comments

Assisted-by: Claude Opus 5.5

* llama : fix CRLF line endings from the review suggestions

Assisted-by: Claude Opus 5.5

* tests : add the LFM3 Metal fusion baseline

Assisted-by: Claude Opus 5.5

* llama : keep the LFM3-VL depthwise conv kernels in F32 when quantizing

Assisted-by: Claude Opus 5.5

---------

Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co>
Co-authored-by: Xuan-Son Nguyen <son@huggingface.co>
2026-10-27 17:06:40 +01:00
Mendy Berger e86f5fd1e9 cmake: WASI doesn't support spawning (#30751) 2026-10-27 15:14:03 +01:00
Aman GuptaandGeorgi Gerganov 27b7a8a009 llama : build the k-pool sparse selection independently of the kq_mask shape (#31243)
* llama : build the k-pool sparse selection independently of the kq_mask shape

qwen4exp (QSA) and glm5-next pick cache cells by index and no longer read the kq_mask values, but they still shaped the selection like the dense kq_mask. Build the selection as [n_kv, n_tokens] over the cache cells instead, and split it into the streams of the K view right before the attention.

This lets the dense mask change its layout (e.g. per-sequence row lists) without touching the sparse layers.

* improve comment

Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>

* llama : drop the unused build_fused_top_k_mask args, review nits

---------

Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>
2026-10-27 21:57:45 +08:00
Katara 7ecb57f7ad rpc : catch std::length_error in recv_msg (#31204) 2026-10-27 15:04:55 +02:00
Georgi Gerganov 8542e94350 tests : speed up tensor data generation and model saving (#31145)
* tests : speed up tensor data generation and model saving

Refactor save_models to create only the model, not a llama_context,
which is not needed for saving. Replace the slow std::normal_distribution
draw with a splitmix64 + box-muller normal generator.

Assisted-by: dsh:llama.cpp/ggml-org/DeepSeek-V4-Flash-Vision-Exp-GGUF (Mac Studio, M2 Ultra, 192GB)

* tests : use a shared pool of normal draws for tensor data

Generate a pool of normal draws once with std::mt19937 and
std::normal_distribution, then have every tensor copy a cyclic slice of
it instead of running a per-tensor box-muller. The name-keyed slice
selection keeps the draw deterministic.

Assisted-by: dsh:llama.cpp/ggml-org/DeepSeek-V4-Flash-Vision-Exp-GGUF (Mac Studio, M2 Ultra, 192GB)

* tests : log the size of saved models

Report the produced file size in bytes and MB after each save.

Assisted-by: dsh:llama.cpp/ggml-org/DeepSeek-V4-Flash-Vision-Exp-GGUF (Mac Studio, M2 Ultra, 192GB)

* tests : simplify the model save log

Put the model name, target and size on one line per saved model.

Assisted-by: dsh:llama.cpp/ggml-org/DeepSeek-V4-Flash-Vision-Exp-GGUF (Mac Studio, M2 Ultra, 192GB)
2026-10-27 14:44:41 +02:00
Pascal 922f53ed33 vulkan: fix CI llvmpipe by allowing dim 3 broadcast in MUL_MAT (#31239)
* vulkan: allow dim 3 broadcast in MUL_MAT

supports_op rejected every MUL_MAT whose src0 and src1 differ in ne3,
although mul_mm, mul_mmq and mul_mat_vec already broadcast src0 over
dim 3 through broadcast3. Such a matmul then ran on the CPU with a
copy of the weight, and the split layout changed with the number of
streams in the ubatch.

Keep the strict check for MUL_MAT_ID only, and route the broadcast
cases away from mul_mat_vec_nc, the one path that requires equal ne3.

* tests : harden MUL_MAT with permuted dim 3 broadcast cases

Harden the MUL_MAT coverage with a weight broadcast over dim 3 against
a permuted per-stream activation, the shape of an MLA output
projection with a non unified KV cache, for a single token, several
heads and several tokens per stream.
2026-10-27 14:39:03 +02:00
Sam Malayek 0c8c59ef3d mtmd-cli: Reduced scope of batch pointer (#31155) (#31165)
* mtmd-cli: Reduced scope of batch pointer (#31155)

* mtmd-cli: Remove CLI regression test
2026-10-27 12:56:03 +01:00
Aleksander Grygier 3ab37aedfe ui: fix system message row stretching to full prompt height in WebKit (#31246)
The expand button wrapping the system message card defaulted to
inline-block. WebKit takes an inline-block's baseline from its last
line even when that line is clipped by the card's max-height, so the
row grew to the height of the whole prompt and left a large empty gap
below the card. Make the button a block so the row matches the card.

Assisted-by: Claude (Opus 5.5 Medium)
2026-10-27 13:19:02 +02:00
85cc432e2c server: add router device memory margin parameter for dynamic unloading (#21231)
* server: add --models-memory-max parameter to allow dynamically unloading models when they exceed a memory size threshold

estimate with to-be-loaded model size included

use no_alloc to get memory requirements for model load

only set model memory_mb if not previously calculated

use memory margin instead of total size limit, apply to each device separately

add server memory debug logging

move llama_context_device_memory function to llama-ext.h

fix model count exceeded check

improve memory_per_device map naming

improve variable naming, fix style

also strip models memory margin from child processes

cont : clean-up

replace device memory map with buft memory map. Use llama_get_memory_breakdown

extract duplicated check into helper function

move model memory estimation to subprocess

precompute name->buft map, map GPU host types to CPU buft

cleanup unused variable

remove duplicated init calls

* adapt to upstream changes

* wip

* common_memory_reports internface

* SERVER_ROLE_MEASURE

* server-models-sched

* fix build

* server : fix memory accounting of measure children and claim spin

A measure child runs no_alloc, so the free memory it reports does not
include its estimate: subtracting self there erased the memory held by
other processes, down to zero with --fit on, and the next load could
overshoot the margin. Subtract self only for instance reports.

A queued load only checked --models-max before claiming, so when the
memory margin blocked it behind a busy model, load() threw and the
waiter retried in a tight loop. Refuse the claim while the limits are
exceeded and no idle model can be unloaded, so the waiter sleeps on
the condition variable until one goes idle.

---------

Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
Co-authored-by: Pascal <admin@serveurperso.com>
2026-10-27 12:14:42 +01:00
Julien Chaumond 74a1fd3d03 common : --model or -hf is required (#31242)
Assisted-by: Codex
2026-10-27 11:55:48 +01:00
tc-mbandXuan Son Nguyen 7eed6b7685 model: Add MiniCPM-OCR-Jev (#30980)
* model : add MiniCPM-OCR-Jev support

The model is a Qwen3.5-VL trunk with a value head read at the last token: it
scores an OCR text against an image rendered from it, it does not generate.

Add the openbmb-minicpm-ocr-jev decision type, served by POST /v1/systemone.
The class scores are the pooled output of the sequence, read with last pooling,
so the conversion turns score.weight into cls.output and sets
embedding_length_out. The checkpoint has no LM head, so text generation is
rejected instead of using the output weights duplicated from the token
embeddings.

The task is fixed, so its prompt is entirely in the systemone template, the
same way h2o-lightning keeps its default there. The processor of the checkpoint
allows up to 16384 merged tokens per image, more than the Qwen-VL default of
mtmd, so the mmproj keeps the image budget of the preprocessor config. The
checkpoint declares MiniCPMOCRForSequenceClassification in its config.json, as
other models do.

Signed-off-by: tc-mb <tianchi_cai@icloud.com>

* fix code

Signed-off-by: tc-mb <tianchi_cai@icloud.com>

* use gguf meta classifier_output_labels

* remove redudant code changes

---------

Signed-off-by: tc-mb <tianchi_cai@icloud.com>
Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
2026-10-27 11:49:12 +01:00
Georgi Gerganov 276378515c metal : add GGML_OP_TOP_K_MASK (#31230)
* metal : add GGML_OP_TOP_K_MASK

Port the fused top-k mask from the CUDA backend: a grid-stride kernel fills the output with -inf, then one
threadgroup per row copies the mask values of the kept cells over it. Function constants drop the score,
blocks and extra paths that a given graph does not use.

Assisted-by: dsh:llama.cpp/ggml-org/Qwen3.8-Flash-Next-GGUF (MacBook, M5 Max, 128GB)

* metal : clean up top-k mask kernel formatting

Assisted-by: dsh:llama.cpp/ggml-org/Qwen3.8-Flash-Next-GGUF (MacBook, M5 Max, 128GB)
2026-10-27 12:13:23 +02:00
Alexey Kopytko d78f40d9d3 glm5-next : QSA without kq_mask (+ DSA) (#31009)
* glm5-next : QSA without kq_mask

* DSA: dsa top-k mask change tested with layers 0,3 of DeepSeek-V3.2
2026-10-27 18:54:36 +09:00
85 changed files with 6039 additions and 647 deletions
+1 -1
View File
@@ -103,7 +103,7 @@ endif()
# subprocess spawning isn't a supported/sandbox-friendly operation on mobile OSes or in WASM
if (CMAKE_SYSTEM_NAME STREQUAL "iOS" OR CMAKE_SYSTEM_NAME STREQUAL "Android" OR ANDROID
OR CMAKE_SYSTEM_NAME STREQUAL "Emscripten" OR EMSCRIPTEN)
OR CMAKE_SYSTEM_NAME STREQUAL "Emscripten" OR EMSCRIPTEN OR CMAKE_SYSTEM_NAME STREQUAL "WASI")
set(LLAMA_SUBPROCESS_DEFAULT OFF)
else()
set(LLAMA_SUBPROCESS_DEFAULT ON)
+16 -1
View File
@@ -924,7 +924,7 @@ static bool common_params_parse_ex(int argc, char ** argv, common_params_context
// TODO @ngxson : maybe show a list of available models in CLI in this case
bool can_skip_model = params.usage || params.completion || !params.server_base.empty();
if (!can_skip_model && params.model.path.empty()) {
throw std::invalid_argument("error: --model is required\n");
throw std::invalid_argument("error: --model or -hf is required\n");
}
}
@@ -3708,6 +3708,13 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
params.models_max = value;
}
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_MODELS_MAX"));
add_opt(common_arg(
{"--models-memory-margin"}, "N",
string_format("for router server, MiB of device memory to keep free per device, unload LRU models until a new one fits (default: %d, 0 = disabled)", params.models_memory_margin),
[](common_params & params, int value) {
params.models_memory_margin = value;
}
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_MODELS_MEMORY_MARGIN"));
add_opt(common_arg(
{"--models-autoload"},
{"--no-models-autoload"},
@@ -4505,6 +4512,14 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
params.tts_lang = value;
}
).set_examples({LLAMA_EXAMPLE_TTS}));
add_opt(common_arg(
{"--tts-voice"}, "VOICE",
"voice for audio generation (model-dependent)\n"
"see tts/README.md for per-model usage notes",
[](common_params & params, const std::string & value) {
params.tts_voice = value;
}
).set_examples({LLAMA_EXAMPLE_TTS}));
add_opt(common_arg(
{"--tts-speaker-file"}, "FNAME",
"speaker file path for audio generation",
+103 -17
View File
@@ -190,10 +190,17 @@ json common_chat_msg::to_json_oaicompat(bool concat_typed_text) const {
} else {
auto & parts = jmsg["content"] = json::array();
for (const auto & part : content_parts) {
parts.push_back({
{"type", part.type},
{"text", part.text},
});
if (part.type == "tool_reference") {
parts.push_back({
{"type", part.type},
{"name", part.name},
});
} else {
parts.push_back({
{"type", part.type},
{"text", part.text},
});
}
}
}
} else {
@@ -309,6 +316,7 @@ struct common_chat_templates {
bool has_explicit_template; // Model had builtin template or template overridden was specified.
std::unique_ptr<common_chat_template> template_default; // always set (defaults to chatml)
std::unique_ptr<common_chat_template> template_tool_use;
std::unique_ptr<common_chat_template> template_tts; // the prompt of a text to speak
};
common_chat_tool_choice common_chat_tool_choice_parse_oaicompat(const std::string & tool_choice) {
@@ -370,12 +378,16 @@ std::vector<common_chat_msg> common_chat_msgs_parse_oaicompat(const json & messa
throw std::invalid_argument("Missing content part type: " + part.dump());
}
const auto & type = part.at("type");
if (type != "text" && type != "media_marker") {
throw std::invalid_argument("Unsupported content part type: " + type.dump());
}
common_chat_msg_content_part msg_part;
msg_part.type = type;
msg_part.text = part.at("text");
if (type == "text" || type == "media_marker") {
msg_part.text = part.at("text");
} else if (type == "tool_reference") {
// names a request tool, the template renders its definition
msg_part.name = part.at("name");
} else {
throw std::invalid_argument("Unsupported content part type: " + type.dump());
}
msg.content_parts.push_back(msg_part);
}
} else if (!content.is_null()) {
@@ -610,13 +622,17 @@ json common_chat_tools_to_json_oaicompat(const std::vector<common_chat_tool> & t
auto result = json::array();
for (const auto & tool : tools) {
json function {
{ "name", tool.name },
{ "description", tool.description },
{ "parameters", json::parse(tool.parameters) },
};
if (tool.defer_loading) {
function["defer_loading"] = true;
}
result.push_back({
{ "type", "function" },
{ "function", {
{ "name", tool.name },
{ "description", tool.description },
{ "parameters", json::parse(tool.parameters) },
}},
{ "function", function },
});
}
return result;
@@ -653,10 +669,19 @@ std::vector<common_chat_tool> common_chat_tools_parse_oaicompat(const json & too
}
const auto & function = tool.at("function");
// "defer_loading" sits on the function, or on the tool next to "type"; the function wins
const json & defer = function.contains("defer_loading") ? function.at("defer_loading")
: tool.value("defer_loading", json(false));
if (!defer.is_boolean()) {
throw std::invalid_argument("Expected 'defer_loading' to be a boolean, got " + defer.dump());
}
result.push_back({
/* .name = */ function.at("name"),
/* .description = */ function.value("description", ""),
/* .parameters = */ function.value("parameters", json::object()).dump(),
/* .defer_loading = */ defer.get<bool>(),
});
}
}
@@ -779,19 +804,70 @@ bool common_chat_templates_was_explicit(const struct common_chat_templates * tmp
return tmpls->has_explicit_template;
}
common_chat_prompt_preset common_chat_get_asr_prompt(const common_chat_templates * chat_templates) {
// renders a task template (e.g. "tts") with the given variables
static std::string common_chat_template_render(const common_chat_template & tmpl, const json & vars) {
jinja::context ctx(tmpl.source());
jinja::global_from_json(ctx, vars, false);
ctx.set_val("bos_token", jinja::mk_val<jinja::value_string>(tmpl.bos_token()));
ctx.set_val("eos_token", jinja::mk_val<jinja::value_string>(tmpl.eos_token()));
jinja::runtime runtime(ctx);
return jinja::runtime::gather_string_parts(runtime.execute(tmpl.prog))->as_string().str();
}
// LFM3 format detection: template renders deferred tools inside <additional_tools>...</additional_tools>
static bool is_lfm3_template(const std::string & src) {
return src.find("<additional_tools>") != std::string::npos;
}
// LFM3-Audio: a known language code maps to its name, no language lets the model detect it
static std::string lfm3_asr_system_prompt(const std::string & language) {
static const std::map<std::string, std::string> names = {
{ "ar", "Arabic" }, { "da", "Danish" }, { "de", "German" }, { "el", "Greek" }, { "en", "English" },
{ "es", "Spanish" }, { "fi", "Finnish" }, { "fr", "French" }, { "he", "Hebrew" }, { "it", "Italian" },
{ "ja", "Japanese" }, { "ko", "Korean" }, { "nl", "Dutch" }, { "pl", "Polish" }, { "pt", "Portuguese" },
{ "sv", "Swedish" }, { "tr", "Turkish" }, { "zh", "Chinese" },
};
if (language.empty()) {
return "Perform ASR.";
}
std::string code = language;
std::transform(code.begin(), code.end(), code.begin(), [](unsigned char c) { return (char) std::tolower(c); });
const auto it = names.find(code);
return "Perform ASR in " + (it != names.end() ? it->second : language) + ".";
}
common_chat_prompt_preset common_chat_get_asr_prompt(const common_chat_templates * chat_templates, const std::string & language) {
common_chat_prompt_preset asr_preset;
asr_preset.system = "";
asr_preset.user = "Transcribe audio to text";
if (chat_templates && chat_templates->template_default && is_lfm2_template(chat_templates->template_default->source())) {
asr_preset.system = "Perform ASR.";
asr_preset.user = "";
if (chat_templates && chat_templates->template_default) {
const std::string src = chat_templates->template_default->source();
if (is_lfm2_template(src)) {
asr_preset.system = "Perform ASR.";
asr_preset.user = "";
} else if (is_lfm3_template(src)) {
asr_preset.system = lfm3_asr_system_prompt(language);
asr_preset.user = "";
asr_preset.has_language = true;
}
}
return asr_preset;
}
std::string common_chat_get_tts_prompt(const common_chat_templates * chat_templates, const std::string & text,
const std::string & language, const std::string & voice) {
if (!chat_templates || !chat_templates->template_tts) {
return "";
}
return common_chat_template_render(*chat_templates->template_tts, {
{"messages", json::array({{{"role", "user"}, {"content", text}}})},
{"language", language},
{"voice", voice},
});
}
std::string common_chat_templates_source(const struct common_chat_templates * tmpls, const std::string & variant) {
if (!variant.empty()) {
if (variant == "tool_use") {
@@ -902,6 +978,16 @@ common_chat_templates_ptr common_chat_templates_init(const struct llama_model *
LOG_ERR("%s: failed to parse tool use chat template (ignoring it): %s\n", __func__, e.what());
}
}
// task templates of the model, kept with a chat template override
if (model) {
if (const char * src = llama_model_chat_template(model, "tts")) {
try {
tmpls->template_tts = std::make_unique<common_chat_template>(src, token_bos, token_eos);
} catch (const std::exception & e) {
LOG_ERR("%s: failed to parse the tts chat template (ignoring it): %s\n", __func__, e.what());
}
}
}
return tmpls;
}
+12 -2
View File
@@ -45,6 +45,7 @@ struct common_chat_tool_call {
struct common_chat_msg_content_part {
std::string type;
std::string text;
std::string name; // tool_reference: name of a tool from the request tools
// TODO @ngxson : no known chat templates support reasoning_content in content parts yet
// this can be useful for models with interleaved thinking (like Kimi-K2)
@@ -52,7 +53,7 @@ struct common_chat_msg_content_part {
// std::string reasoning_content;
bool operator==(const common_chat_msg_content_part & other) const {
return type == other.type && text == other.text;
return type == other.type && text == other.text && name == other.name;
}
};
@@ -226,6 +227,9 @@ struct common_chat_tool {
std::string name;
std::string description;
std::string parameters;
// withheld from the system prompt, a tool_reference reveals it later in the conversation
bool defer_loading = false;
};
enum common_chat_tool_choice {
@@ -487,6 +491,12 @@ std::optional<common_chat_params> common_chat_try_specialized_template(
struct common_chat_prompt_preset {
std::string system;
std::string user;
bool has_language = false; // the preset already names the requested language
};
common_chat_prompt_preset common_chat_get_asr_prompt(const common_chat_templates * chat_templates);
common_chat_prompt_preset common_chat_get_asr_prompt(const common_chat_templates * chat_templates, const std::string & language = "");
// prompt of the model's "tts" chat template for the text to speak, empty if the model has none
// language and voice are optional, the template defines their defaults
std::string common_chat_get_tts_prompt(const common_chat_templates * chat_templates, const std::string & text,
const std::string & language = "", const std::string & voice = "");
+47 -36
View File
@@ -1187,6 +1187,7 @@ static const std::map<common_decision_type, std::string> COMMON_DECISION_TYPE_NA
{ COMMON_DECISION_TYPE_LFM2_D1_OMNI, "lfm2-d1-omni" },
{ COMMON_DECISION_TYPE_GRANITE_SWITCH, "granite-switch"},
{ COMMON_DECISION_TYPE_H2O_LIGHTNING, "h2o-lightning" },
{ COMMON_DECISION_TYPE_MINICPM_OCR_JEV, "openbmb-minicpm-ocr-jev" },
// for future models, ALWAYS add org name prefix to avoid name collisions, e.g. "orgname-modelname"
};
@@ -1252,44 +1253,50 @@ common_gguf_info common_get_gguf_info(const std::string & fname) {
return info;
}
void common_fit_apply(common_params & params, llama_model_params & mparams, llama_context_params & cparams) {
if (!params.fit_params) {
return;
}
COM_TRC("%s", "fitting params to device memory ...\n");
COM_TRC("%s", "(for bugs during this step try to reproduce them with -fit off, or provide --verbose logs if the bug only occurs with -fit on)\n");
// the draft context is created from the same base params and follows the main context, fit both together
const bool has_draft = params.speculative.has_dft();
const bool spec_mtp = std::find(params.speculative.types.begin(), params.speculative.types.end(),
COMMON_SPECULATIVE_TYPE_DRAFT_MTP) != params.speculative.types.end();
common_params params_dft = common_base_params_to_speculative(params);
auto mparams_dft = common_model_params_to_llama(params_dft);
auto cparams_dft = common_context_params_to_llama(params_dft);
if (spec_mtp) {
cparams_dft.ctx_type = LLAMA_CONTEXT_TYPE_MTP;
}
cparams_dft.n_rs_seq = 0;
const common_fit_extra_model extra = {
/*.path_model =*/ params_dft.model.path.c_str(),
/*.mparams =*/ &mparams_dft,
/*.cparams =*/ &cparams_dft,
/*.shares_model =*/ !has_draft, // an MTP context runs on the weights of the main model
};
common_fit_params(params.model.path.c_str(), &mparams, &cparams,
params.tensor_split,
params.tensor_buft_overrides.data(),
params.fit_params_target.data(),
params.fit_params_min_ctx,
has_draft || spec_mtp ? &extra : nullptr,
params.verbosity >= LOG_LEVEL_DEBUG ? GGML_LOG_LEVEL_DEBUG : GGML_LOG_LEVEL_ERROR);
}
common_init_result::common_init_result(common_params & params, bool model_only) :
pimpl(new impl{}) {
auto mparams = common_model_params_to_llama(params);
auto cparams = common_context_params_to_llama(params);
if (params.fit_params) {
COM_TRC("%s", "fitting params to device memory ...\n");
COM_TRC("%s", "(for bugs during this step try to reproduce them with -fit off, or provide --verbose logs if the bug only occurs with -fit on)\n");
// the draft context is created from the same base params and follows the main context, fit both together
const bool has_draft = params.speculative.has_dft();
const bool spec_mtp = std::find(params.speculative.types.begin(), params.speculative.types.end(),
COMMON_SPECULATIVE_TYPE_DRAFT_MTP) != params.speculative.types.end();
common_params params_dft = common_base_params_to_speculative(params);
auto mparams_dft = common_model_params_to_llama(params_dft);
auto cparams_dft = common_context_params_to_llama(params_dft);
if (spec_mtp) {
cparams_dft.ctx_type = LLAMA_CONTEXT_TYPE_MTP;
}
cparams_dft.n_rs_seq = 0;
const common_fit_extra_model extra = {
/*.path_model =*/ params_dft.model.path.c_str(),
/*.mparams =*/ &mparams_dft,
/*.cparams =*/ &cparams_dft,
/*.shares_model =*/ !has_draft, // an MTP context runs on the weights of the main model
};
common_fit_params(params.model.path.c_str(), &mparams, &cparams,
params.tensor_split,
params.tensor_buft_overrides.data(),
params.fit_params_target.data(),
params.fit_params_min_ctx,
has_draft || spec_mtp ? &extra : nullptr,
params.verbosity >= LOG_LEVEL_DEBUG ? GGML_LOG_LEVEL_DEBUG : GGML_LOG_LEVEL_ERROR);
}
common_fit_apply(params, mparams, cparams);
llama_model * model = llama_model_load_from_file(params.model.path.c_str(), mparams);
if (model == NULL) {
@@ -1307,13 +1314,17 @@ common_init_result::common_init_result(common_params & params, bool model_only)
// these decision models return a score for each token via the embeddings output
// TODO: maybe improve this in the future
const auto decision_type = common_get_decision_type(model);
// minicpm-ocr-jev reads its class scores from the pooled output, the pooling type is the one of the GGUF
if (decision_type == COMMON_DECISION_TYPE_LAYA || decision_type == COMMON_DECISION_TYPE_KEV || decision_type == COMMON_DECISION_TYPE_CLEF ||
decision_type == COMMON_DECISION_TYPE_LFM2_D1_OMNI || decision_type == COMMON_DECISION_TYPE_VLLM_DECISION2) {
decision_type == COMMON_DECISION_TYPE_LFM2_D1_OMNI || decision_type == COMMON_DECISION_TYPE_VLLM_DECISION2 ||
decision_type == COMMON_DECISION_TYPE_MINICPM_OCR_JEV) {
const auto pooling_type = decision_type == COMMON_DECISION_TYPE_MINICPM_OCR_JEV ? LLAMA_POOLING_TYPE_UNSPECIFIED : LLAMA_POOLING_TYPE_NONE;
params.embedding = true;
params.pooling_type = LLAMA_POOLING_TYPE_NONE;
params.pooling_type = pooling_type;
cparams.embeddings = true;
cparams.pooling_type = LLAMA_POOLING_TYPE_NONE;
cparams.pooling_type = pooling_type;
cparams.n_outputs_max = cparams.n_batch;
cparams.n_outputs_max_per_seq = 1;
+6
View File
@@ -701,6 +701,7 @@ struct common_params {
std::string models_dir = ""; // directory containing models for the router server
std::string models_preset = ""; // directory containing model presets for the router server
int models_max = 4; // maximum number of models to load simultaneously
int models_memory_margin = 0; // MiB of device memory to keep free when loading models (0 = no memory accounting)
bool models_autoload = true; // automatically load models when requested via the router server
std::string models_preset_hf = ""; // show a warning about remote presets on router loaded (if not empty)
@@ -771,6 +772,7 @@ struct common_params {
// TTS params
std::string tts_lang = "";
std::string tts_voice = "";
std::string tts_speaker_file = "";
bool is_gen_docs = false; // whether we are running inside llama-gen-docs
@@ -978,6 +980,7 @@ enum common_decision_type {
COMMON_DECISION_TYPE_LFM2_D1_OMNI, // same as laya, other prompt layout
COMMON_DECISION_TYPE_GRANITE_SWITCH, // same as openjev, run by the decision aLoRA of granite switch
COMMON_DECISION_TYPE_H2O_LIGHTNING, // same as openjev, the labels are read after an answer prefill
COMMON_DECISION_TYPE_MINICPM_OCR_JEV, // one score per class, the pooled embeddings output of the last token
COMMON_DECISION_TYPE_UNKNOWN, // a decision model of a type that is not supported
};
@@ -1017,6 +1020,9 @@ common_init_result_ptr common_init_from_params(common_params & params, bool mode
struct llama_model_params common_model_params_to_llama ( common_params & params);
struct llama_context_params common_context_params_to_llama(const common_params & params);
// apply --fit to mparams and cparams, no-op if params.fit_params is false
void common_fit_apply(common_params & params, struct llama_model_params & mparams, struct llama_context_params & cparams);
// clear LoRA adapters from context, then apply new list of adapters
void common_set_adapter_lora(struct llama_context * ctx, std::vector<common_adapter_lora_info> & lora);
+197 -103
View File
@@ -1,6 +1,8 @@
#include "fit.h"
#include "common.h"
#include "json.h"
#include "llama-cpp.h"
#include "log.h"
#include "../src/llama-ext.h"
@@ -27,6 +29,32 @@ class common_params_fit_exception : public std::runtime_error {
using std::runtime_error::runtime_error;
};
// RAII: send llama logs below min_level to the debug log
struct common_fit_log_scope {
common_fit_log_scope(ggml_log_level min_level) : min_level(min_level) {
llama_log_get(&original_logger.callback, &original_logger.user_data);
llama_log_set([](ggml_log_level level, const char * text, void * user_data) {
const common_fit_log_scope * self = (const common_fit_log_scope *) user_data;
const ggml_log_level level_eff = level >= self->min_level ? level : GGML_LOG_LEVEL_DEBUG;
self->original_logger.callback(level_eff, text, self->original_logger.user_data);
}, this);
}
~common_fit_log_scope() {
llama_log_set(original_logger.callback, original_logger.user_data);
}
common_fit_log_scope(const common_fit_log_scope &) = delete;
common_fit_log_scope & operator=(const common_fit_log_scope &) = delete;
private:
struct {
ggml_log_callback callback;
void * user_data;
} original_logger;
ggml_log_level min_level;
};
static std::vector<llama_device_memory_data> common_get_device_memory_data_impl(
const char * path_model,
const llama_model_params * mparams,
@@ -36,22 +64,7 @@ static std::vector<llama_device_memory_data> common_get_device_memory_data_impl(
uint32_t & hp_n_ctx_train,
uint32_t & hp_n_expert,
ggml_log_level log_level) {
struct user_data_t {
struct {
ggml_log_callback callback;
void * user_data;
} original_logger;
ggml_log_level min_level; // prints below this log level go to debug log
};
user_data_t ud;
llama_log_get(&ud.original_logger.callback, &ud.original_logger.user_data);
ud.min_level = log_level;
llama_log_set([](ggml_log_level level, const char * text, void * user_data) {
const user_data_t * ud = (const user_data_t *) user_data;
const ggml_log_level level_eff = level >= ud->min_level ? level : GGML_LOG_LEVEL_DEBUG;
ud->original_logger.callback(level_eff, text, ud->original_logger.user_data);
}, &ud);
common_fit_log_scope log_scope(log_level);
llama_model_params mparams_copy = *mparams;
mparams_copy.no_alloc = true;
@@ -59,14 +72,12 @@ static std::vector<llama_device_memory_data> common_get_device_memory_data_impl(
llama_model * model = llama_model_load_from_file(path_model, mparams_copy);
if (model == nullptr) {
llama_log_set(ud.original_logger.callback, ud.original_logger.user_data);
throw std::runtime_error("failed to load model");
}
llama_context * ctx = llama_init_from_model(model, *cparams);
if (ctx == nullptr) {
llama_model_free(model);
llama_log_set(ud.original_logger.callback, ud.original_logger.user_data);
throw std::runtime_error("failed to create llama_context from model");
}
@@ -148,11 +159,81 @@ static std::vector<llama_device_memory_data> common_get_device_memory_data_impl(
llama_free(ctx);
llama_model_free(model);
llama_log_set(ud.original_logger.callback, ud.original_logger.user_data);
return ret;
}
std::vector<common_memory_breakdown_row> common_fit_measure(
const char * path_model,
const llama_model_params * mparams,
const llama_context_params * cparams,
uint32_t * n_ctx_train,
uint32_t * n_ctx,
ggml_log_level log_level) {
common_fit_log_scope log_scope(log_level);
llama_model_params mparams_copy = *mparams;
mparams_copy.no_alloc = true;
mparams_copy.load_mode = LLAMA_LOAD_MODE_NONE;
llama_model_ptr model(llama_model_load_from_file(path_model, mparams_copy));
if (!model) {
throw std::runtime_error("failed to load model");
}
llama_context_ptr ctx(llama_init_from_model(model.get(), *cparams));
if (!ctx) {
throw std::runtime_error("failed to create llama_context from model");
}
if (n_ctx_train) {
*n_ctx_train = llama_model_n_ctx_train(model.get());
}
if (n_ctx) {
*n_ctx = llama_n_ctx(ctx.get());
}
return common_memory_breakdown_rows(ctx.get());
}
common_memory_reports common_measure_memory(common_params & params) {
const ggml_log_level log_level = params.verbosity >= LOG_LEVEL_DEBUG ? GGML_LOG_LEVEL_DEBUG : GGML_LOG_LEVEL_ERROR;
const char * path_model = params.model.path.c_str();
common_memory_reports res;
auto mparams = common_model_params_to_llama(params);
auto cparams = common_context_params_to_llama(params);
// max: what --fit picks now, no-op with -fit off
{
auto mparams_fit = mparams;
auto cparams_fit = cparams;
common_fit_apply(params, mparams_fit, cparams_fit);
res.info_max = common_fit_measure(path_model, &mparams_fit, &cparams_fit, &res.n_ctx_train, &res.n_ctx_max, log_level);
}
// min: the smallest context --fit may pick (--fit only changes n_ctx when it is 0)
uint32_t n_ctx_min = cparams.n_ctx;
if (n_ctx_min == 0 && params.fit_params) {
const uint32_t n_streams = cparams.kv_unified ? 1 : std::max<uint32_t>(1, cparams.n_seq_max);
// -c 0 sets fit_params_min_ctx to UINT32_MAX, cap at n_ctx_train like --fit
n_ctx_min = (uint32_t) std::min<uint64_t>(
uint64_t(params.fit_params_min_ctx) * n_streams,
uint64_t(res.n_ctx_train) * n_streams);
}
if (n_ctx_min == 0 || n_ctx_min >= res.n_ctx_max) {
// nothing to reduce, or -fit off with the trained context
res.n_ctx_min = res.n_ctx_max;
res.info_min = res.info_max;
} else {
cparams.n_ctx = n_ctx_min;
res.info_min = common_fit_measure(path_model, &mparams, &cparams, nullptr, &res.n_ctx_min, log_level);
}
return res;
}
common_device_memory_data_vec common_get_device_memory_data(
const char * path_model,
const llama_model_params * mparams,
@@ -903,8 +984,7 @@ enum common_params_fit_status common_fit_params(
return status;
}
void common_memory_breakdown_print(const struct llama_context * ctx) {
//const auto & devices = ctx->get_model().devices;
std::vector<common_memory_breakdown_row> common_memory_breakdown_rows(const struct llama_context * ctx) {
const auto * model = llama_get_model(ctx);
std::vector<ggml_backend_dev_t> devices;
@@ -914,18 +994,6 @@ void common_memory_breakdown_print(const struct llama_context * ctx) {
llama_memory_breakdown memory_breakdown = llama_get_memory_breakdown(ctx);
std::vector<std::array<std::string, 9>> table_data;
table_data.reserve(devices.size());
// same data as the table below, for --log-jsonl consumers
common_json rows = common_json::array();
const std::string template_header = "%s: | %s | %s %s %s %s %s %s %s |\n";
const std::string template_gpu = "%s: | %s | %s = %s + (%s = %s + %s + %s) + %s |\n";
const std::string template_other = "%s: | %s | %s %s %s = %s + %s + %s %s |\n";
table_data.push_back({template_header, "memory breakdown [MiB]", "total", "free", "self", "model", "context", "compute", "unaccounted"});
constexpr size_t MiB = 1024 * 1024;
const std::vector<std::string> desc_prefixes_strip = {"NVIDIA ", "GeForce ", "Tesla ", "AMD ", "Radeon ", "Instinct "};
// track seen buffer types to avoid double counting:
@@ -964,104 +1032,130 @@ void common_memory_breakdown_print(const struct llama_context * ctx) {
}
}
// print memory breakdown for each device:
std::vector<common_memory_breakdown_row> rows;
rows.reserve(devices.size() + 1 + memory_breakdown.size());
for (size_t i = 0; i < devices.size(); i++) {
ggml_backend_dev_t dev = devices[i];
llama_memory_breakdown_data mb = mb_dev[i];
const std::string name = ggml_backend_dev_name(dev);
std::string desc = ggml_backend_dev_description(dev);
common_memory_breakdown_row row;
row.kind = common_memory_breakdown_row::DEVICE;
row.name = ggml_backend_dev_name(dev);
row.description = ggml_backend_dev_description(dev);
for (const std::string & prefix : desc_prefixes_strip) {
if (desc.length() >= prefix.length() && desc.substr(0, prefix.length()) == prefix) {
desc = desc.substr(prefix.length());
if (row.description.length() >= prefix.length() && row.description.substr(0, prefix.length()) == prefix) {
row.description = row.description.substr(prefix.length());
}
}
size_t free, total;
ggml_backend_dev_memory(dev, &free, &total);
const size_t self = mb.model + mb.context + mb.compute;
const int64_t unaccounted = static_cast<int64_t>(total) - static_cast<int64_t>(free) - static_cast<int64_t>(self);
table_data.push_back({
template_gpu,
" - " + name + " (" + desc + ")",
std::to_string(total / MiB),
std::to_string(free / MiB),
std::to_string(self / MiB),
std::to_string(mb.model / MiB),
std::to_string(mb.context / MiB),
std::to_string(mb.compute / MiB),
std::to_string(unaccounted / static_cast<int64_t>(MiB))});
rows.push_back({
{"kind", "device"},
{"name", name},
{"description", desc},
{"total", total / MiB},
{"free", free / MiB},
{"self", self / MiB},
{"model", mb.model / MiB},
{"context", mb.context / MiB},
{"compute", mb.compute / MiB},
{"unaccounted", unaccounted / static_cast<int64_t>(MiB)},
});
row.total = total;
row.free = free;
row.model = mb_dev[i].model;
row.context = mb_dev[i].context;
row.compute = mb_dev[i].compute;
rows.push_back(std::move(row));
}
// print memory breakdown for host:
{
const size_t self = mb_host.model + mb_host.context + mb_host.compute;
table_data.push_back({
template_other,
" - Host",
"", // total
"", // free
std::to_string(self / MiB),
std::to_string(mb_host.model / MiB),
std::to_string(mb_host.context / MiB),
std::to_string(mb_host.compute / MiB),
""}); // unaccounted
rows.push_back({
{"kind", "host"},
{"name", "Host"},
{"self", self / MiB},
{"model", mb_host.model / MiB},
{"context", mb_host.context / MiB},
{"compute", mb_host.compute / MiB},
});
common_memory_breakdown_row row;
row.kind = common_memory_breakdown_row::HOST;
row.name = "Host";
row.model = mb_host.model;
row.context = mb_host.context;
row.compute = mb_host.compute;
rows.push_back(std::move(row));
}
// print memory breakdown for all remaining buffer types:
// all remaining buffer types:
for (const auto & buft_mb : memory_breakdown) {
ggml_backend_buffer_type_t buft = buft_mb.first;
const llama_memory_breakdown_data & mb = buft_mb.second;
if (seen_buffer_types.count(buft) == 1) {
continue;
}
const std::string name = ggml_backend_buft_name(buft);
const size_t self = mb.model + mb.context + mb.compute;
common_memory_breakdown_row row;
row.kind = common_memory_breakdown_row::BUFFER_TYPE;
row.name = ggml_backend_buft_name(buft);
row.model = mb.model;
row.context = mb.context;
row.compute = mb.compute;
rows.push_back(std::move(row));
seen_buffer_types.insert(buft);
}
return rows;
}
void common_memory_breakdown_print(const struct llama_context * ctx) {
const std::vector<common_memory_breakdown_row> rows = common_memory_breakdown_rows(ctx);
std::vector<std::array<std::string, 9>> table_data;
table_data.reserve(rows.size() + 1);
// same data as the table below, for --log-jsonl consumers
common_json json_rows = common_json::array();
const std::string template_header = "%s: | %s | %s %s %s %s %s %s %s |\n";
const std::string template_gpu = "%s: | %s | %s = %s + (%s = %s + %s + %s) + %s |\n";
const std::string template_other = "%s: | %s | %s %s %s = %s + %s + %s %s |\n";
table_data.push_back({template_header, "memory breakdown [MiB]", "total", "free", "self", "model", "context", "compute", "unaccounted"});
constexpr size_t MiB = 1024 * 1024;
for (const auto & row : rows) {
const size_t self = row.model + row.context + row.compute;
if (row.kind == common_memory_breakdown_row::DEVICE) {
const int64_t unaccounted = row.total - row.free - static_cast<int64_t>(self);
table_data.push_back({
template_gpu,
" - " + row.name + " (" + row.description + ")",
std::to_string(row.total / MiB),
std::to_string(row.free / MiB),
std::to_string(self / MiB),
std::to_string(row.model / MiB),
std::to_string(row.context / MiB),
std::to_string(row.compute / MiB),
std::to_string(unaccounted / static_cast<int64_t>(MiB))});
json_rows.push_back({
{"kind", "device"},
{"name", row.name},
{"description", row.description},
{"total", row.total / MiB},
{"free", row.free / MiB},
{"self", self / MiB},
{"model", row.model / MiB},
{"context", row.context / MiB},
{"compute", row.compute / MiB},
{"unaccounted", unaccounted / static_cast<int64_t>(MiB)},
});
continue;
}
table_data.push_back({
template_other,
" - " + name,
" - " + row.name,
"", // total
"", // free
std::to_string(self / MiB),
std::to_string(mb.model / MiB),
std::to_string(mb.context / MiB),
std::to_string(mb.compute / MiB),
std::to_string(row.model / MiB),
std::to_string(row.context / MiB),
std::to_string(row.compute / MiB),
""}); // unaccounted
rows.push_back({
{"kind", "buffer_type"},
{"name", name},
json_rows.push_back({
{"kind", row.kind == common_memory_breakdown_row::HOST ? "host" : "buffer_type"},
{"name", row.name},
{"self", self / MiB},
{"model", mb.model / MiB},
{"context", mb.context / MiB},
{"compute", mb.compute / MiB},
{"model", row.model / MiB},
{"context", row.context / MiB},
{"compute", row.compute / MiB},
});
seen_buffer_types.insert(buft);
}
for (size_t j = 1; j < table_data[0].size(); j++) {
@@ -1081,7 +1175,7 @@ void common_memory_breakdown_print(const struct llama_context * ctx) {
LOG_JSON("fit_memory_breakdown", common_json({
{"unit", "MiB"},
{"rows", rows},
{"rows", json_rows},
}));
}
+42
View File
@@ -3,6 +3,7 @@
#include "ggml.h"
#include "llama.h"
#include <string>
#include <vector>
enum common_params_fit_status {
@@ -45,6 +46,47 @@ void common_fit_print(
void common_memory_breakdown_print(const llama_context * ctx);
// one row of the memory breakdown: a device, the host or another buffer type
struct common_memory_breakdown_row {
enum kind_t { DEVICE, HOST, BUFFER_TYPE };
kind_t kind = DEVICE;
std::string name; // device or buffer type name, "Host" for the host row
std::string description; // device description, empty otherwise
int64_t total = 0; // device memory in bytes, 0 unless kind == DEVICE
int64_t free = 0; // free device memory in bytes, 0 unless kind == DEVICE
size_t model = 0; // bytes held by this context, by kind of buffer
size_t context = 0;
size_t compute = 0;
};
// memory a model needs at the min and max context --fit may pick
struct common_memory_reports {
uint32_t n_ctx_train = 0;
uint32_t n_ctx_max = 0; // the largest context that fits the free device memory right now
uint32_t n_ctx_min = 0; // --fit-ctx (the smallest context --fit may pick), with the placement set in params
std::vector<common_memory_breakdown_row> info_max; // breakdown at n_ctx_max
std::vector<common_memory_breakdown_row> info_min; // breakdown at n_ctx_min
};
// rows of common_memory_breakdown_print: model devices first, then host
std::vector<common_memory_breakdown_row> common_memory_breakdown_rows(const llama_context * ctx);
// load model + context with no_alloc and return the memory breakdown, params are used as-is
// n_ctx_train and n_ctx are optional outputs; throws on failure
std::vector<common_memory_breakdown_row> common_fit_measure(
const char * path_model,
const llama_model_params * mparams,
const llama_context_params * cparams,
uint32_t * n_ctx_train,
uint32_t * n_ctx,
ggml_log_level log_level);
struct common_params;
// measure min and max without allocating, params may be modified as with --fit; throws on failure
common_memory_reports common_measure_memory(common_params & params);
struct common_device_memory_data {
int64_t total;
int64_t free;
+4
View File
@@ -184,6 +184,10 @@ caps caps_get(jinja::program & prog) {
{
{"role", "user"},
{"content", json::array({
{
{"type", "text"},
{"text", content_marker}
}
})}
}
});
+8
View File
@@ -172,6 +172,9 @@ TEXT_MODEL_MAP: dict[str, str] = {
"D1OmniModel": "lfm2",
"Lfm2Model": "lfm2",
"Lfm2MoeForCausalLM": "lfm2",
"Lfm3ForCausalLM": "lfm3",
"Lfm3OmniForConditionalGeneration": "lfm3",
"Lfm3VlForConditionalGeneration": "lfm3",
"Llama4ForCausalLM": "llama",
"Llama4ForConditionalGeneration": "llama",
"LlamaBidirectionalModel": "llama",
@@ -197,6 +200,7 @@ TEXT_MODEL_MAP: dict[str, str] = {
"MiniCPMForCausalLM": "minicpm",
"MiniCPMV4_6ForConditionalGeneration": "minicpm",
"MiniCPMV4_7ForConditionalGeneration": "minicpm",
"MiniCPMOCRForSequenceClassification": "qwen",
"MiniCPMOcrForConditionalGeneration": "minicpm",
"MiniMaxText01ForCausalLM": "minimax",
"MiniMaxM1ForCausalLM": "minimax",
@@ -357,6 +361,9 @@ MMPROJ_MODEL_MAP: dict[str, str] = {
"Lfm2AudioForConditionalGeneration": "lfm2",
"D1OmniModel": "lfm2",
"Lfm2VlForConditionalGeneration": "lfm2",
"Lfm3AudioForConditionalGeneration": "lfm3",
"Lfm3OmniForConditionalGeneration": "lfm3",
"Lfm3VlForConditionalGeneration": "lfm3",
"LightOnOCRForConditionalGeneration": "lighton_ocr",
"Llama4ForConditionalGeneration": "llama4",
"LlavaForConditionalGeneration": "llava",
@@ -365,6 +372,7 @@ MMPROJ_MODEL_MAP: dict[str, str] = {
"MiniMaxM3SparseForConditionalGeneration": "minimax",
"MiniCPMV4_6ForConditionalGeneration": "minicpm",
"MiniCPMV4_7ForConditionalGeneration": "minicpm",
"MiniCPMOCRForSequenceClassification": "qwen3vl",
"MiniCPMOcrForConditionalGeneration": "minicpm",
"Mistral3ForConditionalGeneration": "llava",
"Mistral3Model": "llava",
+18 -13
View File
@@ -15,25 +15,30 @@ from .base import MmprojModel, ModelBase, TextModel, gguf, jinja_str_or_json, lo
from .gemma import ConformerAudioModel
def lfm2_feed_forward_length(hparams: dict[str, Any]) -> int:
ff_dim = hparams.get("block_ff_dim", hparams.get("intermediate_size"))
assert ff_dim is not None
auto_adjust_ff_dim = hparams["block_auto_adjust_ff_dim"]
ffn_dim_multiplier = hparams["block_ffn_dim_multiplier"]
multiple_of = hparams["block_multiple_of"]
if auto_adjust_ff_dim:
ff_dim = int(2 * ff_dim / 3)
# custom dim factor multiplier
if ffn_dim_multiplier is not None:
ff_dim = int(ffn_dim_multiplier * ff_dim)
ff_dim = multiple_of * ((ff_dim + multiple_of - 1) // multiple_of)
return ff_dim
@ModelBase.register("Lfm2ForCausalLM", "LFM2ForCausalLM")
@ModelBase.example("LiquidAI/LFM2-1.2B", "LiquidAI/LFM2.5-350M")
class LFM2Model(TextModel):
model_arch = gguf.MODEL_ARCH.LFM2
def _add_feed_forward_length(self):
ff_dim = self.find_hparam(["block_ff_dim", "intermediate_size"])
auto_adjust_ff_dim = self.hparams["block_auto_adjust_ff_dim"]
ffn_dim_multiplier = self.hparams["block_ffn_dim_multiplier"]
multiple_of = self.hparams["block_multiple_of"]
if auto_adjust_ff_dim:
ff_dim = int(2 * ff_dim / 3)
# custom dim factor multiplier
if ffn_dim_multiplier is not None:
ff_dim = int(ffn_dim_multiplier * ff_dim)
ff_dim = multiple_of * ((ff_dim + multiple_of - 1) // multiple_of)
self.gguf_writer.add_feed_forward_length(ff_dim)
self.gguf_writer.add_feed_forward_length(lfm2_feed_forward_length(self.hparams))
def set_gguf_parameters(self):
# set num_key_value_heads only for attention layers
+556
View File
@@ -0,0 +1,556 @@
from __future__ import annotations
import json
import math
from pathlib import Path
from typing import Any, Callable, Iterable, TYPE_CHECKING
import torch
if TYPE_CHECKING:
from torch import Tensor
from .base import MmprojModel, ModelBase, TextModel, gguf
from .lfm2 import lfm2_feed_forward_length
@ModelBase.register("Lfm3ForCausalLM", "Lfm3VlForConditionalGeneration", "Lfm3OmniForConditionalGeneration")
@ModelBase.example("LiquidAI/LFM3-1.7B", "LiquidAI/LFM3-1.7B-Base")
class LFM3Model(TextModel):
model_arch = gguf.MODEL_ARCH.LFM3
# LFM3-Audio/Omni: encoder, projector and audio-output heads live next to the text model
_media_prefixes = ("encoder.", "mlp.", "codebook_embedding.", "depth_embeddings.", "depth_linear.", "depthformer.",
"mask_embedding", "model.codebook_embedding.", "model.depth_decoder.")
@classmethod
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
name, gen = item
if name.startswith(cls._media_prefixes):
return None
name = name.removeprefix("lfm.")
return super().filter_tensors((name, gen))
# LFM3-Audio live ASR prompt of the model card, the audio follows in the assistant turn
_asr_template = (
"{{- bos_token -}}"
"{{- '<|im_start|>system\\nPerform live ASR.<|im_end|>\\n' -}}"
"{%- if add_generation_prompt -%}{{- '<|im_start|>assistant\\n' -}}{%- endif -%}"
)
# TTS prompt of the model card: a language code is looked up, anything else is taken as the language name
_tts_langs = {
"ar": "Arabic", "da": "Danish", "de": "German", "el": "Greek", "en": "American English", "en-us": "American English",
"en-gb": "British English", "fi": "Finnish", "fr": "French", "he": "Hebrew", "it": "Italian", "ja": "Japanese",
"ko": "Korean", "nl": "Dutch", "pl": "Polish", "pt": "Portuguese", "es": "Spanish", "sv": "Swedish", "tr": "Turkish",
"zh": "Chinese",
}
# "language" and "voice" are optional, the text to speak is the first message
def _tts_template(self, think: bool) -> str:
return (
"{%- set names = " + json.dumps(self._tts_langs) + " -%}"
"{%- set code = (language | lower) if language is defined and language else 'en' -%}"
"{%- set lang = names[code] if code in names else language -%}"
"{%- set v = (voice | lower) if voice is defined and voice and (voice | lower) in ['female', 'male'] else 'female' -%}"
"{{- bos_token -}}"
"{{- '<|im_start|>system\\nPerform TTS in ' + lang + ' using a ' + v + ' voice.<|im_end|>\\n' -}}"
"{{- '<|im_start|>user\\n' + messages[0]['content'] + '<|im_end|>\\n' -}}"
"{{- '<|im_start|>assistant\\n" + ("<think></think>" if think else "") + "<|audio_start|>' -}}"
)
def set_vocab(self):
super().set_vocab()
# hparams hold the merged text config, the checkpoint's own model_type is in config.json
with open(self.dir_model / "config.json", encoding="utf-8") as f:
model_type = json.load(f).get("model_type")
is_audio = model_type == "lfm3_audio"
is_omni = model_type == "lfm3_omni"
templates = []
if is_audio:
templates.append({"name": "asr", "template": self._asr_template})
if is_audio or is_omni:
# LFM3-Omni opens every assistant turn with an empty think block
templates.append({"name": "tts", "template": self._tts_template(think=is_omni)})
if templates:
self.gguf_writer.add_chat_template(templates)
def set_gguf_parameters(self):
layer_types = self.hparams["layer_types"]
n_kv_heads = self.hparams["num_key_value_heads"]
# set num_key_value_heads only for attention layers
self.hparams["num_key_value_heads"] = [
n_kv_heads if lt != "conv" else 0
for lt in layer_types
]
super().set_gguf_parameters()
self.gguf_writer.add_rope_dimension_count(self.hparams["head_dim"])
self.gguf_writer.add_sliding_window_pattern([lt == "sliding_attention" for lt in layer_types])
self.gguf_writer.add_sliding_window(self.hparams["sliding_window"])
self.gguf_writer.add_shortconv_l_cache(self.hparams["conv_L_cache"])
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
# Partial RoPE is represented by masking trailing frequency pairs.
# rope_parameters is keyed by layer type; only full_attention layers use partial RoPE.
rope_parameters = self.hparams["rope_parameters"]["full_attention"]
partial = rope_parameters.get("partial_rotary_factor", 1.0)
head_dim = self.hparams["head_dim"]
rope_angles = int(partial * head_dim) // 2
n_pairs = head_dim // 2
assert 0 < rope_angles <= n_pairs
values = [1.0] * rope_angles + [1e30] * (n_pairs - rope_angles)
rope_freqs = torch.tensor(values, dtype=torch.float32)
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FREQS), rope_freqs)
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
# conv op requires 2d tensor
if "conv.conv" in name:
data_torch = data_torch.squeeze(1)
yield from super().modify_tensors(data_torch, name, bid)
class LFM3VLVisionMixin(MmprojModel):
"""LFM3-VL vision tower and projector, shared with LFM3-Omni."""
def _set_lfm3vl_gguf_parameters(self):
assert self.hparams_vision is not None
# clip.cpp hardcodes the GELU projector and its input norm eps
if (self.global_config["projector_hidden_act"] != "gelu" or not self.global_config["projector_bias"]
or self.global_config["projector_layer_norm_eps"] != 1e-6):
raise ValueError("unsupported projector_hidden_act / projector_bias / projector_layer_norm_eps")
# clip.cpp hardcodes the vision rope base, the RGB patchifier and the 3x3 spatial conv
if (self.hparams_vision["rope_parameters"]["rope_theta"] != 1e6 or self.hparams_vision["in_channels"] != 3
or self.hparams_vision["conv_kernel_size"] != 3):
raise ValueError("unsupported vision rope_theta / in_channels / conv_kernel_size")
self.gguf_writer.add_clip_vision_projector_type(gguf.VisionProjectorType.LFM3VL)
self.gguf_writer.add_vision_head_count_kv(self.hparams_vision["num_key_value_heads"])
self.gguf_writer.add_vision_spatial_merge_size(self.global_config["downsample_factor"])
self.gguf_writer.add_vision_attention_layernorm_eps(self.hparams_vision["norm_eps"])
self.gguf_writer.add_vision_use_silu(True)
size = self.preprocessor_config["size"]
self.gguf_writer.add_vision_min_pixels(size["shortest_edge"])
self.gguf_writer.add_vision_max_pixels(size["longest_edge"])
# PIL resample enum, the torchvision backend has no lanczos
resample = self.preprocessor_config["resample"]
resize_algo = {2: "bilinear", 3: "bicubic"}.get(resample)
if resize_algo is None:
raise ValueError(f"unsupported resample: {resample}")
self.gguf_writer.add_vision_image_resize_algo(resize_algo)
def tensor_force_quant(self, name, new_name, bid, n_dims):
if "patch_embd" in new_name:
return gguf.GGMLQuantizationType.F16
if "conv2d_spatial" in new_name:
# CUDA CONV_2D_DW requires an f32 kernel; the tensor is tiny
return gguf.GGMLQuantizationType.F32
return super().tensor_force_quant(name, new_name, bid, n_dims)
def _lfm3vl_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
if name == "model.vision_tower.patchifier.weight":
# qwen2vl-style layout [channel, temporal, ph, pw], one conv kernel per temporal slice
assert self.hparams_vision is not None
t = self.hparams_vision["temporal_patch_size"]
p = self.hparams_vision["patch_size"]
n_embd = data_torch.shape[0]
w = data_torch.view(n_embd, 3, t, p, p)
yield ("v.patch_embd.weight", w[:, :, 0].contiguous())
if t == 2:
yield ("v.patch_embd.weight.1", w[:, :, 1].contiguous())
else:
assert t == 1, f"unsupported vision_temporal_patch_size {t}"
return
if name == "model.vision_tower.embedding_norm.weight":
yield ("v.embedding_norm.weight", data_torch)
return
if "conv.in_proj.weight" in name:
yield (f"v.blk.{bid}.conv2d_in_proj.weight", data_torch)
return
if "conv.spatial_conv.weight" in name:
yield (f"v.blk.{bid}.conv2d_spatial.weight", data_torch)
return
if "conv.out_proj.weight" in name:
yield (f"v.blk.{bid}.conv2d_out_proj.weight", data_torch)
return
# projector: input LayerNorm -> linear_1 -> GELU -> linear_2
if name.startswith("model.multi_modal_projector."):
layer, suffix = name.removeprefix("model.multi_modal_projector.").split(".")
yield ({"layer_norm": "mm.input_norm", "linear_1": "mm.0", "linear_2": "mm.2"}[layer] + "." + suffix, data_torch)
return
yield (self.map_tensor_name(name), data_torch)
@ModelBase.register("Lfm3VlForConditionalGeneration")
@ModelBase.example("LiquidAI/LFM3-VL-2B")
class LFM3VLMmprojModel(LFM3VLVisionMixin):
has_vision_encoder = True
has_audio_encoder = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
assert self.hparams_vision is not None
# dynamic resolution, image_size is only set for MmprojModel compatibility
self.hparams_vision["image_size"] = 512
def set_gguf_parameters(self):
super().set_gguf_parameters()
self._set_lfm3vl_gguf_parameters()
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
if name.startswith(("model.vision_tower.", "model.multi_modal_projector.")):
yield from self._lfm3vl_tensors(data_torch, name, bid)
def _is_lfm3_audio_checkpoint(dir_model: Path) -> bool:
if not (dir_model / "config.json").is_file():
return False
with open(dir_model / "config.json", encoding="utf-8") as f:
config = json.load(f)
return config.get("model_type") == "lfm3_audio" and "lfm" in config
# read the release config.json as is: a transformers that knows lfm3_audio rewrites it into another layout
@ModelBase.register_hparams_loader(_is_lfm3_audio_checkpoint)
def _load_lfm3_audio_hparams(dir_model: Path) -> dict[str, Any]:
with open(dir_model / "config.json", encoding="utf-8") as f:
config = json.load(f)
config["text_config"] = config["lfm"]
return config
@ModelBase.register("Lfm3AudioForConditionalGeneration")
@ModelBase.example("LiquidAI/LFM3-Audio-2B")
class LFM3AudioMmprojModel(MmprojModel):
has_vision_encoder = False
has_audio_encoder = True
# the downsampler has the LFM2-Audio pre-encoder layout: conv, dw conv, pw conv, dw conv, pw conv
_downsample_idx = {"conv_in": 0, "conv_mid": 2, "lin_mid": 3, "conv_end": 5, "lin_end": 6}
# audio output: MaskGIT depthformer (a.gen.code.*) and the codec decoder (a.gen.wav.*)
_depth_layer_map = {
"operator_norm": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_NORM,
"self_attn.q_proj": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_Q,
"self_attn.q_norm": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_Q_NORM,
"self_attn.k_proj": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_K,
"self_attn.k_norm": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_K_NORM,
"self_attn.v_proj": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_V,
"self_attn.o_proj": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_OUT,
"self_attn.gate_proj": gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_GATE,
"ffn_norm": gguf.MODEL_TENSOR.A_GEN_CODE_FFN_NORM,
"feed_forward.w1": gguf.MODEL_TENSOR.A_GEN_CODE_FFN_GATE,
"feed_forward.w3": gguf.MODEL_TENSOR.A_GEN_CODE_FFN_UP,
"feed_forward.w2": gguf.MODEL_TENSOR.A_GEN_CODE_FFN_DOWN,
}
_codec_layer_map = {
"operator_norm": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_NORM,
"post_operator_norm": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_POST_NORM,
"self_attn.q_proj": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_Q,
"self_attn.q_norm": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_Q_NORM,
"self_attn.k_proj": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_K,
"self_attn.k_norm": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_K_NORM,
"self_attn.v_proj": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_V,
"self_attn.o_proj": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_OUT,
"self_attn.gate_proj": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_GATE,
"conv.in_proj": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_CONV_PW1,
"conv.conv": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_CONV_DW,
"conv.out_proj": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_CONV_PW2,
"ffn_norm": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_FFN_NORM,
"post_ffn_norm": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_FFN_POST_NORM,
"feed_forward.w1": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_FFN_GATE,
"feed_forward.w3": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_FFN_UP,
"feed_forward.w2": gguf.MODEL_TENSOR.A_GEN_WAV_TFM_FFN_DOWN,
}
# the codec graph in clip.cpp hardcodes the liquid-codec FSQ front end and decoder shape, conversion fails on a mismatch
_codec = {
"model_type": "liquid_codec", "fsq_levels": [8, 7, 6, 6], "n_q": 12, "quantizer_dim": 256,
"encoder_dim": 512, "stft_n_fft": 1280, "stft_hop_length": 320, "stft_win_length": 1280, "stft_frame_stack": 6,
"decoder_window": 30, "sampling_rate": 24000,
}
# the lfm3a pipeline in mtmd-helper-gen.cpp hardcodes these
_generation = {"codebooks": 12, "maskgit_schedule": [4, 4, 4], "interleaved_n_text": 6, "interleaved_n_audio": 12}
# speech-code sampling of the model card, generation_config.json overrides it
_audio_sampling = {
"audio_temperature": 1.0, "audio_top_k": 128, "audio_repetition_penalty": 1.1, "audio_repetition_window": 4096,
}
# mtmd_audio_preprocessor_lfm3a hardcodes this front end, conversion fails on a mismatch
_feature_extractor = {
"sampling_rate": 16000, "feature_size": 128, "n_fft": 400, "win_length": 400, "hop_length": 160,
"log_floor": -12.0, "normalization_offset": 4.0, "normalization_divisor": 4.0, "encoder_stride": 8,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# audio output is exported when the checkpoint ships the codec
self._audio_output = (self.dir_model / "audio_tokenizer").is_dir()
gen = self.get_generation_config()
self._n_codebooks: int = gen["codebooks"]
self._audio_vocab: int = gen["audio_vocab_size"]
self._depth_embd: dict[int, Tensor] = {}
self._depth_norm: dict[int, Tensor] = {}
def get_audio_config(self) -> dict[str, Any] | None:
enc = self.global_config["encoder"]
cfg = dict(enc["lfm"])
cfg["num_mel_bins"] = enc["dim_in"]
cfg["sliding_window"] = enc["sliding_window"]
return cfg
def get_generation_config(self) -> dict[str, Any]:
c = self.global_config
steps = c["maskgit_steps"]
return {
"codebooks": c["codebooks"], "audio_vocab_size": c["audio_vocab_size"],
"maskgit_schedule": [c["codebooks"] // steps] * steps,
"interleaved_n_text": c["interleaved_n_text"], "interleaved_n_audio": c["interleaved_n_audio"],
}
def get_depth_config(self) -> dict[str, Any]:
return self.global_config["depthformer"]
def set_gguf_parameters(self):
assert self.hparams_audio is not None
fe = self.preprocessor_config["feature_extractor"]
if fe["feature_extractor_type"] not in ("Lfm3AudioFeatureExtractor", "Lfm3OmniFeatureExtractor"):
raise ValueError(f"unsupported feature_extractor_type {fe['feature_extractor_type']!r}")
for k, v in self._feature_extractor.items():
if fe[k] != v:
raise ValueError(f"unsupported feature_extractor.{k}={fe[k]!r}, expected {v!r}")
# clip.cpp hardcodes the encoder rope base
if self.hparams_audio["rope_parameters"]["rope_theta"] != 1e6:
raise ValueError("unsupported audio encoder rope_theta")
self.hparams_audio["intermediate_size"] = lfm2_feed_forward_length(self.hparams_audio)
super().set_gguf_parameters()
self.gguf_writer.add_clip_audio_projector_type(gguf.VisionProjectorType.LFM3A)
self.gguf_writer.add_audio_head_count_kv(self.hparams_audio["num_key_value_heads"])
self.gguf_writer.add_audio_attention_layernorm_eps(self.hparams_audio["norm_eps"])
self.gguf_writer.add_audio_num_mel_bins(self.hparams_audio["num_mel_bins"])
self.gguf_writer.add_audio_window_size(self.hparams_audio["sliding_window"])
if not self._audio_output:
return
# audio generation: depthformer hparams, the codec ones are constant and live in clip.cpp
gen = self.get_generation_config()
for k, v in self._generation.items():
if gen[k] != v:
raise ValueError(f"unsupported {k}={gen[k]!r}, expected {v!r}")
depth = self.get_depth_config()
if depth.get("is_causal", False) or depth["use_sandwich_norm"] or set(depth["layer_types"]) != {"full_attention"}:
raise ValueError("unsupported depthformer, expected a bidirectional all-attention stack")
if depth["rope_parameters"]["full_attention"]["rope_theta"] != 1e6:
raise ValueError("unsupported depthformer rope_theta")
self.gguf_writer.add_clip_has_gen_audio_encoder(True)
self.gguf_writer.add_clip_gen_audio_projector_type(gguf.VisionProjectorType.LFM3A_GEN)
self.gguf_writer.add_gen_audio_projection_dim(self.n_embd_text)
self.gguf_writer.add_gen_audio_embedding_length(depth["hidden_size"])
self.gguf_writer.add_gen_audio_feed_forward_length(depth["intermediate_size"])
self.gguf_writer.add_gen_audio_block_count(depth["num_hidden_layers"])
self.gguf_writer.add_gen_audio_head_count(depth["num_attention_heads"])
self.gguf_writer.add_gen_audio_head_count_kv(depth["num_key_value_heads"])
self.gguf_writer.add_gen_audio_attention_layernorm_eps(depth["norm_eps"])
# sampling defaults of the speech codes, next to the text ones that become general.sampling.*
gen = gguf.Metadata.load_generation_config(self.dir_model)
for key, add in (("audio_temperature", self.gguf_writer.add_gen_audio_sampling_temp),
("audio_top_k", self.gguf_writer.add_gen_audio_sampling_top_k),
("audio_repetition_penalty", self.gguf_writer.add_gen_audio_sampling_penalty_repeat),
("audio_repetition_window", self.gguf_writer.add_gen_audio_sampling_penalty_last_n)):
value = gen.get(key, self._audio_sampling.get(key))
if value is not None:
add(value)
def tensor_force_quant(self, name, new_name, bid, n_dims):
if new_name.startswith("a.conv1d.") or "conv_dw" in new_name or new_name.startswith("a.gen.wav.quant."):
return gguf.GGMLQuantizationType.F32
# the codec decoder is small and its log-magnitude/phase output does not survive quantization
if new_name.startswith("a.gen.wav."):
return gguf.GGMLQuantizationType.F16
return super().tensor_force_quant(name, new_name, bid, n_dims)
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
if self._audio_output:
yield from self._codec_tensors()
def _codec_tensors(self) -> Iterable[tuple[str, Tensor]]:
import json
from safetensors.torch import load_file
T = gguf.MODEL_TENSOR
codec_dir = self.dir_model / "audio_tokenizer"
with open(codec_dir / "config.json", encoding="utf-8") as f:
cfg = json.load(f)
for k, v in self._codec.items():
if cfg.get(k) != v:
raise ValueError(f"unsupported audio_tokenizer {k}={cfg.get(k)!r}, expected {v!r}")
if math.prod(cfg["fsq_levels"]) != self._audio_vocab - 1:
raise ValueError(f"audio_vocab_size {self._audio_vocab} does not match the codec, expected codebook size + 1")
state_dict = load_file(codec_dir / "model.safetensors")
# FSQ code table: index -> the len(levels) quantized values, see FiniteScalarQuantization._indices_to_codes
levels = torch.tensor(cfg["fsq_levels"], dtype=torch.int64)
basis = torch.cumprod(torch.cat([torch.ones(1, dtype=torch.int64), levels[:-1]]), dim=0)
half = levels // 2
idx = torch.arange(int(levels.prod()), dtype=torch.int64)[:, None]
codes = ((idx // basis) % levels - half) / half
yield (self.format_tensor_name(T.A_GEN_WAV_QUANT_FIRST_CB), codes.to(torch.float32))
# one projection over the concatenated code vectors of all codebooks, under quantizer.fsq. in newer exports
quantizer = "quantizer.fsq." if "quantizer.fsq.project_out.weight" in state_dict else "quantizer."
for suffix in ("weight", "bias"):
yield (self.format_tensor_name(T.A_GEN_WAV_QUANT_FIRST_OUT, suffix=f".{suffix}"), state_dict[f"{quantizer}project_out.{suffix}"])
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_IN_PROJ, suffix=f".{suffix}"), state_dict[f"from_quantizer.{suffix}"])
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_OUT_PROJ, suffix=f".{suffix}"), state_dict[f"decoder.lin.{suffix}"])
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_OUTPUT_NORM), state_dict["decoder.lfm.embedding_norm.weight"])
for name, data_torch in state_dict.items():
if not name.startswith("decoder.lfm.layers."):
continue
bid_str, key = name.removeprefix("decoder.lfm.layers.").rsplit(".weight", 1)[0].split(".", 1)
if key == "conv.conv":
data_torch = data_torch.squeeze(1)
yield (self.format_tensor_name(self._codec_layer_map[key], int(bid_str)), data_torch)
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
if name.startswith("a.gen.wav."):
yield (name, data_torch)
return
if name.startswith(("depthformer.", "depth_", "codebook_embedding.", "mask_embedding")):
if self._audio_output:
yield from self._depthformer_tensor(data_torch, name, bid)
return
if not name.startswith(("encoder.", "mlp.")):
return
if name.startswith("encoder.downsample."):
layer, suffix = name.removeprefix("encoder.downsample.").split(".")
if layer == "lin_out":
if suffix == "weight":
data_torch = data_torch.squeeze(-1)
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_ENC_OUT, suffix=f".{suffix}"), data_torch)
return
if suffix == "bias":
data_torch = data_torch[:, None, None]
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_ENC_CONV1D, self._downsample_idx[layer], f".{suffix}"), data_torch)
return
# conv op requires 2d tensor
if name.endswith("conv.conv.weight"):
data_torch = data_torch.squeeze(1)
yield (self.map_tensor_name(name), data_torch)
def _depthformer_tensor(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
T = gguf.MODEL_TENSOR
if name.startswith("depthformer.model.layers."):
key = name.removeprefix("depthformer.model.layers.").split(".", 1)[1].rsplit(".weight", 1)[0]
yield (self.format_tensor_name(self._depth_layer_map[key], bid), data_torch)
return
if name == "depthformer.model.embedding_norm.weight":
yield (self.format_tensor_name(T.A_GEN_CODE_OUTPUT_NORM), data_torch)
return
if name.startswith("depth_linear."):
yield (self.format_tensor_name(T.A_GEN_CODE_PROJ_IN, suffix="." + name.rsplit(".", 1)[1]), data_torch)
return
if name == "mask_embedding":
yield (self.format_tensor_name(T.A_GEN_CODE_MASK_EMBD, suffix=""), data_torch)
return
if name == "codebook_embedding.weight":
assert data_torch.shape[0] == self._n_codebooks * self._audio_vocab
yield (self.format_tensor_name(T.A_GEN_CODE_OUT_EMBD), data_torch)
return
# per-codebook heads: the embedding table is tied to to_logits, merge the 12 tables into one tensor
assert name.startswith("depth_embeddings.")
idx = int(name.split(".")[1])
key = name.split(".", 2)[2]
if key == "embedding.weight":
self._depth_embd[idx] = data_torch
if len(self._depth_embd) == self._n_codebooks:
yield (self.format_tensor_name(T.A_GEN_CODE_EMBD), torch.stack([self._depth_embd[i] for i in range(self._n_codebooks)], dim=0))
elif key == "embedding_norm.weight":
self._depth_norm[idx] = data_torch
if len(self._depth_norm) == self._n_codebooks:
yield (self.format_tensor_name(T.A_GEN_CODE_HEAD_NORM), torch.stack([self._depth_norm[i] for i in range(self._n_codebooks)], dim=0))
elif key == "to_logits.weight":
if not torch.equal(data_torch, self._depth_embd[idx]):
raise ValueError(f"depth_embeddings.{idx} is not tied")
else:
raise ValueError(f"unexpected tensor {name}")
@ModelBase.register("Lfm3OmniForConditionalGeneration")
@ModelBase.example("LiquidAI/LFM3-Omni-2B")
class LFM3OmniMmprojModel(LFM3VLVisionMixin, LFM3AudioMmprojModel):
has_vision_encoder = True
has_audio_encoder = True
_audio_sampling = {"audio_temperature": 0.4, "audio_repetition_penalty": 1.2, "audio_repetition_window": 64}
# the audio side is the LFM3-Audio model under the HF-native names
_audio_renames = (
("model.audio_encoder.lfm.", "encoder.lfm.lfm."),
("model.audio_encoder.", "encoder."),
("model.audio_projector.layer_norm.", "mlp.model.0."),
("model.audio_projector.linear_1.", "mlp.model.1."),
("model.audio_projector.linear_2.", "mlp.model.3."),
("model.depth_decoder.model.", "depthformer.model."),
("model.depth_decoder.", ""),
("model.codebook_embedding.", "codebook_embedding."),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
assert self.hparams_vision is not None
# dynamic resolution, image_size is only set for MmprojModel compatibility
self.hparams_vision["image_size"] = 512
def get_audio_config(self) -> dict[str, Any] | None:
enc = self.global_config["audio_encoder_config"]
cfg = dict(enc["text_config"])
cfg["num_mel_bins"] = enc["num_mel_bins"]
cfg["sliding_window"] = enc["sliding_window"]
return cfg
def get_generation_config(self) -> dict[str, Any]:
c = self.global_config
return {
"codebooks": c["num_codebooks"], "audio_vocab_size": c["codebook_size"], "maskgit_schedule": c["maskgit_schedule"],
"interleaved_n_text": c["interleaved_n_text"], "interleaved_n_audio": c["interleaved_n_audio"],
}
def get_depth_config(self) -> dict[str, Any]:
return self.global_config["depth_config"]
def set_gguf_parameters(self):
super().set_gguf_parameters()
self._set_lfm3vl_gguf_parameters()
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
if name.startswith(("model.vision_tower.", "model.multi_modal_projector.")):
yield from self._lfm3vl_tensors(data_torch, name, bid)
return
for old, new in self._audio_renames:
if name.startswith(old):
yield from super().modify_tensors(data_torch, new + name.removeprefix(old), bid)
return
yield from super().modify_tensors(data_torch, name, bid)
+46
View File
@@ -690,6 +690,52 @@ class Qwen3_5TextModel(_Qwen35MRopeMixin, _LinearAttentionVReorderBase):
self._try_set_pooling_type()
@ModelBase.register("MiniCPMOCRForSequenceClassification")
class MiniCPMOCRSeqClsModel(Qwen3_5TextModel):
# OCR consistency judge: Qwen3.5 VL trunk with a value head read at the last token
# the class scores are [inconsistent, consistent], the softmax temperature is 1
model_arch = gguf.MODEL_ARCH.QWEN35
no_mtp = True # the checkpoint has no MTP head
CLASS_LABELS = ["inconsistent", "consistent"]
def set_vocab(self):
super().set_vocab()
self.gguf_writer.add_chat_template([{"name": "systemone", "template": self._systemone_template()}])
def _systemone_template(self) -> str:
# the prompt of the training data, the two images are the original page and the one
# rendered from the OCR text; the task is fixed, there is nothing to fill in
# the trailing newlines are in an expression, so the template does not drop them
return (
"<|im_start|>user\n{{ images[0] }}\nOCR文本:\n{{ state }}\n{{ images[1] }}\n"
"第一张图是原图,第二张图是上述 OCR 文本渲染得到的图片。\n"
"请判断该 OCR 文本及其对应渲染图是否与原图内容一致。\n"
"这是一个二分类任务:一致标为 1,不一致标为 0。"
"{{ '<|im_end|>\\n<|im_start|>assistant\\n<think>\\n\\n</think>\\n\\n' }}"
)
def set_gguf_parameters(self):
super().set_gguf_parameters()
# the config.json of the checkpoint keeps the one of the base model, it has no num_labels / id2label
if "score.weight" not in self.model_tensors:
raise ValueError("the checkpoint has no score.weight classification head")
self.gguf_writer.add_pooling_type(gguf.PoolingType.LAST)
self.gguf_writer.add_embedding_length_out(len(self.CLASS_LABELS))
self.gguf_writer.add_classifier_output_labels(self.CLASS_LABELS)
self.gguf_writer.add_decision_type(gguf.DecisionType.MINICPM_OCR_JEV)
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
if name == "score.weight":
if data_torch.shape[0] != len(self.CLASS_LABELS):
raise ValueError(f"score.weight must have {len(self.CLASS_LABELS)} rows, got {data_torch.shape[0]}")
yield (gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.CLS_OUT] + ".weight", data_torch)
return
yield from super().modify_tensors(data_torch, name, bid)
def _is_openjev_checkpoint(dir_model: Path) -> bool:
return (dir_model / "helper" / "shim.py").is_file() and (dir_model / "config.json").is_file()
+10
View File
@@ -144,6 +144,16 @@ class Qwen3VLVisionModel(MmprojModel):
yield from MmprojModel.modify_tensors(self, data_torch, name, bid)
@ModelBase.register("MiniCPMOCRForSequenceClassification")
class MiniCPMOCRSeqClsVisionModel(Qwen3VLVisionModel):
def set_gguf_parameters(self):
super().set_gguf_parameters()
# trained with the image budget of its processor, wider than the mtmd default of Qwen-VL
size = self.preprocessor_config["size"]
self.gguf_writer.add_vision_min_pixels(int(size["shortest_edge"]))
self.gguf_writer.add_vision_max_pixels(int(size["longest_edge"]))
@ModelBase.register("Qwen3OmniMoeForConditionalGeneration")
@ModelBase.example("Qwen/Qwen3-Omni-30B-A3B-Instruct")
class Qwen3OmniMmprojModel(Qwen3VLVisionModel, Qwen25AudioModel):
-3
View File
@@ -180,7 +180,6 @@ static __global__ void conv2d_implicit_gemm_f16(const half * __restrict__ input,
const int end = int(int64_t(tiles) * (split + 1) / split_k) * BK;
for (int k0 = begin; k0 < end; k0 += BK) {
if (k_total % 8 == 0 && uintptr_t(weight) % 16 == 0) {
#pragma unroll
for (int i = tid; i < BM * BK / 8; i += nthreads) {
const int row = i / (BK / 8), col = 8 * (i % (BK / 8));
const int4 v = m0 + row < oc && k0 + col < k_total ?
@@ -189,7 +188,6 @@ static __global__ void conv2d_implicit_gemm_f16(const half * __restrict__ input,
*(int4 *) &a_s[row][col / 2] = v;
}
} else {
#pragma unroll
for (int i = tid; i < BM * BK / 2; i += nthreads) {
const int row = i / (BK / 2), col = 2 * (i % (BK / 2));
half lo = __float2half(0.0f), hi = lo;
@@ -202,7 +200,6 @@ static __global__ void conv2d_implicit_gemm_f16(const half * __restrict__ input,
a_s[row][col / 2] = __halves2half2(lo, hi);
}
}
#pragma unroll
for (int k = load_row; k < BK; k += nthreads / (BN / 2)) {
const int ki = k0 + k;
const int ci = ki / (kw * kh), ky = ki / kw % kh, kx = ki % kw;
+41
View File
@@ -507,6 +507,47 @@ ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_lightning_indexe
return res;
}
ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_top_k_mask(
ggml_metal_library_t lib,
const ggml_tensor * op,
bool fill,
bool has_score,
bool has_blocks,
bool has_extra,
bool vec) {
GGML_ASSERT(op->op == GGML_OP_TOP_K_MASK);
char base[256];
char name[256];
snprintf(base, 256, "kernel_top_k_mask%s_%s", fill ? "_fill" : "", ggml_type_name(op->type));
ggml_metal_pipeline_with_params res;
ggml_metal_cv_t cv = ggml_metal_cv_init();
if (fill) {
snprintf(name, 256, "%s_vec=%d", base, vec ? 1 : 0);
ggml_metal_cv_set_bool(cv, vec, FC_TOP_K_MASK + 3);
} else {
snprintf(name, 256, "%s_sc=%d_bl=%d_ex=%d", base, has_score ? 1 : 0, has_blocks ? 1 : 0, has_extra ? 1 : 0);
ggml_metal_cv_set_bool(cv, has_score, FC_TOP_K_MASK + 0);
ggml_metal_cv_set_bool(cv, has_blocks, FC_TOP_K_MASK + 1);
ggml_metal_cv_set_bool(cv, has_extra, FC_TOP_K_MASK + 2);
}
res = ggml_metal_library_get_pipeline(lib, name);
if (!res.pipeline) {
res = ggml_metal_library_compile_pipeline(lib, base, name, cv);
}
ggml_metal_cv_free(cv);
return res;
}
ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_dsv4_hc(ggml_metal_library_t lib, const ggml_tensor * op) {
char name[256];
const char * base = nullptr;
+1
View File
@@ -126,6 +126,7 @@ struct ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_cumsum_ad
struct ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_tri (ggml_metal_library_t lib, const struct ggml_tensor * op);
struct ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_soft_max (ggml_metal_library_t lib, const struct ggml_tensor * op);
struct ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_lightning_indexer (ggml_metal_library_t lib, const struct ggml_tensor * op);
struct ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_top_k_mask (ggml_metal_library_t lib, const struct ggml_tensor * op, bool fill, bool has_score, bool has_blocks, bool has_extra, bool vec);
struct ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_dsv4_hc (ggml_metal_library_t lib, const struct ggml_tensor * op);
struct ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_ssm_conv (ggml_metal_library_t lib, const struct ggml_tensor * op, int32_t nc, bool use_silu);
struct ggml_metal_pipeline_with_params ggml_metal_library_get_pipeline_ssm_conv_batched (ggml_metal_library_t lib, const struct ggml_tensor * op, int ssm_conv_bs, int32_t nc, bool use_silu);
+9
View File
@@ -1835,6 +1835,15 @@ bool ggml_metal_device_supports_op(ggml_metal_device_t dev, const struct ggml_te
ggml_is_contiguous_rows(op->src[1]) &&
ggml_is_contiguous_rows(op->src[2]) &&
(op->src[3] == NULL || ggml_is_contiguous_rows(op->src[3]));
case GGML_OP_TOP_K_MASK:
return (op->type == GGML_TYPE_F16 || op->type == GGML_TYPE_F32) &&
op->src[0]->type == op->type &&
op->src[0]->nb[0] == ggml_type_size(op->src[0]->type) &&
op->nb[0] == ggml_type_size(op->type) &&
op->src[1]->type == GGML_TYPE_I32 &&
(op->src[2] == NULL || op->src[2]->type == GGML_TYPE_F32) &&
(op->src[3] == NULL || op->src[3]->type == GGML_TYPE_I32) &&
(op->src[4] == NULL || op->src[4]->type == GGML_TYPE_I32);
case GGML_OP_SSM_SCAN:
return has_simdgroup_reduction;
case GGML_OP_SSM_CONV:
+33
View File
@@ -125,6 +125,7 @@
#define FC_FLASH_ATTN_EXT_TENSOR 2200
#define FC_LIGHTNING_INDEXER 2200
#define FC_MUL_MV_MMA 2300
#define FC_TOP_K_MASK 2400
// op-specific constants
#define OP_FLASH_ATTN_EXT_NQPSG 8
@@ -1295,6 +1296,38 @@ typedef struct {
uint64_t nbm3;
} ggml_metal_kargs_lightning_indexer;
typedef struct {
int32_t n_kv;
int32_t m1;
int32_t m2;
int32_t m3;
int32_t n_top;
int32_t n_pick;
int32_t n_blk;
int32_t n_extra;
int64_t n_elems;
uint64_t nb1;
uint64_t nb2;
uint64_t nb3;
uint64_t nbm1;
uint64_t nbm2;
uint64_t nbm3;
uint64_t nbk0;
uint64_t nbk1;
uint64_t nbk2;
uint64_t nbk3;
uint64_t nbs0;
uint64_t nbs1;
uint64_t nbs2;
uint64_t nbs3;
uint64_t nbb0;
uint64_t nbb1;
uint64_t nbe0;
uint64_t nbe1;
uint64_t nbe2;
uint64_t nbe3;
} ggml_metal_kargs_top_k_mask;
typedef struct {
int32_t n_tokens;
int32_t n_iter;
+122
View File
@@ -345,6 +345,10 @@ static int ggml_metal_op_encode_impl(ggml_metal_op_t ctx, int idx) {
{
n_fuse = ggml_metal_op_lightning_indexer(ctx, idx);
} break;
case GGML_OP_TOP_K_MASK:
{
n_fuse = ggml_metal_op_top_k_mask(ctx, idx);
} break;
case GGML_OP_DSV4_HC_COMB:
case GGML_OP_DSV4_HC_PRE:
case GGML_OP_DSV4_HC_POST:
@@ -1424,6 +1428,124 @@ int ggml_metal_op_lightning_indexer(ggml_metal_op_t ctx, int idx) {
return 1;
}
int ggml_metal_op_top_k_mask(ggml_metal_op_t ctx, int idx) {
ggml_tensor * op = ctx->node(idx);
ggml_metal_library_t lib = ctx->lib;
ggml_metal_encoder_t enc = ctx->enc;
const ggml_tensor * mask = op->src[0];
const ggml_tensor * top_k = op->src[1];
const ggml_tensor * score = op->src[2];
const ggml_tensor * blocks = op->src[3];
const ggml_tensor * extra = op->src[4];
GGML_ASSERT(op->type == GGML_TYPE_F16 || op->type == GGML_TYPE_F32);
GGML_ASSERT(mask->type == op->type);
GGML_ASSERT(top_k->type == GGML_TYPE_I32);
GGML_ASSERT(!score || score->type == GGML_TYPE_F32);
GGML_ASSERT(!blocks || blocks->type == GGML_TYPE_I32);
GGML_ASSERT(!extra || extra->type == GGML_TYPE_I32);
const int32_t n_kv = mask->ne[0];
const int32_t n_top = (int32_t) top_k->ne[0];
const int32_t n_pick = blocks ? blocks->ne[1] : score ? score->ne[0] : n_kv;
const int32_t n_blk = blocks ? blocks->ne[0] : 0;
const int32_t n_extra = extra ? extra->ne[0] : 0;
const ggml_metal_buffer_id bid = ggml_metal_get_buffer_id(op);
// the vector fill stores 4 elements at a time
const int32_t ts = ggml_type_size(op->type);
const int64_t n_elems = ggml_nelements(op);
const bool has_score = score != NULL;
const bool has_blocks = blocks != NULL;
const bool has_extra = extra != NULL;
const bool vec = n_elems % 4 == 0 && bid.offs % (4*ts) == 0;
ggml_metal_kargs_top_k_mask args = {
/*.n_kv =*/ n_kv,
/*.m1 =*/ (int32_t) mask->ne[1],
/*.m2 =*/ (int32_t) mask->ne[2],
/*.m3 =*/ (int32_t) mask->ne[3],
/*.n_top =*/ n_top,
/*.n_pick =*/ n_pick,
/*.n_blk =*/ n_blk,
/*.n_extra =*/ n_extra,
/*.n_elems =*/ n_elems,
/*.nb1 =*/ op->nb[1],
/*.nb2 =*/ op->nb[2],
/*.nb3 =*/ op->nb[3],
/*.nbm1 =*/ mask->nb[1],
/*.nbm2 =*/ mask->nb[2],
/*.nbm3 =*/ mask->nb[3],
/*.nbk0 =*/ top_k->nb[0],
/*.nbk1 =*/ top_k->nb[1],
/*.nbk2 =*/ top_k->nb[2],
/*.nbk3 =*/ top_k->nb[3],
/*.nbs0 =*/ score ? score->nb[0] : 0,
/*.nbs1 =*/ score ? score->nb[1] : 0,
/*.nbs2 =*/ score ? score->nb[2] : 0,
/*.nbs3 =*/ score ? score->nb[3] : 0,
/*.nbb0 =*/ blocks ? blocks->nb[0] : 0,
/*.nbb1 =*/ blocks ? blocks->nb[1] : 0,
/*.nbe0 =*/ extra ? extra->nb[0] : 0,
/*.nbe1 =*/ extra ? extra->nb[1] : 0,
/*.nbe2 =*/ extra ? extra->nb[2] : 0,
/*.nbe3 =*/ extra ? extra->nb[3] : 0,
};
// the fill covers the whole dst, the scatter overwrites the kept cells with the mask values
{
auto pipeline_fill = ggml_metal_library_get_pipeline_top_k_mask(lib, op, true, false, false, false, vec);
const int64_t n_fill = n_elems/(vec ? 4 : 1);
const int32_t nth_max = ggml_metal_pipeline_max_theads_per_threadgroup(pipeline_fill);
int nth = 32; // SIMD width
while (nth < nth_max && nth < n_fill) {
nth *= 2;
}
ggml_metal_encoder_set_pipeline(enc, pipeline_fill);
ggml_metal_encoder_set_bytes(enc, &args, sizeof(args), 0);
ggml_metal_encoder_set_buffer(enc, bid, 1);
ggml_metal_encoder_dispatch_threadgroups(enc, (n_fill + nth - 1)/nth, 1, 1, nth, 1, 1);
ggml_metal_op_concurrency_reset(ctx);
}
{
auto pipeline = ggml_metal_library_get_pipeline_top_k_mask(lib, op, false, has_score, has_blocks, has_extra, false);
const int32_t n_max = n_top + n_extra;
const int32_t nth_max = ggml_metal_pipeline_max_theads_per_threadgroup(pipeline);
int nth = 32; // SIMD width
while (nth < nth_max && nth < n_max) {
nth *= 2;
}
ggml_metal_encoder_set_pipeline(enc, pipeline);
ggml_metal_encoder_set_bytes(enc, &args, sizeof(args), 0);
ggml_metal_encoder_set_buffer(enc, ggml_metal_get_buffer_id(mask), 1);
ggml_metal_encoder_set_buffer(enc, ggml_metal_get_buffer_id(top_k), 2);
ggml_metal_encoder_set_buffer(enc, ggml_metal_get_buffer_id(score ? score : mask), 3);
ggml_metal_encoder_set_buffer(enc, ggml_metal_get_buffer_id(blocks ? blocks : mask), 4);
ggml_metal_encoder_set_buffer(enc, ggml_metal_get_buffer_id(extra ? extra : mask), 5);
ggml_metal_encoder_set_buffer(enc, bid, 6);
ggml_metal_encoder_dispatch_threadgroups(enc, op->ne[1], op->ne[2], op->ne[3], nth, 1, 1);
}
return 1;
}
int ggml_metal_op_dsv4_hc(ggml_metal_op_t ctx, int idx) {
ggml_tensor * op = ctx->node(idx);
+1
View File
@@ -60,6 +60,7 @@ int ggml_metal_op_get_rows (ggml_metal_op_t ctx, int idx);
int ggml_metal_op_set_rows (ggml_metal_op_t ctx, int idx);
int ggml_metal_op_diag (ggml_metal_op_t ctx, int idx);
int ggml_metal_op_lightning_indexer (ggml_metal_op_t ctx, int idx);
int ggml_metal_op_top_k_mask (ggml_metal_op_t ctx, int idx);
int ggml_metal_op_dsv4_hc (ggml_metal_op_t ctx, int idx);
int ggml_metal_op_soft_max (ggml_metal_op_t ctx, int idx);
int ggml_metal_op_ssm_conv (ggml_metal_op_t ctx, int idx);
+96
View File
@@ -906,3 +906,99 @@ kernel void kernel_dsv4_hc_post_f32(
*(device float *) (dst + i0*args.nb_d0 + idst*args.nb_d1 + it*args.nb_d2) = result[idst];
}
}
constant bool FC_top_k_mask_has_score [[function_constant(FC_TOP_K_MASK + 0)]];
constant bool FC_top_k_mask_has_blocks [[function_constant(FC_TOP_K_MASK + 1)]];
constant bool FC_top_k_mask_has_extra [[function_constant(FC_TOP_K_MASK + 2)]];
constant bool FC_top_k_mask_vec [[function_constant(FC_TOP_K_MASK + 3)]];
// the fill runs before the scatter, both are fully parallel over the whole dst and over the picks of one row
template <typename T, typename T4>
kernel void kernel_top_k_mask_fill(
constant ggml_metal_kargs_top_k_mask & args,
device char * dst,
uint tid[[thread_position_in_grid]],
uint ntot[[threads_per_grid]]) {
const T ninf = T(-INFINITY);
if (FC_top_k_mask_vec) {
device T4 * dst_4 = (device T4 *) dst;
for (int64_t i = tid; i < args.n_elems/4; i += ntot) {
dst_4[i] = T4(ninf);
}
} else {
device T * dst_t = (device T *) dst;
for (int64_t i = tid; i < args.n_elems; i += ntot) {
dst_t[i] = ninf;
}
}
}
typedef decltype(kernel_top_k_mask_fill<half, half4>) kernel_top_k_mask_fill_h_t;
typedef decltype(kernel_top_k_mask_fill<float, float4>) kernel_top_k_mask_fill_f_t;
template [[host_name("kernel_top_k_mask_fill_f16")]] kernel kernel_top_k_mask_fill_h_t kernel_top_k_mask_fill<half, half4>;
template [[host_name("kernel_top_k_mask_fill_f32")]] kernel kernel_top_k_mask_fill_f_t kernel_top_k_mask_fill<float, float4>;
// one threadgroup per row: copy the mask cells of the live picks and of the extra cells over the fill
template <typename T, typename T4>
kernel void kernel_top_k_mask(
constant ggml_metal_kargs_top_k_mask & args,
device const char * mask,
device const char * top_k,
device const char * score,
device const char * blocks,
device const char * extra,
device char * dst,
uint3 tgpig[[threadgroup_position_in_grid]],
uint3 tpitg[[thread_position_in_threadgroup]],
uint3 ntg_max[[threads_per_threadgroup]]) {
const int i1 = tgpig.x;
const int i2 = tgpig.y;
const int i3 = tgpig.z;
const int tiitg = tpitg.x;
const int ntg = ntg_max.x;
device const char * mask_row = mask + (i1 % args.m1)*args.nbm1 + (i2 % args.m2)*args.nbm2 + (i3 % args.m3)*args.nbm3;
device char * dst_row = dst + i1*args.nb1 + i2*args.nb2 + i3*args.nb3;
device const char * top_k_row = top_k + i1*args.nbk1 + i2*args.nbk2 + i3*args.nbk3;
device const T * mask_row_t = (device const T *) mask_row;
device T * dst_row_t = (device T *) dst_row;
for (int i = tiitg; i < args.n_top; i += ntg) {
const int32_t p = *(device const int32_t *) (top_k_row + i*args.nbk0);
if (p < 0 || p >= args.n_pick) {
continue;
}
if (FC_top_k_mask_has_score && !(*(device const float *) (score + p*args.nbs0 + i1*args.nbs1 + i2*args.nbs2 + i3*args.nbs3) > -INFINITY)) {
continue;
}
if (FC_top_k_mask_has_blocks) {
FOR_UNROLL (int k = 0; k < args.n_blk; ++k) {
const int32_t c = *(device const int32_t *) (blocks + k*args.nbb0 + p*args.nbb1);
if (c >= 0 && c < args.n_kv) {
dst_row_t[c] = mask_row_t[c];
}
}
} else if (p < args.n_kv) {
dst_row_t[p] = mask_row_t[p];
}
}
if (FC_top_k_mask_has_extra) {
for (int i = tiitg; i < args.n_extra; i += ntg) {
const int32_t c = *(device const int32_t *) (extra + i*args.nbe0 + i1*args.nbe1 + i2*args.nbe2 + i3*args.nbe3);
if (c >= 0 && c < args.n_kv) {
dst_row_t[c] = mask_row_t[c];
}
}
}
}
typedef decltype(kernel_top_k_mask<half, half4>) kernel_top_k_mask_h_t;
typedef decltype(kernel_top_k_mask<float, float4>) kernel_top_k_mask_f_t;
template [[host_name("kernel_top_k_mask_f16")]] kernel kernel_top_k_mask_h_t kernel_top_k_mask<half, half4>;
template [[host_name("kernel_top_k_mask_f32")]] kernel kernel_top_k_mask_f_t kernel_top_k_mask<float, float4>;
+4 -1
View File
@@ -348,7 +348,10 @@ static bool recv_msg(socket_ptr sock, std::vector<uint8_t> & input) {
}
try {
input.resize(size);
} catch (const std::bad_alloc & e) {
} catch (const std::length_error &) {
LOG_ERROR("Failed to allocate input buffer of size %" PRIu64 "\n", size);
return false;
} catch (const std::bad_alloc &) {
LOG_ERROR("Failed to allocate input buffer of size %" PRIu64 "\n", size);
return false;
}
+4 -12
View File
@@ -7110,7 +7110,7 @@ static void ggml_vk_mul_mat_vec_nc_f16_f32(ggml_backend_vk_context * ctx, vk_con
const uint32_t nb23 = (uint32_t)(dst->nb[3] / sizeof(float));
GGML_ASSERT(ne11 == 1);
GGML_ASSERT(src0->ne[3] == src1->ne[3]); // checked in supports_op
GGML_ASSERT(src0->ne[3] == src1->ne[3]); // checked in ggml_vk_mul_mat
const uint32_t row_stride_x = nb01 / sizeof(ggml_fp16_t);
const uint32_t channel_stride_x = nb02 / sizeof(ggml_fp16_t);
@@ -7382,7 +7382,7 @@ void ggml_vk_mul_mat(ggml_backend_vk_context * ctx, vk_context& subctx, const st
src1->ne[2] <= ctx->device->properties.limits.maxComputeWorkGroupCount[2]) {
ggml_vk_mul_mat_vec_p021_f16_f32(ctx, subctx, cgraph, node_idx);
} else if (src0->type == GGML_TYPE_F16 && src1->type == GGML_TYPE_F32 && !ggml_is_contiguous(src0) && !ggml_is_transposed(src1) && dst->ne[1] == 1 &&
!ggml_is_permuted(src0) && !ggml_is_permuted(src1) &&
!ggml_is_permuted(src0) && !ggml_is_permuted(src1) && src0->ne[3] == src1->ne[3] &&
src0->ne[3] <= ctx->device->properties.limits.maxComputeWorkGroupCount[0] &&
src0->ne[1] <= ctx->device->properties.limits.maxComputeWorkGroupCount[1] &&
src1->ne[2] <= ctx->device->properties.limits.maxComputeWorkGroupCount[2]) {
@@ -15632,16 +15632,8 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm
default:
return false;
}
struct ggml_tensor * a;
struct ggml_tensor * b;
if (op->op == GGML_OP_MUL_MAT) {
a = op->src[0];
b = op->src[1];
} else {
a = op->src[2];
b = op->src[1];
}
if (a->ne[3] != b->ne[3]) {
// MUL_MAT broadcasts src0 over dim 3 like dim 2, MUL_MAT_ID needs the ids to match src1
if (op->op == GGML_OP_MUL_MAT_ID && op->src[2]->ne[3] != op->src[1]->ne[3]) {
return false;
}
if (!(ggml_vk_dim01_contiguous(op->src[0]) || op->src[0]->type == GGML_TYPE_F32 || op->src[0]->type == GGML_TYPE_F16 || op->src[0]->type == GGML_TYPE_BF16) ||
+74
View File
@@ -501,6 +501,7 @@ class Keys:
class Attention:
HEAD_COUNT = "clip.audio.attention.head_count"
HEAD_COUNT_KV = "clip.audio.attention.head_count_kv"
HEAD_DIM = "clip.audio.attention.head_dim" # set when qkv width != n_embd
LAYERNORM_EPS = "clip.audio.attention.layer_norm_epsilon"
@@ -518,6 +519,11 @@ class Keys:
FEED_FORWARD_LENGTH = "clip.gen.audio.feed_forward_length"
BLOCK_COUNT = "clip.gen.audio.block_count"
PROJECTION_DIM = "clip.gen.audio.projection_dim"
# sampling defaults of the generated audio, like general.sampling.* for text
SAMPLING_TEMP = "clip.gen.audio.sampling.temp"
SAMPLING_TOP_K = "clip.gen.audio.sampling.top_k"
SAMPLING_PENALTY_REPEAT = "clip.gen.audio.sampling.penalty_repeat"
SAMPLING_PENALTY_LAST_N = "clip.gen.audio.sampling.penalty_last_n"
class Attention:
HEAD_COUNT = "clip.gen.audio.attention.head_count"
@@ -691,6 +697,7 @@ class MODEL_ARCH(IntEnum):
GPT_OSS = auto()
LFM2 = auto()
LFM2MOE = auto()
LFM3 = auto()
DREAM = auto()
SMALLTHINKER = auto()
LLADA = auto()
@@ -1175,6 +1182,8 @@ class MODEL_TENSOR(IntEnum):
A_ENC_ATTN_Q = auto()
A_ENC_ATTN_K = auto()
A_ENC_ATTN_V = auto()
A_ENC_ATTN_Q_NORM = auto() # lfm3a
A_ENC_ATTN_K_NORM = auto() # lfm3a
A_ENC_ATTN_POST_NORM = auto()
A_ENC_ATTN_PRE_NORM = auto()
A_ENC_ATTN_K_REL = auto() # gemma4
@@ -1219,6 +1228,9 @@ class MODEL_TENSOR(IntEnum):
A_GEN_CODE_FFN_UP = auto()
A_GEN_CODE_FFN_DOWN = auto()
A_GEN_CODE_OUTPUT_NORM = auto()
A_GEN_CODE_ATTN_GATE = auto() # lfm3a: per-head sigmoid gate on the attention output
A_GEN_CODE_HEAD_NORM = auto() # lfm3a: per-codebook norm before the output head, merged 2D [n_codebooks, dim]
A_GEN_CODE_MASK_EMBD = auto() # lfm3a: MaskGIT mask embedding
# qwen3tts code2wav: RVQ codes -> raw PCM
A_GEN_WAV_QUANT_FIRST_IN = auto() # semantic RVQ, in_proj (1x1 conv, loaded as 2D)
A_GEN_WAV_QUANT_FIRST_OUT = auto() # semantic RVQ, out_proj
@@ -1241,6 +1253,14 @@ class MODEL_TENSOR(IntEnum):
A_GEN_WAV_TFM_FFN_UP = auto()
A_GEN_WAV_TFM_FFN_DOWN = auto()
A_GEN_WAV_TFM_FFN_SCALE = auto() # layer scale (gamma) on the FFN output
A_GEN_WAV_TFM_ATTN_Q_NORM = auto() # lfm3a
A_GEN_WAV_TFM_ATTN_K_NORM = auto() # lfm3a
A_GEN_WAV_TFM_CONV_PW1 = auto() # lfm3a: short conv in_proj
A_GEN_WAV_TFM_CONV_DW = auto() # lfm3a: short conv depthwise kernel
A_GEN_WAV_TFM_CONV_PW2 = auto() # lfm3a: short conv out_proj
A_GEN_WAV_TFM_ATTN_GATE = auto() # lfm3a: per-head sigmoid gate on the attention output
A_GEN_WAV_TFM_ATTN_POST_NORM = auto() # lfm3a: sandwich norm after the operator
A_GEN_WAV_TFM_FFN_POST_NORM = auto() # lfm3a: sandwich norm after the FFN
A_GEN_WAV_UP_CONV = auto() # causal ConvTranspose1d, 2x upsample
A_GEN_WAV_UP_DWCONV = auto() # ConvNeXt depthwise conv
A_GEN_WAV_UP_NORM = auto() # ConvNeXt LayerNorm
@@ -1523,6 +1543,7 @@ MODEL_ARCH_NAMES: dict[MODEL_ARCH, str] = {
MODEL_ARCH.GPT_OSS: "gpt-oss",
MODEL_ARCH.LFM2: "lfm2",
MODEL_ARCH.LFM2MOE: "lfm2moe",
MODEL_ARCH.LFM3: "lfm3",
MODEL_ARCH.DREAM: "dream",
MODEL_ARCH.SMALLTHINKER: "smallthinker",
MODEL_ARCH.LLADA: "llada",
@@ -2002,6 +2023,8 @@ TENSOR_NAMES: dict[MODEL_TENSOR, str] = {
MODEL_TENSOR.A_ENC_ATTN_Q: "a.blk.{bid}.attn_q",
MODEL_TENSOR.A_ENC_ATTN_K: "a.blk.{bid}.attn_k",
MODEL_TENSOR.A_ENC_ATTN_V: "a.blk.{bid}.attn_v",
MODEL_TENSOR.A_ENC_ATTN_Q_NORM: "a.blk.{bid}.attn_q_norm",
MODEL_TENSOR.A_ENC_ATTN_K_NORM: "a.blk.{bid}.attn_k_norm",
MODEL_TENSOR.A_ENC_ATTN_POST_NORM: "a.blk.{bid}.attn_post_norm",
MODEL_TENSOR.A_ENC_ATTN_PRE_NORM: "a.blk.{bid}.attn_pre_norm",
MODEL_TENSOR.A_ENC_ATTN_K_REL: "a.blk.{bid}.attn_k_rel",
@@ -2045,6 +2068,9 @@ TENSOR_NAMES: dict[MODEL_TENSOR, str] = {
MODEL_TENSOR.A_GEN_CODE_FFN_UP: "a.gen.code.blk.{bid}.ffn_up",
MODEL_TENSOR.A_GEN_CODE_FFN_DOWN: "a.gen.code.blk.{bid}.ffn_down",
MODEL_TENSOR.A_GEN_CODE_OUTPUT_NORM: "a.gen.code.output_norm",
MODEL_TENSOR.A_GEN_CODE_ATTN_GATE: "a.gen.code.blk.{bid}.attn_gate",
MODEL_TENSOR.A_GEN_CODE_HEAD_NORM: "a.gen.code.head_norm",
MODEL_TENSOR.A_GEN_CODE_MASK_EMBD: "a.gen.code.mask_embd",
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_IN: "a.gen.wav.quant.first.in_proj",
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_OUT: "a.gen.wav.quant.first.out_proj",
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_CB: "a.gen.wav.quant.first.codebook",
@@ -2066,6 +2092,14 @@ TENSOR_NAMES: dict[MODEL_TENSOR, str] = {
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_UP: "a.gen.wav.tfm.blk.{bid}.ffn_up",
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_DOWN: "a.gen.wav.tfm.blk.{bid}.ffn_down",
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_SCALE: "a.gen.wav.tfm.blk.{bid}.ls2",
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_Q_NORM: "a.gen.wav.tfm.blk.{bid}.attn_q_norm",
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_K_NORM: "a.gen.wav.tfm.blk.{bid}.attn_k_norm",
MODEL_TENSOR.A_GEN_WAV_TFM_CONV_PW1: "a.gen.wav.tfm.blk.{bid}.conv_pw1",
MODEL_TENSOR.A_GEN_WAV_TFM_CONV_DW: "a.gen.wav.tfm.blk.{bid}.conv_dw",
MODEL_TENSOR.A_GEN_WAV_TFM_CONV_PW2: "a.gen.wav.tfm.blk.{bid}.conv_pw2",
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_GATE: "a.gen.wav.tfm.blk.{bid}.attn_gate",
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_POST_NORM: "a.gen.wav.tfm.blk.{bid}.attn_post_norm",
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_POST_NORM: "a.gen.wav.tfm.blk.{bid}.ffn_post_norm",
MODEL_TENSOR.A_GEN_WAV_UP_CONV: "a.gen.wav.up.blk.{bid}.conv",
MODEL_TENSOR.A_GEN_WAV_UP_DWCONV: "a.gen.wav.up.blk.{bid}.dwconv",
MODEL_TENSOR.A_GEN_WAV_UP_NORM: "a.gen.wav.up.blk.{bid}.norm",
@@ -2362,6 +2396,8 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
MODEL_TENSOR.A_ENC_ATTN_Q,
MODEL_TENSOR.A_ENC_ATTN_K,
MODEL_TENSOR.A_ENC_ATTN_V,
MODEL_TENSOR.A_ENC_ATTN_Q_NORM,
MODEL_TENSOR.A_ENC_ATTN_K_NORM,
MODEL_TENSOR.A_ENC_ATTN_POST_NORM,
MODEL_TENSOR.A_ENC_ATTN_PRE_NORM,
MODEL_TENSOR.A_ENC_ATTN_K_REL,
@@ -2429,6 +2465,9 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
MODEL_TENSOR.A_GEN_CODE_FFN_UP,
MODEL_TENSOR.A_GEN_CODE_FFN_DOWN,
MODEL_TENSOR.A_GEN_CODE_OUTPUT_NORM,
MODEL_TENSOR.A_GEN_CODE_ATTN_GATE,
MODEL_TENSOR.A_GEN_CODE_HEAD_NORM,
MODEL_TENSOR.A_GEN_CODE_MASK_EMBD,
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_IN,
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_OUT,
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_CB,
@@ -2450,6 +2489,14 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_UP,
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_DOWN,
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_SCALE,
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_Q_NORM,
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_K_NORM,
MODEL_TENSOR.A_GEN_WAV_TFM_CONV_PW1,
MODEL_TENSOR.A_GEN_WAV_TFM_CONV_DW,
MODEL_TENSOR.A_GEN_WAV_TFM_CONV_PW2,
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_GATE,
MODEL_TENSOR.A_GEN_WAV_TFM_ATTN_POST_NORM,
MODEL_TENSOR.A_GEN_WAV_TFM_FFN_POST_NORM,
MODEL_TENSOR.A_GEN_WAV_UP_CONV,
MODEL_TENSOR.A_GEN_WAV_UP_DWCONV,
MODEL_TENSOR.A_GEN_WAV_UP_NORM,
@@ -5522,6 +5569,29 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
MODEL_TENSOR.FFN_UP_EXP,
MODEL_TENSOR.FFN_EXP_PROBS_B,
],
MODEL_ARCH.LFM3: [
MODEL_TENSOR.TOKEN_EMBD,
MODEL_TENSOR.OUTPUT_NORM,
MODEL_TENSOR.OUTPUT,
MODEL_TENSOR.ROPE_FREQS,
MODEL_TENSOR.SHORTCONV_CONV,
MODEL_TENSOR.SHORTCONV_INPROJ,
MODEL_TENSOR.SHORTCONV_OUTPROJ,
MODEL_TENSOR.FFN_GATE,
MODEL_TENSOR.FFN_DOWN,
MODEL_TENSOR.FFN_UP,
MODEL_TENSOR.FFN_NORM,
MODEL_TENSOR.FFN_POST_NORM,
MODEL_TENSOR.ATTN_NORM, # operator_norm
MODEL_TENSOR.ATTN_POST_NORM,
MODEL_TENSOR.ATTN_Q_NORM,
MODEL_TENSOR.ATTN_K_NORM,
MODEL_TENSOR.ATTN_Q,
MODEL_TENSOR.ATTN_K,
MODEL_TENSOR.ATTN_V,
MODEL_TENSOR.ATTN_OUT,
MODEL_TENSOR.ATTN_GATE,
],
MODEL_ARCH.SMALLTHINKER: [
MODEL_TENSOR.TOKEN_EMBD,
MODEL_TENSOR.OUTPUT_NORM,
@@ -6476,6 +6546,7 @@ class DecisionType:
LFM2_D1_OMNI = "lfm2-d1-omni" # same head as laya on a bidirectional LFM2 trunk, other prompt layout
GRANITE_SWITCH = "granite-switch" # same as openjev, run by the decision aLoRA of granite switch
H2O_LIGHTNING = "h2o-lightning" # same as openjev, labels read after an answer prefill
MINICPM_OCR_JEV = "openbmb-minicpm-ocr-jev" # class scores of a value head read at the last token, one fixed task
class VisionProjectorType:
@@ -6506,6 +6577,7 @@ class VisionProjectorType:
VOXTRAL_RT = "voxtral_rt"
MERALION = "meralion" # audio: Whisper + gated MLP adaptor
LFM2 = "lfm2"
LFM3VL = "lfm3vl"
KIMIVL = "kimivl"
PADDLEOCR = "paddleocr"
KIMIK25 = "kimik25"
@@ -6520,6 +6592,8 @@ class VisionProjectorType:
DEEPSEEK4V = "deepseek4v"
DEEPSEEK41V = "deepseek41v"
LFM2A = "lfm2a" # audio
LFM3A = "lfm3a" # audio
LFM3A_GEN = "lfm3a_gen" # audio generation: MaskGIT depthformer + liquid-codec decoder
MUSIC_FLAMINGO = "musicflamingo" # audio
GLM4V = "glm4v"
GLM5V = "glm5v"
+15
View File
@@ -1628,6 +1628,9 @@ class GGUFWriter:
def add_audio_head_count(self, value: int) -> None:
self.add_uint32(Keys.ClipAudio.Attention.HEAD_COUNT, value)
def add_audio_head_count_kv(self, value: int) -> None:
self.add_uint32(Keys.ClipAudio.Attention.HEAD_COUNT_KV, value)
def add_audio_head_dim(self, value: int) -> None:
self.add_uint32(Keys.ClipAudio.Attention.HEAD_DIM, value)
@@ -1708,6 +1711,18 @@ class GGUFWriter:
def add_gen_audio_block_count(self, value: int) -> None:
self.add_uint32(Keys.ClipGenAudio.BLOCK_COUNT, value)
def add_gen_audio_sampling_temp(self, value: float) -> None:
self.add_float32(Keys.ClipGenAudio.SAMPLING_TEMP, value)
def add_gen_audio_sampling_top_k(self, value: int) -> None:
self.add_int32(Keys.ClipGenAudio.SAMPLING_TOP_K, value)
def add_gen_audio_sampling_penalty_repeat(self, value: float) -> None:
self.add_float32(Keys.ClipGenAudio.SAMPLING_PENALTY_REPEAT, value)
def add_gen_audio_sampling_penalty_last_n(self, value: int) -> None:
self.add_int32(Keys.ClipGenAudio.SAMPLING_PENALTY_LAST_N, value)
def add_gen_audio_head_count(self, value: int) -> None:
self.add_uint32(Keys.ClipGenAudio.Attention.HEAD_COUNT, value)
+36
View File
@@ -119,6 +119,7 @@ class TensorNameMap:
"model.transformer.ln_f", # llada
"final_norm", # modern-bert
"model.norm", # cogvlm
"model.embedding_norm", # lfm3
),
# Rope frequencies
@@ -418,6 +419,7 @@ class TensorNameMap:
"model.layers.{bid}.post_self_attn_layernorm", # glm-4-0414
"model.layers.{bid}.post_attn_norm", # kolibri1
"model.layers.layers.{bid}.post_mixer_norm.weight", # plamo2
"model.layers.{bid}.post_operator_norm", # lfm3
),
# Rotary embeddings
@@ -504,6 +506,7 @@ class TensorNameMap:
"model.layers.layers.{bid}.post_mlp_norm.weight", # plamo2
"model.layers.{bid}.feed_forward.up_proj",
"model.layers.{bid}.post_moe_norm", # grok-2
"model.layers.{bid}.post_ffn_norm", # lfm3
),
MODEL_TENSOR.FFN_POST_NORM_1: (
@@ -1855,6 +1858,7 @@ class TensorNameMap:
"vision_model.model.layers.{bid}.self_attn.q_proj", # llama4
"vision_tower.transformer.layers.{bid}.attention.q_proj", # pixtral-hf
"vision_encoder.transformer.layers.{bid}.attention.wq", # pixtral
"model.vision_tower.layers.{bid}.self_attn.q_proj", # lfm3vl
"visual.blocks.{bid}.attn.q", # qwen2vl, generated
"vision_tower.encoder.blocks.{bid}.wq", # kimi-vl, generated
"siglip2.vision_model.encoder.layers.{bid}.self_attn.q_proj", # youtuvl
@@ -1870,6 +1874,7 @@ class TensorNameMap:
"model.vision_tower.encoder.layer.{bid}.attention.q_norm", # Intern-S1
"visual.blocks.{bid}.attn.q_norm", # GLM-OCR
"vision_model.model.layers.{bid}.self_attn.q_norm", # gemma4
"model.vision_tower.layers.{bid}.self_attn.q_layernorm", # lfm3vl
),
MODEL_TENSOR.V_ENC_ATTN_K: (
@@ -1884,6 +1889,7 @@ class TensorNameMap:
"vision_model.model.layers.{bid}.self_attn.k_proj", # llama4
"vision_tower.transformer.layers.{bid}.attention.k_proj", # pixtral-hf
"vision_encoder.transformer.layers.{bid}.attention.wk", # pixtral
"model.vision_tower.layers.{bid}.self_attn.k_proj", # lfm3vl
"visual.blocks.{bid}.attn.k", # qwen2vl, generated
"vision_tower.encoder.blocks.{bid}.wk", # kimi-vl, generated
"model.vision_model.transformer.layers.{bid}.self_attn.k_proj", # Deepseek-OCR CLIP, generated
@@ -1899,6 +1905,7 @@ class TensorNameMap:
"model.vision_tower.encoder.layer.{bid}.attention.k_norm", # Intern-S1
"visual.blocks.{bid}.attn.k_norm", # GLM-OCR
"vision_model.model.layers.{bid}.self_attn.k_norm", # gemma4
"model.vision_tower.layers.{bid}.self_attn.k_layernorm", # lfm3vl
),
MODEL_TENSOR.V_ENC_ATTN_V: (
@@ -1913,6 +1920,7 @@ class TensorNameMap:
"vision_model.model.layers.{bid}.self_attn.v_proj", # llama4
"vision_tower.transformer.layers.{bid}.attention.v_proj", # pixtral-hf
"vision_encoder.transformer.layers.{bid}.attention.wv", # pixtral
"model.vision_tower.layers.{bid}.self_attn.v_proj", # lfm3vl
"visual.blocks.{bid}.attn.v", # qwen2vl, generated
"vision_tower.encoder.blocks.{bid}.wv", # kimi-vl, generated
"siglip2.vision_model.encoder.layers.{bid}.self_attn.v_proj",
@@ -1935,6 +1943,7 @@ class TensorNameMap:
"vit.layers.{bid}.input_layernorm", # HunyuanVL
"vision_tower.transformer.layers.{bid}.attention_norm", # pixtral-hf
"vision_encoder.transformer.layers.{bid}.attention_norm", # pixtral
"model.vision_tower.layers.{bid}.operator_norm", # lfm3vl
"vision_model.model.layers.{bid}.input_layernorm", # llama4, gemma4
"visual.blocks.{bid}.norm1", # qwen2vl
"vision_tower.encoder.blocks.{bid}.norm0", # kimi-vl (norm0/norm1)
@@ -1964,6 +1973,7 @@ class TensorNameMap:
"vision_model.model.layers.{bid}.self_attn.o_proj", # llama4
"vision_tower.transformer.layers.{bid}.attention.o_proj", # pixtral-hf
"vision_encoder.transformer.layers.{bid}.attention.wo", # pixtral
"model.vision_tower.layers.{bid}.self_attn.out_proj", # lfm3vl
"visual.blocks.{bid}.attn.proj", # qwen2vl
"vision_tower.encoder.blocks.{bid}.wo", # kimi-vl
"model.vision.transformer.layers.{bid}.attention.dense", # cogvlm
@@ -1996,6 +2006,7 @@ class TensorNameMap:
"vision_model.model.layers.{bid}.post_attention_layernorm", # llama4
"vision_tower.transformer.layers.{bid}.ffn_norm", # pixtral-hf
"vision_encoder.transformer.layers.{bid}.ffn_norm", # pixtral
"model.vision_tower.layers.{bid}.ffn_norm", # lfm3vl
"visual.blocks.{bid}.norm2", # qwen2vl
"vision_tower.encoder.blocks.{bid}.norm1", # kimi-vl (norm0/norm1)
"model.vision.transformer.layers.{bid}.post_attention_layernorm", # cogvlm
@@ -2023,6 +2034,7 @@ class TensorNameMap:
"vit.layers.{bid}.mlp.dense_h_to_4h", # HunyuanVL
"vision_tower.transformer.layers.{bid}.feed_forward.up_proj", # pixtral-hf
"vision_encoder.transformer.layers.{bid}.feed_forward.w3", # pixtral
"model.vision_tower.layers.{bid}.feed_forward.w3", # lfm3vl
"vision_model.model.layers.{bid}.mlp.fc1", # llama4
"visual.blocks.{bid}.mlp.fc1", # qwen2vl
"visual.blocks.{bid}.mlp.up_proj", # qwen2.5vl
@@ -2043,6 +2055,7 @@ class TensorNameMap:
"vision_encoder.blocks.{bid}.mlp.fc1", # dots3note
"vision_tower.transformer.layers.{bid}.feed_forward.gate_proj", # pixtral-hf
"vision_encoder.transformer.layers.{bid}.feed_forward.w1", # pixtral
"model.vision_tower.layers.{bid}.feed_forward.w1", # lfm3vl
"visual.blocks.{bid}.mlp.gate_proj", # qwen2.5vl
"vision_model.model.layers.{bid}.mlp.gate_proj", # gemma4
"model.qwen2_model.model.model.layers.{bid}.mlp.gate_proj", # Deepseek-OCR-2 qwen2
@@ -2061,6 +2074,7 @@ class TensorNameMap:
"vit.layers.{bid}.mlp.dense_4h_to_h", # HunyuanVL
"vision_tower.transformer.layers.{bid}.feed_forward.down_proj", # pixtral-hf
"vision_encoder.transformer.layers.{bid}.feed_forward.w2", # pixtral
"model.vision_tower.layers.{bid}.feed_forward.w2", # lfm3vl
"vision_model.model.layers.{bid}.mlp.fc2", # llama4
"visual.blocks.{bid}.mlp.fc2", # qwen2vl
"visual.blocks.{bid}.mlp.down_proj", # qwen2.5vl
@@ -2540,12 +2554,14 @@ class TensorNameMap:
"audio_encoder.dots_encoder.speech_encoder.layer_norm", # dots3note
"audio_tower.ln_post", # qwen2omni
"encoder.layer_norm", # mimo-audio-tokenizer
"encoder.lfm.lfm.embedding_norm", # lfm3a
),
MODEL_TENSOR.A_ENC_ATTN_Q: (
"audio_tower.layers.{bid}.self_attn.q_proj", # ultravox
"audio_encoder.dots_encoder.speech_encoder.layers.{bid}.self_attn.q_proj", # dots3note
"conformer.layers.{bid}.self_attn.linear_q", # lfm2
"encoder.lfm.lfm.layers.{bid}.self_attn.q_proj", # lfm3a
"conformer.layers.{bid}.attention.attn.q_proj", # gemma3n
"conformer.layers.{bid}.self_attn.q_proj", # gemma4
"sound_encoder.encoder.layers.{bid}.self_attn.q_proj", # parakeet
@@ -2557,6 +2573,7 @@ class TensorNameMap:
"audio_tower.layers.{bid}.self_attn.k_proj", # ultravox
"audio_encoder.dots_encoder.speech_encoder.layers.{bid}.self_attn.k_proj", # dots3note
"conformer.layers.{bid}.self_attn.linear_k", # lfm2
"encoder.lfm.lfm.layers.{bid}.self_attn.k_proj", # lfm3a
"conformer.layers.{bid}.attention.attn.k_proj", # gemma3n
"conformer.layers.{bid}.self_attn.k_proj", # gemma4
"sound_encoder.encoder.layers.{bid}.self_attn.k_proj", # parakeet
@@ -2568,6 +2585,7 @@ class TensorNameMap:
"audio_tower.layers.{bid}.self_attn.v_proj", # ultravox
"audio_encoder.dots_encoder.speech_encoder.layers.{bid}.self_attn.v_proj", # dots3note
"conformer.layers.{bid}.self_attn.linear_v", # lfm2
"encoder.lfm.lfm.layers.{bid}.self_attn.v_proj", # lfm3a
"conformer.layers.{bid}.attention.attn.v_proj", # gemma3n
"conformer.layers.{bid}.self_attn.v_proj", # gemma4
"sound_encoder.encoder.layers.{bid}.self_attn.v_proj", # parakeet
@@ -2575,6 +2593,14 @@ class TensorNameMap:
"encoder.layers.{bid}.self_attn.v_proj", # mimo-audio-tokenizer
),
MODEL_TENSOR.A_ENC_ATTN_Q_NORM: (
"encoder.lfm.lfm.layers.{bid}.self_attn.q_layernorm", # lfm3a
),
MODEL_TENSOR.A_ENC_ATTN_K_NORM: (
"encoder.lfm.lfm.layers.{bid}.self_attn.k_layernorm", # lfm3a
),
MODEL_TENSOR.A_ENC_ATTN_K_REL: (
"conformer.layers.{bid}.self_attn.relative_k_proj", # gemma4
),
@@ -2600,6 +2626,7 @@ class TensorNameMap:
"audio_tower.layers.{bid}.self_attn_layer_norm", # ultravox
"audio_encoder.dots_encoder.speech_encoder.layers.{bid}.self_attn_layer_norm", # dots3note
"conformer.layers.{bid}.norm_self_att", # lfm2
"encoder.lfm.lfm.layers.{bid}.operator_norm", # lfm3a
"conformer.layers.{bid}.attention.pre_attn_norm", # gemma3n
"sound_encoder.encoder.layers.{bid}.norm_self_att", # parakeet
"encoder.layers.{bid}.attn.pre_norm", # granite_speech
@@ -2611,6 +2638,7 @@ class TensorNameMap:
"audio_tower.layers.{bid}.self_attn.o_proj", # voxtral realtime
"audio_encoder.dots_encoder.speech_encoder.layers.{bid}.self_attn.out_proj", # dots3note
"conformer.layers.{bid}.self_attn.linear_out", # lfm2
"encoder.lfm.lfm.layers.{bid}.self_attn.out_proj", # lfm3a
"conformer.layers.{bid}.attention.post", # gemma3n
"conformer.layers.{bid}.self_attn.post", # gemma4
"sound_encoder.encoder.layers.{bid}.self_attn.o_proj", # parakeet
@@ -2622,6 +2650,7 @@ class TensorNameMap:
"audio_tower.layers.{bid}.final_layer_norm", # ultravox
"audio_encoder.dots_encoder.speech_encoder.layers.{bid}.final_layer_norm", # dots3note
"conformer.layers.{bid}.norm_out", # lfm2
"encoder.lfm.lfm.layers.{bid}.ffn_norm", # lfm3a
"conformer.layers.{bid}.attention.post_norm", # gemma3n
"sound_encoder.encoder.layers.{bid}.norm_out", # parakeet
"encoder.layers.{bid}.post_norm", # granite_speech
@@ -2650,6 +2679,7 @@ class TensorNameMap:
"audio_tower.layers.{bid}.fc1", # ultravox
"audio_tower.layers.{bid}.mlp.up_proj", # voxtral realtime
"conformer.layers.{bid}.feed_forward1.linear1", # lfm2
"encoder.lfm.lfm.layers.{bid}.feed_forward.w3", # lfm3a
"conformer.layers.{bid}.ffw_layer_start.ffw_layer_1", # gemma3n
"conformer.layers.{bid}.feed_forward1.ffw_layer_1", # gemma4
"sound_encoder.encoder.layers.{bid}.feed_forward1.linear1", # parakeet
@@ -2659,6 +2689,7 @@ class TensorNameMap:
MODEL_TENSOR.A_ENC_FFN_GATE: (
"audio_encoder.dots_encoder.speech_encoder.layers.{bid}.fc1_gate", # dots3note (split from fc1 in conversion code)
"encoder.lfm.lfm.layers.{bid}.feed_forward.w1", # lfm3a
"audio_tower.layers.{bid}.mlp.gate_proj", # voxtral realtime
),
@@ -2667,6 +2698,7 @@ class TensorNameMap:
"audio_tower.layers.{bid}.fc2", # ultravox
"audio_tower.layers.{bid}.mlp.down_proj", # voxtral realtime
"conformer.layers.{bid}.feed_forward1.linear2", # lfm2
"encoder.lfm.lfm.layers.{bid}.feed_forward.w2", # lfm3a
"conformer.layers.{bid}.ffw_layer_start.ffw_layer_2", # gemma3n
"conformer.layers.{bid}.feed_forward1.ffw_layer_2", # gemma4
"sound_encoder.encoder.layers.{bid}.feed_forward1.linear2", # parakeet
@@ -2750,6 +2782,7 @@ class TensorNameMap:
"audio.multi_modal_projector.linear_{bid}", # ultravox, meralion
"audio_encoder.audio_adapter.proj.{bid}", # dots3note (proj.1, proj.3)
"audio_adapter.model.{bid}", # lfm2
"mlp.model.{bid}", # lfm3a
"audio_tower.proj{bid}", # qwen3omni
"sound_projection.linear{bid}", # parakeet (linear1, linear2)
),
@@ -2809,6 +2842,7 @@ class TensorNameMap:
MODEL_TENSOR.A_ENC_CONV_DW: (
"conformer.layers.{bid}.conv.depthwise_conv", # lfm2
"encoder.lfm.lfm.layers.{bid}.conv.conv", # lfm3a
"conformer.layers.{bid}.lconv1d.depthwise_conv1d", # gemma3n
"sound_encoder.encoder.layers.{bid}.conv.depthwise_conv", # parakeet
"encoder.layers.{bid}.conv.depth_conv.conv", # granite_speech
@@ -2831,6 +2865,7 @@ class TensorNameMap:
MODEL_TENSOR.A_ENC_CONV_PW1: (
"conformer.layers.{bid}.conv.pointwise_conv1", # lfm2
"encoder.lfm.lfm.layers.{bid}.conv.in_proj", # lfm3a
"conformer.layers.{bid}.lconv1d.linear_start", # gemma3n
"sound_encoder.encoder.layers.{bid}.conv.pointwise_conv1", # parakeet
"encoder.layers.{bid}.conv.up_conv", # granite_speech
@@ -2839,6 +2874,7 @@ class TensorNameMap:
MODEL_TENSOR.A_ENC_CONV_PW2: (
"conformer.layers.{bid}.conv.pointwise_conv2", # lfm2
"encoder.lfm.lfm.layers.{bid}.conv.out_proj", # lfm3a
"conformer.layers.{bid}.lconv1d.linear_end", # gemma3n
"sound_encoder.encoder.layers.{bid}.conv.pointwise_conv2", # parakeet
"encoder.layers.{bid}.conv.down_conv", # granite_speech
+280
View File
@@ -0,0 +1,280 @@
{{- bos_token -}}
{%- set preserve_thinking = preserve_thinking | default(false) -%}
{%- set enable_thinking = enable_thinking | default(true) -%}
{%- macro render_value(v) -%}
{%- if v is string -%}
{{- v -}}
{%- elif v is boolean -%}
{{- "true" if v else "false" -}}
{%- elif v is none -%}
{{- "null" -}}
{%- elif v is mapping or (v is sequence and v is not string) -%}
{{- v | tojson -}}
{%- else -%}
{{- v -}}
{%- endif -%}
{%- endmacro -%}
{%- macro parse_content(content) -%}
{%- if content is string -%}
{{- content -}}
{%- elif content is mapping and content.get("type") == "image" -%}
{{- "<|vision_start|><|image|><|vision_end|>" -}}
{%- elif content is mapping and content.get("type") == "video" -%}
{{- "<|vision_start|><|video|><|vision_end|>" -}}
{%- elif content is mapping and content.get("type") == "text" -%}
{{- (content.get("text") or "") | string -}}
{%- elif content is mapping -%}
{{- content | tojson -}}
{%- elif content is iterable -%}
{%- set _ns = namespace(result="") -%}
{%- for item in content -%}
{%- if item is string -%}
{%- set _ns.result = _ns.result + item -%}
{%- elif item is mapping and item.get("type") == "image" -%}
{%- set _ns.result = _ns.result + "<|vision_start|><|image|><|vision_end|>" -%}
{%- elif item is mapping and item.get("type") == "video" -%}
{%- set _ns.result = _ns.result + "<|vision_start|><|video|><|vision_end|>" -%}
{%- elif item is mapping and item.get("type") == "text" -%}
{%- set _ns.result = _ns.result + ((item.get("text") or "") | string) -%}
{%- else -%}
{%- set _ns.result = _ns.result + (item | tojson) -%}
{%- endif -%}
{%- endfor -%}
{{- _ns.result -}}
{%- elif content is not none -%}
{{- content | tojson -}}
{%- endif -%}
{%- endmacro -%}
{%- macro render_tool_calls(tool_calls) -%}
{%- for tool_call in tool_calls -%}
{%- if not loop.first -%}{{- "\n\n" -}}{%- endif -%}
{%- set func = tool_call["function"] if "function" in tool_call else tool_call -%}
{%- set func_args = func.get("arguments") -%}
{%- if func_args is string and (func_args | trim) not in ["", "{}", "null"] -%}
{{- raise_exception("Tool call arguments must be a mapping, got a JSON-encoded string: parse arguments with json.loads() before applying the chat template") -}}
{%- endif -%}
{{- "<tool_call>\n<function=" + func["name"] + ">\n" -}}
{%- if func_args is mapping -%}
{%- for arg_name, arg_value in func_args.items() -%}
{{- "<parameter=" + arg_name + ">\n" + render_value(arg_value) + "\n</parameter>\n" -}}
{%- endfor -%}
{%- endif -%}
{{- "</function>\n</tool_call>" -}}
{%- endfor -%}
{%- endmacro -%}
{%- macro tool_to_json(tool) -%}
{%- set fn = tool.function if tool.function is defined else tool -%}
{%- set ns_t = namespace(first=true) -%}
{{- "{" -}}
{%- for k, v in fn.items() -%}
{%- if k != "defer_loading" -%}
{%- if not ns_t.first -%}{{- ", " -}}{%- endif -%}
{%- set ns_t.first = false -%}
{{- k | tojson -}}{{- ": " -}}{{- v | tojson -}}
{%- endif -%}
{%- endfor -%}
{{- "}" -}}
{%- endmacro -%}
{%- macro is_tool_reference(item) -%}
{{- "1" if item is mapping and item.get("type") == "tool_reference" else "" -}}
{%- endmacro -%}
{%- macro referenced_tool_to_json(name) -%}
{%- set _ns = namespace(found=none) -%}
{%- for tool in tools or [] -%}
{%- set fn = tool.function if tool.function is defined else tool -%}
{%- if fn.name == name -%}
{%- set _ns.found = fn -%}
{%- endif -%}
{%- endfor -%}
{%- if _ns.found is none -%}
{{- raise_exception("Tool reference '" + name | string + "' not found in available tools. Every tool referenced by a tool_reference must be in `tools` on every request.") -}}
{%- endif -%}
{{- tool_to_json(_ns.found) -}}
{%- endmacro -%}
{%- macro render_tool_response(content) -%}
{%- if content is string and "<tool_response>" in content -%}
{{- content -}}
{%- else -%}
{{- "<tool_response>\n" -}}
{%- if content is string -%}
{{- content -}}
{%- elif content is mapping -%}
{{- parse_content(content) -}}
{%- elif content is iterable -%}
{%- for item in content -%}
{%- if not loop.first -%}{{- "\n" -}}{%- endif -%}
{%- if is_tool_reference(item) -%}
{%- if loop.first or not is_tool_reference(loop.previtem) -%}
{{- "<additional_tools>\n" -}}
{%- endif -%}
{{- referenced_tool_to_json(item.name) -}}
{%- if loop.last or not is_tool_reference(loop.nextitem) -%}
{{- "\n</additional_tools>" -}}
{%- endif -%}
{%- elif item is mapping and item.result is defined and item.result is mapping and item.result.tools is defined -%}
{{- "<additional_tools>\n" -}}
{%- set _tl = item.result.tools -%}
{%- if _tl is string -%}
{{- _tl -}}
{%- elif _tl is mapping -%}
{{- tool_to_json(_tl) -}}
{%- else -%}
{%- for t in _tl -%}
{%- if not loop.first -%}{{- "\n" -}}{%- endif -%}
{{- tool_to_json(t) -}}
{%- endfor -%}
{%- endif -%}
{{- "\n</additional_tools>" -}}
{%- else -%}
{{- parse_content(item) -}}
{%- endif -%}
{%- endfor -%}
{%- elif content is not none -%}
{{- content | tojson -}}
{%- endif -%}
{{- "\n</tool_response>" -}}
{%- endif -%}
{%- endmacro -%}
{%- set ns = namespace(system_prompt="", last_user_index=-1) -%}
{%- if messages and messages[0]["role"] == "system" -%}
{%- if messages[0].get("content") -%}
{%- set ns.system_prompt = parse_content(messages[0]["content"]) -%}
{%- endif -%}
{%- set messages = messages[1:] -%}
{%- endif -%}
{%- if tools -%}
{%- set tools_ns = namespace(lines=[]) -%}
{%- for tool in tools -%}
{%- set fn = tool.function if tool.function is defined else tool -%}
{%- set _dyn = (fn.defer_loading is defined and fn.defer_loading) or (tool.defer_loading is defined and tool.defer_loading) -%}
{%- if not _dyn -%}
{%- set tools_ns.lines = tools_ns.lines + [tool_to_json(fn)] -%}
{%- endif -%}
{%- endfor -%}
{%- if tools_ns.lines -%}
{%- set ns.system_prompt = ns.system_prompt + ("\n\n" if ns.system_prompt else "") + "<tools>\n" + (tools_ns.lines | join("\n")) + "\n</tools>" -%}
{%- endif -%}
{%- endif -%}
{%- if ns.system_prompt -%}
{{- "<|im_start|>system\n" + ns.system_prompt + "<|im_end|>\n" -}}
{%- endif -%}
{%- for message in messages -%}
{%- if message["role"] == "user" -%}
{%- set ns.last_user_index = loop.index0 -%}
{%- endif -%}
{%- endfor -%}
{%- set carry = namespace(pending=[]) -%}
{%- for message in messages -%}
{%- set _next = messages[loop.index0 + 1] if not loop.last else none -%}
{%- set _merge = message.role == "tool" and not message.get("additional_tools")
and _next is not none and _next.role == "tool" and not _next.get("additional_tools")
and message.get("tool_call_id") and _next.get("tool_call_id") == message.get("tool_call_id") -%}
{%- set _items = [] -%}
{%- if message.role == "tool" and not message.get("additional_tools") -%}
{%- set _c = message.get("content") -%}
{%- if _c is string -%}
{%- set _items = [{"type": "text", "text": _c}] if _c else [] -%}
{%- elif _c is mapping -%}
{%- set _items = [_c] -%}
{%- elif _c is iterable -%}
{%- set _items = _c | list -%}
{%- elif _c is not none -%}
{%- set _items = [_c] -%}
{%- endif -%}
{%- endif -%}
{%- if _merge -%}
{%- set carry.pending = carry.pending + _items -%}
{%- else -%}
{{- "<|im_start|>" + message.role + "\n" -}}
{%- if message.role == "assistant" -%}
{%- generation -%}
{%- set thinking = message.thinking if message.thinking is defined else message.reasoning if message.reasoning is defined else message.reasoning_content -%}
{%- set keep_thinking = preserve_thinking or loop.index0 > ns.last_user_index -%}
{%- if thinking is defined and keep_thinking -%}
{{- "<think>" + (thinking or "") + "</think>" -}}
{%- elif keep_thinking and "</think>" not in (message.get("content") if message.get("content") is string else "") -%}
{{- "<think></think>" -}}
{%- endif -%}
{%- set _cfm_tag = "CONTINUE_FINAL_MESSAGE_TAG " -%}
{%- set _has_cfm = false -%}
{%- set content = "" -%}
{%- if message.get("content") -%}
{%- set content = parse_content(message.content) -%}
{%- endif -%}
{%- if not keep_thinking -%}
{%- if "</think>" in content -%}
{%- set content = content.split("</think>")[-1] | trim -%}
{%- endif -%}
{%- set content = "<think></think>" + content -%}
{%- endif -%}
{%- if content.endswith(_cfm_tag) -%}
{%- set _has_cfm = true -%}
{%- set _trunc_len = (content | length) - (_cfm_tag | length) -%}
{%- set content = content[:_trunc_len] -%}
{%- endif -%}
{{- content -}}
{%- if message.tool_calls -%}
{{- render_tool_calls(message.tool_calls) -}}
{%- endif -%}
{%- if _has_cfm -%}
{{- _cfm_tag -}}
{%- endif -%}
{{- "<|im_end|>\n" -}}
{%- endgeneration -%}
{%- elif message.role == "tool" -%}
{%- if message.get("additional_tools") -%}
{%- set _at = message["additional_tools"] -%}
{%- if _at is mapping or (_at is sequence and _at is not string) -%}
{%- set _src = _at -%}
{%- else -%}
{%- set _src = message.get("content") -%}
{%- endif -%}
{%- if _src is string and "<tool_response>" in _src -%}
{{- _src -}}
{%- elif _src is string and "<additional_tools>" in _src -%}
{{- "<tool_response>\n" + _src + "\n</tool_response>" -}}
{%- elif _src -%}
{{- "<tool_response>\n<additional_tools>\n" -}}
{%- if _src is string -%}
{{- _src -}}
{%- elif _src is mapping -%}
{{- tool_to_json(_src) -}}
{%- else -%}
{%- for t in _src -%}
{%- if not loop.first -%}{{- "\n" -}}{%- endif -%}
{{- tool_to_json(t) -}}
{%- endfor -%}
{%- endif -%}
{{- "\n</additional_tools>\n</tool_response>" -}}
{%- endif -%}
{%- elif carry.pending -%}
{{- render_tool_response(carry.pending + _items) -}}
{%- set carry.pending = [] -%}
{%- else -%}
{{- render_tool_response("" if message.get("content") is none else message.get("content")) -}}
{%- endif -%}
{{- "<|im_end|>\n" -}}
{%- else %}
{%- if message.get("content") -%}
{{- parse_content(message["content"]) -}}
{%- endif -%}
{{- "<|im_end|>\n" -}}
{%- endif %}
{%- endif -%}
{%- endfor -%}
{%- if add_generation_prompt -%}
{{- "<|im_start|>assistant\n" -}}
{%- if enable_thinking -%}
{{- "<think>" -}}
{%- else -%}
{{- "<think></think>" -}}
{%- endif -%}
{%- endif -%}
+13 -4
View File
@@ -36,6 +36,7 @@ import sys
import tempfile
import time
import xml.etree.ElementTree as ET
import zipfile
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable
@@ -406,11 +407,19 @@ def fetch_logs_and_parse_tests(
with tempfile.TemporaryDirectory() as tmpdir:
for lf in log_files:
zip_path = os.path.join(tmpdir, "log.zip")
dl_path = os.path.join(tmpdir, "log.bin")
log.info("Downloading log file: %s", lf.filename)
qdc_api.download_job_log_files(client, lf.filename, zip_path)
try:
shutil.unpack_archive(zip_path, tmpdir, "zip")
qdc_api.download_job_log_files_v2(client, lf.filename, dl_path)
except Exception as e:
log.warning("Could not download %s: %s", lf.filename, e)
continue
# v2 returns some files raw instead of zipped
if not zipfile.is_zipfile(dl_path):
os.replace(dl_path, os.path.join(tmpdir, os.path.basename(lf.filename)))
continue
try:
shutil.unpack_archive(dl_path, tmpdir, "zip")
except Exception as e:
log.warning("Could not unpack %s as zip: %s", lf.filename, e)
@@ -617,7 +626,7 @@ def main() -> int:
args.pkg_dir, Path(tmpdir), args.test, args.model_url
)
log.info("Uploading artifact (%d MB) ...", zip_path.stat().st_size // 1_000_000)
artifact_id = qdc_api.upload_file(client, str(zip_path), ArtifactType.TESTSCRIPT)
artifact_id = qdc_api.upload_file_v2(client, str(zip_path), ArtifactType.TESTSCRIPT)
if artifact_id is None:
log.error("Artifact upload failed")
+4
View File
@@ -135,6 +135,7 @@ static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = {
{ LLM_ARCH_OPENAI_MOE, "gpt-oss" },
{ LLM_ARCH_LFM2, "lfm2" },
{ LLM_ARCH_LFM2MOE, "lfm2moe" },
{ LLM_ARCH_LFM3, "lfm3" },
{ LLM_ARCH_DREAM, "dream" },
{ LLM_ARCH_SMALLTHINKER, "smallthinker" },
{ LLM_ARCH_LLADA, "llada" },
@@ -1251,6 +1252,7 @@ bool llm_arch_is_hybrid(const llm_arch & arch) {
case LLM_ARCH_GRANITE_HYBRID:
case LLM_ARCH_LFM2:
case LLM_ARCH_LFM2MOE:
case LLM_ARCH_LFM3:
case LLM_ARCH_NEMOTRON_H:
case LLM_ARCH_NEMOTRON_H_MOE:
case LLM_ARCH_QWEN3NEXT:
@@ -1295,6 +1297,7 @@ bool llm_arch_supports_rs_rollback(const llm_arch & arch) {
case LLM_ARCH_NEMOTRON_H_MOE:
case LLM_ARCH_LFM2:
case LLM_ARCH_LFM2MOE:
case LLM_ARCH_LFM3:
case LLM_ARCH_BAILINGMOE3:
return true;
default:
@@ -1341,6 +1344,7 @@ bool llm_arch_supports_sm_tensor(const llm_arch & arch) {
case LLM_ARCH_NEMOTRON_H:
case LLM_ARCH_NEMOTRON_H_MOE:
case LLM_ARCH_GRANITE_HYBRID:
case LLM_ARCH_LFM3:
case LLM_ARCH_MINIMAX_01:
case LLM_ARCH_MINIMAX_M2:
case LLM_ARCH_MINIMAX_M3:
+1
View File
@@ -139,6 +139,7 @@ enum llm_arch {
LLM_ARCH_OPENAI_MOE,
LLM_ARCH_LFM2,
LLM_ARCH_LFM2MOE,
LLM_ARCH_LFM3,
LLM_ARCH_DREAM,
LLM_ARCH_SMALLTHINKER,
LLM_ARCH_LLADA,
+17 -16
View File
@@ -634,7 +634,10 @@ bool llm_graph_input_attn_kv_msa::can_reuse(const llm_graph_params & params) {
void llm_graph_input_attn_k_dsa::set_input(const llama_ubatch * ubatch) {
mctx->get_mla()->set_input_k_idxs(self_k_idxs_mla, ubatch);
mctx->get_mla()->set_input_kq_mask(self_kq_mask_mla, ubatch, cparams.causal_attn);
// leave unallocated when the top-k layers use the indexer mask
if (self_kq_mask_mla->buffer) {
mctx->get_mla()->set_input_kq_mask(self_kq_mask_mla, ubatch, cparams.causal_attn);
}
mctx->get_lid()->set_input_k_idxs(self_k_idxs_lid, ubatch);
@@ -1218,7 +1221,10 @@ bool llm_graph_input_mem_hybrid::can_reuse(const llm_graph_params & params) {
void llm_graph_input_mem_hybrid_k::set_input(const llama_ubatch * ubatch) {
mctx->get_attn()->set_input_k_idxs(inp_attn->self_k_idxs, ubatch);
mctx->get_attn()->set_input_kq_mask(inp_attn->self_kq_mask, ubatch, cparams.causal_attn);
// leave the mask unallocated if all attention layers have their own (e.g. glm5-next DSA)
if (inp_attn->self_kq_mask->buffer) {
mctx->get_attn()->set_input_kq_mask(inp_attn->self_kq_mask, ubatch, cparams.causal_attn);
}
const int64_t n_rs = mctx->get_recr()->get_n_rs();
@@ -3392,20 +3398,10 @@ ggml_tensor * llm_graph_context::build_attn(
ggml_tensor * llm_graph_context::build_fused_top_k_mask(
ggml_tensor * kq_mask,
ggml_tensor * top_k,
ggml_tensor * score,
ggml_tensor * blocks,
ggml_tensor * extra,
ggml_tensor * mask,
int il) const {
const int64_t m1 = kq_mask->ne[1];
const int64_t m2 = kq_mask->ne[2];
const int64_t m3 = kq_mask->ne[3];
top_k = ggml_reshape_4d(ctx0, top_k, top_k->ne[0], kq_mask->ne[1], kq_mask->ne[2], kq_mask->ne[3]);
auto rows = [&](ggml_tensor * t) {
return t ? ggml_reshape_4d(ctx0, t, t->ne[0], m1, m2, m3) : nullptr;
};
ggml_tensor * cur = ggml_top_k_mask(ctx0, mask ? mask : kq_mask, rows(top_k), rows(score), blocks, rows(extra));
ggml_tensor * cur = ggml_top_k_mask(ctx0, kq_mask, top_k, nullptr, nullptr, nullptr);
res->add_fused_node({LLM_FUSED_OP_TOP_K_MASK, cur, il});
return cur;
@@ -3441,11 +3437,16 @@ ggml_tensor * llm_graph_context::build_attn(
ggml_build_forward_expand(gf, mctx_cur->cpy_k(ctx0, k_cur, k_idxs, il));
}
const auto & kq_mask = inp->get_kq_mask_mla();
// top_k indexes the cells, and the indexer mask of the same type is built the same way
// let's use it, so when no other layer reads the MLA mask is not allocated
ggml_tensor * kq_mask = inp->get_kq_mask_mla();
if (inp->get_kq_mask_lid()->type == kq_mask->type) {
kq_mask = inp->get_kq_mask_lid();
}
ggml_tensor * kq_mask_top_k = nullptr;
if (cparams.fused_top_k_mask) {
kq_mask_top_k = build_fused_top_k_mask(kq_mask, top_k, nullptr, nullptr, nullptr, nullptr, il);
kq_mask_top_k = build_fused_top_k_mask(kq_mask, top_k, il);
} else {
// prepare new kq mask - starts filled with -INFINITY
ggml_tensor * kq_mask_all = ggml_fill(ctx0, kq_mask, -INFINITY);
+1 -6
View File
@@ -1336,15 +1336,10 @@ struct llm_graph_context {
float kq_scale,
int il) const;
// ggml_top_k_mask with top_k, score and extra viewed in the row layout of kq_mask, see ggml.h
// mask replaces kq_mask as the source of the kept values, it broadcasts to the rows
// ggml_top_k_mask with top_k viewed in the row layout of kq_mask, see ggml.h
ggml_tensor * build_fused_top_k_mask(
ggml_tensor * kq_mask, // [n_kv, n_tokens/n_stream, 1, n_stream]
ggml_tensor * top_k, // [n_top, n_tokens]
ggml_tensor * score, // [n_pick, n_tokens], can be null
ggml_tensor * blocks, // [n_blk, n_pick], can be null
ggml_tensor * extra, // [n_extra, n_tokens], can be null
ggml_tensor * mask, // [n_kv], kq_mask if null
int il) const;
llm_graph_input_attn_kv_iswa * build_attn_inp_kv_iswa() const;
+3
View File
@@ -314,6 +314,8 @@ static llama_model * llama_model_mapping(llm_arch arch, const llama_model_params
return new llama_model_lfm2(params);
case LLM_ARCH_LFM2MOE:
return new llama_model_lfm2moe(params);
case LLM_ARCH_LFM3:
return new llama_model_lfm3(params);
case LLM_ARCH_SMALLTHINKER:
return new llama_model_smallthinker(params);
case LLM_ARCH_GROVEMOE:
@@ -3637,6 +3639,7 @@ llama_rope_type llama_model_rope_type(const llama_model * model) {
case LLM_ARCH_HY_V3:
case LLM_ARCH_LFM2:
case LLM_ARCH_LFM2MOE:
case LLM_ARCH_LFM3:
case LLM_ARCH_SMALLTHINKER:
case LLM_ARCH_SEED_OSS:
case LLM_ARCH_GROVEMOE:
+8
View File
@@ -342,6 +342,14 @@ static bool tensor_allows_quantization(const llama_model_quantize_params * param
quantize &= name.find("ssm_conv1d") == std::string::npos;
quantize &= name.find("shortconv.conv.weight") == std::string::npos;
// do not quantize LFM3-VL's depthwise conv kernels, CONV_2D_DW takes F32 kernels
quantize &= name.find("conv2d_spatial.weight") == std::string::npos;
// do not quantize LFM3-Audio's encoder convs and codec, the codec output does not survive quantization
quantize &= name.rfind("a.conv1d.", 0) != 0;
quantize &= name.find("conv_dw") == std::string::npos;
quantize &= name.rfind("a.gen.wav.", 0) != 0;
// do not quantize MiniMax's indexer projection weights, they are tiny
quantize &= name.find("indexer.k_proj.weight") == std::string::npos;
quantize &= name.find("indexer.q_proj.weight") == std::string::npos;
+1 -1
View File
@@ -686,7 +686,7 @@ ggml_tensor * llama_model_deepseek4::graph::build_top_k_mask(
GGML_ASSERT(top_k);
if (cparams.fused_top_k_mask) {
ggml_tensor * kq_mask_top_k = build_fused_top_k_mask(kq_mask, top_k, nullptr, nullptr, nullptr, nullptr, il);
ggml_tensor * kq_mask_top_k = build_fused_top_k_mask(kq_mask, top_k, il);
cb(kq_mask_top_k, name, il);
return kq_mask_top_k;
+21 -18
View File
@@ -887,7 +887,7 @@ ggml_tensor * llama_model_glm5_next::graph::build_kda_layer(
// Scores pools of kpool consecutive tokens, expands the selected pools and the incomplete tail into an additive mask
ggml_tensor * llama_model_glm5_next::graph::build_kpool_select(
ggml_tensor * cur, ggml_tensor * qr, ggml_tensor * kq_mask, const llama_layer & layer,
ggml_tensor * cur, ggml_tensor * qr, const llama_layer & layer,
const llama_memory_hybrid_idx_context * mctx_hyb, llm_graph_input_kpool * inp_kpool, int il) {
const auto * mctx_lid = mctx_hyb->get_idx();
@@ -919,6 +919,9 @@ ggml_tensor * llama_model_glm5_next::graph::build_kpool_select(
auto kpool_cache = mctx_hyb->get_kpool_access(ctx0, il, n_embd_indexer);
const int64_t n_kv = mctx_lid->get_n_kv();
// flash attention requires an f16 mask
const ggml_type type_mask = cparams.flash_attn ? GGML_TYPE_F16 : GGML_TYPE_F32;
// Pool the entries completed by this ubatch. The last one is a dummy when the ubatch completes none.
ggml_tensor * rows = kpool_cache.gather_key_gate(inp_kpool->new_pool_idxs);
rows = ggml_reshape_3d(ctx0, rows, 2*n_embd_indexer, kpool, n_new);
@@ -971,12 +974,15 @@ ggml_tensor * llama_model_glm5_next::graph::build_kpool_select(
const int64_t n_top_pool = std::min<int64_t>(n_pool, hparams.indexer_top_k / kpool);
ggml_tensor * top_k = ggml_top_k(ctx0, score, n_top_pool); // [n_top_pool, n_tokens], UNORDERED
// keep the kq_mask cells of the live picked pools and the tail, -inf elsewhere
// keep the cells of the live picked pools and the tail, -inf elsewhere
// all retained cells are in the token's sequence, so the kq_mask is not needed
if (cparams.fused_top_k_mask) {
ggml_tensor * tail = hparams.indexer_kpool_select_tail ? inp_kpool->tail_idxs : nullptr;
GGML_ASSERT(kpool*n_top_pool + (tail ? tail->ne[0] : 0) == (int64_t) inp_kpool->n_sel);
ggml_tensor * sel = build_fused_top_k_mask(kq_mask, top_k, score, inp_kpool->pool_idxs, tail, nullptr, il);
ggml_tensor * keep = ggml_fill(ctx0, ggml_new_tensor_1d(ctx0, type_mask, n_kv), 0.0f);
ggml_tensor * sel = ggml_top_k_mask(ctx0, keep, top_k, score, inp_kpool->pool_idxs, tail); // [n_kv, n_tokens]
res->add_fused_node({LLM_FUSED_OP_TOP_K_MASK, sel, il});
cb(sel, "indexer_sel", il);
return sel;
@@ -1006,12 +1012,12 @@ ggml_tensor * llama_model_glm5_next::graph::build_kpool_select(
ggml_build_forward_expand(gf, sel_idx);
ggml_tensor * mask_all = ggml_new_tensor_4d(ctx0, kq_mask->type, n_kv + n_sel, 1, 1, 1);
ggml_tensor * mask_all = ggml_new_tensor_4d(ctx0, type_mask, n_kv + n_sel, 1, 1, 1);
mask_all = ggml_fill(ctx0, mask_all, -INFINITY);
mask_all = ggml_repeat_4d(ctx0, mask_all, n_kv + n_sel, n_tokens, 1, 1);
mask_all = ggml_reshape_3d(ctx0, mask_all, 1, n_kv + n_sel, n_tokens);
ggml_tensor * zeros = ggml_new_tensor_4d(ctx0, kq_mask->type, n_sel, 1, 1, 1);
ggml_tensor * zeros = ggml_new_tensor_4d(ctx0, type_mask, n_sel, 1, 1, 1);
zeros = ggml_fill(ctx0, zeros, 0.0f);
zeros = ggml_repeat_4d(ctx0, zeros, n_sel, n_tokens, 1, 1);
zeros = ggml_reshape_3d(ctx0, zeros, 1, n_sel, n_tokens);
@@ -1027,12 +1033,8 @@ ggml_tensor * llama_model_glm5_next::graph::build_kpool_select(
ggml_tensor * sel = ggml_set_rows(ctx0, mask_all, zeros, ggml_reshape_3d(ctx0, sel_idx, n_sel, n_tokens, 1));
// Fold causal visibility before shared-indexer reuse.
GGML_ASSERT(kq_mask->ne[0] == n_kv && kq_mask->ne[1]*kq_mask->ne[2]*kq_mask->ne[3] == n_tokens);
const size_t row = sel->nb[2];
sel = ggml_view_4d(ctx0, sel, n_kv, kq_mask->ne[1], kq_mask->ne[2], kq_mask->ne[3],
row, row*kq_mask->ne[1], row*kq_mask->ne[1]*kq_mask->ne[2], 0);
sel = ggml_add(ctx0, sel, kq_mask);
sel = ggml_view_2d(ctx0, sel, n_kv, n_tokens, sel->nb[2], 0);
sel = ggml_cont(ctx0, sel);
cb(sel, "indexer_sel", il);
return sel;
@@ -1073,11 +1075,17 @@ llama_model_glm5_next::graph::build_dsa_layer(
q_absorbed = ggml_permute(ctx0, q_absorbed, 0, 2, 1, 3);
cb(q_absorbed, "q_absorbed", il);
ggml_tensor * kq_mask = inp_attn->get_kq_mask();
ggml_tensor * k = mctx_mla->get_k(ctx0, il);
ggml_tensor * v = ggml_view_4d(ctx0, k, kv_lora_rank, k->ne[1], k->ne[2], k->ne[3], k->nb[1], k->nb[2], k->nb[3], 0);
ggml_tensor * sel = prev_sel;
if (il >= (int) hparams.n_layer() || hparams.is_indexer_full(il)) { // the NextN block always has a full indexer
sel = build_kpool_select(cur, qr, kq_mask, layer, mctx_hyb, inp_kpool, il);
sel = build_kpool_select(cur, qr, layer, mctx_hyb, inp_kpool, il);
// the selection replaces the kq_mask: split it into the streams of the K view
const int64_t n_stream = k->ne[3];
GGML_ASSERT(sel->ne[0] == k->ne[2] && sel->ne[1] % n_stream == 0);
sel = ggml_reshape_4d(ctx0, sel, sel->ne[0], sel->ne[1]/n_stream, 1, n_stream);
} else {
GGML_ASSERT(sel != nullptr && "shared indexer layer must follow a full indexer layer");
}
@@ -1086,13 +1094,8 @@ llama_model_glm5_next::graph::build_dsa_layer(
ggml_build_forward_expand(gf, kv_cmpr);
ggml_build_forward_expand(gf, mctx_mla->cpy_k(ctx0, kv_cmpr, inp_attn->get_k_idxs(), il));
// The scatter selection already includes the causal mask, and it comes in the mask layout
GGML_ASSERT(ggml_are_same_shape(sel, kq_mask));
cb(sel, "kq_mask_dsa", il);
ggml_tensor * k = mctx_mla->get_k(ctx0, il);
ggml_tensor * v = ggml_view_4d(ctx0, k, kv_lora_rank, k->ne[1], k->ne[2], k->ne[3], k->nb[1], k->nb[2], k->nb[3], 0);
ggml_tensor * out = build_attn_mha(q_absorbed, k, v, nullptr, sel, nullptr, layer.wv_b, inp_kpool->n_sel, kq_scale, il);
cb(out, "kqv_out", il);
+197
View File
@@ -0,0 +1,197 @@
#include "models.h"
#include "../llama-memory-hybrid-iswa.h"
void llama_model_lfm3::load_arch_hparams(llama_model_loader & ml) {
ml.get_key(LLM_KV_SHORTCONV_L_CACHE, hparams.n_shortconv_l_cache);
ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps);
for (uint32_t il = 0; il < hparams.n_layer(); ++il) {
hparams.is_recr_impl[il] = hparams.n_head_kv(il) == 0;
}
// LFM3 always mixes full and sliding window attention layers
ml.get_arr(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, hparams.is_swa_impl);
if (!hparams.is_swa_any()) {
throw std::runtime_error("LFM3 requires sliding window attention layers");
}
hparams.swa_type = LLAMA_SWA_TYPE_STANDARD;
ml.get_key(LLM_KV_ATTENTION_SLIDING_WINDOW, hparams.n_swa);
ml.get_key(LLM_KV_ROPE_FREQ_BASE_SWA, hparams.rope_freq_base_train_swa);
switch (hparams.n_ff()) {
case 7680: type = LLM_TYPE_1_7B; break;
default: type = LLM_TYPE_UNKNOWN;
}
}
void llama_model_lfm3::load_arch_tensors(llama_model_loader &) {
LLAMA_LOAD_LOCALS;
tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0);
output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM, "weight"), {n_embd}, 0);
output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), {n_embd, n_vocab}, TENSOR_NOT_REQUIRED);
if (output == NULL) {
output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, TENSOR_DUPLICATED);
}
int rope_freqs_flag = 0;
for (int i = 0; i < n_layer; ++i) {
auto & layer = layers[i];
layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0);
layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, 0);
// absent when use_sandwich_norm is false
layer.attn_post_norm = create_tensor(tn(LLM_TENSOR_ATTN_POST_NORM, "weight", i), {n_embd}, TENSOR_NOT_REQUIRED);
layer.ffn_post_norm = create_tensor(tn(LLM_TENSOR_FFN_POST_NORM, "weight", i), {n_embd}, TENSOR_NOT_REQUIRED);
if ((layer.attn_post_norm == nullptr) != (layer.ffn_post_norm == nullptr)) {
throw std::runtime_error("LFM3 requires both post norms or neither");
}
layer.ffn_gate = create_tensor(tn(LLM_TENSOR_FFN_GATE, "weight", i), {n_embd, n_ff}, 0);
layer.ffn_down = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "weight", i), { n_ff, n_embd}, 0);
layer.ffn_up = create_tensor(tn(LLM_TENSOR_FFN_UP, "weight", i), {n_embd, n_ff}, 0);
if (!hparams.is_recr(i)) {
// note: n_embd_head_k * n_head may differ from n_embd
const int64_t n_embd_q = n_embd_head_k * n_head;
const int64_t n_embd_k = hparams.n_embd_k_gqa(i);
const int64_t n_embd_v = hparams.n_embd_v_gqa(i);
if (n_embd_k != n_embd_v) {
throw std::runtime_error("LFM3 requires equal key and value sizes");
}
layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", i), {n_embd_head_k}, 0);
layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", i), {n_embd_head_k}, 0);
create_tensor_qkv(layer, i, n_embd, n_embd_q, n_embd_k, n_embd_v, 0);
layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd_q, n_embd}, 0);
layer.wqkv_gate = create_tensor(tn(LLM_TENSOR_ATTN_GATE, "weight", i), {n_embd, n_head}, 0);
if (!hparams.is_swa(i)) {
layer.rope_freqs = create_tensor(tn(LLM_TENSOR_ROPE_FREQS, "weight", i), {n_embd_head_k / 2}, rope_freqs_flag);
rope_freqs_flag = TENSOR_DUPLICATED;
}
} else {
layer.shortconv.conv = create_tensor(tn(LLM_TENSOR_SHORTCONV_CONV, "weight", i), {hparams.n_shortconv_l_cache, n_embd}, 0);
layer.shortconv.in_proj = create_tensor(tn(LLM_TENSOR_SHORTCONV_INPROJ, "weight", i), {n_embd, 3 * n_embd}, 0);
layer.shortconv.out_proj = create_tensor(tn(LLM_TENSOR_SHORTCONV_OUTPROJ, "weight", i), {n_embd, n_embd}, 0);
}
}
}
std::unique_ptr<llm_graph_context> llama_model_lfm3::build_arch_graph(const llm_graph_params & params) const {
return std::make_unique<graph>(*this, params);
}
llama_model_lfm3::graph::graph(const llama_model & model, const llm_graph_params & params) :
llm_build_lfm_base(params) {
auto build_attn_block = [&model, this](ggml_tensor * cur_in,
ggml_tensor * inp_pos,
llm_graph_input_attn_kv_iswa * inp_attn,
int il) -> ggml_tensor * {
const int64_t n_embd_head = hparams.n_embd_head_k(il);
const int64_t n_head_il = hparams.n_head(il);
const int64_t n_head_kv = hparams.n_head_kv(il);
const int n_rot_l = hparams.n_rot(il);
const float freq_base_l = model.get_rope_freq_base (cparams, il);
const float freq_scale_l = model.get_rope_freq_scale(cparams, il);
ggml_tensor * rope_factors = hparams.is_swa(il) ? nullptr : model.layers[il].rope_freqs;
auto [q, k, v] = build_qkv(model.layers[il], cur_in, n_embd_head, n_head_il, n_head_kv, il);
q = build_norm(q, model.layers[il].attn_q_norm, NULL, LLM_NORM_RMS, il);
cb(q, "model.layers.{}.self_attn.q_layernorm", il);
k = build_norm(k, model.layers[il].attn_k_norm, NULL, LLM_NORM_RMS, il);
cb(k, "model.layers.{}.self_attn.k_layernorm", il);
q = ggml_rope_ext(ctx0, q, inp_pos, rope_factors, n_rot_l, rope_type, n_ctx_orig,
freq_base_l, freq_scale_l, ext_factor, attn_factor, beta_fast, beta_slow);
k = ggml_rope_ext(ctx0, k, inp_pos, rope_factors, n_rot_l, rope_type, n_ctx_orig,
freq_base_l, freq_scale_l, ext_factor, attn_factor, beta_fast, beta_slow);
ggml_tensor * attn_out = build_attn(inp_attn,
NULL, NULL, NULL,
q, k, v, nullptr, nullptr, nullptr, 1.0f / sqrtf(float(n_embd_head)), il);
cb(attn_out, "attn_out", il);
ggml_tensor * gate = build_lora_mm(model.layers[il].wqkv_gate, cur_in);
gate = ggml_sigmoid(ctx0, gate);
cb(gate, "model.layers.{}.self_attn.gate", il);
ggml_tensor * attn_3d = ggml_reshape_3d(ctx0, attn_out, n_embd_head, n_head_il, n_tokens);
ggml_tensor * gate_3d = ggml_reshape_3d(ctx0, gate, 1, n_head_il, n_tokens);
attn_3d = ggml_mul(ctx0, attn_3d, gate_3d);
attn_out = ggml_reshape_2d(ctx0, attn_3d, n_embd_head * n_head_il, n_tokens);
ggml_tensor * out = build_lora_mm(model.layers[il].wo, attn_out, model.layers[il].wo_s);
cb(out, "model.layers.{}.self_attn.out_proj", il);
return out;
};
ggml_tensor * cur = build_inp_embd(model.tok_embd);
cb(cur, "model.embed_tokens", -1);
ggml_build_forward_expand(gf, cur);
auto * inp_hybrid = build_inp_mem_hybrid_iswa();
ggml_tensor * inp_pos = build_inp_pos();
ggml_tensor * inp_out_ids = build_inp_out_ids();
for (int il = 0; il < n_layer; ++il) {
auto * prev_cur = cur;
cur = build_norm(cur, model.layers[il].attn_norm, NULL, LLM_NORM_RMS, il);
cb(cur, "model.layers.{}.operator_norm", il);
if (hparams.is_recr(il)) {
const auto * mctx_recr = static_cast<const llama_memory_hybrid_iswa_context *>(mctx)->get_recr();
cur = build_lfm_shortconv(model, mctx_recr, inp_hybrid->get_recr(), cur, il);
} else {
cur = build_attn_block(cur, inp_pos, inp_hybrid->get_attn(), il);
}
if (model.layers[il].attn_post_norm) {
cur = build_norm(cur, model.layers[il].attn_post_norm, NULL, LLM_NORM_RMS, il);
cb(cur, "model.layers.{}.post_operator_norm", il);
}
if (il == n_layer - 1 && inp_out_ids) {
cur = ggml_get_rows(ctx0, cur, inp_out_ids);
prev_cur = ggml_get_rows(ctx0, prev_cur, inp_out_ids);
}
cur = ggml_add(ctx0, prev_cur, cur);
auto * ffn_norm_out = build_norm(cur, model.layers[il].ffn_norm, NULL, LLM_NORM_RMS, il);
cb(ffn_norm_out, "model.layers.{}.ffn_norm", il);
ggml_tensor * ffn_out = build_lfm_dense_ffn(model, ffn_norm_out, il);
if (model.layers[il].ffn_post_norm) {
ffn_out = build_norm(ffn_out, model.layers[il].ffn_post_norm, NULL, LLM_NORM_RMS, il);
cb(ffn_out, "model.layers.{}.post_ffn_norm", il);
}
cur = ggml_add(ctx0, cur, ffn_out);
cur = build_cvec(cur, il);
cb(cur, "l_out", il);
}
cur = build_norm(cur, model.output_norm, NULL, LLM_NORM_RMS, -1);
cb(cur, "result_norm", -1);
res->t_embd = cur;
cur = build_lora_mm(model.output, cur, model.output_s);
cb(cur, "result_output", -1);
res->t_logits = cur;
}
+15 -3
View File
@@ -2433,6 +2433,19 @@ struct llama_model_lfm2moe : public llama_model_base {
};
struct llama_model_lfm3 : public llama_model_base {
llama_model_lfm3(const struct llama_model_params & params) : llama_model_base(params) {}
void load_arch_hparams(llama_model_loader & ml) override;
void load_arch_tensors(llama_model_loader & ml) override;
struct graph : public llm_build_lfm_base {
graph(const llama_model & model, const llm_graph_params & params);
};
std::unique_ptr<llm_graph_context> build_arch_graph(const llm_graph_params & params) const override;
};
struct llama_model_smallthinker : public llama_model_base {
llama_model_smallthinker(const struct llama_model_params & params) : llama_model_base(params) {}
void load_arch_hparams(llama_model_loader & ml) override;
@@ -2792,13 +2805,12 @@ struct llama_model_qwen4exp : public llama_model_base {
// the QSA layers share one set of k-pool inputs, see llama_memory_hybrid_idx
llm_graph_input_kpool * build_inp_kpool(const llama_memory_hybrid_idx_context * mctx_hyb);
// QSA: the additive mask [n_kv, n_tokens] of the top blocks and the tail, kq_mask included
// QSA: the additive mask [n_kv, n_tokens] of the top blocks and the tail
ggml_tensor * build_qsa_sel(
const llama_memory_hybrid_idx_context * mctx_hyb,
llm_graph_input_kpool * inp_kpool,
ggml_tensor * cur,
ggml_tensor * inp_pos,
ggml_tensor * kq_mask,
int * sections,
int il);
@@ -3026,7 +3038,7 @@ struct llama_model_glm5_next : public llama_model_base {
int64_t d_conv, int64_t head_dim, int64_t n_head_kda,
int64_t d_inner, int64_t n_seq_tokens, int64_t n_seqs, int il);
ggml_tensor * build_kpool_select(ggml_tensor * cur, ggml_tensor * qr, ggml_tensor * kq_mask, const llama_layer & layer,
ggml_tensor * build_kpool_select(ggml_tensor * cur, ggml_tensor * qr, const llama_layer & layer,
const llama_memory_hybrid_idx_context * mctx_hyb, llm_graph_input_kpool * inp_kpool, int il);
// attention output (cur) and the selection to carry to the next shared layer (sel)
+16 -16
View File
@@ -818,7 +818,6 @@ ggml_tensor * llama_model_qwen4exp::graph::build_qsa_sel(
llm_graph_input_kpool * inp_kpool,
ggml_tensor * cur,
ggml_tensor * inp_pos,
ggml_tensor * kq_mask,
int * sections,
int il) {
const llama_kv_cache_context * mctx_idx = mctx_hyb->get_idx();
@@ -888,13 +887,17 @@ ggml_tensor * llama_model_qwen4exp::graph::build_qsa_sel(
ggml_tensor * top_k = ggml_top_k(ctx0, score, n_top_pool); // [n_top_pool, n_tokens], unordered
cb(top_k, "indexer_top_k", il);
// flash attention requires an f16 mask
const ggml_type type_mask = cparams.flash_attn ? GGML_TYPE_F16 : GGML_TYPE_F32;
// keep the cells of the live picked pools and the tail, -inf elsewhere
// all retained cells are in the token's sequence, so the kq_mask is not needed
if (cparams.fused_top_k_mask) {
GGML_ASSERT(kpool*n_top_pool + inp_kpool->tail_idxs->ne[0] == inp_kpool->n_sel);
ggml_tensor * keep = ggml_fill(ctx0, ggml_new_tensor_1d(ctx0, kq_mask->type, inp_kpool->n_kv), 0.0f);
ggml_tensor * sel = build_fused_top_k_mask(kq_mask, top_k, score, inp_kpool->pool_idxs, inp_kpool->tail_idxs, keep, il);
ggml_tensor * keep = ggml_fill(ctx0, ggml_new_tensor_1d(ctx0, type_mask, inp_kpool->n_kv), 0.0f);
ggml_tensor * sel = ggml_top_k_mask(ctx0, keep, top_k, score, inp_kpool->pool_idxs, inp_kpool->tail_idxs); // [n_kv, n_tokens]
res->add_fused_node({LLM_FUSED_OP_TOP_K_MASK, sel, il});
cb(sel, "indexer_sel", il);
return sel;
@@ -916,12 +919,12 @@ ggml_tensor * llama_model_qwen4exp::graph::build_qsa_sel(
// seeding from sel_idx ties the scatter storage lifetime to this layer
const int64_t n_kv = inp_kpool->n_kv;
ggml_tensor * mask_all = ggml_new_tensor_4d(ctx0, kq_mask->type, n_kv + n_sel, 1, 1, 1);
ggml_tensor * mask_all = ggml_new_tensor_4d(ctx0, type_mask, n_kv + n_sel, 1, 1, 1);
mask_all = ggml_fill(ctx0, mask_all, -INFINITY);
mask_all = ggml_repeat_4d(ctx0, mask_all, n_kv + n_sel, n_tokens, 1, 1);
mask_all = ggml_reshape_3d(ctx0, mask_all, 1, n_kv + n_sel, n_tokens);
ggml_tensor * zeros = ggml_new_tensor_4d(ctx0, kq_mask->type, n_sel, 1, 1, 1);
ggml_tensor * zeros = ggml_new_tensor_4d(ctx0, type_mask, n_sel, 1, 1, 1);
zeros = ggml_fill(ctx0, zeros, 0.0f);
zeros = ggml_repeat_4d(ctx0, zeros, n_sel, n_tokens, 1, 1);
zeros = ggml_reshape_3d(ctx0, zeros, 1, n_sel, n_tokens);
@@ -946,10 +949,7 @@ ggml_tensor * llama_model_qwen4exp::graph::build_qsa_sel(
ggml_tensor * sel = ggml_set_rows(ctx0, mask_all, zeros, ggml_reshape_3d(ctx0, sel_idx, n_sel, n_tokens, 1));
GGML_ASSERT(kq_mask->ne[0] == n_kv && kq_mask->ne[1]*kq_mask->ne[2]*kq_mask->ne[3] == n_tokens);
const size_t row = sel->nb[2];
sel = ggml_view_4d(ctx0, sel, n_kv, kq_mask->ne[1], kq_mask->ne[2], kq_mask->ne[3],
row, row*kq_mask->ne[1], row*kq_mask->ne[1]*kq_mask->ne[2], 0);
sel = ggml_view_2d(ctx0, sel, n_kv, n_tokens, sel->nb[2], 0);
sel = ggml_cont(ctx0, sel);
cb(sel, "indexer_sel", il);
@@ -995,16 +995,16 @@ ggml_tensor * llama_model_qwen4exp::graph::build_attn_qsa(
ggml_build_forward_expand(gf, mctx_cur->cpy_v(ctx0, v_cur, v_idxs, il));
}
// the selection replaces the kq_mask and has the same shape
// it is not allocated without dense layers reading the kq_mask
const ggml_tensor * kq_mask = inp->get_kq_mask();
GGML_ASSERT(ggml_are_same_shape(sel, kq_mask));
cb(sel, "kq_mask_qsa", il);
ggml_tensor * q = q_cur;
ggml_tensor * k = mctx_cur->get_k(ctx0, il);
ggml_tensor * v = mctx_cur->get_v(ctx0, il);
// the selection replaces the kq_mask: split it into the streams of the K view
const int64_t n_stream = k->ne[3];
GGML_ASSERT(sel->ne[0] == k->ne[2] && sel->ne[1] % n_stream == 0);
sel = ggml_reshape_4d(ctx0, sel, sel->ne[0], sel->ne[1]/n_stream, 1, n_stream);
cb(sel, "kq_mask_qsa", il);
ggml_tensor * cur = build_attn_mha(q, k, v, nullptr, sel, nullptr, nullptr, n_sel, kq_scale, il);
cb(cur, "kqv_out", il);
@@ -1030,7 +1030,7 @@ ggml_tensor * llama_model_qwen4exp::graph::build_layer_attn(
// indexer reads the same block input as q/k/v; no cache or no ratio means dense
const bool qsa = inp_kpool != nullptr && hparams.dsv4_compress_ratios[il] > 0;
ggml_tensor * sel = qsa ? build_qsa_sel(mctx_hyb, inp_kpool, cur, inp_pos, inp->get_kq_mask(), sections, il) : nullptr;
ggml_tensor * sel = qsa ? build_qsa_sel(mctx_hyb, inp_kpool, cur, inp_pos, sections, il) : nullptr;
// Qwen3Next uses a single Q projection that outputs query + gate
ggml_tensor * Qcur_full = build_lora_mm(model.layers[il].wq, cur, model.layers[il].wq_s); // [ (n_embd_head * 2) * n_head, n_tokens ]
+1
View File
@@ -155,6 +155,7 @@ laguna ,0 ,any ,RMS_NORM+MUL , 9
lfm2 ,0 ,any ,RMS_NORM+MUL , 7
lfm2moe ,1 ,any ,MUL+ADD , 2
lfm2moe ,1 ,any ,RMS_NORM+MUL , 7
lfm3 ,0 ,any ,RMS_NORM+MUL , 17
llada ,0 ,any ,RMS_NORM+MUL , 5
llada-moe ,1 ,any ,MUL+ADD , 4
llada-moe ,1 ,any ,RMS_NORM+MUL , 9
1 # test-fusion baseline for device MTL
155 lfm2 ,0 ,any ,RMS_NORM+MUL , 7
156 lfm2moe ,1 ,any ,MUL+ADD , 2
157 lfm2moe ,1 ,any ,RMS_NORM+MUL , 7
158 lfm3 ,0 ,any ,RMS_NORM+MUL , 17
159 llada ,0 ,any ,RMS_NORM+MUL , 5
160 llada-moe ,1 ,any ,MUL+ADD , 4
161 llada-moe ,1 ,any ,RMS_NORM+MUL , 9
+5
View File
@@ -11293,6 +11293,11 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() {
test_cases.emplace_back(new test_mul_mat(type_a, type_b, 16, 16, k, {2, 3}, {1, 1}, {0, 2, 1, 3}));
test_cases.emplace_back(new test_mul_mat(type_a, type_b, 16, 16, k, {2, 3}, {1, 1}, {0, 1, 3, 2}));
test_cases.emplace_back(new test_mul_mat(type_a, type_b, 16, 16, k, {2, 3}, {1, 1}, {0, 3, 2, 1}));
// test cases with permutation and a broadcast of a in dim 3 (e.g. a weight against a per-stream attention output)
test_cases.emplace_back(new test_mul_mat(type_a, type_b, 16, 1, k, {1, 1}, {1, 2}, {0, 2, 1, 3}));
test_cases.emplace_back(new test_mul_mat(type_a, type_b, 16, 8, k, {1, 1}, {1, 2}, {0, 2, 1, 3}));
test_cases.emplace_back(new test_mul_mat(type_a, type_b, 16, 8, k, {1, 1}, {4, 2}, {0, 2, 1, 3}));
}
// test cases with large ne00/ne10 to cover stream-k fixup
+84 -5
View File
@@ -986,8 +986,8 @@ const common_chat_msg message_user_parts{
/* .content = */ "",
/* .content_parts = */
{
{ "text", "Hey" },
{ "text", "there" },
{ "text", "Hey", "" },
{ "text", "there", "" },
},
/* .tool_calls = */
{ },
@@ -3302,9 +3302,9 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
common_chat_msg message_parts;
message_parts.role = "user";
message_parts.content_parts = {
{ /* .type = */ "text", /* .text = */ "First part.\n\n" },
{ /* .type = */ "media_marker", /* .text = */ "<__media__>" },
{ /* .type = */ "text", /* .text = */ "Second part.\n\n" },
{ /* .type = */ "text", /* .text = */ "First part.\n\n", /* .name = */ "" },
{ /* .type = */ "media_marker", /* .text = */ "<__media__>", /* .name = */ "" },
{ /* .type = */ "text", /* .text = */ "Second part.\n\n", /* .name = */ "" },
};
common_chat_templates_inputs inputs;
@@ -5589,6 +5589,85 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
test_lfm2_parser(tmpl, detailed_debug);
}
{
auto tst = peg_tester("models/templates/LFM3.jinja", detailed_debug);
tst.test("Hello, world!\nWhat's up?")
.enable_thinking(false)
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.expect(message_assist)
.run();
tst.test("I'm\nthinking</think>Hello, world!\nWhat's up?")
.enable_thinking(true)
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.expect(message_assist_thoughts)
.run();
tst.test(
"<tool_call>\n"
"<function=special_function>\n"
"<parameter=arg1>\n1\n</parameter>\n"
"</function>\n"
"</tool_call>")
.enable_thinking(false)
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.tools({ special_function_tool })
.expect(message_assist_call)
.run();
tst.test(
"I'm\nthinking</think>"
"<tool_call>\n"
"<function=special_function>\n"
"<parameter=arg1>\n1\n</parameter>\n"
"</function>\n"
"</tool_call>")
.enable_thinking(true)
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.tools({ special_function_tool })
.expect(message_assist_call_thoughts)
.run();
// a deferred tool is left out of <tools>, a tool_reference renders it, and its schema types the call
const auto deferred_tools = common_chat_tools_parse_oaicompat(json::parse(R"([
{"type": "function", "function": {"name": "tool_search", "description": "Search for tools",
"parameters": {"type": "object", "properties": {"query": {"type": "string"}}}}},
{"type": "function", "function": {"name": "buy", "description": "Buy an item", "defer_loading": true,
"parameters": {"type": "object", "properties": {"id": {"type": "string"}, "qty": {"type": "integer"}}}}}
])"));
{
auto tmpls = read_templates("models/templates/LFM3.jinja");
common_chat_templates_inputs inputs;
inputs.messages = common_chat_msgs_parse_oaicompat(json::parse(R"([
{"role": "user", "content": "Buy the blue one"},
{"role": "assistant", "tool_calls": [{"id": "c0", "type": "function",
"function": {"name": "tool_search", "arguments": "{\"query\": \"buy\"}"}}]},
{"role": "tool", "tool_call_id": "c0", "name": "tool_search", "content": [{"type": "tool_reference", "name": "buy"}]}
])"));
inputs.tools = deferred_tools;
inputs.add_generation_prompt = true;
const auto prompt = common_chat_templates_apply(tmpls.get(), inputs).prompt.str();
assert_contains(prompt, "<tool_response>\n<additional_tools>\n{\"name\": \"buy\"");
assert_equals(true, prompt.find("\"name\": \"buy\"") > prompt.find("</tools>"));
assert_not_contains(prompt, "defer_loading");
assert_not_contains(prompt, "tool_reference");
}
tst.test(
"<tool_call>\n"
"<function=buy>\n"
"<parameter=id>\nblue\n</parameter>\n"
"<parameter=qty>\n2\n</parameter>\n"
"</function>\n"
"</tool_call>")
.enable_thinking(false)
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.tools(deferred_tools)
.expect_tool_calls({ { "buy", R"({"id": "blue", "qty": 2})", {} } })
.run();
}
// Thinking cases only apply to LFM2.5-8B-A1B, the one LFM2 template that emits <think>
{
auto tst = peg_tester("models/templates/LFM2.5-8B-A1B.jinja", detailed_debug);
+17
View File
@@ -2550,6 +2550,23 @@ static void test_caps(testing & t) {
t.assert_true("does not support string content", !caps.supports_string_content);
t.assert_true("supports typed content", caps.supports_typed_content);
});
t.test("string and typed content, behind a truthiness check", [](testing & t) {
// an empty content array is falsy, the parts are only read when there are some (LFM3 case)
auto caps = get_caps(
"{% for message in messages %}"
"{% if message['content'] %}"
"{% if message['content'] is string %}"
"{{ message['content'] }}"
"{% else %}"
"{% for part in message['content'] %}{{ part['text'] }}{% endfor %}"
"{% endif %}"
"{% endif %}"
"{% endfor %}"
);
t.assert_true("supports string content", caps.supports_string_content);
t.assert_true("supports typed content", caps.supports_typed_content);
});
}
static void test_string_parts(testing & t) {
+63 -20
View File
@@ -8,6 +8,7 @@
#include "llama-cpp.h"
#include "../src/llama-ext.h"
#include <algorithm>
#include <cinttypes>
#include <cmath>
#include <cstddef>
@@ -54,12 +55,34 @@ struct tensor_data_params {
// fills the values of one tensor, the name carries whatever the property needs
typedef void (*tensor_filler)(std::vector<float> & dst, const char * name, const tensor_data_params & params);
// a pool of normal draws, generated once and reused for every model
static std::vector<float> g_normal_pool;
static const size_t NORMAL_POOL_SIZE = 16 * 1024 * 1024;
// fill the pool once; each tensor copies a slice of it
static void init_normal_pool(const size_t seed, const float stdev) {
g_normal_pool.resize(NORMAL_POOL_SIZE);
std::mt19937 gen(seed);
std::normal_distribution<float> dist(0.0f, stdev);
for (float & v : g_normal_pool) {
v = dist(gen);
}
}
// a normal draw keyed on a name, so that tensors which share a draw ask for the same key
static void fill_normal(std::vector<float> & dst, const char * key, const tensor_data_params & params) {
std::mt19937 gen(params.seed ^ std::hash<std::string>{}(key));
std::normal_distribution<float> dis(0.0f, params.stdev);
for (float & v : dst) {
v = dis(gen);
static void fill_normal(std::vector<float> & dst, const char * key, const tensor_data_params &) {
const size_t n = dst.size();
const size_t pool_size = g_normal_pool.size();
const size_t start = std::hash<std::string>{}(key) % pool_size;
// copy a cyclic slice of the pool, so a tensor larger than the pool still fills
size_t i = 0;
size_t pos = start;
while (i < n) {
const size_t chunk = std::min(pool_size - pos, n - i);
std::memcpy(dst.data() + i, g_normal_pool.data() + pos, chunk * sizeof(float));
i += chunk;
pos = 0;
}
}
@@ -228,6 +251,8 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
n_head = 1;
n_ff = 96;
n_layer = 22; // hparams.n_layer_kv_from_start = 20 is hardcoded
} else if (arch == LLM_ARCH_LFM3) {
n_layer = 3;
} else if (arch == LLM_ARCH_DEEPSEEK4) {
// head size 64 so that GPU flash attention kernels support the model
n_embd = 512;
@@ -313,8 +338,9 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
ms.add_kv(LLM_KV_FULL_ATTENTION_INTERVAL, uint32_t(2));
if (arch == LLM_ARCH_PLAMO2 || arch == LLM_ARCH_JAMBA || arch == LLM_ARCH_NEMOTRON_H || arch == LLM_ARCH_NEMOTRON_H_MOE ||
arch == LLM_ARCH_GRANITE_HYBRID || arch == LLM_ARCH_LFM2 || arch == LLM_ARCH_LFM2MOE || arch == LLM_ARCH_KIMI_LINEAR ||
arch == LLM_ARCH_BAILINGMOE3 || arch == LLM_ARCH_KIMI_K3 || arch == LLM_ARCH_GLM5_NEXT) {
arch == LLM_ARCH_GRANITE_HYBRID || arch == LLM_ARCH_LFM2 || arch == LLM_ARCH_LFM2MOE || arch == LLM_ARCH_LFM3 ||
arch == LLM_ARCH_KIMI_LINEAR || arch == LLM_ARCH_BAILINGMOE3 || arch == LLM_ARCH_KIMI_K3 || arch == LLM_ARCH_GLM5_NEXT
) {
GGML_ASSERT(n_layer >= 2);
std::vector<uint32_t> n_head_per_layer;
n_head_per_layer.reserve(n_layer);
@@ -416,6 +442,9 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
if (arch == LLM_ARCH_FLINT) {
ms.add_kv(LLM_KV_ATTENTION_ROPE_PATTERN, pattern);
}
} else if (arch == LLM_ARCH_LFM3) {
ms.add_kv(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, std::vector<uint32_t>({0, 0, 1}));
ms.add_kv(LLM_KV_ROPE_FREQ_BASE_SWA, 10000.0f);
} else {
ms.add_kv(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, uint32_t(2));
}
@@ -682,11 +711,11 @@ static std::string get_tmp_gguf_path() {
return (std::filesystem::temp_directory_path() / ("test-llama-archs-" + std::to_string(++id) + ".gguf")).string();
}
static std::pair<llama_model_ptr, llama_context_ptr> get_model_and_ctx(
static llama_model_ptr get_model(
struct gguf_context * gguf_ctx, FILE * file, const size_t seed, const float stdev,
const std::vector<ggml_backend_dev_t> & devs,
const llama_split_mode split_mode = LLAMA_SPLIT_MODE_LAYER, bool encode = false,
const llama_model_tensor_buft_override * tensor_buft_overrides = nullptr, const size_t moe_cache_size = 0) {
const llama_split_mode split_mode = LLAMA_SPLIT_MODE_LAYER,
const llama_model_tensor_buft_override * tensor_buft_overrides = nullptr) {
GGML_ASSERT((gguf_ctx == nullptr) != (file == nullptr));
llama_model_params model_params = llama_model_default_params();
model_params.progress_callback = silent_model_load_progress;
@@ -696,6 +725,23 @@ static std::pair<llama_model_ptr, llama_context_ptr> get_model_and_ctx(
model_params.split_mode = split_mode;
model_params.tensor_buft_overrides = tensor_buft_overrides;
tensor_data_params tensor_params = { seed, stdev };
llama_model_ptr model(gguf_ctx != nullptr ?
llama_model_init_from_user(gguf_ctx, set_tensor_data, &tensor_params, model_params) :
llama_model_load_from_file_ptr(file, model_params));
if (!model) {
throw std::runtime_error("failed to create llama model");
}
return model;
}
static std::pair<llama_model_ptr, llama_context_ptr> get_model_and_ctx(
struct gguf_context * gguf_ctx, FILE * file, const size_t seed, const float stdev,
const std::vector<ggml_backend_dev_t> & devs,
const llama_split_mode split_mode = LLAMA_SPLIT_MODE_LAYER, bool encode = false,
const llama_model_tensor_buft_override * tensor_buft_overrides = nullptr, const size_t moe_cache_size = 0) {
llama_model_ptr model = get_model(gguf_ctx, file, seed, stdev, devs, split_mode, tensor_buft_overrides);
llama_context_params ctx_params = llama_context_default_params();
ctx_params.n_ctx = 0;
ctx_params.n_threads = 4;
@@ -709,13 +755,6 @@ static std::pair<llama_model_ptr, llama_context_ptr> get_model_and_ctx(
ctx_params.n_ubatch = 2;
}
tensor_data_params tensor_params = { seed, stdev };
llama_model_ptr model(gguf_ctx != nullptr ?
llama_model_init_from_user(gguf_ctx, set_tensor_data, &tensor_params, model_params) :
llama_model_load_from_file_ptr(file, model_params));
if (!model) {
throw std::runtime_error("failed to create llama model");
}
llama_context_ptr lctx(llama_init_from_model(model.get(), ctx_params));
if (!lctx) {
throw std::runtime_error("failed to create llama context");
@@ -1027,10 +1066,12 @@ static int save_models(const std::string & arch_filter, const size_t seed, const
continue;
}
gguf_context_ptr gguf_ctx = get_gguf_ctx(arch, moe);
auto model_and_ctx = get_model_and_ctx(gguf_ctx.get(), nullptr, seed, stdev, {});
llama_model_ptr model = get_model(gguf_ctx.get(), nullptr, seed, stdev, {});
const std::string path = dir + "/" + llama_arch_name(arch) + (moe ? "-moe.gguf" : "-dense.gguf");
LOG_INF("%s: Saving %s model (%s) to %s...\n", __func__, llama_arch_name(arch), moe ? "MoE" : "dense", path.c_str());
llama_model_save_to_file(model_and_ctx.first.get(), path.c_str());
LOG_INF("%s: '%s %s' -> %s ...", __func__, llama_arch_name(arch), moe ? "MoE" : "dense", path.c_str());
llama_model_save_to_file(model.get(), path.c_str());
const std::uintmax_t size = std::filesystem::file_size(path);
LOG_CNT(" (%.2f MB)\n", (double) size / (1024.0 * 1024.0));
}
}
llama_log_set(ud.log_old.callback, ud.log_old.user_data);
@@ -1419,6 +1460,8 @@ int main(int argc, char ** argv) {
}
LOG_INF("%s: using seed %zu, stdev %f\n", __func__, seed, stdev);
init_normal_pool(seed, stdev);
try {
if (!out.empty()) {
return save_models(arch_filter, seed, stdev, verbosity, out);
+114
View File
@@ -4,6 +4,8 @@
#include "../tools/server/server-decision.h"
#include "../tools/server/server-schema.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <regex>
#include <string>
@@ -466,6 +468,116 @@ static void test_rerank_template(testing & t) {
}
//
// server_decision_context::format_answer, 2 class scores (minicpm-ocr-jev)
//
static void test_decision_class_scores(testing & t) {
server_decision_context ctx;
ctx.type = COMMON_DECISION_TYPE_MINICPM_OCR_JEV;
const server_decision_question choice = {"q", SERVER_DECISION_QUESTION_CHOICE, json(), {{"0", json()}, {"1", json()}}};
const server_decision_question noul = {"q", SERVER_DECISION_QUESTION_NOUL, json(), {{"false", json()}, {"true", json()}}};
const std::vector<std::pair<float, float>> cases = {
{0.0f, 0.0f}, {-1.5f, 2.25f}, {3.0f, -0.5f}, {80.0f, -80.0f}, {1000.0f, 1001.0f},
};
for (const auto & [z0, z1] : cases) {
const double p1 = 1.0 / (1.0 + std::exp((double) z0 - (double) z1));
const json a = ctx.format_answer(choice, {{z0, z1}});
const double ap0 = a.at("probabilities").at("0").get<double>();
const double ap1 = a.at("probabilities").at("1").get<double>();
t.assert_true("p1", std::fabs(ap1 - p1) <= 1e-6);
t.assert_true("sum", std::fabs(ap0 + ap1 - 1.0) <= 1e-9);
// a tie picks the first class
t.assert_equal("choice", std::string(z1 > z0 ? "1" : "0"), a.at("choice").get<std::string>());
t.assert_true("confidence", std::fabs(a.at("confidence").get<double>() - (2.0 * std::max(ap0, ap1) - 1.0)) <= 1e-9);
const json n = ctx.format_answer(noul, {{z0, z1}});
t.assert_true("noul is p1", n.at("noul").get<double>() == ap1);
}
for (const float bad : {std::nanf(""), INFINITY}) {
bool thrown = false;
try {
ctx.format_answer(choice, {{bad, 0.0f}});
} catch (const std::exception &) {
thrown = true;
}
t.assert_true("nan or +inf", thrown);
}
}
//
// server_decision_context::parse_questions / parse_state, the request checks of minicpm-ocr-jev
//
static void test_decision_ocr_jev_validation(testing & t) {
server_decision_context ctx;
ctx.type = COMMON_DECISION_TYPE_MINICPM_OCR_JEV;
// only base64-decoded, the image itself is not parsed here
const std::string image = "data:image/png;base64,AAAA";
const json choice = {
{"type", "choice"},
{"criteria", json{{"0", nullptr}, {"1", nullptr}}},
};
const auto questions_of = [](const json & question) {
return json{{"state", "some OCR text"}, {"questions", json{{"q", question}}}};
};
const auto files_of = [&](size_t n) {
json files = json::array();
for (size_t i = 0; i < n; i++) {
files.push_back(image);
}
return json{{"state", "some OCR text"}, {"files", files}};
};
const auto reject_questions = [&](const char * name, const json & body, const std::string & expected) {
std::string error;
try {
ctx.parse_questions(body);
} catch (const std::invalid_argument & e) {
error = e.what();
}
t.assert_true(std::string(name) + ": " + error, error.find(expected) != std::string::npos);
};
const auto reject_state = [&](const char * name, const json & body, const std::string & expected) {
std::vector<raw_buffer> files;
std::string error;
try {
ctx.parse_state(body, files);
} catch (const std::invalid_argument & e) {
error = e.what();
}
t.assert_true(std::string(name) + ": " + error, error.find(expected) != std::string::npos);
};
reject_state("state is text", {{"state", 42}}, "\"state\" must be the OCR text");
// one fixed question, no instructions
reject_questions("one question", {{"state", "x"}, {"questions", json{{"a", choice}, {"b", choice}}}},
"answers exactly one question");
reject_questions("no instructions",
questions_of(json{{"type", "choice"}, {"instructions", "judge it"}, {"criteria", json{{"0", nullptr}, {"1", nullptr}}}}),
"\"instructions\" is not used by this model");
// the classes are fixed, so are the criteria
reject_questions("criteria are the classes",
questions_of(json{{"type", "choice"}, {"criteria", json{{"yes", nullptr}, {"no", nullptr}}}}),
"\"criteria\" must be {\"0\": null, \"1\": null}");
reject_questions("noul without criteria",
questions_of(json{{"type", "noul"}, {"criteria", json{{"false", "no"}, {"true", "yes"}}}}),
"\"criteria\" is not supported for a noul question");
reject_questions("choice or noul only",
questions_of(json{{"type", "score"}, {"criteria", json::array({"low", "high"})}}),
"only choice and noul questions are supported");
// the prompt has the original image, then the one rendered from the OCR text
reject_state("two images", files_of(1), "this model needs exactly 2 images");
reject_state("two images, three given", files_of(3), "this model needs exactly 2 images");
}
// server_decision_context: the recursive functions stop at RECURSIVE_LIMIT
//
@@ -573,6 +685,8 @@ int main(int argc, char ** argv) {
t.test("server_pipe", test_server_pipe);
t.test("decision_escape", test_decision_escape);
t.test("rerank_template", test_rerank_template);
t.test("decision_class_scores", test_decision_class_scores);
t.test("decision_ocr_jev_validation", test_decision_ocr_jev_validation);
t.test("decision_depth", test_decision_depth);
t.test("glob_match", test_glob_match);
t.test("dry_sequence_breakers", test_dry_sequence_breakers);
+3
View File
@@ -58,6 +58,9 @@ add_library(mtmd
models/sensenova-u1.cpp
models/minimax-m3.cpp
models/qwen3vl.cpp
models/lfm3vl.cpp
models/lfm3a.cpp
models/lfm3a-gen.cpp
models/ling3vl.cpp
models/mimovl.cpp
models/qwen3a.cpp
+25
View File
@@ -106,6 +106,11 @@
#define KEY_GEN_AUDIO_PROJ_TYPE "clip.gen.audio.projector_type" // for models with mixed modalities
// name of the weight variant, for settings that are not in the checkpoint
#define KEY_GEN_AUDIO_VARIANT "clip.gen.audio.model_variant"
// sampling defaults of the generated audio, like general.sampling.* for text
#define KEY_GEN_AUDIO_TEMP "clip.gen.audio.sampling.temp"
#define KEY_GEN_AUDIO_TOP_K "clip.gen.audio.sampling.top_k"
#define KEY_GEN_AUDIO_PENALTY_REPEAT "clip.gen.audio.sampling.penalty_repeat"
#define KEY_GEN_AUDIO_PENALTY_LAST_N "clip.gen.audio.sampling.penalty_last_n"
#define KEY_AUDIO_SUBSMPL_FACTOR "clip.audio.subsampling_factor"
//
@@ -173,6 +178,12 @@
#define TN_DEEPSTACK_FC1 "v.deepstack.%d.fc1.%s" // qwen3vl deepstack
#define TN_DEEPSTACK_FC2 "v.deepstack.%d.fc2.%s" // qwen3vl deepstack
// lfm3vl
#define TN_LFM3VL_EMBEDDING_NORM "%s.embedding_norm.%s"
#define TN_LFM3VL_CONV2D_IN_PROJ "%s.blk.%d.conv2d_in_proj.%s"
#define TN_LFM3VL_CONV2D_SPATIAL "%s.blk.%d.conv2d_spatial.%s"
#define TN_LFM3VL_CONV2D_OUT_PROJ "%s.blk.%d.conv2d_out_proj.%s"
// mimicpmv
#define TN_MINICPMV_POS_EMBD_K "resampler.pos_embed_k"
#define TN_MINICPMV_QUERY "resampler.query"
@@ -240,6 +251,11 @@
#define TN_A_GEN_CODE_OUT_EMBD "a.gen.code.out_embd.%s"
#define TN_A_GEN_CODE_NORM "a.gen.code.output_norm.%s"
// lfm3a depthformer (MaskGIT), the codec decoder uses the generic layer names with prefix "a.gen.wav.tfm"
#define TN_A_GEN_CODE_HEAD_NORM "a.gen.code.head_norm.%s"
#define TN_A_GEN_CODE_MASK_EMBD "a.gen.code.mask_embd"
#define TN_ATTN_GATE "%s.blk.%d.attn_gate.%s"
// qwen3tts code2wav (RVQ codes -> raw PCM)
// pre_transformer layers use the generic TN_ATTN_*/TN_FFN_*/TN_LN_*/TN_LS_* macros, prefix "a.gen.wav.tfm"
#define TN_A_GEN_WAV_QUANT_FIRST_IN "a.gen.wav.quant.first.in_proj.%s"
@@ -468,6 +484,9 @@ struct clip_enc_state {
ggml_backend_buffer_ptr buf;
std::vector<ggml_tensor *> k; // per layer [n_embd_kv, n_ctx]
std::vector<ggml_tensor *> v;
// lfm3a: same order as list_lfm3a_state_slots(), F32 [ne0, ne1], updated in place
std::vector<ggml_tensor *> slots;
};
// per-sequence state of audio generation, see clip_gen_state_init()
@@ -517,6 +536,7 @@ enum projector_type {
PROJECTOR_TYPE_MERALION,
PROJECTOR_TYPE_MUSIC_FLAMINGO,
PROJECTOR_TYPE_LFM2,
PROJECTOR_TYPE_LFM3VL,
PROJECTOR_TYPE_D1OMNI_V,
PROJECTOR_TYPE_KIMIVL,
PROJECTOR_TYPE_PADDLEOCR,
@@ -532,6 +552,7 @@ enum projector_type {
PROJECTOR_TYPE_DEEPSEEK41V,
PROJECTOR_TYPE_LFM2A,
PROJECTOR_TYPE_D1OMNI_A,
PROJECTOR_TYPE_LFM3A,
PROJECTOR_TYPE_GLM4V,
PROJECTOR_TYPE_GLM5V,
PROJECTOR_TYPE_YOUTUVL,
@@ -555,6 +576,7 @@ enum projector_type {
PROJECTOR_TYPE_POCKETTTS_SPKENC,
PROJECTOR_TYPE_POCKETTTS_GEN,
PROJECTOR_TYPE_SOPRANO,
PROJECTOR_TYPE_LFM3A_GEN,
PROJECTOR_TYPE_MUSE_GLIMMER,
PROJECTOR_TYPE_SENSENOVA_U1,
PROJECTOR_TYPE_COHERE2V,
@@ -595,6 +617,7 @@ static std::map<projector_type, std::string> PROJECTOR_TYPE_NAMES = {
{ PROJECTOR_TYPE_MERALION, "meralion"},
{ PROJECTOR_TYPE_MUSIC_FLAMINGO, "musicflamingo"},
{ PROJECTOR_TYPE_LFM2, "lfm2"},
{ PROJECTOR_TYPE_LFM3VL, "lfm3vl"},
{ PROJECTOR_TYPE_D1OMNI_V, "d1omni_v"},
{ PROJECTOR_TYPE_KIMIVL, "kimivl"},
{ PROJECTOR_TYPE_PADDLEOCR, "paddleocr"},
@@ -610,6 +633,7 @@ static std::map<projector_type, std::string> PROJECTOR_TYPE_NAMES = {
{ PROJECTOR_TYPE_DEEPSEEK41V, "deepseek41v"},
{ PROJECTOR_TYPE_LFM2A, "lfm2a"},
{ PROJECTOR_TYPE_D1OMNI_A, "d1omni_a"},
{ PROJECTOR_TYPE_LFM3A, "lfm3a"},
{ PROJECTOR_TYPE_GLM4V, "glm4v"},
{ PROJECTOR_TYPE_GLM5V, "glm5v"},
{ PROJECTOR_TYPE_YOUTUVL, "youtuvl"},
@@ -633,6 +657,7 @@ static std::map<projector_type, std::string> PROJECTOR_TYPE_NAMES = {
{ PROJECTOR_TYPE_POCKETTTS_SPKENC, "pockettts_spkenc"},
{ PROJECTOR_TYPE_POCKETTTS_GEN, "pockettts_gen"},
{ PROJECTOR_TYPE_SOPRANO, "soprano"},
{ PROJECTOR_TYPE_LFM3A_GEN, "lfm3a_gen"},
{ PROJECTOR_TYPE_MUSE_GLIMMER, "muse-glimmer"},
{ PROJECTOR_TYPE_SENSENOVA_U1, "sensenova_u1"},
{ PROJECTOR_TYPE_COHERE2V, "cohere2v"},
+23
View File
@@ -175,6 +175,12 @@ struct clip_hparams {
// name of the weight variant, some pipelines tune themselves on it
std::string gen_model_variant;
// sampling defaults of the generated audio: greedy, no top-k, no repetition penalty
float gen_temp = 0.0f;
int32_t gen_top_k = 0;
float gen_penalty_repeat = 1.0f;
int32_t gen_penalty_last_n = 0;
// pocket-tts
static constexpr int32_t pockettts_max_spk_seconds = 30;
int32_t seanet_n_stage = 0;
@@ -195,6 +201,7 @@ struct clip_hparams {
int32_t wav_dac_n_block = 0;
int32_t wav_dac_n_res = 0;
int32_t wav_tfm_swa = 0; // pre_transformer's KV cache size, in frames
int32_t wav_frame_stack = 0; // lfm3a codec: decoder positions per frame
// mimo-v2.5: LLM-side connector (input_local_transformer)
int32_t audio_local_n_layer = 0;
@@ -286,6 +293,9 @@ struct clip_layer {
ggml_tensor * k_norm = nullptr;
ggml_tensor * q_norm = nullptr;
// lfm3: per-head sigmoid gate on the attention output
ggml_tensor * attn_gate_w = nullptr;
ggml_tensor * attn_post_norm_w = nullptr;
ggml_tensor * ff_up_w = nullptr;
@@ -325,6 +335,11 @@ struct clip_layer {
ggml_tensor * deepstack_fc2_w = nullptr;
ggml_tensor * deepstack_fc2_b = nullptr;
// lfm3vl conv layers
ggml_tensor * conv2d_in_proj_w = nullptr;
ggml_tensor * conv2d_spatial_w = nullptr; // may be nullptr
ggml_tensor * conv2d_out_proj_w = nullptr;
// sam rel_pos
ggml_tensor * rel_pos_w = nullptr;
ggml_tensor * rel_pos_h = nullptr;
@@ -561,6 +576,7 @@ struct clip_code2wav {
// quantizer: RVQ codebook decode
ggml_tensor * quant_first_in_w = nullptr; // semantic RVQ, in_proj (1x1 conv, loaded as 2D)
ggml_tensor * quant_first_out_w = nullptr;
ggml_tensor * quant_first_out_b = nullptr;
ggml_tensor * quant_first_cb_w = nullptr; // codebook (1 layer)
ggml_tensor * quant_rest_in_w = nullptr; // acoustic RVQ
ggml_tensor * quant_rest_out_w = nullptr;
@@ -786,6 +802,9 @@ struct clip_model {
ggml_tensor * yasa_vision_pos_embed = nullptr;
std::vector<yasa2_stage> yasa_stages;
// lfm3vl
ggml_tensor * lfm3vl_embedding_norm_w = nullptr;
// pixtral, glm4v
ggml_tensor * token_embd_img_break = nullptr;
@@ -844,6 +863,10 @@ struct clip_model {
ggml_tensor * gen_code_out_embd_w = nullptr; // codebook-0 embedding, fed back into the talker
ggml_tensor * gen_code_norm_w = nullptr; // final norm
// lfm3a depthformer (MaskGIT)
ggml_tensor * gen_code_head_norm_w = nullptr; // per-codebook norm before the tied head, merged 2D
ggml_tensor * gen_code_mask_embd = nullptr;
// qwen3tts code2wav: RVQ codes -> raw PCM
clip_code2wav c2w;
+389 -8
View File
@@ -1031,6 +1031,10 @@ static std::unique_ptr<clip_graph> clip_get_graph_builder(clip_ctx * ctx, const
{
builder = std::make_unique<clip_graph_sensenova_u1>(ctx, img);
} break;
case PROJECTOR_TYPE_LFM3VL:
{
builder = std::make_unique<clip_graph_lfm3vl>(ctx, img);
} break;
case PROJECTOR_TYPE_LING3VL:
{
builder = std::make_unique<clip_graph_ling3vl>(ctx, img);
@@ -1144,6 +1148,10 @@ static std::unique_ptr<clip_graph> clip_get_graph_builder(clip_ctx * ctx, const
{
builder = std::make_unique<clip_graph_conformer>(ctx, img);
} break;
case PROJECTOR_TYPE_LFM3A:
{
builder = std::make_unique<clip_graph_lfm3a>(ctx, img);
} break;
case PROJECTOR_TYPE_GEMMA4A:
{
builder = std::make_unique<clip_graph_gemma4a>(ctx, img);
@@ -1192,6 +1200,24 @@ static std::unique_ptr<clip_graph> clip_get_graph_builder(clip_ctx * ctx, const
const int n_frames = params && params->feats ? (int) (params->feats->size() / n_latent) : 1;
builder = std::make_unique<clip_graph_pockettts_gen>(ctx, img, gen_process, n_step, n_frames);
} break;
case PROJECTOR_TYPE_LFM3A_GEN:
{
const auto gen_process = params ? params->gen_process : CLIP_GEN_PROCESS_GEN_CODE;
const int64_t n_cb = ctx->model.gen_code_embd_w->ne[2];
bool all_known = false;
int n_frames = 1;
if (params && params->codes) {
GGML_ASSERT(params->codes->size() % (size_t) n_cb == 0);
if (gen_process == CLIP_GEN_PROCESS_GEN_WAV) {
n_frames = (int) (params->codes->size() / n_cb);
GGML_ASSERT(n_frames > 0);
} else {
// -1 marks a masked codebook, none masked means only the feedback embedding is needed
all_known = std::all_of(params->codes->begin(), params->codes->end(), [](int32_t c) { return c >= 0; });
}
}
builder = std::make_unique<clip_graph_lfm3a_gen>(ctx, img, gen_process, all_known, n_frames);
} break;
case PROJECTOR_TYPE_QWEN3TTS_GEN:
{
const auto gen_process = params ? params->gen_process : CLIP_GEN_PROCESS_GEN_CODE;
@@ -1876,6 +1902,39 @@ struct clip_model_loader {
}
hparams.set_warmup_n_tokens(16*16);
} break;
case PROJECTOR_TYPE_LFM3VL:
{
std::string resize_algo;
get_string(KEY_IMAGE_RESIZE_ALGO, resize_algo);
// Lfm3VlImageProcessor resizes with torchvision
if (resize_algo == "bilinear") {
hparams.set_image_resize_algo(RESIZE_ALGO_BILINEAR, true);
} else if (resize_algo == "bicubic") {
hparams.set_image_resize_algo(RESIZE_ALGO_BICUBIC, true);
} else {
throw std::runtime_error(string_format("%s: unsupported image resize algo: %s\n", __func__, resize_algo.c_str()));
}
// smart_resize stretches to the aligned size, no letterbox
hparams.image_resize_pad = PAD_NONE;
get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge);
// token counts and the projector graph assume a 2x2 merge
if (hparams.n_merge != 2) {
throw std::runtime_error(string_format("%s: unsupported spatial_merge_size %d for lfm3vl, only 2 is supported\n", __func__, hparams.n_merge));
}
get_u32(string_format(KEY_N_HEAD_KV, "vision"), hparams.n_head_kv);
hparams.rope_theta = 1000000.0f; // vision_rope_theta
// HF processor size; the max is also capped at 4096 tokens as an OOM guard
int min_pixels = 0;
int max_pixels = 0;
get_u32(KEY_IMAGE_MIN_PIXELS, min_pixels);
get_u32(KEY_IMAGE_MAX_PIXELS, max_pixels);
if (min_pixels <= 0 || max_pixels < min_pixels) {
throw std::runtime_error(string_format("%s: invalid image min/max pixels %d/%d\n", __func__, min_pixels, max_pixels));
}
const int patch_area = hparams.patch_size * hparams.patch_size * hparams.n_merge * hparams.n_merge;
hparams.set_limit_image_tokens(min_pixels / patch_area, std::min(4096, max_pixels / patch_area));
hparams.set_warmup_n_tokens(46*46); // avoid OOM on warmup
} break;
case PROJECTOR_TYPE_MINIMAX_M3:
{
hparams.n_merge = 2; // spatial_merge_size
@@ -2099,6 +2158,33 @@ struct clip_model_loader {
hparams.audio_window_len = 1024;
hparams.audio_hop_len = 256;
} break;
case PROJECTOR_TYPE_LFM3A_GEN:
{
// fixed by LiquidCodecConfig, head counts come from the tensors
hparams.rope_theta = 1000000.0f;
hparams.wav_tfm_n_layer = 8;
hparams.wav_tfm_n_embd = 512;
hparams.wav_tfm_eps = 1e-5f;
hparams.wav_tfm_rope_theta = 1000000.0f;
hparams.wav_tfm_swa = 30; // decoder_window
hparams.wav_frame_stack = 6; // stft_frame_stack, STFT frames per codec frame
// the ISTFT runs on the host, see mtmd_gen_audio_process()
hparams.audio_sample_rate = 24000;
hparams.audio_n_fft = 1280;
hparams.audio_window_len = 1280;
hparams.audio_hop_len = 320;
// speech-code sampling, the converter writes the model card values; else the LFM3-Omni card
hparams.gen_temp = 0.4f;
hparams.gen_top_k = 0;
hparams.gen_penalty_repeat = 1.2f;
hparams.gen_penalty_last_n = 64;
get_f32(KEY_GEN_AUDIO_TEMP, hparams.gen_temp, false);
get_i32(KEY_GEN_AUDIO_TOP_K, hparams.gen_top_k, false);
get_f32(KEY_GEN_AUDIO_PENALTY_REPEAT, hparams.gen_penalty_repeat, false);
get_i32(KEY_GEN_AUDIO_PENALTY_LAST_N, hparams.gen_penalty_last_n, false);
} break;
case PROJECTOR_TYPE_QWEN3TTS_GEN:
{
// TODO: hardcoded for now, read from code_predictor_config instead
@@ -2204,6 +2290,20 @@ struct clip_model_loader {
hparams.audio_window_len = 400;
hparams.audio_hop_len = 160;
} break;
case PROJECTOR_TYPE_LFM3A:
{
get_u32(KEY_A_ATTN_WINDOW_SIZE, hparams.attn_window_size);
if (hparams.attn_window_size <= 0) {
throw std::runtime_error(string_format("%s: invalid audio attention window %d\n", __func__, hparams.attn_window_size));
}
hparams.rope_theta = 1000000.0f; // checked by the converter
// audio preprocessing params, see mtmd_audio_preprocessor_lfm3a
hparams.audio_chunk_len = 0;
hparams.audio_sample_rate = 16000;
hparams.audio_n_fft = 400;
hparams.audio_window_len = 400;
hparams.audio_hop_len = 160;
} break;
case PROJECTOR_TYPE_EXAONE4_5:
{
hparams.n_merge = 2;
@@ -2515,6 +2615,9 @@ struct clip_model_loader {
model.position_embeddings = get_tensor(string_format(TN_POS_EMBD, prefix), false);
// lfm3vl
model.lfm3vl_embedding_norm_w = get_tensor(string_format(TN_LFM3VL_EMBEDDING_NORM, prefix, "weight"), false);
const bool has_standard_layers = (
model.proj_type != PROJECTOR_TYPE_GEMMA3NV &&
model.proj_type != PROJECTOR_TYPE_QWEN3TTS_SPKENC &&
@@ -2527,10 +2630,13 @@ struct clip_model_loader {
model.layers.resize(n_layers_to_load);
for (int il = 0; il < n_layers_to_load; ++il) {
auto & layer = model.layers[il];
// lfm3vl / lfm3a conv layers have no attention tensors
const bool o_w_required = model.proj_type != PROJECTOR_TYPE_LFM3VL
&& model.proj_type != PROJECTOR_TYPE_LFM3A;
layer.k_w = get_tensor(string_format(TN_ATTN_K, prefix, il, "weight"), false);
layer.q_w = get_tensor(string_format(TN_ATTN_Q, prefix, il, "weight"), false);
layer.v_w = get_tensor(string_format(TN_ATTN_V, prefix, il, "weight"), false);
layer.o_w = get_tensor(string_format(TN_ATTN_OUTPUT, prefix, il, "weight"));
layer.o_w = get_tensor(string_format(TN_ATTN_OUTPUT, prefix, il, "weight"), o_w_required);
layer.qkv_w = get_tensor(string_format(TN_ATTN_QKV, prefix, il, "weight"), false);
layer.k_norm = get_tensor(string_format(TN_ATTN_K_NORM, prefix, il, "weight"), false);
layer.q_norm = get_tensor(string_format(TN_ATTN_Q_NORM, prefix, il, "weight"), false);
@@ -2580,6 +2686,11 @@ struct clip_model_loader {
model.n_deepstack_layers++;
}
// lfm3vl conv layers
layer.conv2d_in_proj_w = get_tensor(string_format(TN_LFM3VL_CONV2D_IN_PROJ, prefix, il, "weight"), false);
layer.conv2d_spatial_w = get_tensor(string_format(TN_LFM3VL_CONV2D_SPATIAL, prefix, il, "weight"), false);
layer.conv2d_out_proj_w = get_tensor(string_format(TN_LFM3VL_CONV2D_OUT_PROJ, prefix, il, "weight"), false);
// some models already exported with legacy (incorrect) naming which is quite messy, let's fix it here
// note: Qwen model converted from the old surgery script has n_ff = 0, so we cannot use n_ff to check!
bool is_ffn_swapped = (
@@ -2764,6 +2875,24 @@ struct clip_model_loader {
model.mm_1_w = get_tensor(string_format(TN_LLAVA_PROJ, 2, "weight"));
model.mm_1_b = get_tensor(string_format(TN_LLAVA_PROJ, 2, "bias"));
} break;
case PROJECTOR_TYPE_LFM3VL:
{
model.mm_input_norm_w = get_tensor(TN_MM_INP_NORM);
model.mm_input_norm_b = get_tensor(TN_MM_INP_NORM_B);
model.mm_0_w = get_tensor(string_format(TN_LLAVA_PROJ, 0, "weight"));
model.mm_0_b = get_tensor(string_format(TN_LLAVA_PROJ, 0, "bias"));
model.mm_1_w = get_tensor(string_format(TN_LLAVA_PROJ, 2, "weight"));
model.mm_1_b = get_tensor(string_format(TN_LLAVA_PROJ, 2, "bias"));
// each layer must be either a conv block or a complete attention block
for (size_t il = 0; il < model.layers.size(); ++il) {
const auto & layer = model.layers[il];
const bool is_conv = layer.conv2d_in_proj_w != nullptr;
const bool is_attn = layer.q_w && layer.k_w && layer.v_w && layer.o_w;
if (is_conv == is_attn) {
throw std::runtime_error(string_format("%s: lfm3vl layer %zu is neither a complete conv nor a complete attention block\n", __func__, il));
}
}
} break;
case PROJECTOR_TYPE_LING3VL:
{
model.mm_input_norm_w = get_tensor(TN_MM_INP_NORM); // merger.norm
@@ -3373,6 +3502,65 @@ struct clip_model_loader {
layer.ls_2_w = get_tensor(string_format(TN_LS_2, p, il, "weight"));
}
} break;
case PROJECTOR_TYPE_LFM3A_GEN:
{
// depthformer, the generic loop above loaded the a.gen.code.blk.* tensors
for (int il = 0; il < hparams.n_layer; ++il) {
model.layers[il].attn_gate_w = get_tensor(string_format(TN_ATTN_GATE, prefix, il, "weight"));
}
model.gen_code_proj_in_w = get_tensor(string_format(TN_A_GEN_CODE_PROJ_IN, "weight"));
model.gen_code_proj_in_b = get_tensor(string_format(TN_A_GEN_CODE_PROJ_IN, "bias"));
model.gen_code_embd_w = get_tensor(string_format(TN_A_GEN_CODE_EMBD, "weight")); // tied head
model.gen_code_out_embd_w = get_tensor(string_format(TN_A_GEN_CODE_OUT_EMBD, "weight"));
model.gen_code_norm_w = get_tensor(string_format(TN_A_GEN_CODE_NORM, "weight"));
model.gen_code_head_norm_w = get_tensor(string_format(TN_A_GEN_CODE_HEAD_NORM, "weight"));
model.gen_code_mask_embd = get_tensor(TN_A_GEN_CODE_MASK_EMBD);
// codec decoder: FSQ level table, one projection over all codebooks, LFM3 decoder -> polar STFT frames
auto & c2w = model.c2w;
c2w.quant_first_cb_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_FIRST_CB, "weight"));
c2w.quant_first_out_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_FIRST_OUT, "weight"));
c2w.quant_first_out_b = get_tensor(string_format(TN_A_GEN_WAV_QUANT_FIRST_OUT, "bias"));
c2w.tfm_in_proj_w = get_tensor(string_format(TN_A_GEN_WAV_TFM_IN_PROJ, "weight"));
c2w.tfm_in_proj_b = get_tensor(string_format(TN_A_GEN_WAV_TFM_IN_PROJ, "bias"));
c2w.tfm_out_proj_w = get_tensor(string_format(TN_A_GEN_WAV_TFM_OUT_PROJ, "weight"));
c2w.tfm_out_proj_b = get_tensor(string_format(TN_A_GEN_WAV_TFM_OUT_PROJ, "bias"));
c2w.tfm_output_norm_w = get_tensor(string_format(TN_A_GEN_WAV_TFM_OUT_NORM, "weight"));
// each layer is a short conv block or a gated attention block, both with sandwich norms
c2w.tfm_layers.resize(hparams.wav_tfm_n_layer);
for (int il = 0; il < hparams.wav_tfm_n_layer; il++) {
auto & layer = c2w.tfm_layers[il];
const char * p = "a.gen.wav.tfm";
layer.ln_1_w = get_tensor(string_format(TN_LN_1, p, il, "weight"));
layer.attn_post_norm_w = get_tensor(string_format(TN_ATTN_POST_NORM, p, il, "weight"));
layer.ln_2_w = get_tensor(string_format(TN_LN_2, p, il, "weight"));
layer.ff_post_norm_w = get_tensor(string_format(TN_FFN_POST_NORM, p, il, "weight"));
layer.ff_gate_w = get_tensor(string_format(TN_FFN_GATE, p, il, "weight"));
layer.ff_up_w = get_tensor(string_format(TN_FFN_UP, p, il, "weight"));
layer.ff_down_w = get_tensor(string_format(TN_FFN_DOWN, p, il, "weight"));
layer.conv_pw1_w = get_tensor(string_format(TN_CONV_PW1, p, il, "weight"), false);
layer.conv_dw_w = get_tensor(string_format(TN_CONV_DW, p, il, "weight"), false);
layer.conv_pw2_w = get_tensor(string_format(TN_CONV_PW2, p, il, "weight"), false);
layer.q_w = get_tensor(string_format(TN_ATTN_Q, p, il, "weight"), false);
layer.k_w = get_tensor(string_format(TN_ATTN_K, p, il, "weight"), false);
layer.v_w = get_tensor(string_format(TN_ATTN_V, p, il, "weight"), false);
layer.o_w = get_tensor(string_format(TN_ATTN_OUTPUT, p, il, "weight"), false);
layer.q_norm = get_tensor(string_format(TN_ATTN_Q_NORM, p, il, "weight"), false);
layer.k_norm = get_tensor(string_format(TN_ATTN_K_NORM, p, il, "weight"), false);
layer.attn_gate_w = get_tensor(string_format(TN_ATTN_GATE, p, il, "weight"), false);
const bool is_conv = layer.conv_pw1_w && layer.conv_dw_w && layer.conv_pw2_w;
const bool is_attn = layer.q_w && layer.k_w && layer.v_w && layer.o_w && layer.q_norm && layer.k_norm && layer.attn_gate_w;
if (is_conv == is_attn) {
throw std::runtime_error(string_format("%s: codec layer %d is neither a conv nor an attention block\n", __func__, il));
}
if (is_attn) {
const int d_head = layer.q_norm->ne[0];
hparams.wav_tfm_n_head = layer.q_w->ne[1] / d_head;
hparams.wav_tfm_n_head_kv = layer.k_w->ne[1] / d_head;
}
}
} break;
case PROJECTOR_TYPE_QWEN3TTS_GEN:
{
// code_predictor, proj_in is absent when the talker and the predictor share the hidden size
@@ -3765,6 +3953,39 @@ struct clip_model_loader {
layer.conv_pw2_b = get_tensor(string_format(TN_CONV_PW2, prefix, il, "bias"));
}
} break;
case PROJECTOR_TYPE_LFM3A:
{
for (int i : {0, 2, 3, 5, 6}) {
model.pre_encode_conv_X_w[i] = get_tensor(string_format(TN_CONV1D, i, "weight"));
model.pre_encode_conv_X_b[i] = get_tensor(string_format(TN_CONV1D, i, "bias"));
}
model.pre_encode_out_w = get_tensor(string_format(TN_PRE_ENCODE_OUT, "weight"));
model.pre_encode_out_b = get_tensor(string_format(TN_PRE_ENCODE_OUT, "bias"));
model.mm_0_w = get_tensor(string_format(TN_MM_AUDIO_MLP, 0, "weight"));
model.mm_0_b = get_tensor(string_format(TN_MM_AUDIO_MLP, 0, "bias"));
model.mm_1_w = get_tensor(string_format(TN_MM_AUDIO_MLP, 1, "weight"));
model.mm_1_b = get_tensor(string_format(TN_MM_AUDIO_MLP, 1, "bias"));
model.mm_3_w = get_tensor(string_format(TN_MM_AUDIO_MLP, 3, "weight"));
model.mm_3_b = get_tensor(string_format(TN_MM_AUDIO_MLP, 3, "bias"));
if (!model.post_ln_w) {
throw std::runtime_error(string_format("%s: missing tensor %s\n", __func__, string_format(TN_LN_POST, prefix, "weight").c_str()));
}
// each layer is either a short conv block or a complete attention block
for (int il = 0; il < hparams.n_layer; ++il) {
auto & layer = model.layers[il];
layer.conv_pw1_w = get_tensor(string_format(TN_CONV_PW1, prefix, il, "weight"), false);
layer.conv_dw_w = get_tensor(string_format(TN_CONV_DW, prefix, il, "weight"), false);
layer.conv_pw2_w = get_tensor(string_format(TN_CONV_PW2, prefix, il, "weight"), false);
const bool is_conv = layer.conv_pw1_w && layer.conv_dw_w && layer.conv_pw2_w;
const bool is_attn = layer.q_w && layer.k_w && layer.v_w && layer.o_w && layer.q_norm && layer.k_norm;
if (is_conv == is_attn) {
throw std::runtime_error(string_format("%s: lfm3a layer %d is neither a complete conv nor a complete attention block\n", __func__, il));
}
}
} break;
case PROJECTOR_TYPE_PARAKEET:
{
@@ -4384,6 +4605,10 @@ struct clip_init_result clip_init(const char * fname, struct clip_context_params
if (loader.has_gen_audio) {
ctx_gen_audio = new clip_ctx(ctx_params);
loader.load_hparams(ctx_gen_audio->model, CLIP_MODALITY_GEN_AUDIO);
// TODO: CUDA has no flash attention kernel for the depthformer head dim 32, enable it once it does
if (ctx_gen_audio->model.proj_type == PROJECTOR_TYPE_LFM3A_GEN && ctx_gen_audio->flash_attn_type == CLIP_FLASH_ATTN_TYPE_AUTO) {
ctx_gen_audio->flash_attn_type = CLIP_FLASH_ATTN_TYPE_DISABLED;
}
loader.load_tensors(*ctx_gen_audio);
// TODO: fix warmup
ctx_gen_audio->buf_compute_meta.resize(ctx_gen_audio->max_nodes * ggml_tensor_overhead() + ggml_graph_overhead());
@@ -4452,6 +4677,7 @@ int clip_n_output_tokens_x(const clip_ctx * ctx, const clip_image_f32 * img) {
case PROJECTOR_TYPE_HUNYUANVL:
case PROJECTOR_TYPE_YOUTUVL:
case PROJECTOR_TYPE_MUSE_GLIMMER:
case PROJECTOR_TYPE_LFM3VL:
return (img->nx() / params.patch_size) / 2;
case PROJECTOR_TYPE_STEP3VL:
case PROJECTOR_TYPE_MINICPMV4_6:
@@ -4484,6 +4710,7 @@ int clip_n_output_tokens_y(const clip_ctx * ctx, const clip_image_f32 * img) {
case PROJECTOR_TYPE_HUNYUANVL:
case PROJECTOR_TYPE_YOUTUVL:
case PROJECTOR_TYPE_MUSE_GLIMMER:
case PROJECTOR_TYPE_LFM3VL:
return (img->ny() / params.patch_size) / 2;
case PROJECTOR_TYPE_STEP3VL:
case PROJECTOR_TYPE_MINICPMV4_6:
@@ -4571,6 +4798,7 @@ int clip_n_output_tokens(const clip_ctx * ctx, const clip_image_f32 * img) {
case PROJECTOR_TYPE_GLM5V:
case PROJECTOR_TYPE_YOUTUVL:
case PROJECTOR_TYPE_MUSE_GLIMMER:
case PROJECTOR_TYPE_LFM3VL:
{
// dynamic size (2 conv, so double patch size)
int x_patch = img->nx() / (params.patch_size * 2);
@@ -4746,6 +4974,7 @@ int clip_n_output_tokens(const clip_ctx * ctx, const clip_image_f32 * img) {
} break;
case PROJECTOR_TYPE_LFM2A:
case PROJECTOR_TYPE_D1OMNI_A:
case PROJECTOR_TYPE_LFM3A:
{
n_patches = ((((img->nx() + 1) / 2) + 1) / 2 + 1) / 2;
} break;
@@ -4788,6 +5017,7 @@ int clip_n_output_tokens(const clip_ctx * ctx, const clip_image_f32 * img) {
n_patches = 1;
} break;
case PROJECTOR_TYPE_QWEN3TTS_GEN:
case PROJECTOR_TYPE_LFM3A_GEN:
{
// one hidden-state vector fed back to the talker per call
n_patches = 1;
@@ -4848,11 +5078,12 @@ bool clip_image_batch_encode(clip_ctx * ctx, int n_threads, const clip_image_f32
return clip_encode(ctx, &params);
}
// persisted state slots of the gen-audio decoder, per pipeline
// persisted state slots of the gen-audio decoders and the streaming audio encoders
static std::vector<c2w_state_slot> list_gen_state_slots(const clip_hparams & hparams, const clip_model & model) {
switch (model.proj_type) {
case PROJECTOR_TYPE_QWEN3TTS_GEN: return list_c2w_state_slots(hparams, model);
case PROJECTOR_TYPE_POCKETTTS_GEN: return list_pockettts_state_slots(hparams, model);
case PROJECTOR_TYPE_LFM3A_GEN: return list_lfm3a_gen_state_slots(hparams, model);
default: return {};
}
}
@@ -4912,6 +5143,30 @@ void clip_gen_state_reset(clip_gen_state * state) {
clip_enc_state * clip_enc_state_init(clip_ctx * ctx, int32_t n_frames_max) {
const auto & hparams = ctx->model.hparams;
if (ctx->proj_type() == PROJECTOR_TYPE_LFM3A) {
// downsampler context, short-conv taps and attention window, see list_lfm3a_state_slots()
const auto slots = list_lfm3a_state_slots(hparams, ctx->model);
auto st = std::make_unique<clip_enc_state>();
ggml_init_params params = {
/*.mem_size =*/ ggml_tensor_overhead() * slots.size(),
/*.mem_buffer =*/ nullptr,
/*.no_alloc =*/ true,
};
st->ctx_data.reset(ggml_init(params));
for (const auto & slot : slots) {
ggml_tensor * t = ggml_new_tensor_2d(st->ctx_data.get(), GGML_TYPE_F32, slot.ne0, slot.ne1);
ggml_format_name(t, "enc_state_%s", slot.name.c_str());
st->slots.push_back(t);
}
st->buf.reset(ggml_backend_alloc_ctx_tensors(st->ctx_data.get(), ctx->backend));
if (!st->buf) {
LOG_ERR("%s: failed to allocate the encoder state\n", __func__);
return nullptr;
}
// a zero state is the cold start
ggml_backend_buffer_clear(st->buf.get(), 0);
return st.release();
}
if (ctx->proj_type() != PROJECTOR_TYPE_VOXTRAL_RT || hparams.attn_window_size <= 0) {
LOG_ERR("%s: the encoder does not support streaming input\n", __func__);
return nullptr;
@@ -4983,6 +5238,11 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {
// build the inference graph
ggml_backend_sched_reset(ctx->sched.get());
ggml_cgraph * gf = clip_get_graph_builder(ctx, imgs, params)->build();
// the last node is the embedding tensor, code2wav has no out_embd
// read it before scheduling: backends may reorder the nodes of graphs with several outputs (Metal)
ggml_tensor * embeddings = params->out_embd ? ggml_graph_node(gf, -1) : nullptr;
if (!ggml_backend_sched_alloc_graph(ctx->sched.get(), gf)) {
LOG_ERR("%s: failed to allocate compute graph\n", __func__);
return false;
@@ -5051,6 +5311,28 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {
}
};
// lfm3a encoder and codec: causal sliding window over [cached window | this call]
auto set_lfm3a_window_inputs = [&](int base, int n_pos, int window) {
std::vector<int32_t> positions(n_pos);
for (int i = 0; i < n_pos; i++) {
positions[i] = base + i;
}
set_input_i32("positions", positions);
// key j holds position base - prefix + j
const int prefix = window - 1;
const int n_kv = prefix + n_pos;
std::vector<float> mask((size_t) n_kv * n_pos, -INFINITY);
for (int q = 0; q < n_pos; q++) {
for (int j = std::max(0, q + prefix - window + 1); j <= q + prefix; j++) {
if (base - prefix + j >= 0) {
mask[(size_t) q * n_kv + j] = 0.0f;
}
}
}
set_input_attn_mask(mask);
};
// rope positions and attention mask of the mimi transformers (pocket-tts).
// the mask is causal with a sliding window, see _build_attention_mask() in the reference
auto set_pockettts_tfm_inputs = [&]() {
@@ -5122,8 +5404,9 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {
}
set_input_f32("inp_raw", inp_raw);
} else if (params->gen_process != CLIP_GEN_PROCESS_GEN_WAV) {
// audio input. GEN_WAV is not here: it takes codes or feats, set in the switch below
} else if (params->gen_process != CLIP_GEN_PROCESS_GEN_WAV && ggml_graph_get_tensor(gf, "inp_raw") != nullptr) {
// audio input. GEN_WAV is not here: it takes codes or feats, set in the switch below.
// a GEN_CODE graph may have no hidden-state input (lfm3a_gen feedback embedding)
GGML_ASSERT(imgs.entries.size() == 1);
const auto & mel_inp = imgs.entries[0];
@@ -5377,6 +5660,24 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {
set_input_i32("merger_ds_idx_2", m_ds_2);
set_input_i32("merger_ds_idx_3", m_ds_3);
} break;
case PROJECTOR_TYPE_LFM3VL:
{
// patches in raster-scan order (no interleave before encoder layers)
const int pw = image_size_width / patch_size;
const int ph = image_size_height / patch_size;
std::vector<int> positions(n_pos * 4);
int ptr = 0;
for (int y = 0; y < ph; y++) {
for (int x = 0; x < pw; x++) {
positions[ ptr] = y;
positions[ num_patches + ptr] = x;
positions[2 * num_patches + ptr] = y;
positions[3 * num_patches + ptr] = x;
ptr++;
}
}
set_input_i32("positions", positions);
} break;
case PROJECTOR_TYPE_QWEN2VL:
case PROJECTOR_TYPE_QWEN3VL:
case PROJECTOR_TYPE_LING3VL:
@@ -5917,6 +6218,53 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {
{
// do nothing
} break;
case PROJECTOR_TYPE_LFM3A_GEN:
{
const int64_t n_cb = model.gen_code_embd_w->ne[2];
const int64_t n_vocab = model.gen_code_embd_w->ne[1];
if (params->gen_process == CLIP_GEN_PROCESS_GEN_WAV) {
GGML_ASSERT(params->codes != nullptr);
// codes index the FSQ table via ggml_get_rows, the end-of-audio code has no waveform
const int64_t n_codes = model.c2w.quant_first_cb_w->ne[1];
for (int32_t c : *params->codes) {
if (c < 0 || c >= n_codes) {
LOG_ERR("%s: code out of range (%d, codebook size %lld)\n", __func__, c, (long long) n_codes);
return false;
}
}
std::vector<int32_t> codes = *params->codes;
set_input_i32("inp_codes", codes);
set_gen_state_in();
// the "pos" slot leads the state
float base = 0.0f;
ggml_backend_tensor_get(params->gen_state->slots[0], &base, 0, sizeof(float));
set_lfm3a_window_inputs((int) base, (int) ggml_nelements(get_inp_tensor("positions")), hparams.wav_tfm_swa);
} else {
// rows of the merged per-codebook tables; a masked codebook (-1) takes row 0, the mask drops it
std::vector<int32_t> codes(n_cb, -1);
if (params->codes) {
GGML_ASSERT(params->codes->size() == (size_t) n_cb);
codes = *params->codes;
}
std::vector<float> mask(n_cb, 0.0f);
for (int64_t i = 0; i < n_cb; i++) {
if (codes[i] >= n_vocab) {
LOG_ERR("%s: code out of range (%d, vocab %lld)\n", __func__, codes[i], (long long) n_vocab);
return false;
}
if (codes[i] < 0) {
codes[i] = 0;
} else {
mask[i] = 1.0f;
}
codes[i] += (int32_t) (i * n_vocab);
}
set_input_i32("inp_codes", codes);
if (ggml_graph_get_tensor(gf, "inp_mask") != nullptr) {
set_input_f32("inp_mask", mask);
}
}
} break;
case PROJECTOR_TYPE_QWEN3TTS_GEN:
{
if (params->gen_process == CLIP_GEN_PROCESS_GEN_WAV) {
@@ -6185,6 +6533,31 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {
}
set_input_f32("pos_emb", pos_emb);
} break;
case PROJECTOR_TYPE_LFM3A:
{
GGML_ASSERT(imgs.entries.size() == 1);
const int n_pos = clip_n_output_tokens(ctx, &imgs.entries.front());
// the "pos" slot leads the state; without a stream the state is zero, the clip is encoded from its start
int base = 0;
if (params->enc_state != nullptr) {
const auto & slots = params->enc_state->slots;
float base_f = 0.0f;
ggml_backend_tensor_get(slots[0], &base_f, 0, sizeof(float));
base = (int) base_f;
size_t i = 0;
for (const auto & slot : list_lfm3a_state_slots(hparams, model)) {
ggml_backend_tensor_copy(slots[i++], get_inp_tensor(("state_in_" + slot.name).c_str()));
}
} else {
for (const auto & slot : list_lfm3a_state_slots(hparams, model)) {
ggml_tensor * t = get_inp_tensor(("state_in_" + slot.name).c_str());
ggml_backend_tensor_memset(t, 0, 0, ggml_nbytes(t));
}
}
set_lfm3a_window_inputs(base, n_pos, hparams.attn_window_size);
} break;
case PROJECTOR_TYPE_PARAKEET:
{
GGML_ASSERT(imgs.entries.size() == 1);
@@ -6404,9 +6777,6 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {
return false;
}
// the last node is the embedding tensor, code2wav has no out_embd
ggml_tensor * embeddings = params->out_embd ? ggml_graph_node(gf, -1) : nullptr;
if (embeddings != nullptr) {
// sanity check (assuming that all images in batch have the same number of tokens, so we only check the first one)
const int n_tokens_out = embeddings->ne[1];
@@ -6486,7 +6856,15 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {
out_audio.resize((size_t) n_frames * hop);
}
}
if (params->enc_state != nullptr) {
if (params->enc_state != nullptr && model.proj_type == PROJECTOR_TYPE_LFM3A) {
const auto & slots = params->enc_state->slots;
size_t i = 0;
for (const auto & slot : list_lfm3a_state_slots(hparams, model)) {
ggml_tensor * t = ggml_graph_get_tensor(gf, ("state_out_" + slot.name).c_str());
GGML_ASSERT(t != nullptr);
copy_gen_state_out(t, slots[i++]);
}
} else if (params->enc_state != nullptr) {
clip_enc_state * st = params->enc_state;
ggml_tensor * tail = ggml_graph_get_tensor(gf, "out_stem_tail");
GGML_ASSERT(tail != nullptr);
@@ -6603,6 +6981,7 @@ int clip_n_mmproj_embd(const struct clip_ctx * ctx) {
return ctx->model.mm_2_w->ne[1];
case PROJECTOR_TYPE_QWEN2VL:
case PROJECTOR_TYPE_QWEN25VL:
case PROJECTOR_TYPE_LFM3VL:
case PROJECTOR_TYPE_EXAONE4_5:
case PROJECTOR_TYPE_JANUS_PRO:
case PROJECTOR_TYPE_YOUTUVL:
@@ -6667,6 +7046,7 @@ int clip_n_mmproj_embd(const struct clip_ctx * ctx) {
case PROJECTOR_TYPE_LFM2A:
return ctx->model.position_embeddings->ne[0];
case PROJECTOR_TYPE_D1OMNI_A:
case PROJECTOR_TYPE_LFM3A:
return ctx->model.mm_3_w->ne[1];
case PROJECTOR_TYPE_GRANITE_SPEECH:
return ctx->model.qf_proj_blocks[0].qf_proj_linear_w->ne[1];
@@ -6680,6 +7060,7 @@ int clip_n_mmproj_embd(const struct clip_ctx * ctx) {
case PROJECTOR_TYPE_QWEN3TTS_SPKENC:
return ctx->model.mm_fc_w->ne[2];
case PROJECTOR_TYPE_QWEN3TTS_GEN:
case PROJECTOR_TYPE_LFM3A_GEN:
return ctx->model.gen_code_out_embd_w->ne[0];
case PROJECTOR_TYPE_POCKETTTS_SPKENC:
return ctx->model.spk_proj_w->ne[1];
+257
View File
@@ -0,0 +1,257 @@
#include "models.h"
#include <cmath>
// LFM3-Audio generation
//
// GEN_CODE: one MaskGIT pass of the depthformer, hidden state + known codes (-1 = masked) -> logits per codebook
// once all codes are known, only the feedback embedding is built
// GEN_WAV : a window of frames -> polar STFT frames of the liquid-codec decoder, the ISTFT runs on the host
// state carried between GEN_WAV calls: rope offset, short-conv taps and attention window of the codec decoder
std::vector<c2w_state_slot> list_lfm3a_gen_state_slots(const clip_hparams & hparams, const clip_model & model) {
std::vector<c2w_state_slot> slots;
if (model.c2w.tfm_layers.empty()) {
return slots;
}
const int64_t prefix = hparams.wav_tfm_swa - 1;
slots.push_back({"pos", 1, 1});
for (size_t il = 0; il < model.c2w.tfm_layers.size(); il++) {
const auto & layer = model.c2w.tfm_layers[il];
if (layer.conv_dw_w) {
slots.push_back({"conv_" + std::to_string(il), layer.conv_dw_w->ne[0] - 1, hparams.wav_tfm_n_embd});
} else {
slots.push_back({"k_" + std::to_string(il), layer.k_w->ne[1], prefix});
slots.push_back({"v_" + std::to_string(il), layer.v_w->ne[1], prefix});
}
}
return slots;
}
// pre-norm LFM block, with the LFM3 sandwich norms when the layer has them
ggml_tensor * clip_graph_lfm3a_gen::build_lfm_layer(ggml_tensor * inpL, const clip_layer & layer, float eps, int il,
const std::function<ggml_tensor *(ggml_tensor *)> & op) {
ggml_tensor * cur = build_norm(inpL, layer.ln_1_w, nullptr, NORM_TYPE_RMS, eps, il);
cur = op(cur);
if (layer.attn_post_norm_w) {
cur = build_norm(cur, layer.attn_post_norm_w, nullptr, NORM_TYPE_RMS, eps, il);
}
inpL = ggml_add(ctx0, cur, inpL);
cur = build_norm(inpL, layer.ln_2_w, nullptr, NORM_TYPE_RMS, eps, il);
cur = build_ffn(cur, layer.ff_up_w, nullptr, layer.ff_gate_w, nullptr, layer.ff_down_w, nullptr, FFN_SILU, il);
if (layer.ff_post_norm_w) {
cur = build_norm(cur, layer.ff_post_norm_w, nullptr, NORM_TYPE_RMS, eps, il);
}
return ggml_add(ctx0, inpL, cur);
}
// GQA with q/k RMSNorm, NEOX rope, an optional cached K/V prefix and the LFM3 per-head sigmoid gate
ggml_tensor * clip_graph_lfm3a_gen::build_lfm_attn(ggml_tensor * cur, const clip_layer & layer, int n_head, int n_head_kv,
float eps, float rope_theta, int il, ggml_tensor * inp_pos,
ggml_tensor * kq_mask, ggml_tensor * k_cache, ggml_tensor * v_cache,
ggml_tensor ** k_all, ggml_tensor ** v_all) {
const int64_t n_pos = cur->ne[1];
const int64_t head_dim = layer.q_norm->ne[0];
ggml_tensor * Qcur = ggml_reshape_3d(ctx0, build_mm(layer.q_w, cur), head_dim, n_head, n_pos);
ggml_tensor * Kcur = ggml_reshape_3d(ctx0, build_mm(layer.k_w, cur), head_dim, n_head_kv, n_pos);
ggml_tensor * Vcur = build_mm(layer.v_w, cur);
Qcur = build_norm(Qcur, layer.q_norm, nullptr, NORM_TYPE_RMS, eps, il);
Kcur = build_norm(Kcur, layer.k_norm, nullptr, NORM_TYPE_RMS, eps, il);
Qcur = ggml_rope_ext(ctx0, Qcur, inp_pos, nullptr, head_dim, GGML_ROPE_TYPE_NEOX, 0, rope_theta, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f);
Kcur = ggml_rope_ext(ctx0, Kcur, inp_pos, nullptr, head_dim, GGML_ROPE_TYPE_NEOX, 0, rope_theta, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f);
ggml_tensor * k = ggml_reshape_2d(ctx0, Kcur, head_dim * n_head_kv, n_pos);
ggml_tensor * v = Vcur;
if (k_cache) {
k = ggml_concat(ctx0, k_cache, k, 1);
v = ggml_concat(ctx0, v_cache, v, 1);
}
if (k_all) {
*k_all = k;
*v_all = v;
}
const int64_t n_kv = k->ne[1];
ggml_tensor * attn = build_attn(layer.attn_gate_w ? nullptr : layer.o_w, nullptr,
Qcur,
ggml_reshape_3d(ctx0, k, head_dim, n_head_kv, n_kv),
ggml_reshape_3d(ctx0, v, head_dim, n_head_kv, n_kv),
kq_mask, 1.0f / sqrtf((float) head_dim), il);
if (layer.attn_gate_w) {
ggml_tensor * gate = ggml_sigmoid(ctx0, build_mm(layer.attn_gate_w, cur));
attn = ggml_mul(ctx0, ggml_reshape_3d(ctx0, attn, head_dim, n_head, n_pos), ggml_reshape_3d(ctx0, gate, 1, n_head, n_pos));
attn = build_mm(layer.o_w, ggml_reshape_2d(ctx0, attn, head_dim * n_head, n_pos));
}
return attn;
}
// short conv: in_proj -> b * x -> causal depthwise conv over [conv_state | x] -> c * conv -> out_proj
ggml_tensor * clip_graph_lfm3a_gen::build_lfm_conv(ggml_tensor * cur, const clip_layer & layer, int il,
ggml_tensor * conv_state, ggml_tensor ** conv_state_out) {
const int64_t n_embd_c = cur->ne[0];
const int64_t n_pos = cur->ne[1];
ggml_tensor * bcx = build_mm(layer.conv_pw1_w, cur);
ggml_tensor * b = ggml_view_2d(ctx0, bcx, n_embd_c, n_pos, bcx->nb[1], 0);
ggml_tensor * c = ggml_view_2d(ctx0, bcx, n_embd_c, n_pos, bcx->nb[1], ggml_row_size(bcx->type, n_embd_c));
ggml_tensor * x = ggml_view_2d(ctx0, bcx, n_embd_c, n_pos, bcx->nb[1], ggml_row_size(bcx->type, 2 * n_embd_c));
ggml_tensor * bx = ggml_cont(ctx0, ggml_transpose(ctx0, ggml_mul(ctx0, b, x)));
bx = ggml_concat(ctx0, conv_state, bx, 0);
const int64_t d_conv = conv_state->ne[0];
*conv_state_out = ggml_cont(ctx0, ggml_view_2d(ctx0, bx, d_conv, n_embd_c, bx->nb[1], (size_t) n_pos * bx->nb[0]));
cur = ggml_ssm_conv(ctx0, ggml_reshape_3d(ctx0, bx, bx->ne[0], n_embd_c, 1), layer.conv_dw_w);
cur = ggml_mul(ctx0, c, ggml_reshape_2d(ctx0, cur, n_embd_c, n_pos));
cb(cur, "conv", il);
return build_mm(layer.conv_pw2_w, cur);
}
ggml_tensor * clip_graph_lfm3a_gen::build_gen_code() {
ggml_tensor * embd_w = model.gen_code_embd_w; // [n_embd, n_vocab, n_codebooks], tied to the output head
const int64_t n_cb = embd_w->ne[2];
const int64_t n_vocab = embd_w->ne[1];
// rows of the merged per-codebook tables: code + codebook * n_vocab
ggml_tensor * inp_codes = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_cb);
ggml_set_name(inp_codes, "inp_codes");
ggml_set_input(inp_codes);
// feedback embedding for the backbone
ggml_tensor * fb = ggml_get_rows(ctx0, model.gen_code_out_embd_w, inp_codes); // [n_mmproj_embd, n_cb]
fb = ggml_sum_rows(ctx0, ggml_cont(ctx0, ggml_transpose(ctx0, fb)));
fb = ggml_reshape_2d(ctx0, fb, n_mmproj_embd, 1);
cb(fb, "gen_embd", -1);
if (all_known) {
return fb;
}
ggml_tensor * h_state = ggml_new_tensor_1d(ctx0, GGML_TYPE_F32, n_mmproj_embd);
ggml_set_name(h_state, "inp_raw");
ggml_set_input(h_state);
// 1 for a revealed codebook, 0 for a masked one
ggml_tensor * inp_mask = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, 1, n_cb);
ggml_set_name(inp_mask, "inp_mask");
ggml_set_input(inp_mask);
// one position per codebook: projected hidden state + token embedding or the mask embedding
ggml_tensor * cur = ggml_add(ctx0, build_mm(model.gen_code_proj_in_w, h_state), model.gen_code_proj_in_b);
cur = ggml_reshape_2d(ctx0, cur, n_embd, n_cb);
ggml_tensor * tok = ggml_get_rows(ctx0, ggml_reshape_2d(ctx0, embd_w, n_embd, n_vocab * n_cb), inp_codes);
tok = ggml_mul(ctx0, tok, inp_mask);
ggml_tensor * msk = ggml_mul(ctx0, ggml_repeat(ctx0, model.gen_code_mask_embd, cur), ggml_scale_bias(ctx0, inp_mask, -1.0f, 1.0f));
cur = ggml_add(ctx0, cur, ggml_add(ctx0, tok, msk));
cb(cur, "depth_inp", -1);
// bidirectional: no mask, no state
ggml_tensor * inp_pos = ggml_cast(ctx0, ggml_arange(ctx0, 0.0f, (float) n_cb, 1.0f), GGML_TYPE_I32);
for (int il = 0; il < n_layer; il++) {
const auto & layer = model.layers[il];
cur = build_lfm_layer(cur, layer, eps, il, [&](ggml_tensor * x) {
return build_lfm_attn(x, layer, hparams.n_head, hparams.n_head_kv, eps, hparams.rope_theta, il, inp_pos,
nullptr, nullptr, nullptr, nullptr, nullptr);
});
}
cur = build_norm(cur, model.gen_code_norm_w, nullptr, NORM_TYPE_RMS, eps, -1);
cur = build_norm(cur, model.gen_code_head_norm_w, nullptr, NORM_TYPE_RMS, eps, -1); // per codebook
ggml_tensor * logits = ggml_mul_mat(ctx0, embd_w, ggml_reshape_3d(ctx0, cur, n_embd, 1, n_cb)); // [n_vocab, 1, n_cb]
logits = ggml_reshape_2d(ctx0, logits, n_vocab, n_cb);
ggml_set_name(logits, "out_feats");
ggml_set_output(logits);
ggml_build_forward_expand(gf, logits);
return fb;
}
ggml_tensor * clip_graph_lfm3a_gen::build_gen_wav() {
const auto & c2w = model.c2w;
const int64_t n_cb = model.gen_code_embd_w->ne[2];
const int64_t n_fs = hparams.wav_frame_stack;
const int64_t n_pos = n_fs * n_frames;
const int64_t prefix = hparams.wav_tfm_swa - 1;
// frame-major codes
ggml_tensor * inp_codes = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_cb * n_frames);
ggml_set_name(inp_codes, "inp_codes");
ggml_set_input(inp_codes);
std::map<std::string, ggml_tensor *> state_in;
std::vector<std::pair<std::string, ggml_tensor *>> state_out;
for (const auto & slot : list_lfm3a_gen_state_slots(hparams, model)) {
ggml_tensor * t = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, slot.ne0, slot.ne1);
ggml_set_name(t, ("state_in_" + slot.name).c_str());
ggml_set_input(t);
state_in[slot.name] = t;
}
// FSQ decode: every codebook shares the level table, one projection over the n_cb code vectors of a frame
ggml_tensor * q = ggml_get_rows(ctx0, c2w.quant_first_cb_w, inp_codes); // [n_lvl, n_cb * n_frames]
q = ggml_reshape_2d(ctx0, q, q->ne[0] * n_cb, n_frames);
ggml_tensor * cur = ggml_add(ctx0, build_mm(c2w.quant_first_out_w, q), c2w.quant_first_out_b);
cur = ggml_add(ctx0, build_mm(c2w.tfm_in_proj_w, cur), c2w.tfm_in_proj_b); // [n_embd_dec, n_frames]
const int64_t n_embd_dec = cur->ne[0];
// nearest-exact upsampling: every frame becomes n_fs decoder positions
cur = ggml_repeat_4d(ctx0, ggml_reshape_3d(ctx0, cur, n_embd_dec, 1, n_frames), n_embd_dec, n_fs, n_frames, 1);
cur = ggml_reshape_2d(ctx0, cur, n_embd_dec, n_pos);
// positions continue across calls from the "pos" slot, see clip_encode()
ggml_tensor * inp_pos = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_pos);
ggml_set_name(inp_pos, "positions");
ggml_set_input(inp_pos);
state_out.push_back({"pos", ggml_scale_bias(ctx0, state_in.at("pos"), 1.0f, (float) n_pos)});
// sliding-window causal mask over [cached window | this chunk]
ggml_tensor * kq_mask = build_inp_attn_mask(prefix + n_pos, n_pos);
const float eps_dec = hparams.wav_tfm_eps;
for (size_t il = 0; il < c2w.tfm_layers.size(); il++) {
const auto & layer = c2w.tfm_layers[il];
cur = build_lfm_layer(cur, layer, eps_dec, (int) il, [&](ggml_tensor * x) {
if (layer.conv_dw_w) {
const std::string name = "conv_" + std::to_string(il);
ggml_tensor * st = nullptr;
ggml_tensor * out = build_lfm_conv(x, layer, (int) il, state_in.at(name), &st);
state_out.push_back({name, st});
return out;
}
// this chunk's tail of the window is kept for the next call
const std::string k_name = "k_" + std::to_string(il);
const std::string v_name = "v_" + std::to_string(il);
ggml_tensor * k = nullptr;
ggml_tensor * v = nullptr;
ggml_tensor * out = build_lfm_attn(x, layer, hparams.wav_tfm_n_head, hparams.wav_tfm_n_head_kv, eps_dec,
hparams.wav_tfm_rope_theta, (int) il, inp_pos, kq_mask,
state_in.at(k_name), state_in.at(v_name), &k, &v);
state_out.push_back({k_name, ggml_cont(ctx0, ggml_view_2d(ctx0, k, k->ne[0], prefix, k->nb[1], (size_t) n_pos * k->nb[1]))});
state_out.push_back({v_name, ggml_cont(ctx0, ggml_view_2d(ctx0, v, v->ne[0], prefix, v->nb[1], (size_t) n_pos * v->nb[1]))});
return out;
});
}
cur = build_norm(cur, c2w.tfm_output_norm_w, nullptr, NORM_TYPE_RMS, eps_dec, -1);
cur = ggml_add(ctx0, build_mm(c2w.tfm_out_proj_w, cur), c2w.tfm_out_proj_b); // [2 * n_bins, n_pos]
cb(cur, "polar_out", -1);
for (const auto & s : state_out) {
ggml_set_name(s.second, ("state_out_" + s.first).c_str());
ggml_set_output(s.second);
ggml_build_forward_expand(gf, s.second);
}
ggml_set_name(cur, "out_feats");
ggml_set_output(cur);
return cur;
}
ggml_cgraph * clip_graph_lfm3a_gen::build() {
ggml_tensor * out = gen_process == CLIP_GEN_PROCESS_GEN_WAV ? build_gen_wav() : build_gen_code();
ggml_build_forward_expand(gf, out);
return gf;
}
+189
View File
@@ -0,0 +1,189 @@
#include "models.h"
// streaming encoder state: base position, conv left context, short-conv taps and attention window
// a zero state gives the same result as encoding the whole clip
std::vector<c2w_state_slot> list_lfm3a_state_slots(const clip_hparams & hparams, const clip_model & model) {
std::vector<c2w_state_slot> slots;
const int64_t n_mel = hparams.n_mel_bins;
const int64_t n_ch = model.pre_encode_conv_X_w[0]->ne[3];
const int64_t prefix = hparams.attn_window_size - 1;
slots.push_back({"pos", 1, 1});
slots.push_back({"ds0", 1, n_mel});
slots.push_back({"ds1", 1, (n_mel / 2) * n_ch});
slots.push_back({"ds2", 1, (n_mel / 4) * n_ch});
for (size_t il = 0; il < model.layers.size(); il++) {
const auto & layer = model.layers[il];
if (layer.conv_dw_w) {
slots.push_back({"conv_" + std::to_string(il), layer.conv_dw_w->ne[0] - 1, hparams.n_embd});
} else {
slots.push_back({"k_" + std::to_string(il), layer.k_w->ne[1], prefix});
slots.push_back({"v_" + std::to_string(il), layer.v_w->ne[1], prefix});
}
}
return slots;
}
// LFM3-Audio encoder: conv downsampler (8x in time) -> causal LFM2 stack -> MLP projector
ggml_cgraph * clip_graph_lfm3a::build() {
const norm_type norm_t = NORM_TYPE_RMS;
const float rope_theta = hparams.rope_theta;
const int64_t prefix = hparams.attn_window_size - 1;
// the graph always streams, a zero state encodes a clip from its start
std::map<std::string, ggml_tensor *> state_in;
std::vector<std::pair<std::string, ggml_tensor *>> state_out;
for (const auto & slot : list_lfm3a_state_slots(hparams, model)) {
ggml_tensor * t = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, slot.ne0, slot.ne1);
ggml_set_name(t, ("state_in_" + slot.name).c_str());
ggml_set_input(t);
state_in[slot.name] = t;
}
// stride-2 conv over [time, freq, ch]; the left column comes from the state and the right edge is zero-padded
auto conv_down = [&](ggml_tensor * x, const std::string & name, ggml_tensor * w, ggml_tensor * b, bool depthwise) {
ggml_tensor * left = ggml_reshape_4d(ctx0, state_in.at(name), 1, x->ne[1], x->ne[2], 1);
x = ggml_concat(ctx0, left, x, 0);
ggml_tensor * last = ggml_view_4d(ctx0, x, 1, x->ne[1], x->ne[2], 1, x->nb[1], x->nb[2], x->nb[3], (x->ne[0] - 1) * x->nb[0]);
state_out.push_back({name, ggml_reshape_2d(ctx0, ggml_cont(ctx0, last), 1, x->ne[1] * x->ne[2])});
x = ggml_pad_ext(ctx0, x, 0, 1, 0, 0, 0, 0, 0, 0);
x = depthwise ? ggml_conv_2d_dw_direct(ctx0, w, x, 2, 2, 0, 1, 1, 1)
: ggml_conv_2d(ctx0, w, x, 2, 2, 0, 1, 1, 1);
return ggml_add(ctx0, x, b);
};
// inp: [n_frames, n_mel, 1]
ggml_tensor * cur = build_inp_raw(1);
// downsampler, same layout as the lfm2a pre-encoder but with SiLU
cur = conv_down(cur, "ds0", model.pre_encode_conv_X_w[0], model.pre_encode_conv_X_b[0], false);
cur = ggml_silu_inplace(ctx0, cur);
cur = conv_down(cur, "ds1", model.pre_encode_conv_X_w[2], model.pre_encode_conv_X_b[2], true);
cur = ggml_conv_2d_direct(ctx0, model.pre_encode_conv_X_w[3], cur, 1, 1, 0, 0, 1, 1);
cur = ggml_add(ctx0, cur, model.pre_encode_conv_X_b[3]);
cur = ggml_silu_inplace(ctx0, cur);
cur = conv_down(cur, "ds2", model.pre_encode_conv_X_w[5], model.pre_encode_conv_X_b[5], true);
cur = ggml_conv_2d_direct(ctx0, model.pre_encode_conv_X_w[6], cur, 1, 1, 0, 0, 1, 1);
cur = ggml_add(ctx0, cur, model.pre_encode_conv_X_b[6]);
cur = ggml_silu_inplace(ctx0, cur);
cb(cur, "downsample.conv", -1);
// [n_pos, n_mel/8, n_ch] -> [n_ch * n_mel/8, n_pos]
const int n_pos = cur->ne[0];
cur = ggml_reshape_2d(ctx0, cur, n_pos, cur->ne[1] * cur->ne[2]);
cur = ggml_cont(ctx0, ggml_transpose(ctx0, cur));
cur = build_mm(model.pre_encode_out_w, cur);
cur = ggml_add(ctx0, cur, model.pre_encode_out_b);
cb(cur, "downsample.out", -1);
ggml_tensor * positions = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_pos);
ggml_set_name(positions, "positions");
ggml_set_input(positions);
state_out.push_back({"pos", ggml_scale_bias(ctx0, state_in.at("pos"), 1.0f, (float) n_pos)});
// causal mask with a sliding window, shared by all attention layers
ggml_tensor * kq_mask = build_inp_attn_mask(prefix + n_pos, n_pos);
ggml_tensor * inpL = cur;
for (int il = 0; il < n_layer; il++) {
const auto & layer = model.layers[il];
cur = build_norm(inpL, layer.ln_1_w, nullptr, norm_t, eps, il);
cb(cur, "operator_norm", il);
if (layer.conv_dw_w) {
// short conv: in_proj -> b * x -> causal depthwise conv -> c * conv -> out_proj
ggml_tensor * bcx = build_mm(layer.conv_pw1_w, cur);
cb(bcx, "conv_in_proj", il);
ggml_tensor * b = ggml_view_2d(ctx0, bcx, n_embd, n_pos, bcx->nb[1], 0);
ggml_tensor * c = ggml_view_2d(ctx0, bcx, n_embd, n_pos, bcx->nb[1], ggml_row_size(bcx->type, n_embd));
ggml_tensor * x = ggml_view_2d(ctx0, bcx, n_embd, n_pos, bcx->nb[1], ggml_row_size(bcx->type, 2 * n_embd));
ggml_tensor * bx = ggml_cont(ctx0, ggml_transpose(ctx0, ggml_mul(ctx0, b, x)));
const int d_conv = layer.conv_dw_w->ne[0] - 1;
const std::string name = "conv_" + std::to_string(il);
bx = ggml_concat(ctx0, state_in.at(name), bx, 0);
state_out.push_back({name, ggml_cont(ctx0, ggml_view_2d(ctx0, bx, d_conv, n_embd, bx->nb[1], (size_t) n_pos * bx->nb[0]))});
bx = ggml_reshape_3d(ctx0, bx, bx->ne[0], n_embd, 1);
cur = ggml_ssm_conv(ctx0, bx, layer.conv_dw_w);
cur = ggml_reshape_2d(ctx0, cur, n_embd, n_pos);
cb(cur, "conv_conv", il);
cur = ggml_mul(ctx0, c, cur);
cur = build_mm(layer.conv_pw2_w, cur);
cb(cur, "conv_out_proj", il);
} else {
ggml_tensor * Qcur = build_mm(layer.q_w, cur);
ggml_tensor * Kcur = build_mm(layer.k_w, cur);
ggml_tensor * Vcur = build_mm(layer.v_w, cur);
Qcur = ggml_reshape_3d(ctx0, Qcur, d_head, n_head, n_pos);
Kcur = ggml_reshape_3d(ctx0, Kcur, d_head, n_head_kv, n_pos);
Vcur = ggml_reshape_3d(ctx0, Vcur, d_head, n_head_kv, n_pos);
Qcur = build_norm(Qcur, layer.q_norm, nullptr, norm_t, eps, il);
Kcur = build_norm(Kcur, layer.k_norm, nullptr, norm_t, eps, il);
Qcur = ggml_rope_ext(ctx0, Qcur, positions, nullptr, d_head, GGML_ROPE_TYPE_NEOX, 0,
rope_theta, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f);
Kcur = ggml_rope_ext(ctx0, Kcur, positions, nullptr, d_head, GGML_ROPE_TYPE_NEOX, 0,
rope_theta, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f);
// the cached window goes in front, this chunk's tail is kept for the next call
const std::string k_name = "k_" + std::to_string(il);
const std::string v_name = "v_" + std::to_string(il);
const int64_t n_embd_kv = d_head * n_head_kv;
ggml_tensor * K = ggml_concat(ctx0, state_in.at(k_name), ggml_reshape_2d(ctx0, Kcur, n_embd_kv, n_pos), 1);
ggml_tensor * V = ggml_concat(ctx0, state_in.at(v_name), ggml_reshape_2d(ctx0, Vcur, n_embd_kv, n_pos), 1);
state_out.push_back({k_name, ggml_cont(ctx0, ggml_view_2d(ctx0, K, n_embd_kv, prefix, K->nb[1], (size_t) n_pos * K->nb[1]))});
state_out.push_back({v_name, ggml_cont(ctx0, ggml_view_2d(ctx0, V, n_embd_kv, prefix, V->nb[1], (size_t) n_pos * V->nb[1]))});
Kcur = ggml_reshape_3d(ctx0, K, d_head, n_head_kv, prefix + n_pos);
Vcur = ggml_reshape_3d(ctx0, V, d_head, n_head_kv, prefix + n_pos);
cur = build_attn(layer.o_w, nullptr, Qcur, Kcur, Vcur, kq_mask, kq_scale, il);
cb(cur, "attn_out", il);
}
cur = ggml_add(ctx0, cur, inpL);
inpL = cur;
cur = build_norm(cur, layer.ln_2_w, nullptr, norm_t, eps, il);
cb(cur, "ffn_norm", il);
cur = build_ffn(cur,
layer.ff_up_w, nullptr,
layer.ff_gate_w, nullptr,
layer.ff_down_w, nullptr,
FFN_SILU, il);
cb(cur, "ffn_out", il);
cur = ggml_add(ctx0, inpL, cur);
inpL = cur;
}
cur = build_norm(inpL, model.post_ln_w, nullptr, norm_t, eps, -1);
cb(cur, "embedding_norm", -1);
// projector: LayerNorm -> Linear -> GELU -> Linear
cur = build_norm(cur, model.mm_0_w, model.mm_0_b, NORM_TYPE_NORMAL, 1e-5, -1);
cur = build_ffn(cur, model.mm_1_w, model.mm_1_b, nullptr, nullptr, model.mm_3_w, model.mm_3_b, FFN_GELU_ERF, -1);
cb(cur, "projected", -1);
// the state outputs go first so that the embeddings stay the last node of the graph
for (const auto & s : state_out) {
ggml_set_name(s.second, ("state_out_" + s.first).c_str());
ggml_set_output(s.second);
ggml_build_forward_expand(gf, s.second);
}
ggml_build_forward_expand(gf, cur);
return gf;
}
+182
View File
@@ -0,0 +1,182 @@
#include "models.h"
ggml_cgraph * clip_graph_lfm3vl::build() {
GGML_ASSERT(model.patch_embeddings_0 != nullptr);
GGML_ASSERT(model.lfm3vl_embedding_norm_w != nullptr);
GGML_ASSERT(model.class_embedding == nullptr);
GGML_ASSERT(n_batch == 1);
const int batch_size = 1;
const int n_pos = n_patches;
const int num_position_ids = n_pos * 4; // m-rope requires 4 dim per position
const norm_type norm_t = NORM_TYPE_RMS;
// 2D rope: pairs (j, j+n_dims); first d_head/4 pairs rotate by y, next d_head/4 by x
int mrope_sections[4] = {d_head/4, d_head/4, 0, 0};
ggml_tensor * inp_raw = build_inp_raw();
// one patchifier kernel per temporal slice: a still image fills both slices, so both kernels apply to it
ggml_tensor * inp = ggml_conv_2d(ctx0, model.patch_embeddings_0, inp_raw, patch_size, patch_size, 0, 0, 1, 1);
if (model.patch_embeddings_1) {
inp = ggml_add(ctx0, inp, ggml_conv_2d(ctx0, model.patch_embeddings_1, inp_raw, patch_size, patch_size, 0, 0, 1, 1));
}
inp = ggml_reshape_2d(ctx0, inp, n_pos, n_embd);
inp = ggml_cont(ctx0, ggml_transpose(ctx0, inp));
cb(inp, "patchifier_out", -1);
// the 2x2 interleave waits for the projector, the conv layers need raster-scan order
inp = build_norm(inp, model.lfm3vl_embedding_norm_w, nullptr, norm_t, eps, -1);
cb(inp, "embedding_norm", -1);
ggml_tensor * inpL = inp;
// position IDs for 2D RoPE (same format as Qwen3VL)
ggml_tensor * positions = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, num_position_ids);
ggml_set_name(positions, "positions");
ggml_set_input(positions);
const float rope_theta = hparams.rope_theta;
for (int il = 0; il < n_layer; il++) {
auto & layer = model.layers[il];
const bool is_conv_layer = (layer.conv2d_in_proj_w != nullptr);
ggml_tensor * cur = inpL;
cur = build_norm(cur, layer.ln_1_w, layer.ln_1_b, norm_t, eps, il);
cb(cur, "ln1", il);
if (is_conv_layer) {
ggml_tensor * proj = build_mm(layer.conv2d_in_proj_w, cur);
cb(proj, "conv_in_proj", il);
ggml_tensor * first = ggml_view_2d(ctx0, proj, n_embd, n_pos,
proj->nb[1], 0);
ggml_tensor * middle = ggml_view_2d(ctx0, proj, n_embd, n_pos,
proj->nb[1], ggml_row_size(proj->type, n_embd));
ggml_tensor * third = ggml_view_2d(ctx0, proj, n_embd, n_pos,
proj->nb[1], ggml_row_size(proj->type, 2 * n_embd));
ggml_tensor * gated = ggml_mul(ctx0, first, third);
cb(gated, "conv_gated", il);
ggml_tensor * conv_out;
if (layer.conv2d_spatial_w) {
ggml_tensor * gated_3d = ggml_reshape_4d(ctx0, gated,
n_embd, n_patches_x, n_patches_y, batch_size);
ggml_tensor * gated_2d = ggml_cont(ctx0,
ggml_permute(ctx0, gated_3d, 2, 0, 1, 3));
cb(gated_2d, "conv_gated_2d", il);
conv_out = ggml_conv_2d_dw_direct(ctx0, layer.conv2d_spatial_w, gated_2d,
1, 1, 1, 1, 1, 1);
cb(conv_out, "conv_spatial", il);
conv_out = ggml_cont(ctx0,
ggml_permute(ctx0, conv_out, 1, 2, 0, 3));
conv_out = ggml_reshape_2d(ctx0, conv_out, n_embd, n_pos);
} else {
conv_out = gated;
}
cur = ggml_mul(ctx0, middle, conv_out);
cb(cur, "conv_mul", il);
cur = build_mm(layer.conv2d_out_proj_w, cur);
cb(cur, "conv_out_proj", il);
} else {
ggml_tensor * Qcur = build_mm(layer.q_w, cur);
ggml_tensor * Kcur = build_mm(layer.k_w, cur);
ggml_tensor * Vcur = build_mm(layer.v_w, cur);
cb(Qcur, "Qcur", il);
cb(Kcur, "Kcur", il);
cb(Vcur, "Vcur", il);
Qcur = ggml_reshape_3d(ctx0, Qcur, d_head, n_head, n_pos);
Kcur = ggml_reshape_3d(ctx0, Kcur, d_head, n_head_kv, n_pos);
Vcur = ggml_reshape_3d(ctx0, Vcur, d_head, n_head_kv, n_pos);
if (layer.q_norm) {
Qcur = build_norm(Qcur, layer.q_norm, nullptr, norm_t, eps, il);
}
if (layer.k_norm) {
Kcur = build_norm(Kcur, layer.k_norm, nullptr, norm_t, eps, il);
}
Qcur = ggml_rope_multi(
ctx0, Qcur, positions, nullptr,
d_head/2, mrope_sections, GGML_ROPE_TYPE_VISION,
32768, rope_theta, 1, 0, 1, 32, 1);
Kcur = ggml_rope_multi(
ctx0, Kcur, positions, nullptr,
d_head/2, mrope_sections, GGML_ROPE_TYPE_VISION,
32768, rope_theta, 1, 0, 1, 32, 1);
cb(Qcur, "Qcur_rope", il);
cb(Kcur, "Kcur_rope", il);
cur = build_attn(layer.o_w, layer.o_b,
Qcur, Kcur, Vcur, nullptr, kq_scale, il);
cb(cur, "attn_out", il);
}
cur = ggml_add(ctx0, cur, inpL);
inpL = cur;
cb(cur, "ffn_inp", il);
cur = build_norm(cur, layer.ln_2_w, layer.ln_2_b, norm_t, eps, il);
cb(cur, "ffn_inp_normed", il);
cur = build_ffn(cur,
layer.ff_up_w, layer.ff_up_b,
layer.ff_gate_w, layer.ff_gate_b,
layer.ff_down_w, layer.ff_down_b,
hparams.ffn_op, il);
cb(cur, "ffn_out", il);
cur = ggml_add(ctx0, inpL, cur);
cb(cur, "layer_out", il);
inpL = cur;
}
// no post-layernorm for LFM3-VL
ggml_tensor * embeddings = inpL;
// projector input LayerNorm, eps is projector_layer_norm_eps (not the encoder eps)
embeddings = build_norm(embeddings, model.mm_input_norm_w, model.mm_input_norm_b, NORM_TYPE_NORMAL, 1e-6f, -1);
cb(embeddings, "proj_input_norm", -1);
// 2x2 patch interleave (same as Qwen3VL) so the reshape below merges spatial 2x2 blocks
{
embeddings = ggml_cont_4d(
ctx0, embeddings,
n_embd * 2, n_patches_x / 2, n_patches_y, batch_size);
embeddings = ggml_reshape_4d(
ctx0, embeddings,
n_embd * 2, n_patches_x / 2, 2, batch_size * (n_patches_y / 2));
embeddings = ggml_permute(ctx0, embeddings, 0, 2, 1, 3);
embeddings = ggml_cont_3d(
ctx0, embeddings,
n_embd, n_patches_x * n_patches_y, batch_size);
}
cb(embeddings, "proj_interleaved", -1);
embeddings = ggml_reshape_3d(ctx0, embeddings, n_embd * 4, n_pos / 4, batch_size);
// projector MLP: linear_1 -> GELU (erf, as HF "gelu") -> linear_2
embeddings = build_ffn(embeddings,
model.mm_0_w, model.mm_0_b,
nullptr, nullptr,
model.mm_1_w, model.mm_1_b,
ffn_op_type::FFN_GELU_ERF, -1);
ggml_build_forward_expand(gf, embeddings);
return gf;
}
+44
View File
@@ -2,6 +2,7 @@
#include "../clip-graph.h"
#include <functional>
#include <map>
#include <string>
#include <utility>
@@ -54,6 +55,11 @@ struct clip_graph_sensenova_u1 : clip_graph {
ggml_cgraph * build() override;
};
struct clip_graph_lfm3vl : clip_graph {
clip_graph_lfm3vl(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
ggml_cgraph * build() override;
};
struct clip_graph_ling3vl : clip_graph_qwen3vl {
clip_graph_ling3vl(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph_qwen3vl(ctx, img) {}
ggml_cgraph * build() override;
@@ -184,6 +190,11 @@ struct clip_graph_conformer : clip_graph {
ggml_cgraph * build() override;
};
struct clip_graph_lfm3a : clip_graph {
clip_graph_lfm3a(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
ggml_cgraph * build() override;
};
struct clip_graph_granite_speech : clip_graph {
clip_graph_granite_speech(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
ggml_cgraph * build() override;
@@ -428,6 +439,39 @@ struct clip_graph_soprano : clip_graph {
int n_frames;
};
//
// LFM3-Audio generation:
// GEN_CODE = one MaskGIT pass of the depthformer, or only the feedback embedding once all codes are known
// GEN_WAV = liquid-codec decoder, a window of frames -> polar STFT frames
//
struct clip_graph_lfm3a_gen : clip_graph {
clip_graph_lfm3a_gen(clip_ctx * ctx, const clip_image_f32 & img, clip_gen_process_type gen_process, bool all_known, int n_frames)
: clip_graph(ctx, img), gen_process(gen_process), all_known(all_known), n_frames(n_frames) {}
ggml_cgraph * build() override;
clip_gen_process_type gen_process;
bool all_known; // GEN_CODE only
int n_frames; // GEN_WAV only
ggml_tensor * build_gen_code();
ggml_tensor * build_gen_wav();
ggml_tensor * build_lfm_layer(ggml_tensor * inpL, const clip_layer & layer, float eps, int il,
const std::function<ggml_tensor *(ggml_tensor *)> & op);
ggml_tensor * build_lfm_attn(ggml_tensor * cur, const clip_layer & layer, int n_head, int n_head_kv,
float eps, float rope_theta, int il, ggml_tensor * inp_pos,
ggml_tensor * kq_mask, ggml_tensor * k_cache, ggml_tensor * v_cache,
ggml_tensor ** k_all, ggml_tensor ** v_all);
ggml_tensor * build_lfm_conv(ggml_tensor * cur, const clip_layer & layer, int il,
ggml_tensor * conv_state, ggml_tensor ** conv_state_out);
};
// state slots of the streaming codec decoder (lfm3a_gen GEN_WAV)
std::vector<c2w_state_slot> list_lfm3a_gen_state_slots(const clip_hparams & hparams, const clip_model & model);
// state slots of the streaming LFM3-Audio encoder (lfm3a)
std::vector<c2w_state_slot> list_lfm3a_state_slots(const clip_hparams & hparams, const clip_model & model);
struct clip_graph_kimik25 : clip_graph {
clip_graph_kimik25(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
ggml_cgraph * build() override;
+55
View File
@@ -1055,6 +1055,61 @@ bool mtmd_audio_preprocessor_d1omni::preprocess(const float * sa
return true;
}
//
// mtmd_audio_preprocessor_lfm3a
//
void mtmd_audio_preprocessor_lfm3a::initialize() {
cache.fill_sin_cos_table(hparams.audio_n_fft);
cache.fill_hann_window(hparams.audio_window_len, true);
cache.fill_mel_filterbank_matrix(hparams.n_mel_bins, hparams.audio_n_fft, hparams.audio_sample_rate);
}
bool mtmd_audio_preprocessor_lfm3a::preprocess(const float * samples,
size_t n_samples,
std::vector<mtmd_audio_mel> & output) const {
if (n_samples == 0) {
return false;
}
const size_t win = hparams.audio_window_len;
const size_t hop = hparams.audio_hop_len;
// pad on the right so that no sample is dropped by the last frame
size_t n_padded = win;
if (n_samples > win) {
n_padded = n_samples + (hop - (n_samples - win) % hop) % hop;
}
std::vector<float> padded(samples, samples + n_samples);
padded.resize(n_padded, 0.0f);
filter_params params;
params.n_mel = hparams.n_mel_bins;
params.n_fft_bins = 1 + (hparams.audio_n_fft / 2);
params.hann_window_size = hparams.audio_window_len;
params.hop_length = hparams.audio_hop_len;
params.sample_rate = hparams.audio_sample_rate;
params.no_padding = true;
params.mel_floor = 1e-12f; // log10 floor of -12
GGML_ASSERT(!cache.sin_vals.empty());
GGML_ASSERT(!cache.cos_vals.empty());
GGML_ASSERT(!cache.filters.data.empty());
mtmd_audio_mel out;
bool ok = log_mel_spectrogram(padded.data(), (int) padded.size(), 4, params, cache, out);
if (!ok) {
return false;
}
for (auto & v : out.data) {
v = (v + 4.0f) / 4.0f;
}
output.push_back(std::move(out));
return true;
}
//
// mtmd_audio_preprocessor_granite_speech
//
+9
View File
@@ -95,6 +95,15 @@ struct mtmd_audio_preprocessor_d1omni : mtmd_audio_preprocessor_conformer {
bool preprocess(const float * samples, size_t n_samples, std::vector<mtmd_audio_mel> & output) const override;
};
struct mtmd_audio_preprocessor_lfm3a : mtmd_audio_preprocessor {
mtmd_audio_preprocessor_lfm3a(const clip_ctx * ctx) : mtmd_audio_preprocessor(ctx) {}
void initialize() override;
bool preprocess(const float * samples, size_t n_samples, std::vector<mtmd_audio_mel> & output) const override;
private:
mtmd_audio_cache cache;
};
struct mtmd_audio_preprocessor_granite_speech : mtmd_audio_preprocessor {
mtmd_audio_preprocessor_granite_speech(const clip_ctx * ctx) : mtmd_audio_preprocessor(ctx) {}
void initialize() override;
+15 -11
View File
@@ -91,8 +91,6 @@ struct mtmd_cli_context {
mtmd_helper_init_opt init_opt = mtmd_helper_init_opt_default();
std::string video_ffmpeg_bin_dir;
mtmd::batch_ptr mbatch;
// chat template
common_chat_templates_ptr tmpls;
std::vector<common_chat_msg> chat_history;
@@ -122,7 +120,8 @@ struct mtmd_cli_context {
init_vision_context(params);
mtmd_cap = mtmd_helper_get_capabilities(model, ctx_vision.get());
if (!mtmd_cap.chat) {
// a realtime transcription model has no chat, it streams the audio below
if (!mtmd_cap.chat && !mtmd_cap.audio_stream) {
LOG_ERR("Model does not support chat mode\n");
LOG_ERR("Hint: for TTS models, please use llama-tts\n");
exit(1);
@@ -408,6 +407,8 @@ static int eval_message(mtmd_cli_context & ctx, common_chat_msg & msg) {
}
mtmd::input_chunks chunks(mtmd_input_chunks_init());
// mbatch must not outlive the chunks it references
mtmd::batch_ptr mbatch;
int32_t res = mtmd_tokenize_from_parts(ctx.ctx_vision.get(),
chunks.ptr.get(), // output
parts_ptr.data(),
@@ -446,8 +447,8 @@ static int eval_message(mtmd_cli_context & ctx, common_chat_msg & msg) {
} else {
// media chunk: try to get embd from existing batch, or create a new batch
float * embd = nullptr;
if (ctx.mbatch) {
embd = mtmd_batch_get_output_embd(ctx.mbatch.get(), chunk);
if (mbatch) {
embd = mtmd_batch_get_output_embd(mbatch.get(), chunk);
if (embd) {
LOG_DBG("found embd for media chunk %zu in existing batch\n", i);
@@ -458,8 +459,8 @@ static int eval_message(mtmd_cli_context & ctx, common_chat_msg & msg) {
if (!embd) {
// create and encode a new batch with as many media chunks as possible
ctx.mbatch.reset(mtmd_batch_init(ctx.ctx_vision.get()));
res = mtmd_batch_add_chunk(ctx.mbatch.get(), chunk);
mbatch.reset(mtmd_batch_init(ctx.ctx_vision.get()));
res = mtmd_batch_add_chunk(mbatch.get(), chunk);
GGML_ASSERT(res == 0); // first chunk must always succeed
int n_added = 1;
@@ -470,7 +471,7 @@ static int eval_message(mtmd_cli_context & ctx, common_chat_msg & msg) {
if (next_type == MTMD_INPUT_CHUNK_TYPE_TEXT) {
break; // text chunk splits the batch
}
res = mtmd_batch_add_chunk(ctx.mbatch.get(), next_chunk);
res = mtmd_batch_add_chunk(mbatch.get(), next_chunk);
if (res != 0) {
break; // batch full or incompatible
}
@@ -479,14 +480,14 @@ static int eval_message(mtmd_cli_context & ctx, common_chat_msg & msg) {
int64_t time_start = ggml_time_ms();
LOG_INF("encoding mtmd batch, n_chunks = %d (done = %zu, total = %zu)\n", n_added, i, n_chunks);
res = mtmd_batch_encode(ctx.mbatch.get());
res = mtmd_batch_encode(mbatch.get());
if (res != 0) {
LOG_ERR("Failed to encode mtmd batch, res = %d\n", res);
return 1;
}
LOG_INF("mtmd batch encoding done in %d ms\n", (int)(ggml_time_ms() - time_start));
embd = mtmd_batch_get_output_embd(ctx.mbatch.get(), chunk);
embd = mtmd_batch_get_output_embd(mbatch.get(), chunk);
}
GGML_ASSERT(embd != nullptr);
@@ -567,7 +568,10 @@ int main(int argc, char ** argv) {
if (g_is_interrupted) return 130;
if (ctx.mtmd_cap.audio_stream) {
// realtime transcription models (no chat) transcribe while they listen, the others answer after the audio
const bool transcribes = ctx.mtmd_cap.audio_stream && !ctx.mtmd_cap.chat;
if (transcribes) {
if (params.image.size() != 1) {
LOG_ERR("This model transcribes audio in realtime, pass exactly one --audio file\n");
return 1;
+491
View File
@@ -8,7 +8,9 @@
#include <cctype>
#include <cmath>
#include <cstring>
#include <functional>
#include <memory>
#include <random>
#include <string>
#include <unordered_map>
#include <vector>
@@ -103,6 +105,11 @@ public:
virtual int32_t get_output(int32_t * out_sample_rate, const char ** out_data, size_t * out_data_len, int64_t * out_n_samples) = 0;
// forces any buffered codes through code2wav now, regardless of window_frames
virtual int32_t flush() = 0;
// continuing a sequence (inp->n_past > 0) with the model's own text/audio switching
virtual bool supports_continue() const { return false; }
virtual bool next_is_text() const { return false; }
std::vector<llama_logit_bias> logit_bias; // see mtmd_helper_gen_audio_get_logit_bias()
protected:
// one state per generation, so a fixed seed always gives the same output
@@ -1261,6 +1268,455 @@ private:
bool sent = false;
};
// LFM3-Audio TTS, see generate() in the reference: each step samples a frame of codes by MaskGIT
// and feeds its embedding back, the end-of-audio code stops
class lfm3a_gen_audio_pipeline : public mtmd_gen_audio_pipeline {
public:
using mtmd_gen_audio_pipeline::mtmd_gen_audio_pipeline;
void reset() override {
seq_id = 0;
pos = 0;
continuing = false;
mode = MODE_AUDIO;
slots_left = 0;
speaking = false;
text_done = false;
prompt_tokens.clear();
prompt_pos = 0;
hist.clear();
gen_state.reset();
pending_tail = false;
pcm_sent = 0;
wav_header_sent = false;
audio_pcm.clear();
h_state_buf.clear();
out_buf.clear();
}
int32_t set_input(const mtmd_helper_gen_audio_inp * inp) override {
reset();
seq_id = inp->seq_id;
if (inp->speaker_ref) {
LOG_WRN("mtmd_helper_gen_audio: the model has no voice conditioning, the speaker file is ignored\n");
}
// the prompt is rendered by the caller: the "tts" chat template of the model, or the next turn of a conversation
continuing = inp->n_past > 0;
pos = continuing ? inp->n_past : 0;
const std::string full(inp->prompt, inp->prompt_len);
prompt_tokens.resize(full.size() + 16);
const int n_ids = llama_tokenize(vocab, full.c_str(), (int32_t) full.size(), prompt_tokens.data(),
(int32_t) prompt_tokens.size(), false, true);
if (n_ids <= 0) {
LOG_ERR("mtmd_helper_gen_audio: tokenization failed\n");
return 1;
}
prompt_tokens.resize((size_t) n_ids);
if (!find_tokens()) {
return 1;
}
// a continued response opens with the token of the asked mode, a text-only one cannot switch to audio
if (continuing) {
switch (inp->out_mode) {
case MTMD_HELPER_GEN_AUDIO_OUT_AUDIO: prompt_tokens.push_back(tok_audio_start); break;
case MTMD_HELPER_GEN_AUDIO_OUT_MIXED: prompt_tokens.push_back(tok_mixed_start); break;
case MTMD_HELPER_GEN_AUDIO_OUT_TEXT:
logit_bias = { { tok_audio_start, -INFINITY }, { tok_mixed_start, -INFINITY } };
break;
default: break;
}
}
// the last prompt token picks the mode, see generate() of the reference
const llama_token last = prompt_tokens.back();
mode = last == tok_audio_start ? MODE_AUDIO : last == tok_mixed_start ? MODE_MIXED : MODE_TEXT;
slots_left = n_text_block;
if (!continuing && mode != MODE_AUDIO) {
LOG_ERR("mtmd_helper_gen_audio: the prompt must come from the \"tts\" chat template of the model\n");
return 1;
}
// the sampling defaults of the model, the caller's top-k/top-p are not used
const mtmd_gen_inp def = mtmd_gen_inp_default(mctx);
temp = def.temp;
top_k = def.top_k;
rep_penalty = def.penalty_repeat;
rep_window = def.penalty_last_n;
out_type = inp->out_type;
stream = inp->stream;
rng.seed(inp->seed == UINT32_MAX ? std::random_device{}() : inp->seed);
return init_gen_state(inp->seed) ? 0 : 1;
}
int32_t step_prompt(int32_t n_batch) override {
GGML_ASSERT(n_batch > 0);
const int n_prompt = (int) prompt_tokens.size();
if (prompt_pos >= n_prompt) {
return 0;
}
const int n = std::min(n_batch, n_prompt - prompt_pos);
llama_batch batch = llama_batch_init(n, 0, 1);
for (int i = 0; i < n; i++) {
batch.token [i] = prompt_tokens[(size_t) (prompt_pos + i)];
batch.pos [i] = pos + i;
batch.n_seq_id[i] = 1;
batch.seq_id [i][0] = seq_id;
batch.logits [i] = prompt_pos + i == n_prompt - 1;
}
batch.n_tokens = n;
const int ret = llama_decode(lctx, batch);
llama_batch_free(batch);
if (ret != 0) {
LOG_ERR("mtmd_helper_gen_audio: prompt decode failed\n");
return -1;
}
pos += n;
prompt_pos += n;
return n_prompt - prompt_pos;
}
bool supports_continue() const override {
return true;
}
bool next_is_text() const override {
return mode == MODE_TEXT || (mode == MODE_MIXED && !speaking);
}
// one generate() step: a text token from the caller or an audio frame from the hidden state
int32_t step_gen(llama_token sampled, const float * h_state_in, const float ** h_state_out, bool * out_stop) override {
const bool interleaving = mode == MODE_MIXED;
if (next_is_text()) {
// <|im_end|> ends the response in any text slot, <|text_end|> outside an interleaved block too
if (sampled == tok_im_end || (!interleaving && sampled == tok_text_end)) {
*out_stop = true;
*h_state_out = nullptr;
return 0;
}
// the switching tokens are fed back like any text token
if (!interleaving) {
if (sampled == tok_audio_start) {
mode = MODE_AUDIO;
} else if (sampled == tok_mixed_start) {
mode = MODE_MIXED;
slots_left = n_text_block;
speaking = false;
text_done = false;
}
} else if (sampled == tok_text_end) {
text_done = true;
speaking = true;
}
llama_batch batch = llama_batch_get_one(&sampled, 1);
if (!decode(batch)) {
return 1;
}
} else {
std::vector<int32_t> codes;
const float * fb = nullptr;
bool is_eos = false;
if (!sample_frame(h_state_in, codes, &fb, &is_eos)) {
return 1;
}
if (is_eos) {
if (!continuing) {
// TTS: the end of audio ends the generation
*out_stop = true;
*h_state_out = nullptr;
return 0;
}
// the segment ends, the codec tail goes out and the response continues in text
if (flush() != 0) {
return 1;
}
mode = MODE_TEXT;
} else {
hist.insert(hist.end(), codes.begin(), codes.end());
if (!decode_wav(codes.data(), codes.size())) {
return 1;
}
}
decode_embd_batch batch_embd(const_cast<float *>(fb), 1, 1, n_embd);
batch_embd.set_position_normal(pos, seq_id);
batch_embd.logits[0] = 1;
if (llama_process(lctx, LLAMA_PROCESS_TYPE_DECODE, batch_embd.render(lctx, 0, 1)) != 0) {
LOG_ERR("mtmd_helper_gen_audio: decode failed\n");
return 1;
}
pos++;
}
// a slot emitted while interleaving counts down the block, the switching token itself does not
if (interleaving && !text_done && --slots_left == 0) {
speaking = !speaking;
slots_left = speaking ? n_audio_block : n_text_block;
}
const float * he = llama_get_embeddings_ith(lctx, -1);
h_state_buf.assign(he, he + n_embd);
*h_state_out = h_state_buf.data();
return 0;
}
int32_t get_output(int32_t * out_sample_rate, const char ** out_data, size_t * out_data_len, int64_t * out_n_samples) override {
*out_sample_rate = info.sample_rate;
if (!stream) {
if (flush() != 0) {
return 1;
}
if (out_n_samples) {
*out_n_samples = (int64_t) audio_pcm.size();
}
if (out_type == MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM) {
*out_data = (const char *) audio_pcm.data();
*out_data_len = audio_pcm.size() * sizeof(float);
return 0;
}
out_buf.clear();
if (!write_wav16(out_buf, audio_pcm, info.sample_rate)) {
LOG_ERR("mtmd_helper_gen_audio: output too large for WAV\n");
return 1;
}
*out_data = out_buf.data();
*out_data_len = out_buf.size();
return 0;
}
// streaming: only the audio produced since the previous call, every frame is decoded as it is sampled
const size_t n_new = audio_pcm.size() - pcm_sent;
if (out_n_samples) {
*out_n_samples = (int64_t) n_new;
}
if (out_type == MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM) {
*out_data = (const char *) (audio_pcm.data() + pcm_sent);
*out_data_len = n_new * sizeof(float);
pcm_sent = audio_pcm.size();
return 0;
}
out_buf.clear();
if (!wav_header_sent) {
write_wav16_header(out_buf, UINT32_MAX, info.sample_rate);
wav_header_sent = true;
}
append_wav16_pcm(out_buf, audio_pcm.data() + pcm_sent, n_new);
pcm_sent = audio_pcm.size();
*out_data = out_buf.data();
*out_data_len = out_buf.size();
return 0;
}
// the ISTFT holds back the overlap of the last frame
int32_t flush() override {
if (pending_tail) {
if (!decode_wav(nullptr, 0)) {
return 1;
}
pending_tail = false;
}
return 0;
}
private:
// checked by the converter
static constexpr int n_cb = 12;
static constexpr int schedule[] = { 4, 4, 4 }; // codebooks revealed per depthformer pass
// a random order of the codebooks, revealed by the schedule; fb is the feedback embedding of the codes.
// at the end of audio every code is the end code, its feedback embedding is only built when continuing
bool sample_frame(const float * h_state, std::vector<int32_t> & codes, const float ** fb, bool * is_eos) {
std::vector<int> order(n_cb);
for (int i = 0; i < n_cb; i++) {
order[(size_t) i] = i;
}
std::shuffle(order.begin(), order.end(), rng);
codes.assign(n_cb, -1);
mtmd_gen_inp inp = mtmd_gen_inp_default(mctx);
inp.type = MTMD_GEN_PROCESS_TYPE_GEN_CODE;
inp.embd = const_cast<float *>(h_state);
inp.codes = codes.data();
inp.n_codes = codes.size();
mtmd_gen_out out{};
int k = 0;
for (int n_pick : schedule) {
if (mtmd_gen_audio_process(mctx, gen_state.get(), &inp, &out) != 0 || out.n_feats == 0 || out.n_feats % n_cb != 0) {
LOG_ERR("mtmd_helper_gen_audio: depthformer pass failed\n");
return false;
}
const size_t n_vocab = out.n_feats / n_cb;
logits.assign(out.feats, out.feats + out.n_feats);
for (int j = 0; j < n_pick; j++, k++) {
const int i = order[(size_t) k];
float * l = logits.data() + (size_t) i * n_vocab;
apply_rep_penalty(l, n_vocab, i);
codes[(size_t) i] = sample(l, n_vocab);
}
}
// the last code of every codebook ends the audio
const int32_t eos = (int32_t) (logits.size() / n_cb) - 1;
*is_eos = std::find(codes.begin(), codes.end(), eos) != codes.end();
if (*is_eos) {
codes.assign(n_cb, eos);
if (!continuing) {
*fb = nullptr;
return true;
}
}
// all codes known: only the feedback embedding
if (mtmd_gen_audio_process(mctx, gen_state.get(), &inp, &out) != 0) {
LOG_ERR("mtmd_helper_gen_audio: codebook embedding failed\n");
return false;
}
fb_buf.assign(out.embd, out.embd + n_embd);
*fb = fb_buf.data();
return true;
}
// CTRL penalty over the codes codebook cb took in the last rep_window frames
void apply_rep_penalty(float * l, size_t n_vocab, int cb) const {
if (rep_penalty <= 1.0f || rep_window <= 0) {
return;
}
const size_t n_hist = hist.size() / n_cb;
const size_t n_win = std::min(n_hist, (size_t) rep_window);
std::vector<bool> seen(n_vocab, false);
for (size_t f = n_hist - n_win; f < n_hist; f++) {
const int32_t c = hist[f * n_cb + (size_t) cb];
if (c >= 0 && (size_t) c < n_vocab && !seen[(size_t) c]) {
seen[(size_t) c] = true;
l[c] = l[c] > 0.0f ? l[c] / rep_penalty : l[c] * rep_penalty;
}
}
}
// TODO: reuse the samplers of common (top-k, temperature, penalties) instead of this one
int32_t sample(const float * l, size_t n_vocab) {
if (temp <= 0.0f) {
return (int32_t) (std::max_element(l, l + n_vocab) - l);
}
// top-k keeps the codes scoring at least the k-th best, ties included
float lmin = -INFINITY;
if (top_k > 0 && (size_t) top_k < n_vocab) {
std::vector<float> sorted(l, l + n_vocab);
std::nth_element(sorted.begin(), sorted.begin() + (top_k - 1), sorted.end(), std::greater<float>());
lmin = sorted[(size_t) top_k - 1];
}
const float lmax = *std::max_element(l, l + n_vocab);
std::vector<double> prob(n_vocab);
double sum = 0.0;
for (size_t v = 0; v < n_vocab; v++) {
prob[v] = l[v] < lmin ? 0.0 : std::exp((double) (l[v] - lmax) / temp);
sum += prob[v];
}
double r = std::uniform_real_distribution<double>(0.0, 1.0)(rng) * sum;
for (size_t v = 0; v < n_vocab; v++) {
r -= prob[v];
if (r <= 0.0) {
return (int32_t) v;
}
}
return (int32_t) n_vocab - 1;
}
// n_codes == 0 flushes the ISTFT tail
bool decode_wav(const int32_t * codes, size_t n_codes) {
mtmd_gen_inp inp = mtmd_gen_inp_default(mctx);
inp.type = MTMD_GEN_PROCESS_TYPE_GEN_WAV;
inp.codes = const_cast<int32_t *>(codes);
inp.n_codes = n_codes;
mtmd_gen_out out{};
if (mtmd_gen_audio_process(mctx, gen_state.get(), &inp, &out) != 0) {
LOG_ERR("mtmd_helper_gen_audio: codec decode failed\n");
return false;
}
audio_pcm.insert(audio_pcm.end(), out.audio, out.audio + out.n_samples);
pending_tail = n_codes > 0;
return true;
}
bool find_tokens() {
tok_audio_start = find_special_token(vocab, "<|audio_start|>");
tok_mixed_start = find_special_token(vocab, "<|mixed_start|>");
tok_text_end = find_special_token(vocab, "<|text_end|>");
tok_im_end = find_special_token(vocab, "<|im_end|>");
if (tok_audio_start == LLAMA_TOKEN_NULL || tok_mixed_start == LLAMA_TOKEN_NULL ||
tok_text_end == LLAMA_TOKEN_NULL || tok_im_end == LLAMA_TOKEN_NULL) {
LOG_ERR("mtmd_helper_gen_audio: missing LFM3-Audio control tokens in vocab\n");
return false;
}
return true;
}
bool decode(llama_batch batch) {
std::vector<llama_pos> positions(batch.n_tokens);
std::vector<int32_t> n_seq(batch.n_tokens, 1);
std::vector<llama_seq_id> seq(batch.n_tokens, seq_id);
std::vector<llama_seq_id *> seq_ptr(batch.n_tokens);
std::vector<int8_t> logits(batch.n_tokens, 1);
for (int i = 0; i < batch.n_tokens; i++) {
positions[i] = pos + i;
seq_ptr[i] = &seq[i];
}
batch.pos = positions.data();
batch.n_seq_id = n_seq.data();
batch.seq_id = seq_ptr.data();
batch.logits = logits.data();
if (llama_decode(lctx, batch) != 0) {
LOG_ERR("mtmd_helper_gen_audio: decode failed\n");
return false;
}
pos += batch.n_tokens;
return true;
}
// generate() modes: text, audio until end of audio, interleaved until <|text_end|>
enum gen_mode { MODE_TEXT, MODE_AUDIO, MODE_MIXED };
// checked by the converter
static constexpr int n_text_block = 6; // interleaved_n_text
static constexpr int n_audio_block = 12; // interleaved_n_audio
llama_token tok_audio_start = LLAMA_TOKEN_NULL;
llama_token tok_mixed_start = LLAMA_TOKEN_NULL;
llama_token tok_text_end = LLAMA_TOKEN_NULL;
llama_token tok_im_end = LLAMA_TOKEN_NULL;
bool continuing = false; // the prompt continues a sequence, the response follows the model's switching
gen_mode mode = MODE_AUDIO;
int slots_left = 0;
bool speaking = false; // interleaving: the current block is audio
bool text_done = false; // interleaving: <|text_end|> was emitted, audio until the end-of-audio frame
llama_seq_id seq_id = 0;
int pos = 0;
std::vector<llama_token> prompt_tokens;
int prompt_pos = 0;
float temp = 0.0f;
int32_t top_k = 0;
float rep_penalty = 1.0f;
int32_t rep_window = 0; // frames
std::mt19937 rng;
std::vector<float> logits;
std::vector<float> fb_buf;
std::vector<int32_t> hist; // frame-major codes of the frames so far
bool pending_tail = false; // the ISTFT holds back the overlap of the last frame
std::vector<float> audio_pcm;
std::vector<float> h_state_buf;
mtmd_helper_gen_audio_outtype out_type = MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV;
std::vector<char> out_buf;
bool stream = false;
size_t pcm_sent = 0; // samples already returned by get_output()
bool wav_header_sent = false;
};
static std::unique_ptr<mtmd_gen_audio_pipeline> make_pipeline(llama_context * lctx, mtmd_context * mctx) {
switch (mtmd_gen_audio_get_info(mctx).type) {
case MTMD_GEN_AUDIO_TYPE_QWEN3TTS:
@@ -1269,6 +1725,8 @@ static std::unique_ptr<mtmd_gen_audio_pipeline> make_pipeline(llama_context * lc
return std::unique_ptr<mtmd_gen_audio_pipeline>(new pockettts_gen_audio_pipeline(lctx, mctx));
case MTMD_GEN_AUDIO_TYPE_SOPRANO:
return std::unique_ptr<mtmd_gen_audio_pipeline>(new soprano_gen_audio_pipeline(lctx, mctx));
case MTMD_GEN_AUDIO_TYPE_LFM3A:
return std::unique_ptr<mtmd_gen_audio_pipeline>(new lfm3a_gen_audio_pipeline(lctx, mctx));
default:
return nullptr;
}
@@ -1308,6 +1766,11 @@ int32_t mtmd_helper_gen_audio_set_input(mtmd_helper_gen_audio * ctx, const mtmd_
LOG_ERR("mtmd_helper_gen_audio: unsupported or missing gen-audio pipeline\n");
return 1;
}
if (inp->n_past > 0 && !ctx->pipeline->supports_continue()) {
LOG_ERR("mtmd_helper_gen_audio: the pipeline cannot continue a sequence\n");
return 1;
}
ctx->pipeline->logit_bias.clear();
return ctx->pipeline->set_input(inp);
}
@@ -1318,6 +1781,34 @@ int32_t mtmd_helper_gen_audio_step_prompt(mtmd_helper_gen_audio * ctx, int32_t n
return ctx->pipeline->step_prompt(n_batch);
}
bool mtmd_helper_gen_audio_next_is_text(mtmd_helper_gen_audio * ctx) {
return ctx->pipeline && ctx->pipeline->next_is_text();
}
const llama_logit_bias * mtmd_helper_gen_audio_get_logit_bias(mtmd_helper_gen_audio * ctx, size_t * n_bias) {
*n_bias = ctx->pipeline ? ctx->pipeline->logit_bias.size() : 0;
return *n_bias ? ctx->pipeline->logit_bias.data() : nullptr;
}
int32_t mtmd_helper_audio_stream_get_logit_bias(const mtmd_context * mctx, const llama_model * model,
llama_logit_bias * out, int32_t n_max) {
std::vector<llama_logit_bias> bias;
if (mtmd_gen_audio_get_info(mctx).type == MTMD_GEN_AUDIO_TYPE_LFM3A) {
// LFM3-Audio live ASR: only <|text_end|> ends the text of a chunk, the stream adds the final <|mixed_end|>
const llama_vocab * vocab = llama_model_get_vocab(model);
for (const char * piece : { "<|mixed_end|>", "<|mixed_start|>", "<|audio_start|>", "<|im_start|>", "<|im_end|>", "<|endoftext|>" }) {
const llama_token tok = find_special_token(vocab, piece);
if (tok != LLAMA_TOKEN_NULL) {
bias.push_back({ tok, -INFINITY });
}
}
}
for (int32_t i = 0; i < n_max && i < (int32_t) bias.size(); i++) {
out[i] = bias[i];
}
return (int32_t) bias.size();
}
int32_t mtmd_helper_gen_audio_step_gen(mtmd_helper_gen_audio * ctx, llama_token sampled,
const float * h_state_in, const float ** h_state_out,
bool * out_stop) {
+5 -2
View File
@@ -11,6 +11,7 @@
#include "mtmd-helper.h"
#include "mtmd-helper-common.h"
#include "llama.h"
#include "../src/llama-ext.h"
#include "hash/hash.h"
@@ -1043,8 +1044,10 @@ mtmd_helper_cap mtmd_helper_get_capabilities(const llama_model * model, const mt
cap.audio_gen_spk_ref = gen_audio_type == MTMD_GEN_AUDIO_TYPE_QWEN3TTS
|| gen_audio_type == MTMD_GEN_AUDIO_TYPE_POCKETTTS;
// tts-only model cannot be used for chat (no chat template)
cap.chat = !cap.audio_gen || llama_model_chat_template(model, nullptr) != nullptr;
// a tts model without a chat template only outputs audio, it cannot be used for chat
// neither can a realtime transcription model (e.g. Voxtral Realtime): its text follows the audio by a token delay
cap.text_gen = !cap.audio_gen || llama_model_chat_template(model, nullptr) != nullptr;
cap.chat = cap.text_gen && llama_model_n_tokens_delay(model) == 0;
return cap;
}
+45 -2
View File
@@ -184,7 +184,7 @@ MTMD_API int32_t mtmd_helper_video_read_next(mtmd_helper_video * ctx,
char ** out_text);
struct mtmd_helper_cap {
bool chat; // false for tts-only model (no chat template)
bool chat; // false for tts-only models (no chat template) and realtime transcription models (token delay)
// input capabilities
bool vision;
@@ -195,7 +195,7 @@ struct mtmd_helper_cap {
bool audio_stream; // realtime transcription, see mtmd_audio_stream_init()
// output capabilities
bool text_gen; // always true for now
bool text_gen; // false for an audio-only model (tts-only, no chat template)
bool audio_gen;
bool audio_gen_spk_ref; // accepts a speaker reference audio for voice cloning
};
@@ -223,15 +223,28 @@ enum mtmd_helper_gen_audio_outtype {
MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM, // raw PCM
MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV, // WAV PCM 16-bit LE, mono
};
// what a response that continues a sequence (n_past > 0) outputs
enum mtmd_helper_gen_audio_out_mode {
MTMD_HELPER_GEN_AUDIO_OUT_AUTO, // the model switches between text and audio itself
MTMD_HELPER_GEN_AUDIO_OUT_TEXT,
MTMD_HELPER_GEN_AUDIO_OUT_AUDIO,
MTMD_HELPER_GEN_AUDIO_OUT_MIXED, // interleaved text and audio
};
struct mtmd_helper_gen_audio_inp {
bool stream; // if true, output() must be called after each step_gen()
llama_seq_id seq_id;
// the text to speak; for a model with a "tts" chat template (e.g. LFM3-Audio), the prompt rendered from it
const char * prompt;
size_t prompt_len;
// > 0: continue a sequence of n_past positions with raw text (LFM3-Audio only)
int32_t n_past;
enum mtmd_helper_gen_audio_out_mode out_mode; // n_past > 0 only (default: auto)
mtmd_bitmap * speaker_ref; // optional, can be NULL
const char * lang; // optional, can be NULL
const char * voice; // optional, can be NULL, model-dependent
int32_t top_k;
float top_p;
@@ -271,6 +284,18 @@ MTMD_API int32_t mtmd_helper_gen_audio_step_gen(
const float ** h_state_out,
bool * out_stop);
// true if the next step_gen() takes a text token: the caller samples it from the logits and passes it as sampled
// false if the next step generates an audio frame from h_state_in
MTMD_API bool mtmd_helper_gen_audio_next_is_text(mtmd_helper_gen_audio * ctx);
// bias for the text tokens the caller samples (e.g. no switch to audio in a text-only response), n_bias is set to its size
MTMD_API const llama_logit_bias * mtmd_helper_gen_audio_get_logit_bias(mtmd_helper_gen_audio * ctx, size_t * n_bias);
// bias for the text the caller samples while it transcribes an audio stream opened with a prompt (see mtmd_audio_stream_params)
// writes up to n_max entries to out, returns the size of the bias
MTMD_API int32_t mtmd_helper_audio_stream_get_logit_bias(const mtmd_context * mctx, const struct llama_model * model,
llama_logit_bias * out, int32_t n_max);
// out_data valid until next get_output() or reset() call
// out_n_samples (optional, can be NULL) receives the number of generated PCM samples
// if inp->stream is true: returns only audio produced since the previous call, and
@@ -293,6 +318,7 @@ MTMD_API int32_t mtmd_helper_gen_audio_flush(mtmd_helper_gen_audio * ctx);
#ifdef __cplusplus
#include <set>
#include <memory>
#include <vector>
namespace mtmd_helper {
@@ -318,6 +344,7 @@ struct gen_audio {
mtmd_helper_gen_audio_inp data = mtmd_helper_gen_audio_inp_default();
std::string prompt_str;
std::string lang_str;
std::string voice_str;
mtmd::bitmap_ptr speaker_ref_ptr;
inp() = default;
@@ -328,6 +355,7 @@ struct gen_audio {
void set_prompt (std::string p) { prompt_str = std::move(p); }
void set_lang (std::string l) { lang_str = std::move(l); }
void set_voice (std::string v) { voice_str = std::move(v); }
void set_speaker_ref(mtmd::bitmap_ptr bmp) { speaker_ref_ptr = std::move(bmp); }
// pointers are only valid as long as *this is alive
@@ -335,6 +363,7 @@ struct gen_audio {
data.prompt = prompt_str.c_str();
data.prompt_len = prompt_str.size();
data.lang = lang_str.empty() ? nullptr : lang_str.c_str();
data.voice = voice_str.empty() ? nullptr : voice_str.c_str();
data.speaker_ref = speaker_ref_ptr.get();
return &data;
}
@@ -359,6 +388,14 @@ struct gen_audio {
int32_t step_gen(llama_token sampled, const float * h_state, const float ** h_state_out, bool * out_stop = nullptr) {
return mtmd_helper_gen_audio_step_gen(ctx.get(), sampled, h_state, h_state_out, out_stop);
}
bool next_is_text() {
return mtmd_helper_gen_audio_next_is_text(ctx.get());
}
std::vector<llama_logit_bias> logit_bias() {
size_t n = 0;
const llama_logit_bias * bias = mtmd_helper_gen_audio_get_logit_bias(ctx.get(), &n);
return std::vector<llama_logit_bias>(bias, bias + n);
}
int32_t get_output(int32_t * out_sample_rate, const char ** out_data, size_t * out_data_len, int64_t * out_n_samples = nullptr) {
return mtmd_helper_gen_audio_get_output(ctx.get(), out_sample_rate, out_data, out_data_len, out_n_samples);
}
@@ -367,6 +404,12 @@ struct gen_audio {
}
};
inline std::vector<llama_logit_bias> audio_stream_logit_bias(const mtmd_context * mctx, const llama_model * model) {
std::vector<llama_logit_bias> bias(mtmd_helper_audio_stream_get_logit_bias(mctx, model, nullptr, 0));
mtmd_helper_audio_stream_get_logit_bias(mctx, model, bias.data(), (int32_t) bias.size());
return bias;
}
} // namespace mtmd_helper
#endif
+29 -2
View File
@@ -809,8 +809,7 @@ clip_image_size mtmd_image_preprocessor_dyn_size::get_target_size(const clip_hpa
mtmd_image_preproc_out mtmd_image_preprocessor_dyn_size::preprocess(const clip_image_u8 & img) const {
clip_image_u8 resized_image;
const clip_image_size target_size = get_target_size(hparams, img.get_size());
img_tool::resize(img, resized_image, target_size,
img_tool::resize(img, resized_image, target_size(img.get_size()),
hparams.image_resize_algo,
hparams.image_resize_pad,
hparams.image_pad_color);
@@ -819,6 +818,34 @@ mtmd_image_preproc_out mtmd_image_preprocessor_dyn_size::preprocess(const clip_i
return output;
}
//
// mtmd_image_preprocessor_lfm3vl
//
// rounds before the pixel checks (an edge may round to 0) and computes in double, as transformers does
clip_image_size mtmd_image_preprocessor_lfm3vl::smart_resize(int width, int height, int factor, int64_t min_pixels, int64_t max_pixels) {
const double h = height;
const double w = width;
int h_bar = static_cast<int>(std::nearbyint(h / factor)) * factor;
int w_bar = static_cast<int>(std::nearbyint(w / factor)) * factor;
if (static_cast<int64_t>(h_bar) * w_bar > max_pixels) {
const double beta = std::sqrt(h * w / max_pixels);
h_bar = static_cast<int>(std::floor(h / beta / factor)) * factor;
w_bar = static_cast<int>(std::floor(w / beta / factor)) * factor;
} else if (static_cast<int64_t>(h_bar) * w_bar < min_pixels) {
const double beta = std::sqrt(min_pixels / (h * w));
h_bar = static_cast<int>(std::ceil(h * beta / factor)) * factor;
w_bar = static_cast<int>(std::ceil(w * beta / factor)) * factor;
}
return {std::max(factor, w_bar), std::max(factor, h_bar)};
}
clip_image_size mtmd_image_preprocessor_lfm3vl::target_size(const clip_image_size & original_size) const {
return smart_resize(original_size.width, original_size.height, hparams.patch_size * hparams.n_merge,
hparams.image_min_pixels, hparams.image_max_pixels);
}
//
// mtmd_image_preprocessor_glm5v
//
+9
View File
@@ -135,6 +135,15 @@ struct mtmd_image_preprocessor_dyn_size : mtmd_image_preprocessor {
mtmd_image_preprocessor_dyn_size(const clip_ctx * ctx) : mtmd_image_preprocessor(ctx) {}
static clip_image_size get_target_size(const clip_hparams & hparams, const clip_image_size & original_size);
mtmd_image_preproc_out preprocess(const clip_image_u8 & img) const override;
virtual clip_image_size target_size(const clip_image_size & original_size) const { return get_target_size(hparams, original_size); }
};
// LFM3-VL, same as dyn_size but sized exactly as the transformers Qwen2-VL smart_resize
struct mtmd_image_preprocessor_lfm3vl : mtmd_image_preprocessor_dyn_size {
mtmd_image_preprocessor_lfm3vl(const clip_ctx * ctx) : mtmd_image_preprocessor_dyn_size(ctx) {}
clip_image_size target_size(const clip_image_size & original_size) const override;
static clip_image_size smart_resize(int width, int height, int factor, int64_t min_pixels, int64_t max_pixels);
};
// GLM 5.3 flash, similar to dyn_size, but each edge is aligned up to patch_size*n_merge, and max budget is met by a search over the height with the width scaled proportionally
+599 -97
View File
@@ -772,6 +772,13 @@ struct mtmd_context {
img_end = "<|image_end|>";
image_preproc = std::make_unique<mtmd_image_preprocessor_muse_glimmer>(ctx_v);
} break;
case PROJECTOR_TYPE_LFM3VL:
{
// <|vision_start|> ... (image embeddings) ... <|vision_end|>
img_beg = "<|vision_start|>";
img_end = "<|vision_end|>";
image_preproc = std::make_unique<mtmd_image_preprocessor_lfm3vl>(ctx_v);
} break;
case PROJECTOR_TYPE_YOUTUVL:
{
// <|vision_start|> ... (image embeddings) ... <|vision_end|>
@@ -1073,6 +1080,15 @@ struct mtmd_context {
{
audio_preproc = std::make_unique<mtmd_audio_preprocessor_d1omni>(ctx_a);
} break;
case PROJECTOR_TYPE_LFM3A:
{
// LFM3-Omni wraps the audio in the image span, LFM3-Audio (no vision tower) places it bare
if (ctx_v) {
aud_beg = img_beg;
aud_end = img_end;
}
audio_preproc = std::make_unique<mtmd_audio_preprocessor_lfm3a>(ctx_a);
} break;
case PROJECTOR_TYPE_GRANITE_SPEECH:
{
audio_preproc = std::make_unique<mtmd_audio_preprocessor_granite_speech>(ctx_a);
@@ -2094,6 +2110,10 @@ mtmd_gen_audio_info mtmd_gen_audio_get_info(const mtmd_context * ctx) {
info.type = MTMD_GEN_AUDIO_TYPE_SOPRANO;
info.sample_rate = 32000;
break;
case PROJECTOR_TYPE_LFM3A_GEN:
info.type = MTMD_GEN_AUDIO_TYPE_LFM3A;
info.sample_rate = 24000;
break;
default:
info.type = MTMD_GEN_AUDIO_TYPE_NONE;
break;
@@ -2124,6 +2144,16 @@ mtmd_gen_inp mtmd_gen_inp_default(const mtmd_context * ctx) {
case PROJECTOR_TYPE_SOPRANO:
inp.type = MTMD_GEN_PROCESS_TYPE_GEN_WAV;
break;
case PROJECTOR_TYPE_LFM3A_GEN:
{
// MaskGIT sampling runs in the helper, with the settings of the checkpoint
const auto * hparams = clip_get_hparams(ctx->ctx_gen_a);
inp.top_k = hparams->gen_top_k;
inp.top_p = 1.0f;
inp.temp = hparams->gen_temp;
inp.penalty_repeat = hparams->gen_penalty_repeat;
inp.penalty_last_n = hparams->gen_penalty_last_n;
} break;
default:
break;
}
@@ -2139,6 +2169,9 @@ struct mtmd_gen_state {
std::vector<float> out_embd; // next-step hidden state fed back to backbone (GEN_CODE)
std::vector<float> out_audio; // decoded PCM samples for the current frame (GEN_WAV)
// lfm3a: the codec graph yields STFT frames, the ISTFT runs on the host
std::unique_ptr<mtmd_audio_streaming_istft> istft;
~mtmd_gen_state() {
clip_gen_state_free(clip_state);
}
@@ -2161,6 +2194,10 @@ mtmd_gen_state * mtmd_gen_state_init(mtmd_context * ctx, mtmd_gen_state_params p
if (!st->clip_state) {
return nullptr;
}
if (clip_get_projector_type(ctx->ctx_gen_a) == PROJECTOR_TYPE_LFM3A_GEN) {
const auto * hparams = clip_get_hparams(ctx->ctx_gen_a);
st->istft = std::make_unique<mtmd_audio_streaming_istft>(hparams->audio_n_fft, hparams->audio_hop_len);
}
return st.release();
}
@@ -2170,6 +2207,72 @@ void mtmd_gen_state_free(mtmd_gen_state * state) {
void mtmd_gen_state_reset(mtmd_gen_state * state) {
clip_gen_state_reset(state->clip_state);
if (state->istft) {
const auto * hparams = clip_get_hparams(state->ctx_clip);
state->istft = std::make_unique<mtmd_audio_streaming_istft>(hparams->audio_n_fft, hparams->audio_hop_len);
}
}
// lfm3a GEN_WAV: ISTFT of the codec output, n_codes == 0 flushes and resets the state
static int32_t mtmd_gen_wav_lfm3a(mtmd_context * ctx, mtmd_gen_state * state, const mtmd_gen_inp * inp, mtmd_gen_out * out) {
clip_ctx * ctx_clip = ctx->ctx_gen_a;
const auto & hparams = *clip_get_hparams(ctx_clip);
const int n_bins = hparams.audio_n_fft / 2 + 1;
const int n_trim = (hparams.audio_n_fft - hparams.audio_hop_len) / 2;
auto & audio = state->out_audio;
audio.clear();
if (!inp->codes || inp->n_codes == 0) {
// the tail beyond the last frame is padding, like the centered ISTFT of the reference
audio = state->istft->flush();
audio.resize(audio.size() - std::min(audio.size(), (size_t) n_trim));
mtmd_gen_state_reset(state);
} else {
std::vector<int32_t> in_codes(inp->codes, inp->codes + inp->n_codes);
std::vector<float> feats;
// no hidden-state input, the batch entry is an unused placeholder
clip_image_f32 dummy;
dummy.set_size({1, 1}, false, true);
dummy.cpy_buf(std::vector<float>(1, 0.0f));
clip_image_f32_batch batch;
batch.is_audio = true;
batch.entries.push_back(std::move(dummy));
clip_encode_params params;
params.imgs = &batch;
params.n_threads = ctx->n_threads;
params.gen_process = CLIP_GEN_PROCESS_GEN_WAV;
params.codes = &in_codes;
params.out_feats = &feats;
params.gen_state = state->clip_state;
if (!clip_encode(ctx_clip, &params)) {
LOG_ERR("%s: clip_encode failed (codec)\n", __func__);
return 1;
}
// [2 * n_bins, n_stft_frames]: log magnitude, then phase
GGML_ASSERT(feats.size() % (2 * (size_t) n_bins) == 0);
const size_t n_frames = feats.size() / (2 * (size_t) n_bins);
std::vector<float> spec(2 * (size_t) n_bins);
for (size_t f = 0; f < n_frames; f++) {
const float * fr = feats.data() + f * 2 * n_bins;
for (int j = 0; j < n_bins; j++) {
const float mag = std::exp(fr[j]);
spec[2 * j + 0] = mag * std::cos(fr[n_bins + j]);
spec[2 * j + 1] = mag * std::sin(fr[n_bins + j]);
}
const std::vector<float> pcm = state->istft->process_frame(spec.data());
audio.insert(audio.end(), pcm.begin(), pcm.end());
}
}
out->audio = audio.data();
out->n_samples = audio.size();
return 0;
}
static int32_t mtmd_gen_audio_process_impl(mtmd_context * ctx, mtmd_gen_state * state, const mtmd_gen_inp * inp, mtmd_gen_out * out) {
@@ -2193,12 +2296,22 @@ static int32_t mtmd_gen_audio_process_impl(mtmd_context * ctx, mtmd_gen_state *
}
}
if (inp->type == MTMD_GEN_PROCESS_TYPE_GEN_WAV && clip_get_projector_type(ctx_clip) == PROJECTOR_TYPE_LFM3A_GEN) {
return mtmd_gen_wav_lfm3a(ctx, state, inp, out);
}
if (inp->type == MTMD_GEN_PROCESS_TYPE_GEN_CODE) {
const size_t n_embd = (size_t) clip_n_mmproj_embd(ctx_clip);
// the hidden state may be omitted when the graph does not use it
clip_image_f32 hidden_state;
hidden_state.set_size({(int) n_embd, 1}, false, true);
hidden_state.cpy_buf(std::vector<float>(inp->embd, inp->embd + n_embd));
hidden_state.cpy_buf(inp->embd ? std::vector<float>(inp->embd, inp->embd + n_embd) : std::vector<float>(n_embd, 0.0f));
std::vector<int32_t> in_codes;
if (inp->codes) {
in_codes.assign(inp->codes, inp->codes + inp->n_codes);
}
clip_image_f32_batch batch;
batch.is_audio = true;
@@ -2220,6 +2333,7 @@ static int32_t mtmd_gen_audio_process_impl(mtmd_context * ctx, mtmd_gen_state *
params.top_k = inp->top_k;
params.top_p = inp->top_p;
params.temp = inp->temp;
params.codes = inp->codes ? &in_codes : nullptr;
params.out_is_eos = &is_eos;
params.gen_state = state->clip_state;
@@ -3088,26 +3202,18 @@ mtmd_memory_usage mtmd_get_memory_usage(const char * mmproj_fname,
// realtime audio input
//
// the model-independent part of a stream, the model-specific part is in the subclasses below
struct mtmd_audio_stream {
mtmd_context * ctx;
clip_ctx * ctx_clip;
clip_enc_state * enc_state = nullptr;
// model constants
int n_mel;
int n_fft;
int hop;
int n_embd; // decoder embedding size
int frames_per_tok; // mel frames per decoder row
int samples_per_tok;
int n_frames_max; // encoder frames per clip_encode() call
int n_step_mel; // mel frames per encoder call
int n_right_pad; // silence tokens added by flush()
float log_mel_max; // global_log_mel_max in params.json
std::vector<llama_token> prefix; // text tokens paired with the first rows
mtmd_audio_cache cache;
// samples not yet turned into mel frames, buf[0] is at absolute index buf_pos
std::vector<float> buf;
@@ -3116,103 +3222,143 @@ struct mtmd_audio_stream {
int64_t n_frames = 0; // mel frames computed so far
std::vector<float> mel; // pending mel frames, frame-major
std::vector<float> mel_ctx; // last 2 mel frames, the left context of the conv stem
std::vector<float> embd; // pending audio embeddings
size_t embd_off = 0; // rows of embd already consumed
std::vector<float> out; // rows of the last step
std::vector<float> tok_embd;
int64_t n_rows = 0; // rows produced so far
int64_t n_rows_flushed = 0; // rows produced once the audio up to the last flush is consumed
int64_t n_rows_done = 0; // audio rows whose text has been sampled, see mtmd_audio_stream_get_n_rows()
int64_t n_rows_flushed = 0; // audio rows up to the end of the last flush
bool pending_flush = false;
bool ended = false;
bool seq_start = false; // the rows of the last step start a new sequence
~mtmd_audio_stream() {
clip_enc_state_free(enc_state);
virtual ~mtmd_audio_stream() = default;
// model constants and initial state, false on failure
virtual bool init(const mtmd_audio_stream_params & params) = 0;
virtual int32_t flush(bool end) = 0;
virtual int32_t step(llama_token last_token) = 0;
void add_silence(int64_t n) {
buf.insert(buf.end(), n, 0.0f);
n_samples += n;
}
};
// Voxtral Realtime: a row is an audio embedding plus the embedding of the previous token
struct mtmd_audio_stream_voxtral_rt : mtmd_audio_stream {
clip_enc_state * enc_state = nullptr;
int n_fft;
int n_frames_max; // encoder frames per clip_encode() call
int n_right_pad; // silence tokens added by flush()
float log_mel_max; // global_log_mel_max in params.json
std::vector<llama_token> prefix; // text tokens paired with the first rows
mtmd_audio_cache cache;
std::vector<float> mel_ctx; // last 2 mel frames, the left context of the conv stem
std::vector<float> tok_embd;
~mtmd_audio_stream_voxtral_rt() override {
clip_enc_state_free(enc_state);
}
bool init(const mtmd_audio_stream_params & params) override;
int32_t flush(bool end) override;
int32_t step(llama_token last_token) override;
};
// LFM3-Audio: the rows are plain audio embeddings of a chat turn, the model answers after the audio
// live ASR (with a prompt), trained layout: <prompt> [A x 12] text<|text_end|> ... [A x r] <|mixed_end|> text<|text_end|>
// training samples are max 30 s: restart the sequence at a silent chunk after 15 s, always after 30 s; the encoder keeps its state
struct mtmd_audio_stream_lfm3a : mtmd_audio_stream {
int win; // samples per mel frame
clip_enc_state * enc_state = nullptr;
// live ASR
static constexpr int64_t chunk_rows = 12;
static constexpr int32_t max_chunk_tokens = 48; // text tokens per chunk, then the chunk is closed
static constexpr int32_t max_final_tokens = 96;
std::vector<llama_token> prompt; // empty: the rows of a chat turn
llama_token tok_text_end = LLAMA_TOKEN_NULL;
llama_token tok_mixed_end = LLAMA_TOKEN_NULL;
int64_t soft_seq_rows = 0;
int64_t max_seq_rows = 0;
bool started = false;
bool done = false; // the text of the final chunk is complete
bool in_chunk = false; // the text of the last chunk is being sampled
bool final_chunk = false;
bool chunk_has_text = false;
int32_t n_chunk_text = 0;
int64_t n_chunk_rows = 0;
int64_t n_seq_rows = 0; // audio rows since the sequence started
std::vector<llama_token> closing; // the tokens that close the last chunk, they go in before the next one
std::vector<float> tok_embd;
// the audio markers of the model (LFM3-Omni), as rows around the audio
std::vector<float> beg_rows;
std::vector<float> end_rows;
bool beg_done = false;
bool end_done = false;
~mtmd_audio_stream_lfm3a() override {
clip_enc_state_free(enc_state);
}
bool init(const mtmd_audio_stream_params & params) override;
int32_t flush(bool end) override;
int32_t step(llama_token last_token) override;
bool add_tokens(const llama_token * tokens, size_t n);
int32_t step_live(llama_token last_token);
};
bool mtmd_support_audio_stream(const mtmd_context * ctx) {
return ctx->ctx_a && ctx->text_model && clip_get_projector_type(ctx->ctx_a) == PROJECTOR_TYPE_VOXTRAL_RT;
if (!ctx->ctx_a || !ctx->text_model) {
return false;
}
const projector_type proj = clip_get_projector_type(ctx->ctx_a);
return proj == PROJECTOR_TYPE_VOXTRAL_RT || proj == PROJECTOR_TYPE_LFM3A;
}
mtmd_audio_stream_params mtmd_audio_stream_params_default(void) {
mtmd_audio_stream_params params;
params.step_ms = 320;
params.step_ms = 320;
params.prompt = nullptr;
params.n_prompt = 0;
return params;
}
static void mtmd_audio_stream_add_silence(mtmd_audio_stream * s, int64_t n) {
s->buf.insert(s->buf.end(), n, 0.0f);
s->n_samples += n;
}
mtmd_audio_stream * mtmd_audio_stream_init(mtmd_context * ctx, mtmd_audio_stream_params params) {
if (!mtmd_support_audio_stream(ctx)) {
LOG_ERR("%s: the model does not support streaming audio input\n", __func__);
return nullptr;
}
const clip_hparams * hparams = clip_get_hparams(ctx->ctx_a);
const llama_vocab * vocab = ctx->vocab;
auto s = std::make_unique<mtmd_audio_stream>();
std::unique_ptr<mtmd_audio_stream> s;
switch (clip_get_projector_type(ctx->ctx_a)) {
case PROJECTOR_TYPE_VOXTRAL_RT: s = std::make_unique<mtmd_audio_stream_voxtral_rt>(); break;
case PROJECTOR_TYPE_LFM3A: s = std::make_unique<mtmd_audio_stream_lfm3a>(); break;
default: GGML_ABORT("unsupported projector");
}
s->ctx = ctx;
s->ctx_clip = ctx->ctx_a;
s->n_mel = hparams->n_mel_bins;
s->n_fft = hparams->audio_n_fft;
s->hop = hparams->audio_hop_len;
s->n_embd = clip_n_mmproj_embd(ctx->ctx_a);
if (s->n_embd != ctx->n_embd_text) {
LOG_ERR("%s: mmproj embedding size %d does not match the text model (%d)\n", __func__, s->n_embd, ctx->n_embd_text);
return nullptr;
}
// one decoder row per 80 ms of audio: the conv stem has stride 2, then frames are stacked
s->frames_per_tok = 2 * hparams->proj_stack_factor;
s->samples_per_tok = s->frames_per_tok * s->hop;
s->n_frames_max = (hparams->warmup_audio_size - 2) / 2;
s->log_mel_max = 1.5f;
// the encoder works in whole decoder rows
int n_step = (int) ((int64_t) params.step_ms * hparams->audio_sample_rate / 1000 / s->hop);
n_step = std::max(n_step, s->frames_per_tok);
n_step = CLIP_ALIGN(n_step, s->frames_per_tok);
s->n_step_mel = std::min(n_step, 2 * s->n_frames_max);
// prefix: BOS then one pad token per silence token and per delay token
// the output lags behind the audio by n_delay rows, the text model is conditioned on the same value
const int n_delay = llama_model_n_tokens_delay(ctx->text_model);
const int n_left_pad = 32;
s->n_right_pad = n_delay + 1 + 10;
{
const std::string pad_str = "[STREAMING_PAD]";
std::vector<llama_token> pad(8);
const int n_pad = llama_tokenize(vocab, pad_str.c_str(), pad_str.size(), pad.data(), pad.size(), false, true);
if (n_pad != 1) {
LOG_ERR("%s: cannot find the %s token\n", __func__, pad_str.c_str());
return nullptr;
}
s->prefix.push_back(llama_vocab_bos(vocab));
s->prefix.insert(s->prefix.end(), n_left_pad + n_delay, pad[0]);
}
s->cache.fill_sin_cos_table(s->n_fft);
s->cache.fill_hann_window(hparams->audio_window_len, true);
s->cache.fill_mel_filterbank_matrix(s->n_mel, s->n_fft, hparams->audio_sample_rate);
s->enc_state = clip_enc_state_init(s->ctx_clip, s->n_frames_max);
if (!s->enc_state) {
if (!s->init(params)) {
return nullptr;
}
// the first mel frame is centered on the first sample, so its window starts before the audio
s->buf.assign(s->n_fft / 2, 0.0f);
s->buf_pos = -s->n_fft / 2;
s->mel_ctx.assign(2 * s->n_mel, 0.0f);
// silence before the audio
mtmd_audio_stream_add_silence(s.get(), (int64_t) n_left_pad * s->samples_per_tok);
return s.release();
}
@@ -3233,18 +3379,99 @@ int32_t mtmd_audio_stream_flush(mtmd_audio_stream * s, bool end) {
if (s->ended) {
return 1;
}
return s->flush(end);
}
int32_t mtmd_audio_stream_step(mtmd_audio_stream * s, llama_token last_token) {
return s->step(last_token);
}
const float * mtmd_audio_stream_get_output_embd(const mtmd_audio_stream * s) {
return s->out.data();
}
bool mtmd_audio_stream_is_seq_start(const mtmd_audio_stream * s) {
return s->seq_start;
}
//
// Voxtral Realtime
//
bool mtmd_audio_stream_voxtral_rt::init(const mtmd_audio_stream_params & params) {
const clip_hparams * hparams = clip_get_hparams(ctx_clip);
const llama_vocab * vocab = ctx->vocab;
n_fft = hparams->audio_n_fft;
// one decoder row per 80 ms of audio: the conv stem has stride 2, then frames are stacked
frames_per_tok = 2 * hparams->proj_stack_factor;
samples_per_tok = frames_per_tok * hop;
n_frames_max = (hparams->warmup_audio_size - 2) / 2;
log_mel_max = 1.5f;
// the encoder works in whole decoder rows
int n_step = (int) ((int64_t) params.step_ms * hparams->audio_sample_rate / 1000 / hop);
n_step = std::max(n_step, frames_per_tok);
n_step = CLIP_ALIGN(n_step, frames_per_tok);
n_step_mel = std::min(n_step, 2 * n_frames_max);
if (params.n_prompt > 0) {
LOG_ERR("%s: Voxtral Realtime takes no prompt\n", __func__);
return false;
}
// prefix: BOS then one pad token per silence token and per delay token
// the output lags behind the audio by n_delay rows, the text model is conditioned on the same value
const int n_delay = llama_model_n_tokens_delay(ctx->text_model);
const int n_left_pad = 32;
n_right_pad = n_delay + 1 + 10;
{
const std::string pad_str = "[STREAMING_PAD]";
std::vector<llama_token> pad(8);
const int n_pad = llama_tokenize(vocab, pad_str.c_str(), pad_str.size(), pad.data(), pad.size(), false, true);
if (n_pad != 1) {
LOG_ERR("%s: cannot find the %s token\n", __func__, pad_str.c_str());
return false;
}
prefix.push_back(llama_vocab_bos(vocab));
prefix.insert(prefix.end(), n_left_pad + n_delay, pad[0]);
}
cache.fill_sin_cos_table(n_fft);
cache.fill_hann_window(hparams->audio_window_len, true);
cache.fill_mel_filterbank_matrix(n_mel, n_fft, hparams->audio_sample_rate);
enc_state = clip_enc_state_init(ctx_clip, n_frames_max);
if (!enc_state) {
return false;
}
// the first mel frame is centered on the first sample, so its window starts before the audio
buf.assign(n_fft / 2, 0.0f);
buf_pos = -n_fft / 2;
mel_ctx.assign(2 * n_mel, 0.0f);
// silence before the audio
add_silence((int64_t) n_left_pad * samples_per_tok);
return true;
}
int32_t mtmd_audio_stream_voxtral_rt::flush(bool end) {
// align to a whole row, then add the silence the model expects after the audio
const int64_t n_align = (s->samples_per_tok - s->n_samples % s->samples_per_tok) % s->samples_per_tok;
mtmd_audio_stream_add_silence(s, n_align + (int64_t) s->n_right_pad * s->samples_per_tok);
const int64_t n_align = (samples_per_tok - n_samples % samples_per_tok) % samples_per_tok;
add_silence(n_align + (int64_t) n_right_pad * samples_per_tok);
// one row per samples_per_tok, the left padding included
s->n_rows_flushed = s->n_samples / s->samples_per_tok;
s->pending_flush = true;
s->ended = end;
n_rows_flushed = n_samples / samples_per_tok;
pending_flush = true;
ended = end;
return 0;
}
// mel frame k is centered on sample k * hop, with force the samples past the end are taken as silence
static void mtmd_audio_stream_compute_mel(mtmd_audio_stream * s, bool force) {
static void mtmd_audio_stream_compute_mel(mtmd_audio_stream_voxtral_rt * s, bool force) {
const int half = s->n_fft / 2;
std::vector<float> window(s->n_fft);
std::vector<float> frame(s->n_mel);
@@ -3274,7 +3501,7 @@ static void mtmd_audio_stream_compute_mel(mtmd_audio_stream * s, bool force) {
}
}
static bool mtmd_audio_stream_encode(mtmd_audio_stream * s, bool force) {
static bool mtmd_audio_stream_encode(mtmd_audio_stream_voxtral_rt * s, bool force) {
while (true) {
const int n_avail = (int) (s->mel.size() / s->n_mel);
int n = n_avail - n_avail % s->frames_per_tok;
@@ -3326,7 +3553,7 @@ static bool mtmd_audio_stream_encode(mtmd_audio_stream * s, bool force) {
}
// out rows = audio embeddings + token embeddings
static bool mtmd_audio_stream_add_rows(mtmd_audio_stream * s, const llama_token * tokens, size_t n) {
static bool mtmd_audio_stream_add_rows(mtmd_audio_stream_voxtral_rt * s, const llama_token * tokens, size_t n) {
s->tok_embd.resize(n * s->n_embd);
if (llama_model_get_tok_embd_rows(s->ctx->text_model, tokens, n, s->tok_embd.data()) == 0) {
LOG_ERR("%s: invalid token\n", __func__);
@@ -3341,55 +3568,330 @@ static bool mtmd_audio_stream_add_rows(mtmd_audio_stream * s, const llama_token
return true;
}
int32_t mtmd_audio_stream_step(mtmd_audio_stream * s, llama_token last_token) {
const bool force = s->pending_flush;
s->pending_flush = false;
int32_t mtmd_audio_stream_voxtral_rt::step(llama_token last_token) {
const bool force = pending_flush;
pending_flush = false;
mtmd_audio_stream_compute_mel(s, force);
if (!mtmd_audio_stream_encode(s, force)) {
// last_token was sampled from the last row, the text of every row so far is out
n_rows_done = n_rows;
mtmd_audio_stream_compute_mel(this, force);
if (!mtmd_audio_stream_encode(this, force)) {
return -2;
}
// drop the consumed rows once in a while
if (s->embd_off >= 1024) {
s->embd.erase(s->embd.begin(), s->embd.begin() + s->embd_off * s->n_embd);
s->embd_off = 0;
if (embd_off >= 1024) {
embd.erase(embd.begin(), embd.begin() + embd_off * n_embd);
embd_off = 0;
}
const int64_t n_avail = (int64_t) (s->embd.size() / s->n_embd) - s->embd_off;
s->out.clear();
const int64_t n_avail = (int64_t) (embd.size() / n_embd) - embd_off;
out.clear();
if (s->n_rows == 0) {
if (n_rows == 0) {
// the prefix rows go out together, the caller samples from the last one
const int64_t n_prefix = (int64_t) s->prefix.size();
const int64_t n_prefix = (int64_t) prefix.size();
if (n_avail < n_prefix) {
return s->ended ? -1 : 0;
return ended ? -1 : 0;
}
if (!mtmd_audio_stream_add_rows(s, s->prefix.data(), s->prefix.size())) {
if (!mtmd_audio_stream_add_rows(this, prefix.data(), prefix.size())) {
return -2;
}
return (int32_t) n_prefix;
}
if (n_avail == 0) {
return s->ended ? -1 : 0;
return ended ? -1 : 0;
}
if (last_token == LLAMA_TOKEN_NULL) {
LOG_ERR("%s: the token sampled from the previous row is required\n", __func__);
return -2;
}
if (!mtmd_audio_stream_add_rows(s, &last_token, 1)) {
if (!mtmd_audio_stream_add_rows(this, &last_token, 1)) {
return -2;
}
return 1;
}
const float * mtmd_audio_stream_get_output_embd(const mtmd_audio_stream * s) {
return s->out.data();
//
// LFM3-Audio
//
bool mtmd_audio_stream_lfm3a::init(const mtmd_audio_stream_params & params) {
const clip_hparams * hparams = clip_get_hparams(ctx_clip);
// one row per 8 mel frames (three stride-2 convs), mel frames are not centered
win = hparams->audio_window_len;
frames_per_tok = 8;
samples_per_tok = frames_per_tok * hop;
const int n_step = (int) ((int64_t) params.step_ms * hparams->audio_sample_rate / 1000 / hop);
n_step_mel = CLIP_ALIGN(std::max(n_step, frames_per_tok), frames_per_tok);
if (params.n_prompt > 0) {
// live ASR, a chunk per encoder call
n_step_mel = (int) chunk_rows * frames_per_tok;
prompt.assign(params.prompt, params.prompt + params.n_prompt);
const llama_vocab * vocab = llama_model_get_vocab(ctx->text_model);
auto find_token = [&](const char * piece) {
llama_token tok[2];
const int n = llama_tokenize(vocab, piece, (int32_t) strlen(piece), tok, 2, false, true);
return n == 1 ? tok[0] : LLAMA_TOKEN_NULL;
};
tok_text_end = find_token("<|text_end|>");
tok_mixed_end = find_token("<|mixed_end|>");
if (tok_text_end == LLAMA_TOKEN_NULL || tok_mixed_end == LLAMA_TOKEN_NULL) {
LOG_ERR("%s: the vocab lacks the tokens of LFM3-Audio live ASR\n", __func__);
return false;
}
soft_seq_rows = std::lround(15.0 * hparams->audio_sample_rate / samples_per_tok);
max_seq_rows = std::lround(30.0 * hparams->audio_sample_rate / samples_per_tok);
}
enc_state = clip_enc_state_init(ctx_clip, n_step_mel);
if (!enc_state) {
return false;
}
// the markers the tokenizer puts around an audio clip go around the stream as well
const llama_vocab * vocab = llama_model_get_vocab(ctx->text_model);
auto marker_rows = [&](const std::string & text, std::vector<float> & rows) {
if (text.empty()) {
return true;
}
std::vector<llama_token> tokens(text.size() + 4);
const int32_t n = llama_tokenize(vocab, text.c_str(), (int32_t) text.size(), tokens.data(), (int32_t) tokens.size(), false, true);
if (n <= 0) {
return false;
}
rows.resize((size_t) n * n_embd);
return llama_model_get_tok_embd_rows(ctx->text_model, tokens.data(), n, rows.data()) != 0;
};
return marker_rows(ctx->aud_beg, beg_rows) && marker_rows(ctx->aud_end, end_rows);
}
// end: pad the tail like mtmd_audio_preprocessor_lfm3a, so the rows match encoding the clip at once
// otherwise only whole rows are encoded, a padded partial row would change the encoder state
// live ASR: pad the last chunk with silence, so its text comes out
int32_t mtmd_audio_stream_lfm3a::flush(bool end) {
if (end) {
const int64_t n_padded = n_samples <= win ? win : n_samples + (hop - (n_samples - win) % hop) % hop;
add_silence(n_padded - n_samples);
} else if (!prompt.empty() && n_samples > 0) {
const int64_t frames_per_chunk = chunk_rows * frames_per_tok;
const int64_t n_frames_audio = n_samples <= win ? 1 : (n_samples - win + hop - 1) / hop + 1;
const int64_t n_frames_padded = (n_frames_audio + frames_per_chunk - 1) / frames_per_chunk * frames_per_chunk;
add_silence(win + (n_frames_padded - 1) * hop - n_samples);
}
n_rows_flushed = (n_samples >= win ? (n_samples - win) / hop + 1 : 0) / frames_per_tok;
pending_flush = true;
ended = end;
return 0;
}
// mel frame k covers samples [k * hop, k * hop + win), computed with the offline preprocessor
static bool mtmd_audio_stream_compute_mel(mtmd_audio_stream_lfm3a * s) {
const int64_t n_total = s->n_samples >= s->win ? (s->n_samples - s->win) / s->hop + 1 : 0;
const int64_t n_new = n_total - s->n_frames;
if (n_new <= 0) {
return true;
}
const int64_t start = s->n_frames * s->hop - s->buf_pos;
const size_t len = (size_t) (s->win + (n_new - 1) * s->hop);
std::vector<mtmd_audio_mel> mel_chunks;
if (!s->ctx->audio_preproc->preprocess(s->buf.data() + start, len, mel_chunks) || mel_chunks.size() != 1) {
LOG_ERR("%s: unable to preprocess audio\n", __func__);
return false;
}
const auto & mel = mel_chunks[0];
GGML_ASSERT(mel.n_len == n_new && mel.n_mel == s->n_mel);
for (int64_t t = 0; t < n_new; t++) {
for (int m = 0; m < s->n_mel; m++) {
s->mel.push_back(mel.data[(size_t) m * mel.n_len + t]);
}
}
s->n_frames = n_total;
const int64_t keep_from = s->n_frames * s->hop;
if (keep_from > s->buf_pos) {
s->buf.erase(s->buf.begin(), s->buf.begin() + (keep_from - s->buf_pos));
s->buf_pos = keep_from;
}
return true;
}
// whole rows only, except for the last call of an ended stream; the encoder state carries the rest
static bool mtmd_audio_stream_encode(mtmd_audio_stream_lfm3a * s, bool flush) {
const int n_avail = (int) (s->mel.size() / s->n_mel);
int n = n_avail - n_avail % s->frames_per_tok;
if (flush && s->ended) {
n = n_avail;
} else if (!flush) {
n -= n % s->n_step_mel;
}
if (n <= 0) {
return true;
}
std::vector<float> data((size_t) s->n_mel * n);
for (int m = 0; m < s->n_mel; m++) {
for (int t = 0; t < n; t++) {
data[(size_t) m * n + t] = s->mel[(size_t) t * s->n_mel + m];
}
}
clip_image_f32 img;
img.set_size({n, s->n_mel}, false, /* is_audio */ true);
img.cpy_buf(data);
const int n_rows = clip_n_output_tokens(s->ctx_clip, &img);
clip_image_f32_batch batch;
batch.is_audio = true;
batch.entries.push_back(std::move(img));
std::vector<float> res((size_t) s->n_embd * n_rows);
clip_encode_params params;
params.n_threads = s->ctx->n_threads;
params.imgs = &batch;
params.out_embd = &res;
params.enc_state = s->enc_state;
if (!clip_encode(s->ctx_clip, &params)) {
LOG_ERR("%s: failed to encode audio\n", __func__);
return false;
}
s->embd.insert(s->embd.end(), res.begin(), res.end());
s->mel.erase(s->mel.begin(), s->mel.begin() + (size_t) n * s->n_mel);
return true;
}
int32_t mtmd_audio_stream_lfm3a::step(llama_token last_token) {
const bool flush = pending_flush;
pending_flush = false;
if (!mtmd_audio_stream_compute_mel(this) || !mtmd_audio_stream_encode(this, flush)) {
return -2;
}
if (!prompt.empty()) {
return step_live(last_token);
}
// rows carry no text, all of them go out at once and last_token is not used
const int64_t n_avail = (int64_t) (embd.size() / n_embd) - embd_off;
out.clear();
if (n_avail > 0 || ended) {
if (!beg_done) {
beg_done = true;
out = beg_rows;
}
out.insert(out.end(), embd.begin() + embd_off * n_embd, embd.end());
embd.clear();
embd_off = 0;
if (ended && !end_done) {
end_done = true;
out.insert(out.end(), end_rows.begin(), end_rows.end());
}
}
const int64_t n_out = (int64_t) (out.size() / n_embd);
if (n_out == 0) {
return ended ? -1 : 0;
}
n_rows += n_out;
n_rows_done += n_avail; // the markers are not audio rows
return (int32_t) n_out;
}
bool mtmd_audio_stream_lfm3a::add_tokens(const llama_token * tokens, size_t n) {
if (n == 0) {
return true;
}
tok_embd.resize(n * n_embd);
if (llama_model_get_tok_embd_rows(ctx->text_model, tokens, n, tok_embd.data()) == 0) {
LOG_ERR("%s: invalid token\n", __func__);
return false;
}
out.insert(out.end(), tok_embd.begin(), tok_embd.end());
return true;
}
// last_token continues the text of the chunk, or <|text_end|> closes it and the next 12 rows go out (the rest at the end)
int32_t mtmd_audio_stream_lfm3a::step_live(llama_token last_token) {
out.clear();
seq_start = false;
// drop the consumed rows once in a while
if (embd_off >= 1024) {
embd.erase(embd.begin(), embd.begin() + embd_off * n_embd);
embd_off = 0;
}
if (in_chunk) {
if (last_token == LLAMA_TOKEN_NULL) {
LOG_ERR("%s: the token sampled from the previous row is required\n", __func__);
return -2;
}
if (last_token != tok_text_end) {
chunk_has_text = true;
if (n_chunk_text + 1 < (final_chunk ? max_final_tokens : max_chunk_tokens)) {
n_chunk_text++;
return add_tokens(&last_token, 1) ? 1 : -2;
}
// close a runaway chunk like the model should have
closing.push_back(last_token);
}
// the text of the chunk is complete, its closing tokens go in before the next chunk
in_chunk = false;
n_rows_done += n_chunk_rows;
if (final_chunk) {
done = true;
}
closing.push_back(tok_text_end);
}
if (done) {
return -1;
}
const int64_t n_avail = (int64_t) (embd.size() / n_embd) - embd_off;
const bool drained = ended && mel.empty();
const bool final = drained && n_avail <= chunk_rows;
if (!final && n_avail < chunk_rows) {
return 0;
}
if (final && n_avail == 0 && !started) {
done = true; // no audio at all
return -1;
}
if (!started || n_seq_rows >= max_seq_rows || (n_seq_rows >= soft_seq_rows && !chunk_has_text)) {
closing = prompt; // the closing tokens go away with the sequence
seq_start = true;
n_seq_rows = 0;
}
const int64_t n_feed = final ? n_avail : chunk_rows;
if (!add_tokens(closing.data(), closing.size())) {
return -2;
}
closing.clear();
const float * rows = embd.data() + embd_off * n_embd;
out.insert(out.end(), rows, rows + (size_t) n_feed * n_embd);
embd_off += n_feed;
if (final && !add_tokens(&tok_mixed_end, 1)) {
return -2;
}
started = true;
in_chunk = true;
final_chunk = final;
chunk_has_text = false;
n_chunk_text = 0;
n_chunk_rows = n_feed;
n_seq_rows += n_feed;
n_rows += n_feed;
return (int32_t) (out.size() / n_embd);
}
int64_t mtmd_audio_stream_get_n_rows(const mtmd_audio_stream * s) {
return s->n_rows;
return s->n_rows_done;
}
int64_t mtmd_audio_stream_get_n_rows_flushed(const mtmd_audio_stream * s) {
+16 -3
View File
@@ -383,6 +383,11 @@ typedef struct mtmd_audio_stream mtmd_audio_stream;
struct mtmd_audio_stream_params {
int32_t step_ms; // audio encoded per encoder call, rounded up to the model step (default: 320)
// tokens that open the stream, they go out with the first rows (default: none)
// with the prompt of its "asr" chat template, LFM3-Audio transcribes live
const llama_token * prompt;
size_t n_prompt;
};
MTMD_API struct mtmd_audio_stream_params mtmd_audio_stream_params_default(void);
@@ -412,11 +417,14 @@ MTMD_API int32_t mtmd_audio_stream_step(mtmd_audio_stream * stream, llama_token
// rows of the last step, the reading size (in bytes) is n_rows * llama_model_n_embd_inp(model) * sizeof(float)
MTMD_API const float * mtmd_audio_stream_get_output_embd(const mtmd_audio_stream * stream);
// rows produced so far, the prefix rows included
// true if the rows of the last step start a new sequence: the caller drops its sequence before decoding them
MTMD_API bool mtmd_audio_stream_is_seq_start(const mtmd_audio_stream * stream);
// audio rows whose text has been sampled (prefix rows included), step() updates it from last_token
MTMD_API int64_t mtmd_audio_stream_get_n_rows(const mtmd_audio_stream * stream);
// rows the stream produces up to the end of the last flush
// once get_n_rows() reaches it, every row of the flushed audio has been produced
// audio rows up to the end of the last flush
// once get_n_rows() reaches it, the text of the flushed audio has been sampled
MTMD_API int64_t mtmd_audio_stream_get_n_rows_flushed(const mtmd_audio_stream * stream);
/////////////////////////////////////////
@@ -428,6 +436,7 @@ enum mtmd_gen_audio_type {
MTMD_GEN_AUDIO_TYPE_QWEN3TTS,
MTMD_GEN_AUDIO_TYPE_POCKETTTS,
MTMD_GEN_AUDIO_TYPE_SOPRANO,
MTMD_GEN_AUDIO_TYPE_LFM3A,
};
struct mtmd_gen_audio_info {
@@ -444,6 +453,7 @@ enum mtmd_gen_process_type {
MTMD_GEN_PROCESS_TYPE_GEN_WAV, // convert semantic to PCM audio
// for qwen3tts, this is code2wav
// for pocket-tts, this is mimi decoder
// for lfm3a, this is the codec decoder + ISTFT, n_codes == 0 flushes the tail
};
struct mtmd_gen_inp {
@@ -455,9 +465,12 @@ struct mtmd_gen_inp {
int32_t top_k;
float top_p;
float temp; // sampling temperature, or noise scale for flow-matching decoders
float penalty_repeat; // repetition penalty of the sampled codes, <= 1 is off
int32_t penalty_last_n; // frames the repetition penalty looks back on
// for MTMD_GEN_PROCESS_TYPE_GEN_WAV
// pass either codes (discrete) or feats (continuous), depending on the pipeline
// lfm3a GEN_CODE also takes codes: the codes revealed so far, -1 for a masked codebook
int32_t * codes;
size_t n_codes;
const float * feats;
+2
View File
@@ -47,6 +47,8 @@ add_library(${TARGET}
server-http.h
server-models.cpp
server-models.h
server-models-sched.cpp
server-models-sched.h
)
set_target_properties(${TARGET} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
+55 -2
View File
@@ -230,6 +230,7 @@ For the full list of features, please refer to [server's changelog](https://gith
| `--models-dir PATH` | directory containing models for the router server (default: disabled)<br/>(env: LLAMA_ARG_MODELS_DIR) |
| `--models-preset PATH` | path to INI file containing model presets for the router server (default: disabled)<br/>(env: LLAMA_ARG_MODELS_PRESET) |
| `--models-max N` | for router server, maximum number of models to load simultaneously (default: 4, 0 = unlimited)<br/>(env: LLAMA_ARG_MODELS_MAX) |
| `--models-memory-margin N` | for router server, MiB of device memory to keep free per device, unload LRU models until a new one fits (default: 0, 0 = disabled)<br/>(env: LLAMA_ARG_MODELS_MEMORY_MARGIN) |
| `--models-autoload, --no-models-autoload` | for router server, whether to automatically load models (default: enabled)<br/>(env: LLAMA_ARG_MODELS_AUTOLOAD) |
| `--jinja, --no-jinja` | whether to use jinja template engine for chat (default: enabled)<br/>(env: LLAMA_ARG_JINJA) |
| `--reasoning-format FORMAT` | controls whether thought tags are allowed and/or extracted from the response, and in which format they're returned; one of:<br/>- none: leaves thoughts unparsed in `message.content`<br/>- deepseek: puts thoughts in `message.reasoning_content`<br/>- deepseek-legacy: keeps `<think>` tags in `message.content` while also populating `message.reasoning_content`<br/>(default: auto)<br/>(env: LLAMA_ARG_THINK) |
@@ -856,6 +857,8 @@ Returns raw audio bytes (`audio/wav` by default) rather than JSON. For more info
`lang`: Language code for the utterance (model-dependent, e.g. `en`, `zh`). Optional.
`voice`: Voice for the utterance (model-dependent, e.g. `female` or `male` for LFM3-Audio). Optional.
`speaker_ref_b64`: Base64-encoded reference audio to clone the speaker's voice. Optional. Alternatively, upload the reference audio as a `speaker_ref` file field via `multipart/form-data` (see example below) - if both are provided, the uploaded file takes precedence.
`top_k`, `top_p`: Sampling params for the acoustic code predictor. Optional, model-dependent defaults apply.
@@ -1842,13 +1845,17 @@ Follows the [TypeSafe API](https://docs.typesafe.ai/api), streaming is not suppo
The questions of a request are answered independently, an answer does not depend on the other questions. The exception is clef: it reads all the questions in one prompt and decides them jointly.
The number of options of a `choice` question is limited by the model, for example: 52 for openjev, 255 for granite-switch, laya, clef, pplx-decider, vllm-decision2, vllm-d3, lfm2-d1, lfm2-d1-omni and h2o-lightning. For laya, long questions and options are truncated to the token budget the model was trained with.
The number of options of a `choice` question is limited by the model, for example: 52 for openjev, 255 for granite-switch, laya, clef, pplx-decider, vllm-decision2, vllm-d3, lfm2-d1, lfm2-d1-omni and h2o-lightning, 2 for openbmb-minicpm-ocr-jev. For laya, long questions and options are truncated to the token budget the model was trained with.
For laya, clef, vllm-d3 and lfm2-d1-omni, the whole prompt is evaluated in one batch: it must fit in `--ubatch-size`. An lfm2-d1-omni prompt is cut to 16384 tokens. For vllm-decision2, the options of a question and the end of the prompt must fit in one batch. An h2o-lightning state longer than 32000 tokens keeps its first and last 16000 tokens. A server that runs clef only serves this endpoint, text generation is not available. A decision model that is also a text model, such as granite-switch, serves both and reports `["text", "decisions"]`.
openbmb-minicpm-ocr-jev answers one fixed question: is an OCR text, rendered as an image, consistent with the original image? `state` is the OCR text, as a string, special tokens written in it are escaped. `files` (or `images`) has exactly 2 images: the original, then the rendered one, both are required. The request has one question and no `instructions`: the task is part of the prompt of the model, so a request that sends one is rejected. It is either a `choice` with the `criteria` `{"0": null, "1": null}`, where `1` means consistent, or a `noul` without `criteria`, which returns the probability of `1`. The model has no LM head, so text generation is not available.
Its prompt carries two images, so it is much longer than a text-only one: size the context with `-c` and the batch with `--batch-size`/`--ubatch-size` accordingly. The prompt of a request is evaluated in batches like a normal causal model, the class scores are read from the pooled output of its last token.
*Image input:*
Image input needs a model that supports it (for example: openjev, clef, pplx-decider, vllm-d3, lfm2-d1, lfm2-d1-omni, h2o-lightning) and its multimodal projector, see `--mmproj`.
Image input needs a model that supports it (for example: openjev, clef, pplx-decider, vllm-d3, lfm2-d1, lfm2-d1-omni, h2o-lightning, openbmb-minicpm-ocr-jev) and its multimodal projector, see `--mmproj`.
Images can be given in two ways, and both can be used in the same request:
@@ -2113,6 +2120,40 @@ For the **WebSocket** endpoint (`/v1/realtime`) The router also uses the `model`
By default, the model will be loaded automatically if it's not loaded. To disable this, add `--no-models-autoload` when starting the server. Additionally, you can include `?autoload=true|false` in the query param to control this behavior per-request.
### Memory-based unloading
By default, the router only limits the *number* of loaded models (`--models-max`, least recently used first). With `--models-memory-margin N`, it also keeps track of device memory and unloads models (LRU) until the model being loaded fits, while leaving `N` MiB free on each device.
How it works:
- On startup, and whenever a model is added or its preset changes, the router spawns one short-lived `llama-server` child per model (one at a time, in the background) that reads the model metadata without allocating memory and reports two measurements:
- `min`: the memory the model needs at the smallest context that `--fit` may pick, i.e. `--fit-ctx` (default 4096) times the number of sequences, with the device placement from its preset. If the preset pins the context with `-c`, or sets `--fit off`, this is the memory at that context.
- `max`: the memory it needs at the largest context that `--fit` picks given the device memory free at that moment. This is a snapshot: it changes as other models load and unload.
- When an instance is loaded, it reports the memory it actually allocated (`effective`). With `--fit` on (the default) an instance takes the largest context that fits at load time, so it typically ends up close to `max`. Pin the context with `-c` in the preset of a model if you want it to leave room for other models.
- Before spawning an instance, the router unloads idle models (LRU) until the `min` of the new model fits in the free memory of every device minus the margin. A model that is serving requests is never unloaded. If nothing is left to unload and the model still does not fit, it is loaded anyway and `--fit` in the instance decides.
- A load request for a model whose measurement has not finished yet waits for it (the model is moved to the front of the measurement queue).
Notes:
- The router itself never touches a GPU, all device memory figures come from the child processes.
- Host (CPU) memory is assumed to be unlimited, as with `--fit`. On a build without GPU devices the memory limit has no effect.
- A model whose measurement fails (for example a broken GGUF, or an `-hf` model that is not downloaded yet) is loaded without memory checks; the measurement is retried once the model has been loaded successfully.
- Multimodal projectors (`mmproj`) are not included in the measurements yet, such a model needs more than its `min` reports.
- The measurements are exposed in the `memory` field of each model in `GET /models` (sizes in bytes), and a `model_memory` event is sent on `/models/sse` when a measurement completes:
```js
"memory": {
"status": "measured", // unmeasured | measuring | measured | failed
"min": {
"n_ctx": 4096,
"devices": [
{ "kind": "device", "name": "CUDA0", "total": 25757220864, "free": 24387584000, "model": 4986635264, "context": 536870912, "compute": 301989888 },
{ "kind": "host", "name": "Host", "model": 263454720, "context": 0, "compute": 16793600 }
]
},
"max": { "n_ctx": 131072, "devices": [ /* ... */ ] },
"effective": { "n_ctx": 131072, "devices": [ /* ... */ ] } // null unless the model is loaded
}
```
### GET `/models`: List available models
Listing all models in cache. The model metadata will also include a field to indicate the status of the model:
@@ -2363,6 +2404,18 @@ Example events:
"event": "model_remove"
}
// only with --models-memory-margin, when the measurement of a model completes (see "Memory-based unloading")
{
"model": "...",
"event": "model_memory",
"data": {
"status": "measured",
"min": { /* ... */ },
"max": { /* ... */ },
"effective": null
}
}
// special event: reload of the list of all models
{
"model": "*",
+16 -6
View File
@@ -464,7 +464,7 @@ json server_chat_convert_anthropic_to_oai(const json & body) {
// Single-pass: build both text and content_parts, decide format at the end
std::string result_text;
json content_parts = json::array();
bool has_images = false;
bool keep_parts = false; // images or tool references
for (const auto & c : result_content) {
std::string c_type = json_value(c, "type", std::string());
@@ -476,7 +476,7 @@ json server_chat_convert_anthropic_to_oai(const json & body) {
{"text", text}
});
} else if (c_type == "image") {
has_images = true;
keep_parts = true;
json source = json_value(c, "source", json::object());
std::string source_type = json_value(source, "type", std::string());
if (source_type == "base64") {
@@ -493,10 +493,17 @@ json server_chat_convert_anthropic_to_oai(const json & body) {
{"image_url", {{"url", json_value(source, "url", std::string())}}}
});
}
} else if (c_type == "tool_reference") {
// tool search result naming a deferred tool
keep_parts = true;
content_parts.push_back({
{"type", "tool_reference"},
{"name", json_value(c, "tool_name", std::string())}
});
}
}
if (!has_images) {
if (!keep_parts) {
// Text-only: collapse to a plain string for maximum compatibility
tool_results.push_back({
{"role", "tool"},
@@ -504,7 +511,7 @@ json server_chat_convert_anthropic_to_oai(const json & body) {
{"content", result_text}
});
} else {
// Mixed or image-only: use array content parts (OpenAI multimodal tool format)
// images or tool references: keep array content parts
tool_results.push_back({
{"role", "tool"},
{"tool_call_id", tool_use_id},
@@ -559,6 +566,9 @@ json server_chat_convert_anthropic_to_oai(const json & body) {
{"parameters", tool.contains("input_schema") ? tool.at("input_schema") : json::object()}
}}
});
if (tool.contains("defer_loading")) {
oai_tools.back()["defer_loading"] = tool.at("defer_loading");
}
}
oai_body["tools"] = oai_tools;
}
@@ -677,11 +687,11 @@ json convert_transcriptions_to_chatcmpl(
if (response_format != "json") {
throw std::invalid_argument("Only 'json' response_format is supported for transcription");
}
const common_chat_prompt_preset preset = common_chat_get_asr_prompt(tmpls);
const common_chat_prompt_preset preset = common_chat_get_asr_prompt(tmpls, language);
if (prompt.empty()) {
prompt = preset.user;
}
if (!language.empty()) {
if (!language.empty() && !preset.has_language) {
prompt += string_format(" (language: %s)", language.c_str());
}
prompt += get_media_marker();
+2 -1
View File
@@ -219,6 +219,7 @@ std::vector<std::string> server_model_output_modalities(common_decision_type dec
case COMMON_DECISION_TYPE_LFM2_D1:
case COMMON_DECISION_TYPE_LFM2_D1_OMNI:
case COMMON_DECISION_TYPE_H2O_LIGHTNING:
case COMMON_DECISION_TYPE_MINICPM_OCR_JEV:
out = {"decisions"};
break;
case COMMON_DECISION_TYPE_GRANITE_SWITCH:
@@ -1315,7 +1316,7 @@ static void oaicompat_content_load_media(json & content, const server_chat_param
p.erase("input_video");
p.erase("video_url");
} else if (type != "text") {
} else if (type != "text" && type != "tool_reference") {
throw std::invalid_argument("unsupported content[].type");
}
}
+8
View File
@@ -737,6 +737,7 @@ enum server_role_type {
SERVER_ROLE_ROUTER, // no model: lists the models, spawns instances on demand and proxies requests to them
SERVER_ROLE_INSTANCE, // serves one model, standalone or as a child of a router
SERVER_ROLE_DOWNLOAD, // child of a router: download a model, report the progress, exit
SERVER_ROLE_MEASURE, // child of a router: report the memory the model needs without loading it, exit
};
static inline const char * server_role_type_to_str(server_role_type role) {
@@ -744,6 +745,7 @@ static inline const char * server_role_type_to_str(server_role_type role) {
case SERVER_ROLE_ROUTER: return "router";
case SERVER_ROLE_INSTANCE: return "instance";
case SERVER_ROLE_DOWNLOAD: return "download";
case SERVER_ROLE_MEASURE: return "measure";
default: GGML_ASSERT(false && "invalid server_role_type");
}
}
@@ -753,6 +755,9 @@ static inline server_role_type server_role_type_from_str(const std::string & str
if (str == "download") {
return SERVER_ROLE_DOWNLOAD;
}
if (str == "measure") {
return SERVER_ROLE_MEASURE;
}
return SERVER_ROLE_INSTANCE;
}
@@ -772,6 +777,7 @@ static inline server_role_type server_role_type_from_str(const std::string & str
// the states a child reports to the router
enum server_state {
SERVER_STATE_DOWNLOADING,
SERVER_STATE_MEASURING, // sent by a measure child, see server_run_measure
SERVER_STATE_LOADING,
SERVER_STATE_READY,
SERVER_STATE_SLEEPING,
@@ -780,6 +786,7 @@ enum server_state {
static inline std::string server_state_to_str(server_state state) {
switch (state) {
case SERVER_STATE_DOWNLOADING: return "downloading";
case SERVER_STATE_MEASURING: return "measuring";
case SERVER_STATE_LOADING: return "loading";
case SERVER_STATE_READY: return "ready";
case SERVER_STATE_SLEEPING: return "sleeping";
@@ -789,6 +796,7 @@ static inline std::string server_state_to_str(server_state state) {
static inline server_state server_state_from_str(const std::string & str) {
if (str == "downloading") return SERVER_STATE_DOWNLOADING;
if (str == "measuring") return SERVER_STATE_MEASURING;
if (str == "loading") return SERVER_STATE_LOADING;
if (str == "ready") return SERVER_STATE_READY;
if (str == "sleeping") return SERVER_STATE_SLEEPING;
+25 -6
View File
@@ -2473,7 +2473,14 @@ private:
const auto & decision = slot.task->decision;
if (!decision.labels.empty()) {
if (decision.pooled) {
const float * embd = llama_get_embeddings_seq(slot.ctx_tgt, slot.id);
if (embd == nullptr) {
send_error(slot, "failed to get the class scores", ERROR_TYPE_SERVER);
return;
}
res->scores.assign(embd, embd + llama_model_n_embd_out(model_tgt));
} else if (!decision.labels.empty()) {
const float * logits = llama_get_logits_ith(slot.ctx_tgt, i_batch);
if (logits == nullptr) {
send_error(slot, "failed to get logits", ERROR_TYPE_SERVER);
@@ -2964,6 +2971,11 @@ private:
case SERVER_TASK_TYPE_TTS:
case SERVER_TASK_TYPE_DECISION:
{
if (task.need_sampling() && !decision.can_generate()) {
send_error(task, "This model is a decision model without text generation, use /v1/systemone", ERROR_TYPE_NOT_SUPPORTED);
break;
}
// special case: if input is provided via CLI, tokenize it first
// otherwise, no need to tokenize as it's already done inside the HTTP thread
if (task.cli) {
@@ -3412,7 +3424,9 @@ private:
return true; // rows from a previous step, not decoded yet
}
// every produced row is decoded and its delta queued, the commits ending there are complete
const int32_t n = mtmd_audio_stream_step(slot.audio_stream.get(), slot.sampled);
// complete the commits whose rows have their text sampled, before the new rows: their text goes to the next item
const int64_t n_rows = mtmd_audio_stream_get_n_rows(slot.audio_stream.get());
while (!slot.audio_commit_rows.empty() && slot.audio_commit_rows.front() <= n_rows) {
slot.audio_commit_rows.pop_front();
@@ -3428,7 +3442,6 @@ private:
queue_results.send(std::move(res));
}
const int32_t n = mtmd_audio_stream_step(slot.audio_stream.get(), slot.sampled);
if (n > 0) {
const float * rows = mtmd_audio_stream_get_output_embd(slot.audio_stream.get());
slot.inp_embd.assign(rows, rows + (size_t) n * batch.n_embd);
@@ -5130,7 +5143,8 @@ server_context_meta server_context::get_meta() const {
/* has_inp_video */ impl->chat_params.allow_video,
/* has_cap_chat */ impl->has_cap_chat(),
/* has_cap_tts */ impl->has_cap_tts(),
/* has_audio_stream */ impl->mtmd_cap.audio_stream,
// only realtime transcription models (no chat) stream without a prompt
/* has_audio_stream */ impl->mtmd_cap.audio_stream && !impl->mtmd_cap.chat,
/* json_ui_settings */ impl->json_ui_settings,
/* slot_n_ctx */ impl->n_ctx_slot(),
/* pooling_type */ llama_pooling_type(impl->ctx_tgt),
@@ -6355,8 +6369,13 @@ void server_routes::init_routes() {
server_task task(SERVER_TASK_TYPE_TTS);
server_schema::eval_tts_schema(params, task, body);
const bool stream = task.params.stream;
task.tts_inp.set_prompt(prompt);
task.tts_inp.set_lang(json_value(body, "lang", std::string()));
// a model with a "tts" chat template (e.g. LFM3-Audio) speaks the prompt rendered from it
const std::string lang = json_value(body, "lang", std::string());
const std::string voice = json_value(body, "voice", std::string());
const std::string tts_prompt = common_chat_get_tts_prompt(meta->chat_params.tmpls.get(), prompt, lang, voice);
task.tts_inp.set_prompt(tts_prompt.empty() ? prompt : tts_prompt);
task.tts_inp.set_lang(lang);
task.tts_inp.set_voice(voice);
task.tts_inp.data.stream = stream;
task.tts_inp.data.out_type = response_format == "pcm"
? MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM
+56 -3
View File
@@ -204,6 +204,15 @@ void server_decision_context::init(const llama_model * model) {
}
n_options_max = labels.size();
noul_true_first = true;
} else if (model_type == COMMON_DECISION_TYPE_MINICPM_OCR_JEV) {
// the classes come from the GGUF classifier labels, the request API is binary: [inconsistent, consistent]
const uint32_t n_classes = llama_model_n_cls_out(model);
if (n_classes != 2 || llama_model_n_embd_out(model) != (int32_t) n_classes) {
throw std::runtime_error(string_format("decision model must have 2 classifier labels and output one score per class, got %u labels and %d scores",
n_classes, llama_model_n_embd_out(model)));
}
n_options_max = n_classes;
choice_sorted = true;
} else {
throw std::runtime_error("unsupported decision model type: " + type_name);
}
@@ -231,6 +240,9 @@ std::vector<server_decision_question> server_decision_context::parse_questions(c
if (!body.contains("questions") || !body.at("questions").is_object() || body.at("questions").empty()) {
throw std::invalid_argument("\"questions\" must be a non-empty object");
}
if (type == COMMON_DECISION_TYPE_MINICPM_OCR_JEV && body.at("questions").size() != 1) {
throw std::invalid_argument("this model answers exactly one question");
}
std::vector<server_decision_question> questions;
for (const auto & [id, q] : body.at("questions").items()) {
@@ -244,7 +256,9 @@ std::vector<server_decision_question> server_decision_context::parse_questions(c
// h2o-lightning: the instructions can be left out, the template has a default, and the type defaults to choice
const bool is_h2o = type == COMMON_DECISION_TYPE_H2O_LIGHTNING;
const json instructions = q.contains("instructions") ? q.at("instructions") : (is_h2o ? json(std::string()) : json());
if (instructions.is_null() && type != COMMON_DECISION_TYPE_VLLM_D3 && !is_h2o) {
// minicpm-ocr-jev answers one fixed question, the prompt is entirely in the template
const bool is_ocr_jev = type == COMMON_DECISION_TYPE_MINICPM_OCR_JEV;
if (instructions.is_null() && type != COMMON_DECISION_TYPE_VLLM_D3 && !is_h2o && !is_ocr_jev) {
throw err("\"instructions\" must be provided");
}
if (strict_payload && (!decision_is_payload(instructions) || instructions == "")) {
@@ -322,6 +336,26 @@ std::vector<server_decision_question> server_decision_context::parse_questions(c
throw err("\"type\" must be one of: choice, score, noul");
}
// the classes are fixed: "0" is inconsistent, "1" is consistent
if (type == COMMON_DECISION_TYPE_MINICPM_OCR_JEV) {
// the task text is in the template, an instructions from the request would be dropped
if (!instructions.is_null()) {
throw err("\"instructions\" is not used by this model, the task is part of its prompt");
}
if (question.type == SERVER_DECISION_QUESTION_CHOICE) {
const auto & opts = question.options;
if (opts.size() != 2 || opts[0].key != "0" || opts[1].key != "1" || !opts[0].description.is_null() || !opts[1].description.is_null()) {
throw err("\"criteria\" must be {\"0\": null, \"1\": null}");
}
} else if (question.type == SERVER_DECISION_QUESTION_NOUL) {
if (!criteria.is_null()) {
throw err("\"criteria\" is not supported for a noul question");
}
} else {
throw err("only choice and noul questions are supported");
}
}
if (question.options.size() > n_options_max) {
throw err(string_format("too many options (%zu), this model supports at most %zu", question.options.size(), n_options_max));
}
@@ -360,6 +394,9 @@ static void decision_load_audio(const json & data, std::vector<raw_buffer> & fil
}
json server_decision_context::parse_state(const json & body, std::vector<raw_buffer> & files) const {
if (type == COMMON_DECISION_TYPE_MINICPM_OCR_JEV && !body.at("state").is_string()) {
throw std::invalid_argument("\"state\" must be the OCR text");
}
if (body.contains("videos") && !body.at("videos").is_null() && !body.at("videos").empty()) {
throw std::invalid_argument("\"videos\" is not supported");
}
@@ -376,6 +413,11 @@ json server_decision_context::parse_state(const json & body, std::vector<raw_buf
}
}
// the prompt has the original image, then the image rendered from the OCR text
if (type == COMMON_DECISION_TYPE_MINICPM_OCR_JEV && files.size() != 2) {
throw std::invalid_argument(string_format("this model needs exactly 2 images (the original, then the rendered one), got %zu", files.size()));
}
const json & state = body.at("state");
const bool is_wrapped = state.is_object() && state.contains("messages");
const json & messages = is_wrapped ? state.at("messages") : state;
@@ -1040,6 +1082,11 @@ std::string server_decision_context::render(
inp["instructions"] = decision_kev_text(question.instructions);
}
// the OCR text must not be parsed as special tokens
if (type == COMMON_DECISION_TYPE_MINICPM_OCR_JEV) {
inp["state"] = server_decision_escape_special_tokens(state.get<std::string>());
}
// the input must not contain the marker of the options
if (!text_marker.empty()) {
inp = decision_replace_text(inp, text_marker, " ");
@@ -1092,6 +1139,12 @@ void server_decision_context::fill_task(
const std::string prompt = render(state, questions, question, variant, files.size());
if (type == COMMON_DECISION_TYPE_MINICPM_OCR_JEV) {
task.tokens = process_mtmd_prompt(mctx, prompt, files, init_opt);
task.decision.pooled = true;
return;
}
if (type == COMMON_DECISION_TYPE_OPENJEV || type == COMMON_DECISION_TYPE_GRANITE_SWITCH || type == COMMON_DECISION_TYPE_LEV || type == COMMON_DECISION_TYPE_NIMBLE || type == COMMON_DECISION_TYPE_PPLX_DECIDER || type == COMMON_DECISION_TYPE_VLLM_D3 ||
type == COMMON_DECISION_TYPE_H2O_LIGHTNING) {
// lev reads the ratings of a noul question at its first labels, not at the digits
@@ -1531,8 +1584,8 @@ json server_decision_context::format_answer(const server_decision_question & que
if (s.size() != n) {
throw std::runtime_error("decision result does not match the number of options");
}
// a joint head returns NaN if it could not use the decision order
if (std::any_of(s.begin(), s.end(), [](float v) { return std::isnan(v); })) {
// a joint head returns NaN if it could not use the decision order, +inf would make the softmax NaN
if (std::any_of(s.begin(), s.end(), [](float v) { return std::isnan(v) || v == INFINITY; })) {
throw std::runtime_error("the model could not evaluate the decision");
}
std::vector<double> logits(s.begin(), s.end());
+8
View File
@@ -3,6 +3,7 @@
#include "server-common.h"
#include "server-task.h"
#include <algorithm>
#include <map>
#include <memory>
#include <string>
@@ -68,12 +69,19 @@ struct server_decision_context {
case COMMON_DECISION_TYPE_LFM2_D1:
case COMMON_DECISION_TYPE_LFM2_D1_OMNI:
case COMMON_DECISION_TYPE_H2O_LIGHTNING:
case COMMON_DECISION_TYPE_MINICPM_OCR_JEV:
return true;
default:
return false;
}
}
// false for a decision-only model, it serves no text generation
bool can_generate() const {
const auto out = server_model_output_modalities(type, true, false);
return std::find(out.begin(), out.end(), "text") != out.end();
}
// throw std::invalid_argument on bad input
std::vector<server_decision_question> parse_questions(const json & body) const;
+593
View File
@@ -0,0 +1,593 @@
#include "server-models-sched.h"
#include "server-models.h"
#include "fit.h"
#include <algorithm>
#include <cinttypes>
static constexpr int MEASURE_TIMEOUT = 300; // seconds, then the measure child is killed
//
// server_model_memory
//
server_model_memory::server_model_memory(uint32_t n_ctx, const std::vector<common_memory_breakdown_row> & rows) : n_ctx(n_ctx) {
for (const auto & row : rows) {
device_t d;
switch (row.kind) {
case common_memory_breakdown_row::DEVICE: d.kind = "device"; break;
case common_memory_breakdown_row::HOST: d.kind = "host"; break;
case common_memory_breakdown_row::BUFFER_TYPE: d.kind = "buffer_type"; break;
}
d.name = row.name;
d.total = row.total;
d.free = row.free;
d.model = row.model;
d.context = row.context;
d.compute = row.compute;
devices.push_back(std::move(d));
}
}
json server_model_memory::to_json() const {
json devs = json::array();
for (const auto & d : devices) {
json jd = {
{"kind", d.kind},
{"name", d.name},
{"model", d.model},
{"context", d.context},
{"compute", d.compute},
};
if (d.is_device()) {
jd["total"] = d.total;
jd["free"] = d.free;
}
devs.push_back(std::move(jd));
}
return {
{"n_ctx", n_ctx},
{"devices", devs},
};
}
server_model_memory server_model_memory::from_json(const json & j) {
server_model_memory mem;
mem.n_ctx = j.at("n_ctx").get<uint32_t>();
for (const auto & jd : j.at("devices")) {
device_t d;
d.kind = jd.at("kind").get<std::string>();
d.name = jd.at("name").get<std::string>();
d.total = json_value(jd, "total", (size_t) 0);
d.free = json_value(jd, "free", (size_t) 0);
d.model = jd.at("model").get<size_t>();
d.context = jd.at("context").get<size_t>();
d.compute = jd.at("compute").get<size_t>();
mem.devices.push_back(std::move(d));
}
return mem;
}
json server_model_memory_info::to_json() const {
return {
{"status", server_model_memory_status_to_string(status)},
{"min", min ? min->to_json() : json(nullptr)},
{"max", max ? max->to_json() : json(nullptr)},
{"effective", effective ? effective->to_json() : json(nullptr)},
};
}
//
// request queue
//
bool server_models_sched::has_capacity(std::unique_lock<std::mutex> & lk) {
check_lock(lk);
return models.base_params.models_max <= 0
|| count_running() < (size_t) models.base_params.models_max;
}
std::string server_models_sched::pick_victim(std::unique_lock<std::mutex> & lk) {
check_lock(lk);
std::string victim;
int64_t victim_last_used = 0;
for (const auto & m : models.mapping) {
// a busy model is mid-request, one still coming up has no request to finish
if (m.second.req_count != 0 || !m.second.meta.is_ready_or_sleep()) {
continue;
}
// already on its way out, or a queued request wants it
if (models.stopping_models.count(m.first) || find(m.first)) {
continue;
}
if (victim.empty() || m.second.meta.last_used < victim_last_used) {
victim = m.first;
victim_last_used = m.second.meta.last_used;
}
}
return victim;
}
void server_models_sched::join(std::unique_lock<std::mutex> & lk, const std::string & model_id) {
check_lock(lk);
if (entry_t * e = find(model_id)) {
e->n_waiters++;
SRV_INF("request for name=%s joined the queue, %d waiting\n", model_id.c_str(), e->n_waiters);
return;
}
queue.push_back({ model_id, 1, false });
SRV_INF("request for name=%s queued at position %zu\n",
model_id.c_str(), queue.size());
}
void server_models_sched::leave(std::unique_lock<std::mutex> & lk, const std::string & model_id) {
check_lock(lk);
for (auto it = queue.begin(); it != queue.end(); ++it) {
if (it->model_id == model_id) {
if (--it->n_waiters <= 0) {
queue.erase(it); // last one waiting for this model went away
}
return;
}
}
}
bool server_models_sched::queue_empty(std::unique_lock<std::mutex> & lk) {
check_lock(lk);
return queue.empty();
}
bool server_models_sched::try_claim(std::unique_lock<std::mutex> & lk, const std::string & model_id) {
check_lock(lk);
if (queue.empty() || queue.front().model_id != model_id || queue.front().loading) {
return false;
}
if (!has_capacity(lk)) {
return false;
}
// over the memory margin with every running model busy: wait for one to go idle
if (memory_enabled() && limits_exceeded(lk, model_id, /* verbose */ false) && pick_victim(lk).empty()) {
return false;
}
queue.front().loading = true;
return true;
}
void server_models_sched::claim_done(std::unique_lock<std::mutex> & lk, const std::string & model_id, bool ok) {
check_lock(lk);
if (ok) {
return;
}
for (auto it = queue.begin(); it != queue.end(); ++it) {
if (it->model_id == model_id) {
it->loading = false;
return;
}
}
}
void server_models_sched::tick(std::unique_lock<std::mutex> & lk) {
check_lock(lk);
if (models.base_params.models_max <= 0 || queue.empty()) {
return;
}
int n_running = 0;
int n_stopping = 0;
for (const auto & m : models.mapping) {
if (m.second.meta.is_running()) {
n_running++;
if (models.stopping_models.count(m.first)) {
n_stopping++;
}
}
}
int n_needed = 0;
int n_claimed = 0; // claimed the slot, but load() has not spawned yet
for (const auto & e : queue) {
if (!e.loading) {
n_needed++;
continue;
}
auto it = models.mapping.find(e.model_id);
if (it != models.mapping.end() && !it->second.meta.is_running()) {
n_claimed++;
}
}
int n_free = models.base_params.models_max - n_running + n_stopping - n_claimed;
while (n_free < n_needed) {
std::string victim = pick_victim(lk);
if (victim.empty()) {
return; // all remaining models are busy, wait for a request to end
}
SRV_INF("evicting idle LRU name=%s for a queued request\n", victim.c_str());
models.request_stop(victim);
n_free++;
}
}
server_models_sched::entry_t * server_models_sched::find(const std::string & model_id) {
for (auto & e : queue) {
if (e.model_id == model_id) {
return &e;
}
}
return nullptr;
}
void server_models_sched::check_lock(std::unique_lock<std::mutex> & lk) {
GGML_ASSERT(lk.owns_lock() && lk.mutex() == &models.mutex);
}
size_t server_models_sched::count_running() {
size_t count = 0;
for (const auto & m : models.mapping) {
if (m.second.meta.is_running()) {
count++;
}
}
return count;
}
//
// loading a model
//
bool server_models_sched::limits_exceeded(std::unique_lock<std::mutex> & lk, const std::string & name, bool verbose) {
if (!has_capacity(lk)) {
return true;
}
if (!memory_enabled()) {
return false;
}
const auto missing = deficit(name);
if (missing.empty()) {
return false;
}
bool any_running = false;
for (const auto & m : models.mapping) {
if (m.second.meta.is_running()) {
any_running = true;
break;
}
}
for (const auto & [dev, bytes] : missing) {
if (!verbose) {
break;
}
if (any_running) {
SRV_DBG("model name=%s is short by %zu MiB on %s\n", name.c_str(), bytes / (1024 * 1024), dev.c_str());
} else {
// nothing to unload: min is pessimistic, let --fit in the instance try
SRV_WRN("model name=%s is projected to be short by %zu MiB on %s with nothing else loaded, loading anyway\n",
name.c_str(), bytes / (1024 * 1024), dev.c_str());
}
}
return any_running;
}
void server_models_sched::make_room(const std::string & name) {
if (memory_enabled()) {
// the measure result decides what to unload
std::unique_lock<std::mutex> lk(models.mutex);
enqueue_measure(name, /* front */ true);
auto measured = [this, &name]() {
auto it = models.mapping.find(name);
return it == models.mapping.end() || it->second.meta.memory.is_settled();
};
if (!measured()) {
SRV_INF("waiting for the memory measurement of model name=%s\n", name.c_str());
models.cv.wait(lk, measured);
}
}
while (true) {
std::string lru_model_name;
{
std::unique_lock<std::mutex> lk(models.mutex);
if (!limits_exceeded(lk, name)) {
return;
}
lru_model_name = pick_victim(lk);
}
if (lru_model_name.empty()) {
return; // every running model is busy, load() reports the limit
}
SRV_INF("limits reached, removing LRU name=%s\n", lru_model_name.c_str());
models.unload(lru_model_name);
// wait for unload to complete
{
std::unique_lock<std::mutex> lk(models.mutex);
models.cv.wait(lk, [this, &lru_model_name]() {
return models.mapping[lru_model_name].meta.status == SERVER_MODEL_STATUS_UNLOADED;
});
}
}
}
//
// memory accounting
//
bool server_models_sched::memory_enabled() const {
return models.base_params.models_memory_margin > 0;
}
void server_models_sched::measure_unmeasured(std::unique_lock<std::mutex> & lk) {
check_lock(lk);
for (const auto & [name, inst] : models.mapping) {
enqueue_measure(name);
}
}
void server_models_sched::on_spawned(std::unique_lock<std::mutex> & lk, const std::string & name) {
check_lock(lk);
reserve(name);
}
void server_models_sched::on_ready(std::unique_lock<std::mutex> & lk, const std::string & name, const std::optional<server_model_memory> & memory) {
check_lock(lk);
if (!memory_enabled()) {
return;
}
auto it = models.mapping.find(name);
if (it == models.mapping.end()) {
return;
}
if (!memory.has_value()) {
reserve(name); // woke up from sleep
return;
}
// use the real usage, not the estimate
auto & info = it->second.meta.memory;
info.effective = memory;
release(name);
reserve(name);
calibrate(*memory, name);
if (info.status == SERVER_MODEL_MEMORY_FAILED && info.n_attempts < 2) {
// the model was maybe not downloaded at measure time, retry
info.status = SERVER_MODEL_MEMORY_UNMEASURED;
enqueue_measure(name);
}
}
void server_models_sched::on_sleeping(std::unique_lock<std::mutex> & lk, const std::string & name) {
check_lock(lk);
release(name);
}
void server_models_sched::on_unloaded(std::unique_lock<std::mutex> & lk, const std::string & name) {
check_lock(lk);
release(name);
auto it = models.mapping.find(name);
if (it != models.mapping.end()) {
it->second.meta.memory.effective.reset();
}
}
void server_models_sched::cancel_measurements(std::unique_lock<std::mutex> & lk) {
check_lock(lk);
measure_queue.clear();
if (measure_proc) {
SRV_INF("stopping measure process for model name=%s\n", measuring.c_str());
measure_proc->terminate();
// the monitor reaps it and clears measuring
}
}
bool server_models_sched::is_measuring(std::unique_lock<std::mutex> & lk) {
check_lock(lk);
return !measuring.empty();
}
void server_models_sched::enqueue_measure(const std::string & name, bool front) {
if (!memory_enabled()) {
return;
}
auto it = models.mapping.find(name);
if (it == models.mapping.end() || it->second.meta.memory.status != SERVER_MODEL_MEMORY_UNMEASURED) {
return;
}
auto pos = std::find(measure_queue.begin(), measure_queue.end(), name);
if (pos != measure_queue.end()) {
if (!front) {
return; // already queued
}
measure_queue.erase(pos);
}
if (front) {
measure_queue.push_front(name);
} else {
measure_queue.push_back(name);
}
measure_next();
}
void server_models_sched::measure_next() {
if (!measuring.empty()) {
return; // one measure child at a time
}
while (!measure_queue.empty()) {
std::string name = std::move(measure_queue.front());
measure_queue.pop_front();
auto it = models.mapping.find(name);
if (it == models.mapping.end() || it->second.meta.memory.status != SERVER_MODEL_MEMORY_UNMEASURED) {
continue; // removed or measured meanwhile
}
auto & meta = it->second.meta;
// same args as the instance, --offline: never download
std::vector<std::string> child_args = meta.args;
child_args.push_back("--offline");
std::vector<std::string> child_env = models.base_env;
child_env.push_back(std::string(SERVER_ENV_ROUTER_PORT) + "=" + std::to_string(models.base_params.port));
child_env.push_back(std::string(SERVER_ENV_CHILD_ROLE) + "=" + server_role_type_to_str(SERVER_ROLE_MEASURE));
auto proc = std::make_shared<server_subproc>();
if (!proc->sproc.create(child_args, subprocess_option_no_window, child_env)) {
SRV_ERR("failed to spawn measure process for model name=%s\n", name.c_str());
meta.memory.status = SERVER_MODEL_MEMORY_FAILED;
continue;
}
SRV_INF("measuring memory for model name=%s\n", name.c_str());
meta.memory.status = SERVER_MODEL_MEMORY_MEASURING;
meta.memory.n_attempts++;
measuring = name;
measure_proc = proc;
models.watch_child(name, proc, SERVER_ROLE_MEASURE, 0, MEASURE_TIMEOUT);
return;
}
}
void server_models_sched::on_measure_exit(const std::string & name, const std::shared_ptr<server_subproc> & proc, int exit_code) {
std::lock_guard<std::mutex> lk(models.mutex);
if (measure_proc != proc) {
return; // not the child we are waiting for
}
measure_proc.reset();
measuring.clear();
auto it = models.mapping.find(name);
if (it != models.mapping.end() && it->second.meta.memory.status == SERVER_MODEL_MEMORY_MEASURING) {
// exited without a result: crashed, or killed after the timeout
SRV_WRN("measure process for model name=%s exited with status %d before reporting, memory limits will not apply to it\n",
name.c_str(), exit_code);
it->second.meta.memory.status = SERVER_MODEL_MEMORY_FAILED;
models.notify_sse("model_memory", name, it->second.meta.memory.to_json());
}
models.cv.notify_all();
measure_next();
}
void server_models_sched::handle_measured(const std::string & name, const json & payload) {
std::lock_guard<std::mutex> lk(models.mutex);
auto it = models.mapping.find(name);
if (it == models.mapping.end()) {
return;
}
auto & info = it->second.meta.memory;
const std::string result = json_value(payload, "result", std::string());
bool ok = result == "measured";
if (ok) {
try {
info.min = server_model_memory::from_json(payload.at("min"));
info.max = server_model_memory::from_json(payload.at("max"));
} catch (const std::exception & e) {
SRV_ERR("malformed memory measurement for model name=%s: %s\n", name.c_str(), e.what());
info.min.reset();
info.max.reset();
ok = false;
}
} else {
SRV_WRN("measuring memory for model name=%s failed: %s\n",
name.c_str(), json_value(payload, "error", std::string("unknown error")).c_str());
}
if (ok) {
info.status = SERVER_MODEL_MEMORY_MEASURED;
calibrate(*info.max, ""); // latest view of the devices
const auto & mn = *info.min;
const auto & mx = *info.max;
for (size_t i = 0; i < mn.devices.size() && i < mx.devices.size(); i++) {
if (!mn.devices[i].is_device() || mn.devices[i].name != mx.devices[i].name) {
continue;
}
SRV_INF("model name=%s needs on %s: %zu MiB at n_ctx=%u (min), %zu MiB at n_ctx=%u (max), %zu MiB free\n",
name.c_str(), mn.devices[i].name.c_str(),
mn.devices[i].self() / (1024 * 1024), mn.n_ctx,
mx.devices[i].self() / (1024 * 1024), mx.n_ctx,
mx.devices[i].free / (1024 * 1024));
}
} else {
info.status = SERVER_MODEL_MEMORY_FAILED;
}
models.notify_sse("model_memory", name, info.to_json());
models.cv.notify_all();
}
void server_models_sched::calibrate(const server_model_memory & mem, const std::string & reporter) {
for (const auto & d : mem.devices) {
if (!d.is_device() || d.total == 0) {
continue;
}
// other instances are tracked, so they are not external (CUDA counts them in free, Metal does not)
int64_t tracked = 0;
for (const auto & [name, devs] : held) {
if (name == reporter) {
continue;
}
auto h = devs.find(d.name);
if (h != devs.end()) {
tracked += (int64_t) h->second;
}
}
// a measure child allocates nothing, its free does not include self
const int64_t self = reporter.empty() ? 0 : (int64_t) d.self();
auto & dm = device_memory[d.name];
dm.total = (int64_t) d.total;
dm.external = std::max<int64_t>(0, dm.total - (int64_t) d.free - self - tracked);
SRV_DBG("device %s: total=%" PRId64 " MiB, external=%" PRId64 " MiB, available=%" PRId64 " MiB\n",
d.name.c_str(), dm.total / (1024 * 1024), dm.external / (1024 * 1024), available(d.name) / (1024 * 1024));
}
}
void server_models_sched::reserve(const std::string & name) {
auto it = models.mapping.find(name);
if (it == models.mapping.end() || held.count(name)) {
return;
}
const auto & info = it->second.meta.memory;
// prefer real usage over the estimate
const auto & mem = info.effective ? info.effective : info.min;
if (!mem) {
return;
}
auto & devs = held[name];
for (const auto & d : mem->devices) {
if (d.is_device() && d.total > 0) {
devs[d.name] = d.self();
}
}
}
void server_models_sched::release(const std::string & name) {
held.erase(name);
}
int64_t server_models_sched::available(const std::string & dev) const {
auto dm = device_memory.find(dev);
if (dm == device_memory.end()) {
return INT64_MAX; // never reported, nothing to check against
}
int64_t n_held = 0;
for (const auto & [name, devs] : held) {
auto h = devs.find(dev);
if (h != devs.end()) {
n_held += (int64_t) h->second;
}
}
return dm->second.total - dm->second.external - n_held;
}
std::map<std::string, size_t> server_models_sched::deficit(const std::string & name) const {
std::map<std::string, size_t> res;
auto it = models.mapping.find(name);
if (it == models.mapping.end() || !it->second.meta.memory.min) {
return res;
}
const int64_t margin = (int64_t) models.base_params.models_memory_margin * 1024 * 1024;
for (const auto & d : it->second.meta.memory.min->devices) {
// host memory is assumed unlimited, same as --fit
if (!d.is_device() || d.total == 0) {
continue;
}
if (device_memory.find(d.name) == device_memory.end()) {
continue;
}
const int64_t avail = available(d.name) - margin;
if ((int64_t) d.self() > avail) {
res[d.name] = (size_t) ((int64_t) d.self() - avail);
}
}
return res;
}
+181
View File
@@ -0,0 +1,181 @@
#pragma once
#include "server-common.h"
#include <deque>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <vector>
// when to load and unload model instances: request queue (--models-max) and memory accounting (--models-memory-margin)
// see "Memory-based unloading" in the README
struct server_models;
// memory of a model instance per device, in bytes; from a measure child or from the running instance
struct server_model_memory {
struct device_t {
std::string kind; // "device", "host" or "buffer_type"
std::string name; // ggml device name, "Host" for host memory
size_t total = 0; // device memory, 0 unless kind == "device"
size_t free = 0; // free device memory when this was measured, 0 unless kind == "device"
size_t model = 0; // held by the instance, by kind of buffer
size_t context = 0;
size_t compute = 0;
bool is_device() const { return kind == "device"; }
size_t self() const { return model + context + compute; }
};
uint32_t n_ctx = 0; // context size this was measured at
std::vector<device_t> devices;
server_model_memory() = default;
server_model_memory(uint32_t n_ctx, const std::vector<struct common_memory_breakdown_row> & rows);
json to_json() const;
static server_model_memory from_json(const json & j); // throws on a malformed input
};
// memory measurement state of a model
enum server_model_memory_status {
SERVER_MODEL_MEMORY_UNMEASURED, // queued for a measure child
SERVER_MODEL_MEMORY_MEASURING, // its measure child is running
SERVER_MODEL_MEMORY_MEASURED,
SERVER_MODEL_MEMORY_FAILED, // measure failed, no memory limit for this model
};
static std::string server_model_memory_status_to_string(server_model_memory_status status) {
switch (status) {
case SERVER_MODEL_MEMORY_UNMEASURED: return "unmeasured";
case SERVER_MODEL_MEMORY_MEASURING: return "measuring";
case SERVER_MODEL_MEMORY_MEASURED: return "measured";
case SERVER_MODEL_MEMORY_FAILED: return "failed";
default: return "unknown";
}
}
struct server_model_memory_info {
server_model_memory_status status = SERVER_MODEL_MEMORY_UNMEASURED;
int n_attempts = 0; // to limit the retries
std::optional<server_model_memory> min = std::nullopt; // at the smallest context --fit may pick
std::optional<server_model_memory> max = std::nullopt; // at the largest context --fit picks, a snapshot
std::optional<server_model_memory> effective = std::nullopt; // what the running instance allocated, reset when it exits
bool is_settled() const {
return status == SERVER_MODEL_MEMORY_MEASURED || status == SERVER_MODEL_MEMORY_FAILED;
}
json to_json() const;
};
// functions taking lk need models.mutex held by lk
struct server_models_sched {
server_models_sched(server_models & models) : models(models) {}
//
// request queue (--models-max)
//
bool has_capacity(std::unique_lock<std::mutex> & lk);
// returns "" if no model can be given up
std::string pick_victim(std::unique_lock<std::mutex> & lk);
// requests wanting the same model share one entry, so they all need only one slot
// and all get unblocked by the single load that entry performs
void join(std::unique_lock<std::mutex> & lk, const std::string & model_id);
void leave(std::unique_lock<std::mutex> & lk, const std::string & model_id);
bool queue_empty(std::unique_lock<std::mutex> & lk);
// true if it is this model's turn to load, and nobody is loading it yet
bool try_claim(std::unique_lock<std::mutex> & lk, const std::string & model_id);
// on failure the entry is back in line; on success it stays until its waiters leave,
// so the model coming up is never picked as a victim before they use it
void claim_done(std::unique_lock<std::mutex> & lk, const std::string & model_id, bool ok);
// evict idle models while queued requests outnumber the slots that are free or being freed
// never blocks, so it is safe from any thread
void tick(std::unique_lock<std::mutex> & lk);
//
// loading a model
//
// true if loading name now exceeds models_max or the memory margin
bool limits_exceeded(std::unique_lock<std::mutex> & lk, const std::string & name, bool verbose = true);
// wait for the measure of name, then unload idle LRU models until it fits; call without models.mutex
void make_room(const std::string & name);
//
// memory accounting (--models-memory-margin), the router never touches a GPU
// track what each instance holds, child reports only calibrate the rest: works for per-process (Metal) and system-wide (CUDA) free memory
//
bool memory_enabled() const;
// queue a measure child for every model not measured yet
void measure_unmeasured(std::unique_lock<std::mutex> & lk);
// count a new instance at its min until it reports
void on_spawned(std::unique_lock<std::mutex> & lk, const std::string & name);
// memory: what the instance allocated, nullopt on wake up from sleep
void on_ready(std::unique_lock<std::mutex> & lk, const std::string & name, const std::optional<server_model_memory> & memory);
// a sleeping instance frees its model and context
void on_sleeping(std::unique_lock<std::mutex> & lk, const std::string & name);
void on_unloaded(std::unique_lock<std::mutex> & lk, const std::string & name);
// stop the measure child and drop the queue, for shutdown
void cancel_measurements(std::unique_lock<std::mutex> & lk);
bool is_measuring(std::unique_lock<std::mutex> & lk);
// called from the monitor thread, take models.mutex themselves
void handle_measured(const std::string & name, const json & payload);
void on_measure_exit(const std::string & name, const std::shared_ptr<server_subproc> & proc, int exit_code);
private:
struct entry_t {
std::string model_id;
int n_waiters; // requests waiting for this model
bool loading; // one of the waiters is doing the load right now
};
struct device_memory_t {
int64_t total = 0; // bytes
int64_t external = 0; // bytes not tracked by the router: other processes, backend contexts
};
server_models & models;
// requests waiting for a slot
std::deque<entry_t> queue;
// one measure child runs at a time
std::deque<std::string> measure_queue;
std::string measuring; // model whose measure child is running, empty when idle
std::shared_ptr<server_subproc> measure_proc;
std::map<std::string, device_memory_t> device_memory; // by device name, empty until the first child reports
std::map<std::string, std::map<std::string, size_t>> held; // model -> device -> bytes counted for its instance
entry_t * find(const std::string & model_id);
void check_lock(std::unique_lock<std::mutex> & lk);
size_t count_running();
// all below need models.mutex
void enqueue_measure(const std::string & name, bool front = false);
void measure_next();
// update device_memory from a child report; reporter: the instance that sent it, empty for a measure child
void calibrate(const server_model_memory & mem, const std::string & reporter);
void reserve(const std::string & name); // count the instance's memory (effective, else min)
void release(const std::string & name);
int64_t available(const std::string & dev) const; // total - external - what the instances hold
// bytes missing per device to load name at its min, empty if it fits
std::map<std::string, size_t> deficit(const std::string & name) const;
};
+72 -211
View File
@@ -7,6 +7,7 @@
#include "build-info.h"
#include "preset.h"
#include "download.h"
#include "fit.h"
#include "hf-cache.h"
#include "http.h"
#include "subproc.h"
@@ -24,6 +25,7 @@
#include <cstdlib>
#include <atomic>
#include <chrono>
#include <deque>
#include <queue>
#include <filesystem>
#include <fstream>
@@ -58,12 +60,16 @@ struct server_monitor {
}
// thread-safe
void watch(const std::string & name, std::shared_ptr<server_subproc> proc, server_role_type role, int port) {
// kill_after > 0: force-kill the child after that many seconds
void watch(const std::string & name, std::shared_ptr<server_subproc> proc, server_role_type role, int port, int kill_after = 0) {
child_t c;
c.name = name;
c.proc = std::move(proc);
c.role = role;
c.port = port;
if (kill_after > 0) {
c.deadline = ggml_time_ms() + (int64_t) kill_after * 1000;
}
if (!c.proc->has_output()) {
SRV_ERR("failed to get stdout/stderr of child process for name=%s\n", name.c_str());
c.eof = true;
@@ -118,7 +124,8 @@ private:
case cmd_t::STOP:
// the newest child with this name is the one the registry knows
for (auto it = children.rbegin(); it != children.rend(); ++it) {
if (it->name != cmd.name) {
// a measure child has the same name, skip it
if (it->name != cmd.name || it->role == SERVER_ROLE_MEASURE) {
continue;
}
if (cmd.send_exit && !it->eof) {
@@ -227,7 +234,7 @@ private:
now = ggml_time_ms();
for (auto it = children.begin(); it != children.end();) {
if (it->deadline && now >= it->deadline && !it->proc->stopped.load(std::memory_order_acquire)) {
SRV_WRN("force-killing model instance name=%s after timeout\n", it->name.c_str());
SRV_WRN("force-killing child process for name=%s after timeout\n", it->name.c_str());
it->proc->terminate();
it->deadline = 0;
}
@@ -254,171 +261,6 @@ private:
std::thread th;
};
struct server_lru_sched {
server_lru_sched(server_models & models) : models(models) {}
bool has_capacity(std::unique_lock<std::mutex> & lk) {
check_lock(lk);
return models.base_params.models_max <= 0
|| count_running() < (size_t) models.base_params.models_max;
}
// returns "" if no model can be given up
std::string pick_victim(std::unique_lock<std::mutex> & lk) {
check_lock(lk);
std::string victim;
int64_t victim_last_used = 0;
for (const auto & m : models.mapping) {
// a busy model is mid-request, one still coming up has no request to finish
if (m.second.req_count != 0 || !m.second.meta.is_ready_or_sleep()) {
continue;
}
// already on its way out, or a queued request wants it
if (models.stopping_models.count(m.first) || find(m.first)) {
continue;
}
if (victim.empty() || m.second.meta.last_used < victim_last_used) {
victim = m.first;
victim_last_used = m.second.meta.last_used;
}
}
return victim;
}
// requests wanting the same model share one entry, so they all need only one slot
// and all get unblocked by the single load that entry performs
void join(std::unique_lock<std::mutex> & lk, const std::string & model_id) {
check_lock(lk);
if (entry_t * e = find(model_id)) {
e->n_waiters++;
SRV_INF("request for name=%s joined the queue, %d waiting\n", model_id.c_str(), e->n_waiters);
return;
}
queue.push_back({ model_id, 1, false });
SRV_INF("request for name=%s queued at position %zu\n",
model_id.c_str(), queue.size());
}
void leave(std::unique_lock<std::mutex> & lk, const std::string & model_id) {
check_lock(lk);
for (auto it = queue.begin(); it != queue.end(); ++it) {
if (it->model_id == model_id) {
if (--it->n_waiters <= 0) {
queue.erase(it); // last one waiting for this model went away
}
return;
}
}
}
bool queue_empty(std::unique_lock<std::mutex> & lk) {
check_lock(lk);
return queue.empty();
}
// true if it is this model's turn to load, and nobody is loading it yet
bool try_claim(std::unique_lock<std::mutex> & lk, const std::string & model_id) {
check_lock(lk);
if (queue.empty() || queue.front().model_id != model_id || queue.front().loading) {
return false;
}
if (!has_capacity(lk)) {
return false;
}
queue.front().loading = true;
return true;
}
// on failure the entry is back in line; on success it stays until its waiters leave,
// so the model coming up is never picked as a victim before they use it
void claim_done(std::unique_lock<std::mutex> & lk, const std::string & model_id, bool ok) {
check_lock(lk);
if (ok) {
return;
}
for (auto it = queue.begin(); it != queue.end(); ++it) {
if (it->model_id == model_id) {
it->loading = false;
return;
}
}
}
// evict idle models while queued requests outnumber the slots that are free or being freed
// caller must hold models.mutex; never blocks, so it is safe from any thread
void tick(std::unique_lock<std::mutex> & lk) {
check_lock(lk);
if (models.base_params.models_max <= 0 || queue.empty()) {
return;
}
int n_running = 0;
int n_stopping = 0;
for (const auto & m : models.mapping) {
if (m.second.meta.is_running()) {
n_running++;
if (models.stopping_models.count(m.first)) {
n_stopping++;
}
}
}
int n_needed = 0;
int n_claimed = 0; // claimed the slot, but load() has not spawned yet
for (const auto & e : queue) {
if (!e.loading) {
n_needed++;
continue;
}
auto it = models.mapping.find(e.model_id);
if (it != models.mapping.end() && !it->second.meta.is_running()) {
n_claimed++;
}
}
int n_free = models.base_params.models_max - n_running + n_stopping - n_claimed;
while (n_free < n_needed) {
std::string victim = pick_victim(lk);
if (victim.empty()) {
return; // all remaining models are busy, wait for a request to end
}
SRV_INF("evicting idle LRU name=%s for a queued request\n", victim.c_str());
models.request_stop(victim);
n_free++;
}
}
private:
struct entry_t {
std::string model_id;
int n_waiters; // requests waiting for this model
bool loading; // one of the waiters is doing the load right now
};
entry_t * find(const std::string & model_id) {
for (auto & e : queue) {
if (e.model_id == model_id) {
return &e;
}
}
return nullptr;
}
void check_lock(std::unique_lock<std::mutex> & lk) {
GGML_ASSERT(lk.owns_lock() && lk.mutex() == &models.mutex);
}
size_t count_running() {
size_t count = 0;
for (const auto & m : models.mapping) {
if (m.second.meta.is_running()) {
count++;
}
}
return count;
}
server_models & models;
std::deque<entry_t> queue;
};
// short loopback budget for the resumable stream router to child JSON calls (probe, lookup,
// delete). distinct from params.timeout_read/write which only applies to the generation proxy
static constexpr int STREAM_LOOKUP_TIMEOUT_MS = 250;
@@ -430,6 +272,7 @@ static void unset_reserved_args(common_preset & preset, bool unset_model_args) {
preset.unset_option("LLAMA_ARG_API_KEY_FILE");
preset.unset_option("LLAMA_ARG_MODELS_DIR");
preset.unset_option("LLAMA_ARG_MODELS_MAX");
preset.unset_option("LLAMA_ARG_MODELS_MEMORY_MARGIN");
preset.unset_option("LLAMA_ARG_MODELS_PRESET");
preset.unset_option("LLAMA_ARG_MODELS_AUTOLOAD");
preset.unset_option("LLAMA_ARG_CONNECT");
@@ -555,7 +398,7 @@ server_models::server_models(
base_params(params),
base_env(get_environment()),
base_preset(ctx_preset.load_from_args(argc, argv)),
sched(std::make_unique<server_lru_sched>(*this)),
sched(std::make_unique<server_models_sched>(*this)),
monitor(std::make_unique<server_monitor>(*this)) {
// propagate base params to child
unset_reserved_args(base_preset, true);
@@ -568,9 +411,10 @@ server_models::server_models(
bin_path = fs_path_to_utf8(get_server_exec_path());
} catch (const std::exception & e) {
bin_path = argv[0];
LOG_WRN("failed to get server executable path: %s\n", e.what());
LOG_WRN("using original argv[0] as fallback: %s\n", argv[0]);
SRV_WRN("failed to get server executable path: %s\n", e.what());
SRV_WRN("using original argv[0] as fallback: %s\n", argv[0]);
}
unfinished_downloads.load();
load_models();
debug_fake_timing = !common_get_env("LLAMA_SERVER_DEBUG_FAKE_TIMING").empty();
@@ -1056,6 +900,7 @@ void server_models::load_models() {
apply_stop_timeout();
apply_hidden();
log_available_models();
sched->measure_unmeasured(lk);
// skipped on reload, see startup_models
if (startup_models.has_value()) {
@@ -1131,6 +976,10 @@ void server_models::load_models() {
auto it = final_presets.find(name);
if (it == final_presets.end()) continue; // erased above
if (preset_options_for_compare(inst.meta.preset) != preset_options_for_compare(it->second)) {
// different args, different memory: measure again
inst.meta.memory = {};
}
inst.meta.preset = it->second;
// re-parse aliases, then validate against other models
@@ -1198,6 +1047,7 @@ void server_models::load_models() {
apply_stop_timeout();
apply_hidden();
sched->measure_unmeasured(lk);
// clear reload flag under the lock, this releases the load() calls waiting on !is_reloading
is_reloading = false;
@@ -1285,32 +1135,6 @@ std::vector<server_model_meta> server_models::get_all_meta() {
return result;
}
void server_models::unload_lru() {
if (base_params.models_max <= 0) {
return; // no limit
}
// remove one of the servers if we passed the models_max (least recently used - LRU)
std::string lru_model_name;
{
std::unique_lock<std::mutex> lk(mutex);
if (sched->has_capacity(lk)) {
return;
}
lru_model_name = sched->pick_victim(lk);
}
if (!lru_model_name.empty()) {
SRV_INF("models_max limit reached, removing LRU name=%s\n", lru_model_name.c_str());
unload(lru_model_name);
// wait for unload to complete
{
std::unique_lock<std::mutex> lk(mutex);
cv.wait(lk, [this, &lru_model_name]() {
return mapping[lru_model_name].meta.status == SERVER_MODEL_STATUS_UNLOADED;
});
}
}
}
void server_models::load(const std::string & name) {
load(name, load_options{});
}
@@ -1326,7 +1150,9 @@ void server_models::load(const std::string & name, const load_options & opts) {
if (!has_model(name)) {
throw std::runtime_error("model name=" + name + " is not found");
}
unload_lru();
if (opts.role == SERVER_ROLE_INSTANCE) {
sched->make_room(name);
}
}
std::unique_lock<std::mutex> lk(mutex);
@@ -1378,20 +1204,12 @@ void server_models::load(const std::string & name, const load_options & opts) {
}
// Re-check capacity under the lock to prevent concurrent loads from
// exceeding models_max. Without this, the window between unload_lru()
// exceeding models_max. Without this, the window between make_room()
// releasing its lock and this lock_guard acquiring allows multiple
// threads to each observe capacity and all proceed to load.
// Download workers do not use models_max slots.
if (opts.role == SERVER_ROLE_INSTANCE && base_params.models_max > 0) {
size_t count_active = 0;
for (const auto & m : mapping) {
if (m.second.meta.is_running()) {
count_active++;
}
}
if (count_active >= (size_t)base_params.models_max) {
throw std::runtime_error("model limit reached, try again later");
}
if (opts.role == SERVER_ROLE_INSTANCE && sched->limits_exceeded(lk, name, /* verbose */ false)) {
throw std::runtime_error("model limit reached, try again later");
}
// prepare new instance info
@@ -1473,6 +1291,9 @@ void server_models::load(const std::string & name, const load_options & opts) {
auto proc = inst.subproc;
int port = inst.meta.port;
mapping[name] = std::move(inst);
if (opts.role == SERVER_ROLE_INSTANCE) {
sched->on_spawned(lk, name);
}
monitor->watch(name, proc, opts.role, port);
cv.notify_all();
}
@@ -1486,7 +1307,15 @@ void server_models::request_stop(const std::string & name, bool send_exit) {
monitor->stop(name, it->second.meta.stop_timeout, send_exit);
}
void server_models::watch_child(const std::string & name, std::shared_ptr<server_subproc> proc, server_role_type role, int port, int kill_after) {
monitor->watch(name, std::move(proc), role, port, kill_after);
}
void server_models::on_child_exit(const std::string & name, const std::shared_ptr<server_subproc> & proc, server_role_type role, int exit_code) {
if (role == SERVER_ROLE_MEASURE) {
sched->on_measure_exit(name, proc, exit_code);
return;
}
{
std::lock_guard<std::mutex> lk(mutex);
auto it = mapping.find(name);
@@ -1537,6 +1366,7 @@ void server_models::unload(const std::string & name) {
void server_models::unload_all() {
std::unique_lock<std::mutex> lk(mutex);
sched->cancel_measurements(lk);
for (auto & [name, inst] : mapping) {
if (inst.meta.status == SERVER_MODEL_STATUS_DOWNLOADING) {
SRV_INF("cancelling download for model name=%s\n", name.c_str());
@@ -1551,7 +1381,10 @@ void server_models::unload_all() {
}
}
// wait for every child to exit, the monitor force-kills the ones that ignore the exit command
cv.wait(lk, [this]() {
cv.wait(lk, [this, &lk]() {
if (sched->is_measuring(lk)) {
return false;
}
for (const auto & [name, inst] : mapping) {
if (inst.meta.is_running() || inst.meta.status == SERVER_MODEL_STATUS_DOWNLOADING) {
return false;
@@ -1598,6 +1431,12 @@ void server_models::update_status(const std::string & name, const update_status_
if (!args.progress.is_null()) {
meta.progress = args.progress;
}
switch (args.status) {
case SERVER_MODEL_STATUS_LOADED: sched->on_ready(lk, name, args.memory); break;
case SERVER_MODEL_STATUS_SLEEPING: sched->on_sleeping(lk, name); break;
case SERVER_MODEL_STATUS_UNLOADED: sched->on_unloaded(lk, name); break;
default: break;
}
// a model that comes up idle or goes down changes the slot count for queued requests
sched->tick(lk);
}
@@ -1615,6 +1454,9 @@ void server_models::update_status(const std::string & name, const update_status_
if (!args.progress.is_null()) {
data["progress"] = args.progress;
}
if (args.memory.has_value()) {
data["memory"] = args.memory->to_json();
}
// note: notify_sse doesn't acquire the lock, so no deadlock here
notify_sse("status_change", name, data);
}
@@ -2019,14 +1861,29 @@ void server_models::handle_child_state(const std::string & name, const std::stri
payload,
});
} break;
case SERVER_STATE_MEASURING:
{
sched->handle_measured(name, payload);
} break;
case SERVER_STATE_READY:
{
std::optional<server_model_memory> memory;
json info = payload;
if (info.contains("memory")) {
try {
memory = server_model_memory::from_json(info.at("memory"));
} catch (const std::exception & e) {
SRV_WRN("malformed memory report from model name=%s: %s\n", name.c_str(), e.what());
}
info.erase("memory");
}
update_status(name, {
SERVER_MODEL_STATUS_LOADED,
0,
// note: payload can be empty if this is a wakeup from sleep
payload.size() > 0 ? payload : nullptr,
info.size() > 0 ? info : nullptr,
{}, // reset progress info
memory,
});
} break;
case SERVER_STATE_SLEEPING:
@@ -2326,6 +2183,10 @@ void server_models_routes::init_routes() {
model_info["context_length"] = meta.n_ctx_train;
}
if (models.sched->memory_enabled()) {
model_info["memory"] = meta.memory.to_json();
}
// the sidecars a model has on disk, resolved as -hf would load them, for a
// model that resolves from a HF repo. A download on its way reports the
// kinds it pulls, as an unfinished one does
+16 -9
View File
@@ -5,8 +5,11 @@
#include "preset.h"
#include "server-common.h"
#include "server-http.h"
#include "server-models-sched.h"
#include "server-queue.h"
#include <atomic>
#include <deque>
#include <mutex>
#include <condition_variable>
#include <thread>
@@ -86,6 +89,9 @@ struct server_model_meta {
json architecture = server_model_architecture_json(false, false, false, {"text"});
uint32_t n_ctx_train = 0; // trained context, read from the GGUF metadata; 0 when unknown
// only populated with --models-memory-margin, see server_models_sched
server_model_memory_info memory = {};
bool is_ready() const {
return status == SERVER_MODEL_STATUS_LOADED;
}
@@ -152,12 +158,11 @@ private:
};
struct server_models_routes;
struct server_lru_sched; // defined in server-models.cpp
struct server_monitor; // defined in server-models.cpp
struct server_monitor; // defined in server-models.cpp
struct server_models {
friend struct server_models_routes;
friend struct server_lru_sched;
friend struct server_models_sched;
friend struct server_monitor;
private:
@@ -252,17 +257,14 @@ private:
std::vector<std::string> base_env;
common_preset base_preset; // base preset from llama-server CLI args
// queue of requests waiting for a models_max slot
std::unique_ptr<server_lru_sched> sched;
// request queue (models_max) and memory accounting
std::unique_ptr<server_models_sched> sched;
// if true, add some delay to simulate works (useful for testing)
bool debug_fake_timing = false;
void update_meta(const std::string & name, const server_model_meta & meta);
// unload least recently used models if the limit is reached
void unload_lru();
// not thread-safe, caller must hold mutex
void add_model(server_model_meta && meta);
@@ -270,6 +272,9 @@ private:
// not thread-safe, caller must hold mutex
void request_stop(const std::string & name, bool send_exit = true);
// let the monitor watch a child; kill_after > 0 force-kills it after that many seconds
void watch_child(const std::string & name, std::shared_ptr<server_subproc> proc, server_role_type role, int port, int kill_after = 0);
// called by the monitor once a child exited and was reaped
void on_child_exit(const std::string & name, const std::shared_ptr<server_subproc> & proc, server_role_type role, int exit_code);
@@ -336,6 +341,7 @@ public:
int exit_code = 0; // only valid if status == UNLOADED
json loaded_info = nullptr;
json progress = nullptr;
std::optional<server_model_memory> memory = std::nullopt; // what the instance allocated, sent with READY
};
// update the status of a model instance (thread-safe)
// also send SSE notification to /models/sse endpoint
@@ -369,8 +375,9 @@ public:
// called from the monitor thread
// payload per state:
// state = loading -> payload = {} (TODO: add progress info)
// state = ready -> payload = model_info (json), or {} if wakeup from sleeping
// state = ready -> payload = model_info (json) with a "memory" field, or {} if wakeup from sleeping
// state = sleeping -> payload = {}
// state = measuring -> payload = {result: "measured", min, max} or {result: "failed", error}
void handle_child_state(const std::string & name, const std::string & raw_input);
private:
+4 -1
View File
@@ -197,6 +197,9 @@ struct server_task {
// if set, embeddings is [q | k], and the output is instead the scaled dot product of q[pointer] and k[marker]
int32_t pointer = -1;
// if set, the outputs are the pooled embeddings of the sequence, one score per class
bool pooled = false;
// first prompt position that is read, -1 if none
int32_t pos_first() const {
int32_t pos = pointer;
@@ -238,7 +241,7 @@ struct server_task {
case SERVER_TASK_TYPE_RERANK:
return true;
case SERVER_TASK_TYPE_DECISION:
return !decision.markers.empty() || !decision.order.empty();
return !decision.markers.empty() || !decision.order.empty() || decision.pooled;
default:
return false;
}
+46 -1
View File
@@ -424,6 +424,45 @@ static int server_run_download(common_params & params, server_role & role) {
return 0;
}
//
// measure worker: a child of the router, reports the memory a model needs without loading it
//
static int server_run_measure(common_params & params, server_role & role, common_models_handler & models_handler) {
// the router sets --offline: only resolve the local path, never download
try {
common_models_handler_apply(models_handler, params);
} catch (const std::exception & e) {
SRV_ERR("failed to resolve model: %s\n", e.what());
return 1;
}
const std::string model_name = params.model.get_name();
// TODO: mmproj is not in llama_get_memory_breakdown, a multimodal model is underestimated
json payload;
try {
common_memory_reports m = common_measure_memory(params);
payload = {
{"result", "measured"},
{"n_ctx_train", m.n_ctx_train},
{"min", server_model_memory(m.n_ctx_min, m.info_min).to_json()},
{"max", server_model_memory(m.n_ctx_max, m.info_max).to_json()},
};
SRV_INF("memory measured for model name=%s at n_ctx=%u (min) and n_ctx=%u (max)\n",
model_name.c_str(), m.n_ctx_min, m.n_ctx_max);
} catch (const std::exception & e) {
SRV_ERR("failed to measure memory for model name=%s: %s\n", model_name.c_str(), e.what());
payload = {
{"result", "failed"},
{"error", e.what()},
};
}
role.notify_to_router(server_state_to_str(SERVER_STATE_MEASURING), payload);
return 0;
}
//
// router
//
@@ -664,7 +703,12 @@ static int server_run_instance(common_params & params, server_role & role, commo
std::thread monitor_thread;
if (role.is_managed_by_router()) {
monitor_thread = role.setup(shutdown_handler);
role.notify_to_router(server_state_to_str(SERVER_STATE_READY), routes.get_model_info());
json info = routes.get_model_info();
// the router tracks device memory from these reports
if (const auto * ll_ctx = ctx_server.get_llama_context()) {
info["memory"] = server_model_memory(llama_n_ctx(ll_ctx), common_memory_breakdown_rows(ll_ctx)).to_json();
}
role.notify_to_router(server_state_to_str(SERVER_STATE_READY), info);
}
// this call blocks the main thread until queue_tasks.terminate() is called
@@ -717,6 +761,7 @@ static int server_run(common_params & params, int argc, char ** argv, server_rol
switch (role_type) {
case SERVER_ROLE_DOWNLOAD: return server_run_download(params, role);
case SERVER_ROLE_MEASURE: return server_run_measure(params, role, models_handler);
case SERVER_ROLE_ROUTER: return server_run_router(params, argc, argv, opts);
case SERVER_ROLE_INSTANCE: return server_run_instance(params, role, models_handler, opts);
}
@@ -498,6 +498,54 @@ def test_anthropic_tool_result_error():
assert res.body["type"] == "message"
def test_anthropic_tool_reference():
"""Anthropic defer_loading and tool_reference convert to their OpenAI forms (behaviour: test_tool_call.py)"""
server.jinja = True
server.chat_template_file = '../../../models/templates/LFM3.jinja'
server.start()
search = {"name": "tool_search", "description": "Search for available tools by keyword",
"parameters": {"type": "object", "properties": {"query": {"type": "string"}}}}
purchase = {"name": "markItemPurchased", "description": "Mark an item as purchased",
"parameters": {"type": "object", "properties": {"id": {"type": "string"}}}}
anthropic = server.make_request("POST", "/v1/messages/count_tokens", data={
"model": "test",
"tools": [
{"name": t["name"], "description": t["description"], "input_schema": t["parameters"], **extra}
for t, extra in ((search, {}), (purchase, {"defer_loading": True}))
],
"messages": [
{"role": "user", "content": "Mark item abc as purchased"},
{"role": "assistant", "content": [
{"type": "tool_use", "id": "call_1", "name": "tool_search", "input": {"query": "purchase"}},
]},
{"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "call_1",
"content": [{"type": "tool_reference", "tool_name": "markItemPurchased"}]},
]},
],
})
oai = server.make_request("POST", "/chat/completions/input_tokens", data={
"tools": [
{"type": "function", "function": search},
{"type": "function", "function": {**purchase, "defer_loading": True}},
],
"messages": [
{"role": "user", "content": "Mark item abc as purchased"},
{"role": "assistant", "content": None, "tool_calls": [
{"id": "call_1", "type": "function",
"function": {"name": "tool_search", "arguments": "{\"query\": \"purchase\"}"}},
]},
{"role": "tool", "tool_call_id": "call_1",
"content": [{"type": "tool_reference", "name": "markItemPurchased"}]},
],
})
assert anthropic.status_code == 200, anthropic.body
assert oai.status_code == 200, oai.body
assert anthropic.body["input_tokens"] == oai.body["input_tokens"]
def test_anthropic_tool_streaming():
"""Test streaming with tool use"""
server.jinja = True
+66
View File
@@ -318,6 +318,72 @@ def test_router_queue_two_waiters_share_one_eviction():
assert _get_model_status(MODEL_A) == "unloaded"
# --models-memory-margin tests
def _get_model_memory(model_id: str) -> dict:
res = server.make_request("GET", "/models")
assert res.status_code == 200
for item in res.body.get("data", []):
if item.get("id") == model_id:
return item["memory"]
raise AssertionError(f"Model {model_id} not found in /models response")
def _wait_for_memory_status(model_id: str, desired: set[str], timeout: int = 120) -> dict:
deadline = time.time() + timeout
mem = None
while time.time() < deadline:
mem = _get_model_memory(model_id)
if mem["status"] in desired:
return mem
time.sleep(0.5)
raise AssertionError(f"memory status of {model_id} is {mem and mem['status']}, expected one of {desired}")
def test_router_memory_margin_measures_models():
"""every model is measured in the background on startup, a loaded instance reports what it allocated"""
global server
server.models_memory_margin = 1
server.start()
mem = _wait_for_memory_status(MODEL_B, {"measured", "failed"})
assert mem["status"] == "measured"
assert mem["effective"] is None
assert mem["min"]["n_ctx"] > 0
assert mem["max"]["n_ctx"] >= mem["min"]["n_ctx"]
kinds = {d["kind"] for d in mem["min"]["devices"]}
assert "host" in kinds
for d in mem["min"]["devices"]:
assert d["model"] + d["context"] + d["compute"] > 0 or d["kind"] != "host"
_load_model_and_wait(MODEL_B, timeout=120)
eff = _get_model_memory(MODEL_B)["effective"]
assert eff is not None
assert eff["n_ctx"] == mem["min"]["n_ctx"] # the test preset pins -c, so min == max == effective
server.make_request("POST", "/models/unload", data={"model": MODEL_B})
_wait_for_model_status(MODEL_B, {"unloaded"})
assert _get_model_memory(MODEL_B)["effective"] is None
def test_router_memory_margin_evicts_lru():
"""with a margin larger than any device, two models never fit together: the idle one is unloaded"""
global server
server.models_max = 4
server.models_memory_margin = 1 << 30 # MiB
server.start()
# nothing else is loaded, so the model is loaded although it does not fit the margin
_load_model_and_wait(MODEL_B, timeout=120)
eff = _get_model_memory(MODEL_B)["effective"]
if not any(d["kind"] == "device" for d in eff["devices"]):
pytest.skip("no GPU device, the memory limit does not apply") # ty: ignore[too-many-positional-arguments]
_load_model_and_wait(MODEL_C, timeout=120)
assert _get_model_status(MODEL_C) == "loaded"
assert _get_model_status(MODEL_B) == "unloaded"
def test_router_no_models_autoload():
global server
server.no_models_autoload = True
+51
View File
@@ -642,3 +642,54 @@ def do_test_hello_world(server: ServerProcess, **kwargs):
code = actual_arguments["code"]
assert isinstance(code, str), f"Expected code to be a string, got {type(code)}: {json.dumps(code)}"
assert re.match(r'''print\(("[Hh]ello,? [Ww]orld!?"|'[Hh]ello,? [Ww]orld!?')\)''', re.sub(r'#.*\n?', '', code)), f'Expected hello world, got {code}'
SEARCH_TOOL = {
"type": "function",
"function": {
"name": "tool_search",
"description": "Search for available tools by keyword",
"parameters": {"type": "object", "properties": {"query": {"type": "string"}}},
},
}
PURCHASE_FUNCTION = {
"name": "markItemPurchased",
"description": "Mark an item as purchased",
"parameters": {"type": "object", "properties": {"id": {"type": "string"}, "qty": {"type": "integer"}}},
}
PURCHASE_TOOL = {"type": "function", "function": PURCHASE_FUNCTION}
def count_deferred_tokens(tools, tool_content):
res = server.make_request("POST", "/chat/completions/input_tokens", data={
"tools": tools,
"messages": [
{"role": "user", "content": "Mark item abc as purchased"},
{"role": "assistant", "content": None, "tool_calls": [
{"id": "call_1", "type": "function",
"function": {"name": "tool_search", "arguments": "{\"query\": \"purchase\"}"}},
]},
{"role": "tool", "tool_call_id": "call_1", "content": tool_content},
],
})
assert res.status_code == 200, res.body
return res.body["input_tokens"]
def test_deferred_tool_reference():
"""A defer_loading tool is withheld from the system prompt, a tool_reference reveals its definition"""
server.jinja = True
server.chat_template_file = '../../../models/templates/LFM3.jinja'
server.start()
deferred = {"type": "function", "function": {**PURCHASE_FUNCTION, "defer_loading": True}}
tools = [SEARCH_TOOL, deferred]
# withheld from the system prompt
assert count_deferred_tokens(tools, "none") == count_deferred_tokens([SEARCH_TOOL], "none")
# the reference renders like the definition sent inline
by_reference = count_deferred_tokens(tools, [{"type": "tool_reference", "name": "markItemPurchased"}])
inline = count_deferred_tokens(tools, "<additional_tools>\n" + json.dumps(PURCHASE_FUNCTION) + "\n</additional_tools>")
assert by_reference == inline
+3
View File
@@ -94,6 +94,7 @@ class ServerProcess:
api_key: str | None = None
models_dir: str | None = None
models_max: int | None = None
models_memory_margin: int | None = None
models_preset: str | None = None
no_models_autoload: bool | None = None
lora_files: List[str] | None = None
@@ -188,6 +189,8 @@ class ServerProcess:
server_args.extend(["--models-dir", self.models_dir])
if self.models_max is not None:
server_args.extend(["--models-max", self.models_max])
if self.models_memory_margin is not None:
server_args.extend(["--models-memory-margin", self.models_memory_margin])
if self.models_preset:
server_args.extend(["--models-preset", self.models_preset])
if self.cors_origins:
+26
View File
@@ -84,3 +84,29 @@ Both [Soprano-80M](https://huggingface.co/ekwek/Soprano-80M) and [Soprano-1.1-80
python convert_hf_to_gguf.py path/to/Soprano-1.1-80M --outtype f32 --outfile soprano.gguf
python convert_hf_to_gguf.py path/to/Soprano-1.1-80M --mmproj --outtype f16 --outfile mmproj-soprano.gguf
```
## LFM3-Audio
Available params:
- `--tts-lang` can be `ar`, `da`, `de`, `el`, `en`, `en-gb`, `es`, `fi`, `fr`, `he`, `it`, `ja`, `ko`, `nl`, `pl`, `pt`, `sv`, `tr`, `zh` (default: `en`), other values are used as the language name
- `--tts-voice` can be `female` (default) or `male`
- Note: `--tts-speaker-file` is not used, the model has no voice cloning
- Note: `--top-k` and `--top-p` are not used, audio frames are sampled with the settings of the model card (LFM3-Audio: temperature 1.0, top-k 128, repetition penalty 1.1 over 4096 frames; LFM3-Omni: temperature 0.4, repetition penalty 1.2 over 64 frames), the `audio_*` keys of `generation_config.json` override them at conversion
- Each frame is 80 ms, generation stops at the end-of-audio frame
Example usage:
```sh
llama-tts -m lfm3-audio.gguf \
-mm mmproj-lfm3-audio.gguf \
-p "Hello world" \
--tts-voice male \
--output out.wav
```
The mmproj holds the audio encoder (for ASR), the depthformer and the codec decoder, the latter two are exported when the checkpoint has an `audio_tokenizer` directory:
```sh
python convert_hf_to_gguf.py path/to/lfm3-audio --outfile lfm3-audio.gguf
python convert_hf_to_gguf.py path/to/lfm3-audio --mmproj --outfile mmproj-lfm3-audio.gguf
```
+12 -3
View File
@@ -1,4 +1,5 @@
#include "arg.h"
#include "chat.h"
#include "common.h"
#include "sampling.h"
#include "log.h"
@@ -36,7 +37,7 @@ static void print_usage(int, char ** argv) {
LOG("\nexample usage:\n");
LOG("\n %s -m backbone.gguf -mm mmproj.gguf -p \"text to speak\" -o output.wav", argv[0]);
LOG("\n %s -hf user/model -p \"text to speak\" -o output.wav\n", argv[0]);
LOG("\nnote: --tts-lang and --tts-speaker-file may not be supported in all models");
LOG("\nnote: --tts-lang, --tts-voice and --tts-speaker-file may not be supported in all models");
LOG("\n use -n to limit the output length");
LOG("\n see tts/README.md for per-model usage notes");
LOG("\n\n");
@@ -111,14 +112,22 @@ int main(int argc, char ** argv) {
speaker_bitmap.reset(wrapper.bitmap);
}
// a model with a "tts" chat template (e.g. LFM3-Audio) speaks the prompt rendered from it
auto tmpls = common_chat_templates_init(model, "");
std::string prompt = common_chat_get_tts_prompt(tmpls.get(), params.prompt, params.tts_lang, params.tts_voice);
if (prompt.empty()) {
prompt = params.prompt;
}
mtmd_helper::gen_audio gen;
gen.init(lctx, mctx.get());
mtmd_helper_gen_audio_inp inp = mtmd_helper_gen_audio_inp_default();
inp.seq_id = 0;
inp.prompt = params.prompt.c_str();
inp.prompt_len = params.prompt.size();
inp.prompt = prompt.c_str();
inp.prompt_len = prompt.size();
inp.speaker_ref = speaker_bitmap.get();
inp.lang = params.tts_lang.c_str();
inp.voice = params.tts_voice.c_str();
inp.top_k = params.sampling.top_k;
inp.top_p = params.sampling.top_p;
inp.seed = params.sampling.seed;
@@ -118,7 +118,7 @@
{#if message.content.trim()}
<div class="relative max-w-[80%]">
<button
class="group/expand w-full text-left {!isExpanded && showExpandButton
class="group/expand block w-full text-left {!isExpanded && showExpandButton
? 'cursor-pointer'
: 'cursor-auto'}"
onclick={showExpandButton && !isExpanded ? toggleExpand : undefined}