mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-10-27 15:07:36 -05:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
338fce6511 | ||
|
|
19ff817fb2 | ||
|
|
eb7818c0b8 | ||
|
|
6c44591049 | ||
|
|
19cce69199 | ||
|
|
e86f5fd1e9 | ||
|
|
27b7a8a009 | ||
|
|
7ecb57f7ad | ||
|
|
8542e94350 | ||
|
|
922f53ed33 | ||
|
|
0c8c59ef3d | ||
|
|
3ab37aedfe | ||
|
|
85cc432e2c | ||
|
|
74a1fd3d03 | ||
|
|
7eed6b7685 | ||
|
|
276378515c | ||
|
|
d78f40d9d3 |
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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},
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -184,6 +184,10 @@ caps caps_get(jinja::program & prog) {
|
||||
{
|
||||
{"role", "user"},
|
||||
{"content", json::array({
|
||||
{
|
||||
{"type", "text"},
|
||||
{"text", content_marker}
|
||||
}
|
||||
})}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 -%}
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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 ]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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
@@ -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, ¶ms);
|
||||
}
|
||||
|
||||
// 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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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
@@ -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, ¶ms)) {
|
||||
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, ¶ms)) {
|
||||
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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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": "*",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
+1
-1
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user