model : add LiquidAI/d1-2B and LiquidAI/d1-omni-2B (#31275)

* model : add LiquidAI/d1-2B and LiquidAI/d1-omni-2B

Assisted-by: Claude Opus 5.5

* convert : keep the speech task templates out of the d1 GGUFs

Assisted-by: Claude Opus 5.5

* small refactoring: no_batching

---------

Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
This commit is contained in:
Tarek DakhranandXuan Son Nguyen authored and GitHub committed 2026-10-27 23:36:32 +01:00
1 parent 1b4554eff1
commit d05706b4f3
19 files changed
+307 -63

No files matched your search

+2
View File
@@ -1188,6 +1188,8 @@ static const std::map<common_decision_type, std::string> COMMON_DECISION_TYPE_NA
{ COMMON_DECISION_TYPE_GRANITE_SWITCH, "granite-switch"},
{ COMMON_DECISION_TYPE_H2O_LIGHTNING, "h2o-lightning" },
{ COMMON_DECISION_TYPE_MINICPM_OCR_JEV, "openbmb-minicpm-ocr-jev" },
{ COMMON_DECISION_TYPE_LFM3_D1, "lfm3-d1" },
{ COMMON_DECISION_TYPE_LFM3_D1_OMNI, "lfm3-d1-omni" },
// for future models, ALWAYS add org name prefix to avoid name collisions, e.g. "orgname-modelname"
};
+2
View File
@@ -981,6 +981,8 @@ enum common_decision_type {
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_LFM3_D1, // same as lfm2-d1, non-causal, label codes A..Z then AA, AB..
COMMON_DECISION_TYPE_LFM3_D1_OMNI, // same as lfm3-d1
COMMON_DECISION_TYPE_UNKNOWN, // a decision model of a type that is not supported
};
+1
View File
@@ -172,6 +172,7 @@ TEXT_MODEL_MAP: dict[str, str] = {
"D1OmniModel": "lfm2",
"Lfm2Model": "lfm2",
"Lfm2MoeForCausalLM": "lfm2",
"Lfm3D1Model": "lfm3",
"Lfm3ForCausalLM": "lfm3",
"Lfm3OmniForConditionalGeneration": "lfm3",
"Lfm3VlForConditionalGeneration": "lfm3",
+3 -1
View File
@@ -77,7 +77,9 @@ def _is_d1_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:
return json.load(f).get("auto_map", {}).get("AutoModel", "").endswith(".D1Model")
config = json.load(f)
# d1 on LFM3 is converted by conversion/lfm3.py
return config.get("auto_map", {}).get("AutoModel", "").endswith(".D1Model") and not config.get("model_type", "").startswith("lfm3")
@ModelBase.register_hparams_loader(_is_d1_checkpoint)
+59 -2
View File
@@ -11,8 +11,8 @@ import torch
if TYPE_CHECKING:
from torch import Tensor
from .base import MmprojModel, ModelBase, TextModel, gguf
from .lfm2 import lfm2_feed_forward_length
from .base import MmprojModel, ModelBase, TextModel, gguf, logger
from .lfm2 import D1Model, lfm2_feed_forward_length
@ModelBase.register("Lfm3ForCausalLM", "Lfm3VlForConditionalGeneration", "Lfm3OmniForConditionalGeneration")
@@ -119,6 +119,63 @@ class LFM3Model(TextModel):
yield from super().modify_tensors(data_torch, name, bid)
# d1 on LFM3-VL / LFM3-Omni: the stock weights, the d1 prompt read at its last token
_D1_TYPES = {"lfm3_vl": gguf.DecisionType.LFM3_D1, "lfm3_omni": gguf.DecisionType.LFM3_D1_OMNI}
def _is_lfm3_d1_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("architectures") == ["D1Model"] and config.get("model_type") in _D1_TYPES
@ModelBase.register_hparams_loader(_is_lfm3_d1_checkpoint)
def _load_lfm3_d1_hparams(dir_model: Path) -> dict[str, Any]:
logger.info("gguf: detected LFM3 d1 checkpoint")
hparams = ModelBase.load_hparams(dir_model, False, guess=False)
hparams["d1_family"] = hparams["model_type"]
# the mmproj is the base model's, the text model writes the decision
hparams["architectures"] = ["Lfm3VlForConditionalGeneration" if hparams["model_type"] == "lfm3_vl" else "Lfm3OmniForConditionalGeneration"]
hparams["text_config"]["architectures"] = ["Lfm3D1Model"]
return hparams
@ModelBase.register("Lfm3D1Model")
@ModelBase.example("LiquidAI/d1-2B", "LiquidAI/d1-omni-2B")
class LFM3D1Model(LFM3Model):
model_arch = gguf.MODEL_ARCH.LFM3
# how the checkpoint serves, hard-coded in the server: assert the checkpoint records the same
_served = {"d1_codes": 2, "d1_attention": "set"}
def set_vocab(self):
# the vocab without the speech task templates of LFM3-Audio/Omni, d1 only decides
TextModel.set_vocab(self)
# the d1 prompt of LFM2, the LFM3 template writes an empty thought before the answer
self.gguf_writer.add_chat_template([{"name": "systemone", "template": D1Model._systemone_template() + "<think></think>"}])
def set_gguf_parameters(self):
super().set_gguf_parameters()
for key, value in self._served.items():
if self.hparams.get(key) != value:
raise ValueError(f"unsupported d1 checkpoint: {key} = {self.hparams.get(key)!r}, expected {value!r}")
# a lone question reads the whole prompt both ways on the full attention layers
self.gguf_writer.add_causal_attention(False)
self.gguf_writer.add_decision_type(_D1_TYPES[self.hparams["d1_family"]])
# the option answers share one temperature
with open(self.dir_model / "calibration.json", encoding="utf-8") as f:
calibration = json.load(f)
if calibration.get("temperatures"):
raise ValueError("unsupported d1 calibration: per-bucket temperatures")
for name in ("noul", "choice", "score"):
self.gguf_writer.add_decision_temperature(name, calibration["default"])
class LFM3VLVisionMixin(MmprojModel):
"""LFM3-VL vision tower and projector, shared with LFM3-Omni."""
+2
View File
@@ -6544,6 +6544,8 @@ class DecisionType:
VLLM_D3 = "vllm-d3" # same as pplx-decider, non-causal attention
LFM2_D1 = "lfm2-d1" # same as openjev, the labels depend on the question type
LFM2_D1_OMNI = "lfm2-d1-omni" # same head as laya on a bidirectional LFM2 trunk, other prompt layout
LFM3_D1 = "lfm3-d1" # same as lfm2-d1 on LFM3-VL, full attention layers read the prompt both ways
LFM3_D1_OMNI = "lfm3-d1-omni" # same as lfm3-d1 on LFM3-Omni
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
+16 -12
View File
@@ -1614,8 +1614,9 @@ int llama_context::encode(const llama_batch_ext & batch_inp) {
// eagle3/DFlash: features as encoder input, and non-draft paths fall back to model's input dim
const int64_t n_vocab = model.vocab.n_tokens();
// note: during encode, we always output all tokens and skip position continuity checks (output_all=true)
if (!balloc->init(batch_inp, model.vocab, true)) {
// note: during encode, we skip position continuity checks and output all tokens
// a decoder without memory (see decode()) outputs the marked tokens, unless it extracts embeddings
if (!balloc->init(batch_inp, model.vocab, memory || cparams.embeddings)) {
LLAMA_LOG_ERROR("%s: failed to initialize batch\n", __func__);
return -1;
}
@@ -1644,17 +1645,20 @@ int llama_context::encode(const llama_batch_ext & batch_inp) {
n_queued_tokens += n_tokens;
const uint32_t n_outputs_all = balloc->get_n_outputs();
// reserve output buffer
if (output_reserve(n_tokens) < n_tokens) {
LLAMA_LOG_ERROR("%s: could not reserve space for batch with %u outputs\n", __func__, n_tokens);
if (output_reserve(n_outputs_all) < n_outputs_all) {
LLAMA_LOG_ERROR("%s: could not reserve space for batch with %u outputs\n", __func__, n_outputs_all);
return -2;
};
for (uint32_t i = 0; i < n_tokens; ++i) {
output_ids[i] = i;
const auto & out_ids = balloc->get_out_ids();
for (uint32_t i = 0; i < n_outputs_all; ++i) {
output_ids[out_ids[i]] = i;
}
n_outputs = n_tokens;
n_outputs = n_outputs_all;
const auto causal_attn_org = cparams.causal_attn;
@@ -1687,7 +1691,7 @@ int llama_context::encode(const llama_batch_ext & batch_inp) {
GGML_ASSERT(backend_res != nullptr);
GGML_ASSERT(logits.data != nullptr);
ggml_backend_tensor_get_async(backend_res, t_logits, logits.data, 0, n_tokens*n_vocab*sizeof(float));
ggml_backend_tensor_get_async(backend_res, t_logits, logits.data, 0, n_outputs*n_vocab*sizeof(float));
}
// extract embeddings
@@ -1702,8 +1706,8 @@ int llama_context::encode(const llama_batch_ext & batch_inp) {
GGML_ASSERT(embd.data != nullptr);
const uint32_t n_embd_out = hparams.n_embd_out();
GGML_ASSERT(n_tokens*n_embd_out <= (int64_t) embd.size);
ggml_backend_tensor_get_async(backend_embd, t_embd, embd.data, 0, n_tokens*n_embd_out*sizeof(float));
GGML_ASSERT(n_outputs*n_embd_out <= (int64_t) embd.size);
ggml_backend_tensor_get_async(backend_embd, t_embd, embd.data, 0, n_outputs*n_embd_out*sizeof(float));
} break;
case LLAMA_POOLING_TYPE_MEAN:
case LLAMA_POOLING_TYPE_CLS:
@@ -1751,8 +1755,8 @@ int llama_context::encode(const llama_batch_ext & batch_inp) {
GGML_ASSERT(backend_h != nullptr);
const uint32_t n_embd = hparams.n_embd_out();
GGML_ASSERT(n_tokens*n_embd <= (int64_t) embd_nextn.size);
ggml_backend_tensor_get_async(backend_h, t_h_nextn, embd_nextn.data, 0, n_tokens*n_embd*sizeof(float));
GGML_ASSERT(n_outputs*n_embd <= (int64_t) embd_nextn.size);
ggml_backend_tensor_get_async(backend_h, t_h_nextn, embd_nextn.data, 0, n_outputs*n_embd*sizeof(float));
}
// TODO: hacky solution
+10 -6
View File
@@ -479,7 +479,7 @@ void llm_graph_input_attn_no_cache::set_input(const llama_ubatch * ubatch) {
const int64_t n_kv = self_kq_mask->ne[0];
const int64_t n_tokens = ubatch->n_tokens;
const auto fill_mask = [&](auto * data, int64_t ne, int n_swa, llama_swa_type swa_type) {
const auto fill_mask = [&](auto * data, int64_t ne, int n_swa, llama_swa_type swa_type, bool causal) {
using T = std::remove_reference_t<decltype(*data)>;
std::fill(data, data + ne, llama_cast<T>(-INFINITY));
@@ -499,7 +499,7 @@ void llm_graph_input_attn_no_cache::set_input(const llama_ubatch * ubatch) {
}
// mask future tokens
if (cparams.causal_attn && p0 > p1) {
if (causal && p0 > p1) {
continue;
}
@@ -517,21 +517,25 @@ void llm_graph_input_attn_no_cache::set_input(const llama_ubatch * ubatch) {
}
};
// see llama_non_causal_type
const bool causal = cparams.causal_attn;
const bool causal_swa = cparams.causal_attn || hparams.non_causal_type == LLAMA_NON_CAUSAL_TYPE_DENSE_ONLY;
GGML_ASSERT(self_kq_mask);
GGML_ASSERT(ggml_backend_buffer_is_host(self_kq_mask->buffer));
if (self_kq_mask->type == GGML_TYPE_F16) {
fill_mask((ggml_fp16_t *) self_kq_mask->data, ggml_nelements(self_kq_mask), 0, LLAMA_SWA_TYPE_NONE);
fill_mask((ggml_fp16_t *) self_kq_mask->data, ggml_nelements(self_kq_mask), 0, LLAMA_SWA_TYPE_NONE, causal);
} else {
fill_mask((float *) self_kq_mask->data, ggml_nelements(self_kq_mask), 0, LLAMA_SWA_TYPE_NONE);
fill_mask((float *) self_kq_mask->data, ggml_nelements(self_kq_mask), 0, LLAMA_SWA_TYPE_NONE, causal);
}
if (hparams.swa_type != LLAMA_SWA_TYPE_NONE) {
GGML_ASSERT(self_kq_mask_swa);
GGML_ASSERT(ggml_backend_buffer_is_host(self_kq_mask_swa->buffer));
if (self_kq_mask_swa->type == GGML_TYPE_F16) {
fill_mask((ggml_fp16_t *) self_kq_mask_swa->data, ggml_nelements(self_kq_mask_swa), hparams.n_swa, hparams.swa_type);
fill_mask((ggml_fp16_t *) self_kq_mask_swa->data, ggml_nelements(self_kq_mask_swa), hparams.n_swa, hparams.swa_type, causal_swa);
} else {
fill_mask((float *) self_kq_mask_swa->data, ggml_nelements(self_kq_mask_swa), hparams.n_swa, hparams.swa_type);
fill_mask((float *) self_kq_mask_swa->data, ggml_nelements(self_kq_mask_swa), hparams.n_swa, hparams.swa_type, causal_swa);
}
}
}
+1
View File
@@ -34,6 +34,7 @@ enum llama_non_causal_type {
LLAMA_NON_CAUSAL_TYPE_ALL = 0, // all layers non-causal, SWA still applied (gemma 3, qwen-vl, ...)
LLAMA_NON_CAUSAL_TYPE_SWA_ONLY = 1, // SWA layers non-causal, dense layers stay causal (gemma 4)
LLAMA_NON_CAUSAL_TYPE_SWA_FULL = 2, // all layers non-causal, SWA not applied between tokens of the current ubatch (deepseek 4)
LLAMA_NON_CAUSAL_TYPE_DENSE_ONLY = 3, // dense layers non-causal, SWA layers stay causal (lfm3 d1)
};
// M-RoPE: which input position slot feeds each RoPE section
+3
View File
@@ -1749,6 +1749,9 @@ void llama_kv_cache::set_input_kq_mask(ggml_tensor * dst, const llama_ubatch * u
if (!causal_attn && hparams.non_causal_type == LLAMA_NON_CAUSAL_TYPE_SWA_ONLY) {
causal_attn = swa_type == LLAMA_SWA_TYPE_NONE;
}
if (!causal_attn && hparams.non_causal_type == LLAMA_NON_CAUSAL_TYPE_DENSE_ONLY) {
causal_attn = swa_type != LLAMA_SWA_TYPE_NONE;
}
//const int64_t t_start = ggml_time_us();
+4 -1
View File
@@ -2801,10 +2801,13 @@ ggml_tensor * llama_model::get_rope_factors(const llama_cparams & cparams, int i
llama_memory_i * llama_model::create_memory(const llama_memory_params & params, const llama_cparams & cparams) const {
llama_memory_i * res;
// the non-causal LFM2 decision graph reads the whole prompt in one batch, nothing is kept
// the non-causal LFM2 and LFM3 decision graphs read the whole prompt in one batch, nothing is kept
if (arch == LLM_ARCH_LFM2 && !hparams.causal_attn && hparams.n_layer_decision > 0) {
return nullptr;
}
if (arch == LLM_ARCH_LFM3 && !hparams.causal_attn) {
return nullptr;
}
switch (arch) {
// Models that need specific instantiation should be handled in the
+27
View File
@@ -84,6 +84,33 @@ ggml_tensor * llm_build_lfm_base::build_lfm_shortconv(const llama_model & model,
return y;
}
ggml_tensor * llm_build_lfm_base::build_lfm_shortconv(const llama_model & model,
ggml_tensor * cur,
int il) {
const int64_t d_conv = hparams.n_shortconv_l_cache - 1;
auto * bcx = build_lora_mm(model.layers[il].shortconv.in_proj, cur);
cb(bcx, "model.layers.{}.conv.in_proj", il);
const int64_t chunk_size = bcx->ne[0] / 3;
auto * b = ggml_view_2d(ctx0, bcx, chunk_size, bcx->ne[1], bcx->nb[1], 0 * chunk_size * ggml_element_size(bcx));
auto * c = ggml_view_2d(ctx0, bcx, chunk_size, bcx->ne[1], bcx->nb[1], 1 * chunk_size * ggml_element_size(bcx));
auto * x = ggml_view_2d(ctx0, bcx, chunk_size, bcx->ne[1], bcx->nb[1], 2 * chunk_size * ggml_element_size(bcx));
// causal from a zero state
auto * bx = ggml_cont(ctx0, ggml_transpose(ctx0, ggml_mul(ctx0, b, x)));
bx = ggml_pad_ext(ctx0, bx, d_conv, 0, 0, 0, 0, 0, 0, 0);
auto * conv_out = ggml_ssm_conv(ctx0, bx, model.layers[il].shortconv.conv);
cb(conv_out, "model.layers.{}.conv.conv", il);
auto * y = ggml_mul(ctx0, c, ggml_reshape_2d(ctx0, conv_out, chunk_size, n_tokens));
y = build_lora_mm(model.layers[il].shortconv.out_proj, y);
cb(y, "model.layers.{}.conv.out_proj", il);
return y;
}
ggml_tensor * llm_build_lfm_base::build_lfm_dense_ffn(const llama_model & model,
ggml_tensor * cur,
int il) {
+29 -9
View File
@@ -19,6 +19,11 @@ void llama_model_lfm3::load_arch_hparams(llama_model_loader & ml) {
ml.get_key(LLM_KV_ROPE_FREQ_BASE_SWA, hparams.rope_freq_base_train_swa);
// d1: full attention layers read the prompt both ways, sliding window layers stay causal
if (!hparams.causal_attn) {
hparams.non_causal_type = LLAMA_NON_CAUSAL_TYPE_DENSE_ONLY;
}
switch (hparams.n_ff()) {
case 7680: type = LLM_TYPE_1_7B; break;
default: type = LLM_TYPE_UNKNOWN;
@@ -84,15 +89,19 @@ void llama_model_lfm3::load_arch_tensors(llama_model_loader &) {
}
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);
if (!hparams.causal_attn) {
return std::make_unique<graph<true>>(*this, params);
}
return std::make_unique<graph<false>>(*this, params);
}
llama_model_lfm3::graph::graph(const llama_model & model, const llm_graph_params & params) :
template <bool decision>
llama_model_lfm3::graph<decision>::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 * {
auto build_attn_block = [&model, this](ggml_tensor * cur_in,
ggml_tensor * inp_pos,
auto * 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);
@@ -140,7 +149,9 @@ llama_model_lfm3::graph::graph(const llama_model & model, const llm_graph_params
ggml_build_forward_expand(gf, cur);
auto * inp_hybrid = build_inp_mem_hybrid_iswa();
// decision: no memory, the whole prompt is read in one batch
auto * inp_hybrid = decision ? nullptr : build_inp_mem_hybrid_iswa();
auto * inp_attn = decision ? build_attn_inp_no_cache() : nullptr;
ggml_tensor * inp_pos = build_inp_pos();
ggml_tensor * inp_out_ids = build_inp_out_ids();
@@ -152,8 +163,14 @@ llama_model_lfm3::graph::graph(const llama_model & model, const llm_graph_params
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);
if constexpr (decision) {
cur = build_lfm_shortconv(model, cur, il);
} else {
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 if constexpr (decision) {
cur = build_attn_block(cur, inp_pos, inp_attn, il);
} else {
cur = build_attn_block(cur, inp_pos, inp_hybrid->get_attn(), il);
}
@@ -195,3 +212,6 @@ llama_model_lfm3::graph::graph(const llama_model & model, const llm_graph_params
res->t_logits = cur;
}
template struct llama_model_lfm3::graph<false>;
template struct llama_model_lfm3::graph<true>;
+7
View File
@@ -111,6 +111,11 @@ struct llm_build_lfm_base : public llm_graph_context {
ggml_tensor * cur,
int il);
// no recurrent state (decision graphs): causal over the ubatch from a zero state
ggml_tensor * build_lfm_shortconv(const llama_model & model,
ggml_tensor * cur,
int il);
ggml_tensor * build_lfm_dense_ffn(const llama_model & model,
ggml_tensor * cur,
int il);
@@ -2438,6 +2443,8 @@ struct llama_model_lfm3 : public llama_model_base {
void load_arch_hparams(llama_model_loader & ml) override;
void load_arch_tensors(llama_model_loader & ml) override;
// decision: d1, the prompt read in one batch, no memory
template <bool decision>
struct graph : public llm_build_lfm_base {
graph(const llama_model & model, const llm_graph_params & params);
};
+2
View File
@@ -220,6 +220,8 @@ std::vector<std::string> server_model_output_modalities(common_decision_type dec
case COMMON_DECISION_TYPE_LFM2_D1_OMNI:
case COMMON_DECISION_TYPE_H2O_LIGHTNING:
case COMMON_DECISION_TYPE_MINICPM_OCR_JEV:
case COMMON_DECISION_TYPE_LFM3_D1:
case COMMON_DECISION_TYPE_LFM3_D1_OMNI:
out = {"decisions"};
break;
case COMMON_DECISION_TYPE_GRANITE_SWITCH:
+8 -1
View File
@@ -529,6 +529,12 @@ struct server_slot {
return false;
}
// lfm3 d1 has no memory, its prompt is read as one flat ubatch and the short convs would mix the sequences
// TODO: batch them along the sequence dimension, this needs ubatches of sequences with unequal lengths
if (task->decision.no_batching || other_slot.task->decision.no_batching) {
return false;
}
return task->type == other_slot.task->type
&& inp_embd.size() == other_slot.inp_embd.size()
&& are_lora_equal(lora, other_slot.lora);
@@ -3955,7 +3961,8 @@ private:
}
// TODO: support memory-less logits computation
if (slot.task->need_logits() && !llama_get_memory(ctx_tgt)) {
// a decision reads its logits from the one batch of its prompt
if (slot.task->need_logits() && !llama_get_memory(ctx_tgt) && slot.task->type != SERVER_TASK_TYPE_DECISION) {
send_error(slot, "the current context does not support logits computation. skipping", ERROR_TYPE_SERVER);
slot.release();
return;
+119 -31
View File
@@ -162,9 +162,13 @@ void server_decision_context::init(const llama_model * model) {
noul_criteria_order = true;
strict_payload = true;
canonical_legend = true;
} else if (model_type == COMMON_DECISION_TYPE_LFM2_D1) {
} else if (model_type == COMMON_DECISION_TYPE_LFM2_D1 || model_type == COMMON_DECISION_TYPE_LFM3_D1 || model_type == COMMON_DECISION_TYPE_LFM3_D1_OMNI) {
n_options_max = 255;
noul_true_first = true;
if (model_type != COMMON_DECISION_TYPE_LFM2_D1) {
// the texts of the special tokens are broken in the input
init_quoted_spellings(LLAMA_TOKEN_ATTR_CONTROL, 2);
}
} else if (model_type == COMMON_DECISION_TYPE_LFM2_D1_OMNI) {
token_marker = llama_vocab_mask(vocab);
if (token_marker == LLAMA_TOKEN_NULL) {
@@ -186,22 +190,7 @@ void server_decision_context::init(const llama_model * model) {
throw std::runtime_error("decision model has no labels");
}
// the texts of the control and user-defined tokens are quoted in the input, so that they stay text
for (llama_token id = 0; id < llama_vocab_n_tokens(vocab); id++) {
if (llama_vocab_get_attr(vocab, id) & (LLAMA_TOKEN_ATTR_CONTROL | LLAMA_TOKEN_ATTR_USER_DEFINED)) {
const std::string text = llama_vocab_get_text(vocab, id);
if (!text.empty()) {
quoted_spellings.push_back(text);
}
}
}
std::stable_sort(quoted_spellings.begin(), quoted_spellings.end(), [](const std::string & a, const std::string & b) {
return a.size() > b.size();
});
for (const auto & spelling : quoted_spellings) {
if (quoted_first.find(spelling[0]) == std::string::npos) {
quoted_first += spelling[0];
}
}
init_quoted_spellings(LLAMA_TOKEN_ATTR_CONTROL | LLAMA_TOKEN_ATTR_USER_DEFINED, 1);
n_options_max = labels.size();
noul_true_first = true;
} else if (model_type == COMMON_DECISION_TYPE_MINICPM_OCR_JEV) {
@@ -221,17 +210,42 @@ void server_decision_context::init(const llama_model * model) {
SRV_INF("decision model type: %s\n", type_name.c_str());
}
void server_decision_context::init_quoted_spellings(uint32_t attrs, size_t min_size) {
for (llama_token id = 0; id < llama_vocab_n_tokens(vocab); id++) {
if (llama_vocab_get_attr(vocab, id) & attrs) {
const std::string text = llama_vocab_get_text(vocab, id);
if (text.size() >= min_size) {
quoted_spellings.push_back(text);
}
}
}
std::stable_sort(quoted_spellings.begin(), quoted_spellings.end(), [](const std::string & a, const std::string & b) {
return a.size() > b.size();
});
for (const auto & spelling : quoted_spellings) {
if (quoted_first.find(spelling[0]) == std::string::npos) {
quoted_first += spelling[0];
}
}
}
//
// request parsing
//
// lfm3-d1-omni: a spoken question, its instructions are an input_audio part
static bool decision_is_spoken(const json & instructions) {
return instructions.is_object() && json_value(instructions, "type", std::string()) == "input_audio";
}
static bool decision_is_payload(const json & val) {
return val.is_string() || val.is_object() || val.is_array();
}
std::vector<server_decision_question> server_decision_context::parse_questions(const json & body) const {
// lfm2-d1, lfm2-d1-omni and vllm-d3 accept a null state, for example to ask about images only
if (!body.contains("state") || (body.at("state").is_null() && type != COMMON_DECISION_TYPE_LFM2_D1 && type != COMMON_DECISION_TYPE_LFM2_D1_OMNI && type != COMMON_DECISION_TYPE_VLLM_D3)) {
// lfm2-d1, lfm2-d1-omni, lfm3-d1 and vllm-d3 accept a null state, for example to ask about images only
const bool null_state = type == COMMON_DECISION_TYPE_LFM2_D1 || type == COMMON_DECISION_TYPE_LFM2_D1_OMNI || type == COMMON_DECISION_TYPE_VLLM_D3 || type == COMMON_DECISION_TYPE_LFM3_D1 || type == COMMON_DECISION_TYPE_LFM3_D1_OMNI;
if (!body.contains("state") || (body.at("state").is_null() && !null_state)) {
throw std::invalid_argument("\"state\" must be provided");
}
if (strict_payload && !decision_is_payload(body.at("state"))) {
@@ -264,6 +278,9 @@ std::vector<server_decision_question> server_decision_context::parse_questions(c
if (strict_payload && (!decision_is_payload(instructions) || instructions == "")) {
throw err("\"instructions\" must be non-empty text, an object or an array");
}
if (type == COMMON_DECISION_TYPE_LFM3_D1 && decision_is_spoken(instructions)) {
throw err("spoken instructions need a model with audio input");
}
server_decision_question question;
question.id = id;
@@ -581,31 +598,65 @@ static std::string decision_kev_text(const json & val) {
return server_decision_escape_special_tokens(decision_kev_render(val));
}
// lfm2-d1-omni: special tokens written in the input must not be parsed as such, in keys too (d1-omni prompt.py: escape)
static json decision_d1omni_escape(const json & val, int depth = 0) {
// lfm2-d1-omni, lfm3-d1: special tokens written in the input must not be parsed as such, in keys too
template <typename F>
static json decision_escape(const json & val, const F & escape, int depth = 0) {
if (depth >= RECURSIVE_LIMIT) {
throw std::invalid_argument("JSON is nested more than " + std::to_string(RECURSIVE_LIMIT) + " levels deep");
}
if (val.is_string()) {
return server_decision_escape_special_tokens(val.get<std::string>());
return escape(val.get<std::string>());
}
if (val.is_array()) {
json out = json::array();
for (const auto & item : val) {
out.push_back(decision_d1omni_escape(item, depth + 1));
out.push_back(decision_escape(item, escape, depth + 1));
}
return out;
}
if (val.is_object()) {
json out = json::object();
for (const auto & [key, item] : val.items()) {
out[server_decision_escape_special_tokens(key)] = decision_d1omni_escape(item, depth + 1);
out[escape(key)] = decision_escape(item, escape, depth + 1);
}
return out;
}
return val;
}
std::string server_decision_context::d1_break(const std::string & text) const {
static const std::string zwsp = "\xE2\x80\x8B";
std::string cur = text;
// again for a match that overlapped the one broken before it
for (bool changed = true; changed;) {
changed = false;
std::string out;
for (size_t i = 0; i < cur.size();) {
const std::string * hit = nullptr;
if (quoted_first.find(cur[i]) != std::string::npos) {
for (const auto & spelling : quoted_spellings) {
if (cur.compare(i, spelling.size(), spelling) == 0) {
hit = &spelling;
break;
}
}
}
if (hit == nullptr) {
out += cur[i++];
continue;
}
// after the first character, its UTF-8 bytes
const unsigned char c = (*hit)[0];
const size_t n = c < 0x80 ? 1 : c < 0xE0 ? 2 : c < 0xF0 ? 3 : 4;
out += hit->substr(0, n) + zwsp + hit->substr(n);
i += hit->size();
changed = true;
}
cur = std::move(out);
}
return cur;
}
//
// h2o-lightning: the texts are rendered the way the reference shim renders them with Python (json.dumps, str.lstrip)
//
@@ -955,6 +1006,9 @@ void server_decision_context::d1_labels(const server_decision_question & questio
codes.push_back(question.options[i].key);
} else if (n_options <= 26) {
codes.push_back(std::string(1, 'A' + i));
} else if (type == COMMON_DECISION_TYPE_LFM3_D1 || type == COMMON_DECISION_TYPE_LFM3_D1_OMNI) {
// A..Z, then AA, AB..
codes.push_back(i < 26 ? std::string(1, 'A' + i) : std::string{(char) ('A' + (i - 26) / 26), (char) ('A' + (i - 26) % 26)});
} else {
codes.push_back(string_format("%02zu", i));
}
@@ -1012,7 +1066,7 @@ json server_decision_context::render_options(const server_decision_question & qu
std::vector<std::string> d1_texts;
std::vector<llama_tokens> d1_groups;
if (type == COMMON_DECISION_TYPE_LFM2_D1) {
if (type == COMMON_DECISION_TYPE_LFM2_D1 || type == COMMON_DECISION_TYPE_LFM3_D1 || type == COMMON_DECISION_TYPE_LFM3_D1_OMNI) {
d1_labels(question, d1_texts, d1_groups);
}
@@ -1092,8 +1146,17 @@ std::string server_decision_context::render(
inp = decision_replace_text(inp, text_marker, " ");
}
// lfm3-d1-omni: a spoken question is heard where its text would be, after the media of the state
const bool is_spoken = decision_is_spoken(question.instructions);
if (is_spoken) {
inp["instructions"] = "";
}
if (is_lfm3_d1()) {
inp = decision_escape(inp, [this](const std::string & text) { return d1_break(text); });
}
if (type == COMMON_DECISION_TYPE_LFM2_D1_OMNI) {
inp = decision_replace_text(decision_d1omni_escape(inp), D1OMNI_MARKER, "<<d1omni ");
inp = decision_replace_text(decision_escape(inp, server_decision_escape_special_tokens), D1OMNI_MARKER, "<<d1omni ");
inp["audio"] = is_audio;
inp["sep"] = D1OMNI_SEP;
inp["mark_state"] = D1OMNI_MARK_STATE;
@@ -1103,13 +1166,16 @@ std::string server_decision_context::render(
// the template puts one media marker per image
json images = json::array();
if (n_images > 0) {
if (n_images > 0 || is_spoken) {
inp = decision_replace_text(inp, get_media_marker(), " ");
for (size_t i = 0; i < n_images; i++) {
images.push_back(get_media_marker());
}
}
inp["images"] = images;
if (is_spoken) {
inp["instructions"] = get_media_marker();
}
jinja::context ctx(tmpl->source());
jinja::global_from_json(ctx, inp, false);
@@ -1145,12 +1211,18 @@ void server_decision_context::fill_task(
return;
}
std::vector<raw_buffer> media = files;
if (decision_is_spoken(question.instructions)) {
const json audio = json_value(question.instructions, "input_audio", json::object());
decision_load_audio(audio.contains("data") ? audio.at("data") : json(), media);
}
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
task.decision.labels.assign(labels.begin(), labels.begin() + n_outputs(question));
}
if (type == COMMON_DECISION_TYPE_LFM2_D1) {
if (type == COMMON_DECISION_TYPE_LFM2_D1 || type == COMMON_DECISION_TYPE_LFM3_D1 || type == COMMON_DECISION_TYPE_LFM3_D1_OMNI) {
std::vector<std::string> texts;
std::vector<llama_tokens> groups;
d1_labels(question, texts, groups);
@@ -1158,11 +1230,13 @@ void server_decision_context::fill_task(
task.decision.labels.insert(task.decision.labels.end(), group.begin(), group.end());
task.decision.label_groups.push_back(group.size());
}
// the short convs of lfm3 d1 have no memory to separate the sequences of a batch
task.decision.no_batching = is_lfm3_d1();
}
if (type == COMMON_DECISION_TYPE_OPENJEV || 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_LFM2_D1 ||
type == COMMON_DECISION_TYPE_H2O_LIGHTNING) {
if (!files.empty()) {
task.tokens = process_mtmd_prompt(mctx, prompt, files, init_opt);
type == COMMON_DECISION_TYPE_H2O_LIGHTNING || type == COMMON_DECISION_TYPE_LFM3_D1 || type == COMMON_DECISION_TYPE_LFM3_D1_OMNI) {
if (!media.empty()) {
task.tokens = process_mtmd_prompt(mctx, prompt, media, init_opt);
return;
}
}
@@ -1556,6 +1630,10 @@ double server_decision_context::get_decision_confidence(server_decision_question
if (type == COMMON_DECISION_TYPE_VLLM_DECISION2 || type == COMMON_DECISION_TYPE_VLLM_D3) {
return decision_confidence_entropy(probs);
}
// lfm3-d1: the probability of the answer (d1 api.py)
if (is_lfm3_d1()) {
return *std::max_element(probs.begin(), probs.end());
}
switch (question_type) {
case SERVER_DECISION_QUESTION_CHOICE: return decision_confidence_choice(probs);
case SERVER_DECISION_QUESTION_SCORE: return decision_confidence_score(probs);
@@ -1644,6 +1722,16 @@ json server_decision_context::format_answer(const server_decision_question & que
const json & description = question.options[i].description;
legend[question.options[i].key] = canonical_legend && !description.is_string() ? json(decision_sort_keys(description).dump()) : description;
}
// lfm3-d1: the median level, the first whose cumulative probability reaches one half (d1 api.py)
if (is_lfm3_d1()) {
double mass = 0.0;
size_t level = n - 1;
for (size_t i = 0; i < n && mass < 0.5; i++) {
mass += probs[i];
level = i;
}
expected = (double) level;
}
answer["score"] = expected;
answer["legend"] = legend;
answer["probabilities"] = probabilities;
+10
View File
@@ -59,6 +59,10 @@ struct server_decision_context {
return type == COMMON_DECISION_TYPE_CLEF;
}
bool is_lfm3_d1() const {
return type == COMMON_DECISION_TYPE_LFM3_D1 || type == COMMON_DECISION_TYPE_LFM3_D1_OMNI;
}
// true if the prompt of the model has a place for images
bool can_use_images() const {
switch (type) {
@@ -70,6 +74,8 @@ struct server_decision_context {
case COMMON_DECISION_TYPE_LFM2_D1_OMNI:
case COMMON_DECISION_TYPE_H2O_LIGHTNING:
case COMMON_DECISION_TYPE_MINICPM_OCR_JEV:
case COMMON_DECISION_TYPE_LFM3_D1:
case COMMON_DECISION_TYPE_LFM3_D1_OMNI:
return true;
default:
return false;
@@ -164,6 +170,10 @@ private:
json render_h2o(const json & state, const server_decision_question & question) const;
std::string h2o_cut_state(const std::string & text) const;
std::string h2o_quote(const std::string & text) const;
// the texts of the tokens with these attributes, quoted_spellings and quoted_first
void init_quoted_spellings(uint32_t attrs, size_t min_size);
// LFM3_D1: a zero-width space after the first character of each special token text (d1 prompt.py: plain)
std::string d1_break(const std::string & text) const;
void fill_task_laya(llama_tokens & tokens, const server_decision_question & question, server_task & task) const;
void fill_task_d1omni(
const json & state,
+2
View File
@@ -196,6 +196,8 @@ struct server_task {
int32_t column = 0;
// 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 prompt must be the only sequence of its batch
bool no_batching = false;
// if set, the outputs are the pooled embeddings of the sequence, one score per class
bool pooled = false;