ggml-org/llama.cpp · #30114

model : add LiquidAI/d1-omni-600M decision model

tdakhran · merged Oct 8, 202621 files · 833 + / 35 −
common/common.cpp3 + / 1 −
@@ -1171,6 +1171,7 @@ static const std::map<common_decision_type, std::string> COMMON_DECISION_TYPE_NA     { COMMON_DECISION_TYPE_CLEF,           "clef"          },     { COMMON_DECISION_TYPE_PPLX_DECIDER,   "pplx-decider"  },     { COMMON_DECISION_TYPE_LFM2_D1,        "lfm2-d1"       },+    { COMMON_DECISION_TYPE_LFM2_D1_OMNI,   "lfm2-d1-omni"  }, };  static common_decision_type common_decision_type_from_string(const std::string & str) {@@ -1284,7 +1285,8 @@ 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);-    if (decision_type == COMMON_DECISION_TYPE_LAYA || decision_type == COMMON_DECISION_TYPE_KEV || decision_type == COMMON_DECISION_TYPE_CLEF) {+    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) {         params.embedding    = true;         params.pooling_type = LLAMA_POOLING_TYPE_NONE; 
common/common.h1 + / 0 −
@@ -964,6 +964,7 @@ enum common_decision_type {     COMMON_DECISION_TYPE_CLEF,    // all questions in one prompt, score of option i read from the embeddings output at row i     COMMON_DECISION_TYPE_PPLX_DECIDER, // same as openjev, label codes of 1 or 2 letters     COMMON_DECISION_TYPE_LFM2_D1, // same as openjev, the labels depend on the question type+    COMMON_DECISION_TYPE_LFM2_D1_OMNI, // same as laya, other prompt layout     COMMON_DECISION_TYPE_UNKNOWN, // a decision model of a type that is not supported }; 
conversion/__init__.py2 + / 0 −
@@ -162,6 +162,7 @@     "Lfm2BidirectionalModel": "lfm2",     "Lfm2ForCausalLM": "lfm2",     "D1Model": "lfm2",+    "D1OmniModel": "lfm2",     "Lfm2Model": "lfm2",     "Lfm2MoeForCausalLM": "lfm2",     "Llama4ForCausalLM": "llama",@@ -335,6 +336,7 @@     "KimiK25ForConditionalGeneration": "kimivl",     "KimiVLForConditionalGeneration": "kimivl",     "Lfm2AudioForConditionalGeneration": "lfm2",+    "D1OmniModel": "lfm2",     "Lfm2VlForConditionalGeneration": "lfm2",     "LightOnOCRForConditionalGeneration": "lighton_ocr",     "Llama4ForConditionalGeneration": "llama4",
conversion/lfm2.py167 + / 0 −
@@ -161,6 +161,121 @@ def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:         yield f"{self.dense_tensor_name}.weight", tensor.clone()  +def _is_d1_omni_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("model_type") == "d1_omni"+++@ModelBase.register_hparams_loader(_is_d1_omni_checkpoint)+def _load_d1_omni_hparams(dir_model: Path) -> dict[str, Any]:+    logger.info("gguf: detected d1-omni checkpoint")+    hparams = ModelBase.load_hparams(dir_model, False, guess=False)+    text = hparams["text_config"]+    n_layer, n_layer_head = text["num_hidden_layers"], hparams["head_layers"]+    # the trunk uses the LFM2 FFN sizing, the head blocks are appended with a plain 4x MLP+    n_ff = int(text["block_ffn_dim_multiplier"] * int(2 * text["intermediate_size"] / 3))+    n_ff = text["block_multiple_of"] * ((n_ff + text["block_multiple_of"] - 1) // text["block_multiple_of"])+    text["num_hidden_layers"] = n_layer + n_layer_head+    text["intermediate_size"] = [n_ff] * n_layer + [4 * text["hidden_size"]] * n_layer_head+    text["block_auto_adjust_ff_dim"] = False+    return hparams+++@ModelBase.register("D1OmniModel")+@ModelBase.example("LiquidAI/d1-omni-600M")+class D1OmniModel(LFM2Model):+    model_arch = gguf.MODEL_ARCH.LFM2++    # the server cuts the text to these lengths, see server-decision.cpp+    _MAX_LENGTH = 16384+    _IMAGE_TEXT_LENGTH = 896+    _AUDIO_TEXT_LENGTH = 15360++    def set_vocab(self):+        super().set_vocab()+        # the systemone template writes the BOS, after the media+        self.gguf_writer.remove_key(gguf.Keys.Tokenizer.ADD_BOS)+        self.gguf_writer.add_add_bos_token(False)+        self.gguf_writer.add_token_type_count(3)  # choice, score, noul+        self.gguf_writer.add_chat_template([{"name": "systemone", "template": self._systemone_template()}])++    @staticmethod+    def _systemone_template() -> str:+        # follows prompt.py of the model repo, the server cuts each marked piece to its token budget+        # the media (images, or an audio clip if audio is true) come first+        description = jinja_str_or_json("o.description")+        has_description = "o.description is not none and o.description != ''"+        yes_no = "{{ 'yes' if o.key == 'true' else 'no' }}"+        option_code = "{% if loop.index0 < 10 %}00{% elif loop.index0 < 100 %}0{% endif %}{{ loop.index0 }}"+        option = (+            "{% if type == 'choice' and audio %}option_" + option_code + ": "+            "{% if " + has_description + " %}" + description + "{% else %}{{ o.key }}{% endif %}"+            "{% elif type == 'choice' %}{{ o.key }}{% if " + has_description + " %}: " + description + "{% endif %}"+            "{% elif type == 'score' %}level {{ o.key }}: " + description+            + "{% elif audio %}{{ o.key }}: " + yes_no+            + "{% else %}{{ o.key }}: {% if " + has_description + " %}" + description+            + "{% elif images and not ns.criteria %}" + yes_no+            + "{% elif o.key == 'true' %}yes, the statement holds"+            "{% else %}no, the statement does not hold{% endif %}{% endif %}"+        )+        state = "{% if state is string %}{{ state }}{% elif state is not none %}{{ state | tojson }}{% elif audio %}{}{% endif %}"+        return (+            "{% set ns = namespace(criteria=false) %}"+            "{% for o in options %}{% if o.description is not none %}{% set ns.criteria = true %}{% endif %}{% endfor %}"+            "{% for image in images %}{{ image }}{% endfor %}{{ sep }}"+            "<|startoftext|><|reserved_7|>{{ sep }}{{ mark_state }}" + state+            + "{{ sep }}{{ mark_question }}<|reserved_8|>" + jinja_str_or_json("instructions")+            + "{% for o in options %}{{ sep }}<|reserved_9|><|mask|>{{ sep }}{{ mark_option }} " + option+            + "{{ sep }}<|reserved_10|>{% endfor %}{{ sep }}<|reserved_11|>"+        )++    def set_gguf_parameters(self):+        lengths = (self.hparams["max_length"], self.hparams["image_text_length"], self.hparams["audio_text_length"])+        if lengths != (self._MAX_LENGTH, self._IMAGE_TEXT_LENGTH, self._AUDIO_TEXT_LENGTH):+            raise ValueError(f"unexpected text lengths: {lengths}")+        n_head, n_layer_head = self.hparams["num_attention_heads"], self.hparams["head_layers"]+        self.hparams["num_key_value_heads"] = [+            self.hparams["num_key_value_heads"] if t != "conv" else 0 for t in self.hparams["layer_types"]+        ] + [n_head] * n_layer_head++        # the head needs per-layer sizes, LFM2Model writes a single feed forward length+        TextModel.set_gguf_parameters(self)+        self.gguf_writer.add_vocab_size(self.hparams["vocab_size"])+        self.gguf_writer.add_shortconv_l_cache(self.hparams["conv_L_cache"])+        self.gguf_writer.add_layer_norm_eps(1e-5)  # nn.LayerNorm of the head+        self.gguf_writer.add_causal_attention(False)++        self.gguf_writer.add_decision_type(gguf.DecisionType.LFM2_D1_OMNI)+        self.gguf_writer.add_decision_block_count(n_layer_head)+        # "choice:3-5" -> "choice.3_5", "choice:11+" -> "choice.11"+        for name, value in self.hparams["temperatures"].items():+            self.gguf_writer.add_decision_temperature(name.replace(":", ".").replace("-", "_").rstrip("+"), value)++    @classmethod+    def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:+        name, gen = item++        if name.startswith(("vision.", "audio.")):+            return None++        name = name.replace("encoder.", "model.", 1) if name.startswith("encoder.") else name+        name = name.replace("head.head.layers.", "head.layers.").replace("in_proj_", "in_proj.")+        name = name.removeprefix("head.") if name.startswith(("head.type_emb", "head.scorer")) else name++        return super().filter_tensors((name, gen))++    def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:+        if name.startswith("head.layers.") and bid is not None:+            # the head blocks come after the trunk blocks+            suffix = name.split(".", 3)[3]+            bid += self.block_count - self.hparams["head_layers"]+            name = f"head.layers.{bid}.{suffix}"++        yield from super().modify_tensors(data_torch, name, bid)++ @ModelBase.register("Lfm2MoeForCausalLM") @ModelBase.example("LiquidAI/LFM2-8B-A1B") class LFM2MoeModel(TextModel):@@ -276,6 +391,58 @@ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iter         yield from super().modify_tensors(data_torch, name, bid)  +@ModelBase.register("D1OmniModel")+@ModelBase.example("LiquidAI/d1-omni-600M")+class D1OmniMmprojModel(ConformerAudioModel):+    has_vision_encoder = True+    has_audio_encoder  = True++    def __init__(self, *args, **kwargs):+        super().__init__(*args, **kwargs)+        assert self.hparams_vision is not None and self.hparams_audio is not None+        # dynamic resolution, as LFM2VLModel+        self.hparams_vision["image_size"] = 256+        # the images are normalized to [-1, 1] (vision.py of the model repo)+        self.preprocessor_config = {**self.preprocessor_config, "image_mean": [0.5] * 3, "image_std": [0.5] * 3}+        self.hparams_audio["hidden_size"] = self.hparams_audio["d_model"]+        self.hparams_audio["intermediate_size"] = self.hparams_audio["d_model"] * self.hparams_audio["ff_expansion_factor"]+        self.hparams_audio["num_attention_heads"] = self.hparams_audio["n_heads"]++    def set_gguf_parameters(self):+        super().set_gguf_parameters()+        self.gguf_writer.add_clip_vision_projector_type(gguf.VisionProjectorType.D1OMNI_V)+        self.gguf_writer.add_vision_attention_layernorm_eps(self.find_vparam(["layer_norm_eps"]))+        self.gguf_writer.add_vision_projector_scale_factor(self.global_config.get("downsample_factor", 2))+        self.gguf_writer.add_vision_use_gelu(True)++        assert self.hparams_audio is not None+        self.gguf_writer.add_clip_audio_projector_type(gguf.VisionProjectorType.D1OMNI_A)+        self.gguf_writer.add_audio_num_mel_bins(self.hparams_audio["feat_in"])+        self.gguf_writer.add_audio_attention_layernorm_eps(1e-5)++    @classmethod+    def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:+        name, gen = item++        if name.startswith(("encoder.", "head.")):+            return None++        name = name.replace("vision.tower.", "vision_tower.").replace("vision.projector.", "multi_modal_projector.")+        name = name.replace("audio.encoder.", "conformer.")+        # the residual block continues the adapter: norm, linear, gelu, linear, then norm, down, up+        for old, new in (("adapter.norm", 0), ("adapter.linear_1", 1), ("adapter.linear_2", 3),+                         ("residual.ln", 4), ("residual.down", 5), ("residual.up", 6)):+            name = name.replace(f"audio.{old}.", f"audio_adapter.model.{new}.")++        return super().filter_tensors((name, gen))++    def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:+        if "patch_embedding.weight" in name:+            data_torch = data_torch.view(data_torch.shape[0], 16, 16, 3).permute(0, 3, 1, 2)++        yield from super().modify_tensors(data_torch, name, bid)++ @ModelBase.register("Lfm2AudioForConditionalGeneration") @ModelBase.example("LiquidAI/LFM2.5-Audio-1.5B", "LiquidAI/LFM2-Audio-1.5B") class LFM2AudioModel(ConformerAudioModel):
gguf-py/gguf/constants.py7 + / 0 −
@@ -5172,6 +5172,10 @@ class MODEL_TENSOR(IntEnum):         MODEL_TENSOR.ATTN_OUT,         MODEL_TENSOR.OUTPUT,         MODEL_TENSOR.DENSE_2_OUT, # LFM2-ColBert-350M+        MODEL_TENSOR.TOKEN_TYPES, # decision head+        MODEL_TENSOR.CLS,+        MODEL_TENSOR.CLS_NORM,+        MODEL_TENSOR.CLS_OUT,     ],     MODEL_ARCH.LFM2MOE: [         MODEL_TENSOR.TOKEN_EMBD,@@ -6072,6 +6076,7 @@ class DecisionType:     CLEF    = "clef"     # joint head over all questions, one score per option     PPLX_DECIDER = "pplx-decider"  # same as openjev, label codes of 1 or 2 letters     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   class VisionProjectorType:@@ -6133,6 +6138,8 @@ class VisionProjectorType:     GRANITE4_VISION = "granite4_vision"     MUSE_GLIMMER   = "muse-glimmer"     COHERE2V       = "cohere2v"+    D1OMNI_V       = "d1omni_v"  # lfm2 vision, without separator tokens+    D1OMNI_A       = "d1omni_a"  # lfm2a audio, with a residual block after the projector   # Items here are (block size, type size)
src/llama-model.cpp5 + / 0 −
@@ -2368,6 +2368,11 @@ 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+    if (arch == LLM_ARCH_LFM2 && !hparams.causal_attn && hparams.n_layer_decision > 0) {+        return nullptr;+    }+     switch (arch) {         // Models that need specific instantiation should be handled in the         // switch statement
src/models/lfm2.cpp291 + / 4 −
@@ -4,6 +4,9 @@  #include <algorithm> +// question types of a decision model: choice, score, noul+static const uint32_t N_DECISION_TYPES = 3;+ void llama_model_lfm2::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);@@ -23,6 +26,15 @@ void llama_model_lfm2::load_arch_hparams(llama_model_loader & ml) {         default:    type = LLM_TYPE_UNKNOWN;     } +    ml.get_key(LLM_KV_DECISION_BLOCK_COUNT, hparams.n_layer_decision, false);+    if (hparams.n_layer_decision > 0) {+        if (hparams.n_layer_decision >= hparams.n_layer() || hparams.causal_attn) {+            throw std::runtime_error("invalid decision head");+        }+        ml.get_key(LLM_KV_ATTENTION_LAYERNORM_EPS, hparams.f_norm_eps);+        hparams.n_embd_out_impl = N_DECISION_TYPES;+    }+     if (const auto is_swa = ml.get_key(LLM_KV_ATTENTION_SLIDING_WINDOW, hparams.n_swa, false); is_swa && hparams.n_swa > 0) {         hparams.swa_type = LLAMA_SWA_TYPE_STANDARD;         for (uint32_t il = 0; il < hparams.n_layer(); ++il) {@@ -37,13 +49,49 @@ void llama_model_lfm2::load_arch_tensors(llama_model_loader &) {     tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0);      output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM_LFM2, "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);+    if (hparams.n_layer_decision > 0) {+        // decision head: plain pre-norm blocks with biases+        for (int i = n_layer - (int) hparams.n_layer_decision; i < n_layer; ++i) {+            auto & layer = layers[i];+            const int64_t n_ff_head = hparams.n_ff(i);++            layer.attn_norm   = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0);+            layer.attn_norm_b = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "bias",   i), {n_embd}, 0);++            layer.wqkv   = create_tensor(tn(LLM_TENSOR_ATTN_QKV, "weight", i), {n_embd, 3 * n_embd}, 0);+            layer.wqkv_b = create_tensor(tn(LLM_TENSOR_ATTN_QKV, "bias",   i), {3 * n_embd}, 0);+            layer.wo     = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd, n_embd}, 0);+            layer.wo_b   = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "bias",   i), {n_embd}, 0);++            layer.ffn_norm   = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, 0);+            layer.ffn_norm_b = create_tensor(tn(LLM_TENSOR_FFN_NORM, "bias",   i), {n_embd}, 0);+            layer.ffn_up     = create_tensor(tn(LLM_TENSOR_FFN_UP,   "weight", i), {n_embd, n_ff_head}, 0);+            layer.ffn_up_b   = create_tensor(tn(LLM_TENSOR_FFN_UP,   "bias",   i), {n_ff_head}, 0);+            layer.ffn_down   = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "weight", i), {n_ff_head, n_embd}, 0);+            layer.ffn_down_b = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "bias",   i), {n_embd}, 0);+        }++        if (n_token_types != N_DECISION_TYPES) {+            throw std::runtime_error("decision model must have one token type per question type");+        }+        type_embd = create_tensor(tn(LLM_TENSOR_TOKEN_TYPES, "weight"), {n_embd, n_token_types}, 0);++        cls_norm   = create_tensor(tn(LLM_TENSOR_CLS_NORM, "weight"), {n_embd},    0);+        cls_norm_b = create_tensor(tn(LLM_TENSOR_CLS_NORM, "bias"),   {n_embd},    0);+        cls        = create_tensor(tn(LLM_TENSOR_CLS,      "weight"), {n_embd, n_embd}, 0);+        cls_b      = create_tensor(tn(LLM_TENSOR_CLS,      "bias"),   {n_embd},    0);+        cls_out    = create_tensor(tn(LLM_TENSOR_CLS_OUT,  "weight"), {n_embd, 1}, 0);+        cls_out_b  = create_tensor(tn(LLM_TENSOR_CLS_OUT,  "bias"),   {1},         0);+    } else {+        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);+        }     } -    for (int i = 0; i < n_layer; ++i) {+    for (int i = 0; i < n_layer - (int) hparams.n_layer_decision; ++i) {         auto & layer = layers[i];          const bool is_moe_layer = i >= static_cast<int>(hparams.n_layer_dense_lead);@@ -87,6 +135,9 @@ void llama_model_lfm2::load_arch_tensors(llama_model_loader &) { }  std::unique_ptr<llm_graph_context> llama_model_lfm2::build_arch_graph(const llm_graph_params & params) const {+    if (hparams.n_layer_decision > 0) {+        return std::make_unique<graph_decision>(*this, params);+    }     if (hparams.swa_type == LLAMA_SWA_TYPE_STANDARD) {         return std::make_unique<graph<true>>(*this, params);     } else {@@ -294,6 +345,242 @@ llama_model_lfm2::graph<iswa>::graph(const llama_model & model, const llm_graph_     ggml_build_forward_expand(gf, cur); } +// media entries (an image or audio prefix) are embeddings, text entries are tokens+static bool lfm2_is_media(const llama_ubatch & ubatch, int64_t i) {+    return ubatch.is_mixed() ? ubatch.type[i] != 0 : ubatch.token == nullptr;+}++// non-causal within a sequence, the media never reads the text, so it is a function of the media alone+// in the head, the text and the media only read their own kind+class llm_graph_input_attn_media : public llm_graph_input_attn_no_cache {+public:+    llm_graph_input_attn_media(const llama_hparams & hparams, const llama_cparams & cparams, bool is_head) :+        llm_graph_input_attn_no_cache(hparams, cparams), is_head(is_head) {}++    void set_input(const llama_ubatch * ubatch) override {+        const int64_t n_tokens = ubatch->n_tokens;++        std::vector<bool> is_media(n_tokens);+        for (int64_t i = 0; i < n_tokens; ++i) {+            is_media[i] = lfm2_is_media(*ubatch, i);+        }++        const auto fill_mask = [&](auto * data, auto zero, auto ninf) {+            for (int64_t i1 = 0; i1 < n_tokens; ++i1) {+                for (int64_t i0 = 0; i0 < n_tokens; ++i0) {+                    bool visible = ubatch->seq_id[i0][0] == ubatch->seq_id[i1][0];+                    if (is_head) {+                        visible = visible && is_media[i0] == is_media[i1];+                    } else {+                        visible = visible && !(is_media[i1] && !is_media[i0]);+                    }+                    data[i1 * n_tokens + i0] = visible ? zero : ninf;+                }+            }+        };++        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_fp32_to_fp16(0.0f), ggml_fp32_to_fp16(-INFINITY));+        } else {+            fill_mask((float *) self_kq_mask->data, 0.0f, -INFINITY);+        }+    }++    const bool is_head;+};++// 1 where the previous (next) token is the left (right) neighbor in the same sequence+// the last media entry does not read the text on its right+class llm_graph_input_conv_mask : public llm_graph_input_i {+public:+    void set_input(const llama_ubatch * ubatch) override {+        const int64_t n_tokens = ubatch->n_tokens;++        std::vector<float> data_left(n_tokens, 0.0f);+        std::vector<float> data_right(n_tokens, 0.0f);+        for (int64_t i = 0; i + 1 < n_tokens; ++i) {+            const bool is_next = ubatch->seq_id[i][0] == ubatch->seq_id[i + 1][0] && ubatch->pos[i] + 1 == ubatch->pos[i + 1];+            data_right[i]    = is_next && !(lfm2_is_media(*ubatch, i) && !lfm2_is_media(*ubatch, i + 1));+            data_left[i + 1] = is_next;+        }+        ggml_backend_tensor_set(left,  data_left.data(),  0, ggml_nbytes(left));+        ggml_backend_tensor_set(right, data_right.data(), 0, ggml_nbytes(right));+    }++    ggml_tensor * left  = nullptr; // F32 [1, n_tokens]+    ggml_tensor * right = nullptr; // F32 [1, n_tokens]+};++llama_model_lfm2::graph_decision::graph_decision(const llama_model & model, const llm_graph_params & params) :+    llm_graph_context(params) {+    const int64_t n_embd_head = hparams.n_embd_head_v();+    const int     n_layer_enc = n_layer - hparams.n_layer_decision;++    ggml_tensor * cur = build_inp_embd(model.tok_embd);+    cb(cur, "model.embed_tokens", -1);++    ggml_tensor * inp_pos     = build_inp_pos();+    ggml_tensor * inp_out_ids = build_inp_out_ids();++    const auto type_mask = cparams.flash_attn ? GGML_TYPE_F16 : GGML_TYPE_F32;++    llm_graph_input_attn_no_cache * inp_attn[2];+    for (bool is_head : {false, true}) {+        auto inp = std::make_unique<llm_graph_input_attn_media>(hparams, cparams, is_head);+        inp->self_kq_mask = ggml_new_tensor_4d(ctx0, type_mask, n_tokens, n_tokens, 1, 1);+        ggml_set_input(inp->self_kq_mask);+        inp->self_kq_mask_cnv = inp->self_kq_mask;+        inp_attn[is_head] = (llm_graph_input_attn_no_cache *) res->add_input(std::move(inp));+    }++    auto inp_conv = std::make_unique<llm_graph_input_conv_mask>();+    inp_conv->left  = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, 1, n_tokens);+    inp_conv->right = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, 1, n_tokens);+    ggml_set_input(inp_conv->left);+    ggml_set_input(inp_conv->right);+    ggml_tensor * conv_left  = inp_conv->left;+    ggml_tensor * conv_right = inp_conv->right;+    res->add_input(std::move(inp_conv));++    for (int il = 0; il < n_layer_enc; ++il) {+        const auto & layer = model.layers[il];++        ggml_tensor * inpL = cur;+        cur = build_norm(cur, layer.attn_norm, NULL, LLM_NORM_RMS, il);+        cb(cur, "model.layers.{}.operator_norm", il);++        if (hparams.is_recr(il)) {+            ggml_tensor * bcx = build_lora_mm(layer.shortconv.in_proj, cur);+            cb(bcx, "model.layers.{}.conv.in_proj", il);++            ggml_tensor * b = ggml_view_2d(ctx0, bcx, n_embd, n_tokens, bcx->nb[1], 0 * n_embd * ggml_element_size(bcx));+            ggml_tensor * c = ggml_view_2d(ctx0, bcx, n_embd, n_tokens, bcx->nb[1], 1 * n_embd * ggml_element_size(bcx));+            ggml_tensor * x = ggml_view_2d(ctx0, bcx, n_embd, n_tokens, bcx->nb[1], 2 * n_embd * ggml_element_size(bcx));++            // centred 3-tap conv, a tap outside the sequence reads 0+            ggml_tensor * bx  = ggml_mul(ctx0, b, x);+            ggml_tensor * bxp = ggml_pad_ext(ctx0, bx, 0, 0, 1, 1, 0, 0, 0, 0);+            ggml_tensor * prv = ggml_view_2d(ctx0, bxp, n_embd, n_tokens, bxp->nb[1], 0);+            ggml_tensor * nxt = ggml_view_2d(ctx0, bxp, n_embd, n_tokens, bxp->nb[1], 2 * bxp->nb[1]);++            GGML_ASSERT(hparams.n_shortconv_l_cache == 3);+            ggml_tensor * taps = ggml_cont(ctx0, ggml_transpose(ctx0, layer.shortconv.conv));+            ggml_tensor * tap0 = ggml_view_1d(ctx0, taps, n_embd, 0 * taps->nb[1]);+            ggml_tensor * tap1 = ggml_view_1d(ctx0, taps, n_embd, 1 * taps->nb[1]);+            ggml_tensor * tap2 = ggml_view_1d(ctx0, taps, n_embd, 2 * taps->nb[1]);++            ggml_tensor * y = ggml_mul(ctx0, bx, tap1);+            y = ggml_add(ctx0, y, ggml_mul(ctx0, ggml_mul(ctx0, prv, tap0), conv_left));+            y = ggml_add(ctx0, y, ggml_mul(ctx0, ggml_mul(ctx0, nxt, tap2), conv_right));+            cb(y, "model.layers.{}.conv.conv", il);++            cur = build_lora_mm(layer.shortconv.out_proj, ggml_mul(ctx0, c, y));+            cb(cur, "model.layers.{}.conv.out_proj", il);+        } else {+            auto [q, k, v] = build_qkv(layer, cur, n_embd_head, n_head, hparams.n_head_kv(il), il);++            q = build_norm(q, layer.attn_q_norm, NULL, LLM_NORM_RMS, il);+            k = build_norm(k, layer.attn_k_norm, NULL, LLM_NORM_RMS, il);++            q = ggml_rope_ext(ctx0, q, inp_pos, nullptr, n_rot, rope_type, n_ctx_orig, freq_base, freq_scale, ext_factor,+                              attn_factor, beta_fast, beta_slow);+            k = ggml_rope_ext(ctx0, k, inp_pos, nullptr, n_rot, rope_type, n_ctx_orig, freq_base, freq_scale, ext_factor,+                              attn_factor, beta_fast, beta_slow);++            cur = build_attn(inp_attn[0],+                    layer.wo, NULL, layer.wo_s,+                    q, k, v, nullptr, nullptr, nullptr, 1.0f / sqrtf(float(n_embd_head)), il);+            cb(cur, "model.layers.{}.self_attn.out_proj", il);+        }++        cur = ggml_add(ctx0, cur, inpL);++        ggml_tensor * ffn_out = build_norm(cur, layer.ffn_norm, NULL, LLM_NORM_RMS, il);+        ffn_out = build_ffn(ffn_out,+                layer.ffn_up,   NULL, NULL,+                layer.ffn_gate, NULL, NULL,+                layer.ffn_down, NULL, NULL,+                NULL, LLM_FFN_SILU, LLM_FFN_PAR, il);++        cur = ggml_add(ctx0, cur, ffn_out);+        cb(cur, "l_out", il);+    }++    cur = build_norm(cur, model.output_norm, NULL, LLM_NORM_RMS, -1);+    cb(cur, "result_norm", -1);++    cur = build_decision_head(model, cur, inp_attn[1], inp_out_ids);++    res->t_embd = cur;+    ggml_build_forward_expand(gf, cur);+}++// same as llama_model_modern_bert::graph::build_decision_head(), with the head counts of the head layers+ggml_tensor * llama_model_lfm2::graph_decision::build_decision_head(+        const llama_model & model,+        ggml_tensor * inp,+        llm_graph_input_attn_no_cache * inp_attn,+        ggml_tensor * inp_out_ids) {+    const int64_t n_embd_head = hparams.n_embd_head_v();+    const int     n_layer_enc = n_layer - hparams.n_layer_decision;++    ggml_tensor * scores = nullptr;++    // the question type is not a graph input, so the head is evaluated for each of them+    for (uint32_t it = 0; it < N_DECISION_TYPES; ++it) {+        ggml_tensor * type_row = ggml_view_1d(ctx0, model.type_embd, n_embd, it * model.type_embd->nb[1]);+        ggml_tensor * inpL = ggml_add(ctx0, inp, type_row);++        for (int il = n_layer_enc; il < n_layer; ++il) {+            const auto & layer = model.layers[il];++            ggml_tensor * cur = build_norm(inpL, layer.attn_norm, layer.attn_norm_b, LLM_NORM, il);+            cb(cur, "attn_norm", il);++            // no positional encoding in the head+            auto [Qcur, Kcur, Vcur] = build_qkv(layer, cur, n_embd_head, hparams.n_head(il), hparams.n_head_kv(il), il);++            cur = build_attn(inp_attn,+                        layer.wo, layer.wo_b, layer.wo_s,+                        Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, 1.0f/sqrtf(float(n_embd_head)), il);+            cb(cur, "kqv_out", il);++            if (il == n_layer - 1 && inp_out_ids) {+                cur  = ggml_get_rows(ctx0,  cur, inp_out_ids);+                inpL = ggml_get_rows(ctx0, inpL, inp_out_ids);+            }++            ggml_tensor * ffn_inp = ggml_add(ctx0, cur, inpL);+            cb(ffn_inp, "ffn_inp", il);++            cur = build_norm(ffn_inp, layer.ffn_norm, layer.ffn_norm_b, LLM_NORM, il);+            cb(cur, "ffn_norm", il);++            cur = build_ffn(cur,+                    layer.ffn_up,   layer.ffn_up_b,   NULL,+                    NULL,           NULL,             NULL,+                    layer.ffn_down, layer.ffn_down_b, NULL,+                    NULL,+                    LLM_FFN_RELU,+                    LLM_FFN_SEQ, il);++            inpL = ggml_add(ctx0, cur, ffn_inp);+        }++        // scorer+        ggml_tensor * cur = build_norm(inpL, model.cls_norm, model.cls_norm_b, LLM_NORM, -1);+        cur = ggml_add(ctx0, build_lora_mm(model.cls, cur), model.cls_b);+        cur = ggml_gelu_erf(ctx0, cur);+        cur = ggml_add(ctx0, build_lora_mm(model.cls_out, cur), model.cls_out_b);++        scores = scores ? ggml_concat(ctx0, scores, cur, 0) : cur;+    }+    cb(scores, "decision_scores", -1);++    return scores;+}+ // Explicit template instantiations template struct llama_model_lfm2::graph<true>; template struct llama_model_lfm2::graph<false>;
src/models/models.h11 + / 0 −
@@ -2186,6 +2186,17 @@ struct llama_model_lfm2 : public llama_model_base {         graph(const llama_model & model, const llm_graph_params & params);     }; +    // non-causal trunk without memory, then the decision head+    struct graph_decision : public llm_graph_context {+        graph_decision(const llama_model & model, const llm_graph_params & params);++        ggml_tensor * build_decision_head(+                const llama_model & model,+                ggml_tensor * inp,+                llm_graph_input_attn_no_cache * inp_attn,+                ggml_tensor * inp_out_ids);+    };+     std::unique_ptr<llm_graph_context> build_arch_graph(const llm_graph_params & params) const override; }; 
tools/mtmd/clip-impl.h4 + / 0 −
@@ -476,6 +476,7 @@ enum projector_type {     PROJECTOR_TYPE_MERALION,     PROJECTOR_TYPE_MUSIC_FLAMINGO,     PROJECTOR_TYPE_LFM2,+    PROJECTOR_TYPE_D1OMNI_V,     PROJECTOR_TYPE_KIMIVL,     PROJECTOR_TYPE_PADDLEOCR,     PROJECTOR_TYPE_LIGHTONOCR,@@ -488,6 +489,7 @@ enum projector_type {     PROJECTOR_TYPE_DEEPSEEKOCR2,     PROJECTOR_TYPE_DEEPSEEK4V,     PROJECTOR_TYPE_LFM2A,+    PROJECTOR_TYPE_D1OMNI_A,     PROJECTOR_TYPE_GLM4V,     PROJECTOR_TYPE_GLM5V,     PROJECTOR_TYPE_YOUTUVL,@@ -544,6 +546,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_D1OMNI_V,          "d1omni_v"},     { PROJECTOR_TYPE_KIMIVL,            "kimivl"},     { PROJECTOR_TYPE_PADDLEOCR,         "paddleocr"},     { PROJECTOR_TYPE_LIGHTONOCR,        "lightonocr"},@@ -556,6 +559,7 @@ static std::map<projector_type, std::string> PROJECTOR_TYPE_NAMES = {     { PROJECTOR_TYPE_DEEPSEEKOCR2,      "deepseekocr2"},     { PROJECTOR_TYPE_DEEPSEEK4V,        "deepseek4v"},     { PROJECTOR_TYPE_LFM2A,             "lfm2a"},+    { PROJECTOR_TYPE_D1OMNI_A,          "d1omni_a"},     { PROJECTOR_TYPE_GLM4V,             "glm4v"},     { PROJECTOR_TYPE_GLM5V,             "glm5v"},     { PROJECTOR_TYPE_YOUTUVL,           "youtuvl"},
tools/mtmd/clip-model.h4 + / 0 −
@@ -623,6 +623,10 @@ struct clip_model {     ggml_tensor * mm_3_b = nullptr;     ggml_tensor * mm_4_w = nullptr;     ggml_tensor * mm_4_b = nullptr;+    ggml_tensor * mm_5_w = nullptr;+    ggml_tensor * mm_5_b = nullptr;+    ggml_tensor * mm_6_w = nullptr;+    ggml_tensor * mm_6_b = nullptr;      // GLMV-Edge projection     ggml_tensor * mm_model_adapter_conv_w = nullptr;
tools/mtmd/clip.cpp27 + / 0 −
@@ -939,6 +939,7 @@ static std::unique_ptr<clip_graph> clip_get_graph_builder(clip_ctx * ctx, const         case PROJECTOR_TYPE_IDEFICS3:         case PROJECTOR_TYPE_COHERE2V:         case PROJECTOR_TYPE_LFM2:+        case PROJECTOR_TYPE_D1OMNI_V:         case PROJECTOR_TYPE_JANUS_PRO:         case PROJECTOR_TYPE_PHI4:             {@@ -1073,6 +1074,7 @@ static std::unique_ptr<clip_graph> clip_get_graph_builder(clip_ctx * ctx, const                 builder = std::make_unique<clip_graph_deepseekocr2>(ctx, img);             } break;         case PROJECTOR_TYPE_LFM2A:+        case PROJECTOR_TYPE_D1OMNI_A:             {                 builder = std::make_unique<clip_graph_conformer>(ctx, img);             } break;@@ -1525,6 +1527,7 @@ struct clip_model_loader {                         }                     } break;                 case PROJECTOR_TYPE_LFM2:+                case PROJECTOR_TYPE_D1OMNI_V:                     {                         // default for older GGUFs                         std::string resize_algo = "bilinear";@@ -1540,6 +1543,10 @@ struct clip_model_loader {                         }                         hparams.image_resize_algo_rf = hparams.image_resize_algo;                         hparams.image_resize_algo_ov = hparams.image_resize_algo;+                        // the tiles stretch the image to the grid (d1-omni vision.py)+                        if (model.proj_type == PROJECTOR_TYPE_D1OMNI_V) {+                            hparams.image_pad_rf = PAD_NONE;+                        }                         get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false);                         // ref: https://huggingface.co/LiquidAI/LFM2.5-VL-1.6B/blob/main/processor_config.json                         hparams.set_limit_image_tokens(64, 256);@@ -1980,6 +1987,7 @@ struct clip_model_loader {                         hparams.set_warmup_n_tokens(32*32);                     } break;                 case PROJECTOR_TYPE_LFM2A:+                case PROJECTOR_TYPE_D1OMNI_A:                     {                         // audio preprocessing params                         hparams.audio_chunk_len        = 1; // in seconds@@ -2799,6 +2807,7 @@ struct clip_model_loader {                     model.mm_2_b = get_tensor(string_format(TN_LLAVA_PROJ, 2, "bias"));                 } break;             case PROJECTOR_TYPE_LFM2:+            case PROJECTOR_TYPE_D1OMNI_V:                 {                     model.mm_input_norm_w = get_tensor(TN_MM_INP_NORM, false);                     model.mm_input_norm_b = get_tensor(TN_MM_INP_NORM_B, false);@@ -3411,6 +3420,7 @@ struct clip_model_loader {                     model.mm_input_proj_w = get_tensor(string_format(TN_A_MM_INP_PROJ, "weight"));                 } break;             case PROJECTOR_TYPE_LFM2A:+            case PROJECTOR_TYPE_D1OMNI_A:                 {                     for (int i : {0, 2, 3, 5, 6}) {                         model.pre_encode_conv_X_w[i] = get_tensor(string_format(TN_CONV1D, i, "weight"));@@ -3426,6 +3436,16 @@ struct clip_model_loader {                     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")); +                    // residual block after the projector: norm, down, up+                    if (model.proj_type == PROJECTOR_TYPE_D1OMNI_A) {+                        model.mm_4_w = get_tensor(string_format(TN_MM_AUDIO_MLP, 4, "weight"));+                        model.mm_4_b = get_tensor(string_format(TN_MM_AUDIO_MLP, 4, "bias"));+                        model.mm_5_w = get_tensor(string_format(TN_MM_AUDIO_MLP, 5, "weight"));+                        model.mm_5_b = get_tensor(string_format(TN_MM_AUDIO_MLP, 5, "bias"));+                        model.mm_6_w = get_tensor(string_format(TN_MM_AUDIO_MLP, 6, "weight"));+                        model.mm_6_b = get_tensor(string_format(TN_MM_AUDIO_MLP, 6, "bias"));+                    }+                     for (int il = 0; il < hparams.n_layer; ++il) {                         auto & layer = model.layers[il]; @@ -4277,6 +4297,7 @@ int clip_n_output_tokens(const clip_ctx * ctx, const clip_image_f32 * img) {                 n_patches = ctx->model.hparams.image_size / ctx->model.hparams.patch_size;             } break;         case PROJECTOR_TYPE_LFM2:+        case PROJECTOR_TYPE_D1OMNI_V:         case PROJECTOR_TYPE_KIMIVL:         case PROJECTOR_TYPE_KIMIK25:             {@@ -4400,6 +4421,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:             {                 n_patches = ((((img->nx() + 1) / 2) + 1) / 2 + 1) / 2;             } break;@@ -5358,6 +5380,7 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {         case PROJECTOR_TYPE_GLMA:         case PROJECTOR_TYPE_ULTRAVOX:         case PROJECTOR_TYPE_LFM2:+        case PROJECTOR_TYPE_D1OMNI_V:         case PROJECTOR_TYPE_VOXTRAL:         case PROJECTOR_TYPE_MERALION:         case PROJECTOR_TYPE_MUSIC_FLAMINGO:@@ -5617,6 +5640,7 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {                 }             } break;         case PROJECTOR_TYPE_LFM2A:+        case PROJECTOR_TYPE_D1OMNI_A:             {                 GGML_ASSERT(imgs.entries.size() == 1);                 const auto n_frames = clip_n_output_tokens(ctx, &imgs.entries.front());@@ -6080,6 +6104,7 @@ int clip_n_mmproj_embd(const struct clip_ctx * ctx) {             return ctx->model.mm_2_w->ne[1];         case PROJECTOR_TYPE_GLMA:         case PROJECTOR_TYPE_LFM2:+        case PROJECTOR_TYPE_D1OMNI_V:         case PROJECTOR_TYPE_KIMIVL:         case PROJECTOR_TYPE_PADDLEOCR:         case PROJECTOR_TYPE_KIMIK25:@@ -6096,6 +6121,8 @@ int clip_n_mmproj_embd(const struct clip_ctx * ctx) {             return ctx->model.mm_fc_w->ne[1];         case PROJECTOR_TYPE_LFM2A:             return ctx->model.position_embeddings->ne[0];+        case PROJECTOR_TYPE_D1OMNI_A:+            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];         case PROJECTOR_TYPE_GRANITE4_VISION:
tools/mtmd/models/conformer.cpp9 + / 2 −
@@ -4,7 +4,7 @@ ggml_cgraph * clip_graph_conformer::build() {     const int n_frames   = img.nx();     const int n_pos      = n_frames / 2;     const int n_pos_embd = (((((n_frames + 1) / 2) + 1) / 2 + 1) / 2) * 2 - 1;-    GGML_ASSERT(model.position_embeddings->ne[1] >= n_pos);+    GGML_ASSERT(!model.position_embeddings || model.position_embeddings->ne[1] >= n_pos);      ggml_tensor * pos_emb = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, 512, n_pos_embd);     ggml_set_name(pos_emb, "pos_emb");@@ -164,7 +164,7 @@ ggml_cgraph * clip_graph_conformer::build() {             // TODO @ngxson : support this ops in ggml             {                 int64_t       d    = x->ne[0] / 2;-                ggml_tensor * gate = ggml_sigmoid(ctx0, ggml_view_2d(ctx0, x, d, x->ne[1], x->nb[1], d * x->nb[0]));+                ggml_tensor * gate = ggml_sigmoid(ctx0, ggml_cont(ctx0, ggml_view_2d(ctx0, x, d, x->ne[1], x->nb[1], d * x->nb[0])));                 x                  = ggml_mul(ctx0, ggml_view_2d(ctx0, x, d, x->ne[1], x->nb[1], 0), gate);                 x                  = ggml_cont(ctx0, ggml_transpose(ctx0, x));             }@@ -207,6 +207,13 @@ ggml_cgraph * clip_graph_conformer::build() {     cb(cur, "audio_adapter.model.{}", 0);     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); +    // d1omni_a: residual block after the projector+    if (model.mm_4_w) {+        ggml_tensor * x = build_norm(cur, model.mm_4_w, model.mm_4_b, NORM_TYPE_NORMAL, 1e-5, -1);+        x   = build_ffn(x, model.mm_5_w, model.mm_5_b, nullptr, nullptr, model.mm_6_w, model.mm_6_b, FFN_GELU_ERF, -1);+        cur = ggml_add(ctx0, cur, x);+    }+     cb(cur, "projected", -1);      ggml_build_forward_expand(gf, cur);
tools/mtmd/models/siglip.cpp4 + / 3 −
@@ -4,7 +4,7 @@ ggml_cgraph * clip_graph_siglip::build() {     ggml_tensor * inp = build_inp();      ggml_tensor * learned_pos_embd = model.position_embeddings;-    if (proj_type == PROJECTOR_TYPE_LFM2 || proj_type == PROJECTOR_TYPE_PHI4) {+    if (proj_type == PROJECTOR_TYPE_LFM2 || proj_type == PROJECTOR_TYPE_D1OMNI_V || proj_type == PROJECTOR_TYPE_PHI4) {         learned_pos_embd = resize_position_embeddings();     } @@ -55,7 +55,7 @@ ggml_cgraph * clip_graph_siglip::build() {         cur = build_mm(model.mm_2_w, cur);         cur = ggml_add(ctx0, cur, model.mm_2_b); -    } else if (proj_type == PROJECTOR_TYPE_LFM2) {+    } else if (proj_type == PROJECTOR_TYPE_LFM2 || proj_type == PROJECTOR_TYPE_D1OMNI_V) {         // pixel unshuffle block         const int scale_factor = model.hparams.n_merge;         cur = build_patch_merge_permute(cur, scale_factor);@@ -70,11 +70,12 @@ ggml_cgraph * clip_graph_siglip::build() {             cur = ggml_add(ctx0, cur, model.mm_input_norm_b);         } +        // d1-omni uses the exact gelu in the projector         cur = build_ffn(cur,             model.mm_1_w, model.mm_1_b,             nullptr, nullptr,             model.mm_2_w, model.mm_2_b,-            FFN_GELU,+            proj_type == PROJECTOR_TYPE_D1OMNI_V ? FFN_GELU_ERF : FFN_GELU,             -1);      } else if (proj_type == PROJECTOR_TYPE_JANUS_PRO) {
tools/mtmd/mtmd-audio.cpp36 + / 0 −
@@ -994,6 +994,42 @@ bool mtmd_audio_preprocessor_conformer::preprocess(const float *     return true; } +//+// mtmd_audio_preprocessor_d1omni+//++bool mtmd_audio_preprocessor_d1omni::preprocess(const float *                 samples,+                                                size_t                        n_samples,+                                                std::vector<mtmd_audio_mel> & output) const {+    if (n_samples == 0) {+        return false;+    }+    const size_t n_max = 30 * hparams.audio_sample_rate;+    const size_t n_min = hparams.audio_sample_rate / 2;++    std::vector<float> buf(samples, samples + std::min(n_samples, n_max));+    buf.resize(std::max(buf.size(), n_min), 0.0f);++    if (!mtmd_audio_preprocessor_conformer::preprocess(buf.data(), buf.size(), output)) {+        return false;+    }++    // the encoder reads one frame per hop, not the extra frame of the centre padding (NeMo: seq_len)+    const int64_t n_frames = buf.size() / hparams.audio_hop_len;+    for (auto & mel : output) {+        if (mel.n_len <= n_frames) {+            continue;+        }+        std::vector<float> data((size_t) mel.n_mel * n_frames);+        for (int64_t j = 0; j < mel.n_mel; ++j) {+            std::copy_n(mel.data.begin() + (size_t) j * mel.n_len, n_frames, data.begin() + (size_t) j * n_frames);+        }+        mel.n_len = n_frames;+        mel.data  = std::move(data);+    }+    return true;+}+ // // mtmd_audio_preprocessor_granite_speech //
tools/mtmd/mtmd-audio.h6 + / 0 −
@@ -80,6 +80,12 @@ struct mtmd_audio_preprocessor_conformer : mtmd_audio_preprocessor {     mtmd_audio_cache cache; }; +// same as conformer, the audio is cut to 30 s and padded to 0.5 s (d1-omni audio.py)+struct mtmd_audio_preprocessor_d1omni : mtmd_audio_preprocessor_conformer {+    using mtmd_audio_preprocessor_conformer::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_granite_speech : mtmd_audio_preprocessor {     mtmd_audio_preprocessor_granite_speech(const clip_ctx * ctx) : mtmd_audio_preprocessor(ctx) {}     void initialize() override;
tools/mtmd/mtmd.cpp9 + / 0 −
@@ -871,6 +871,11 @@ struct mtmd_context {                     ov_img_first       = false;                     image_preproc = std::make_unique<mtmd_image_preprocessor_lfm2>(ctx_v);                 } break;+            case PROJECTOR_TYPE_D1OMNI_V:+                {+                    // same tiles and thumbnail as lfm2, without separator tokens+                    image_preproc = std::make_unique<mtmd_image_preprocessor_lfm2>(ctx_v);+                } break;             case PROJECTOR_TYPE_GLM4V:                 {                     // <|begin_of_image|> ... (image embeddings) ... <|end_of_image|>@@ -982,6 +987,10 @@ struct mtmd_context {                 {                     audio_preproc = std::make_unique<mtmd_audio_preprocessor_conformer>(ctx_a);                 } break;+            case PROJECTOR_TYPE_D1OMNI_A:+                {+                    audio_preproc = std::make_unique<mtmd_audio_preprocessor_d1omni>(ctx_a);+                } break;             case PROJECTOR_TYPE_GRANITE_SPEECH:                 {                     audio_preproc = std::make_unique<mtmd_audio_preprocessor_granite_speech>(ctx_a);
tools/server/README.md10 + / 8 −
@@ -1703,9 +1703,11 @@ Follows the [TypeSafe API](https://docs.typesafe.ai/api), streaming is not suppo  *Options:* -`state`: The content to evaluate. Can be a string, an object or an array. A value that is not a string is given to the model as JSON text. For lfm2-d1, it can be `null`, for example to ask about images only.+`state`: The content to evaluate. Can be a string, an object or an array. A value that is not a string is given to the model as JSON text. For lfm2-d1 and lfm2-d1-omni, it can be `null`, for example to ask about images only. -`images`: Optional. An array of images, the maximum number may be limited depending on the model. Each one is a data URL (`data:image/...;base64,...`). See the image input section below.+`files`: Optional. An array of input files, the maximum number may be limited depending on the model. Each one is a data URL (`data:image/...;base64,...`). For audio-capable models, it can be audio clips (`data:audio/...;base64,...`). See the image input section below.++`images`: Optional. An alias of `files`.  `questions`: An object that maps a question id to a question. Each question has these fields: @@ -1718,20 +1720,20 @@ 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 laya, clef, pplx-decider and lfm2-d1. 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 laya, clef, pplx-decider, lfm2-d1 and lfm2-d1-omni. For laya, long questions and options are truncated to the token budget the model was trained with. -For laya and clef, the whole prompt is evaluated in one batch: it must fit in `--ubatch-size`. A server that runs clef only serves this endpoint, text generation is not available.+For laya, clef 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. A server that runs clef only serves this endpoint, text generation is not available.  *Image input:* -Image input needs a model that supports it (for example: openjev, clef, pplx-decider, lfm2-d1) and its multimodal projector, see `--mmproj`.+Image input needs a model that supports it (for example: openjev, clef, pplx-decider, lfm2-d1, lfm2-d1-omni) and its multimodal projector, see `--mmproj`.  Images can be given in two ways, and both can be used in the same request: -- The `images` field.-- A `state` made of chat messages, either an array of messages or an object with a `messages` array. An `image_url` part in the `content` of a message is taken as an image, in the same format as chat completions. Only data URLs are accepted.+- The `files` field, or its alias `images`.+- A `state` made of chat messages, either an array of messages or an object with a `messages` array. An `image_url` part in the `content` of a message is taken as an image, in the same format as chat completions. Only data URLs are accepted. For lfm2-d1-omni, an `input_audio` part is taken as an audio clip, as base64 data. -All the images are placed before the state in the prompt, the ones from `images` first. The image parts are removed from the state.+All the images are placed before the state in the prompt, the ones from `files` and `images` first. The image parts are removed from the state.  *Response:* 
tools/server/server-common.cpp1 + / 0 −
@@ -157,6 +157,7 @@ std::vector<std::string> server_model_output_modalities(common_decision_type dec         case COMMON_DECISION_TYPE_CLEF:         case COMMON_DECISION_TYPE_PPLX_DECIDER:         case COMMON_DECISION_TYPE_LFM2_D1:+        case COMMON_DECISION_TYPE_LFM2_D1_OMNI:             return {"decisions"};         default:             // fallback when there is no decision type or the metadata is bad
tools/server/server-context.cpp1 + / 1 −
@@ -5660,7 +5660,7 @@ void server_routes::init_routes() {                 scores.push_back(result->scores);                 n_tokens += result->n_tokens;             }-            answers[question.id] = decision.format_answer(question, scores);+            answers[question.id] = decision.format_answer(question, scores, !files.empty());         }          res->ok(json{
tools/server/server-decision.cpp220 + / 13 −
@@ -126,6 +126,12 @@ void server_decision_context::init(const llama_model * model) {     } else if (model_type == COMMON_DECISION_TYPE_LFM2_D1) {         n_options_max   = 255;         noul_true_first = true;+    } else if (model_type == COMMON_DECISION_TYPE_LFM2_D1_OMNI) {+        token_marker = llama_vocab_mask(vocab);+        if (token_marker == LLAMA_TOKEN_NULL) {+            throw std::runtime_error("decision model has no mask token");+        }+        n_options_max = 255;     } else {         throw std::runtime_error("unsupported decision model type: " + type_name);     }@@ -139,8 +145,8 @@ void server_decision_context::init(const llama_model * model) { //  std::vector<server_decision_question> server_decision_context::parse_questions(const json & body) const {-    // d1 accepts a null state (images only)-    if (!body.contains("state") || (body.at("state").is_null() && type != COMMON_DECISION_TYPE_LFM2_D1)) {+    // lfm2-d1 and lfm2-d1-omni 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)) {         throw std::invalid_argument("\"state\" must be provided");     }     if (!body.contains("questions") || !body.at("questions").is_object() || body.at("questions").empty()) {@@ -193,7 +199,15 @@ std::vector<server_decision_question> server_decision_context::parse_questions(c                 throw err("\"criteria\" must be an object");             }             for (const char * key : {"false", "true"}) {-                question.options.push_back({key, criteria.is_object() && criteria.contains(key) ? criteria.at(key) : json()});+                json description;+                if (criteria.is_object() && criteria.contains(key)) {+                    description = criteria.at(key);+                } else if (criteria.is_object() && type == COMMON_DECISION_TYPE_LFM2_D1_OMNI) {+                    // lfm2-d1-omni also reads the descriptions under "no" and "yes"+                    const char * alias = std::string(key) == "true" ? "yes" : "no";+                    description = criteria.contains(alias) ? criteria.at(alias) : json();+                }+                question.options.push_back({key, description});             }             if (noul_true_first) {                 std::swap(question.options[0], question.options[1]);@@ -217,25 +231,41 @@ std::vector<server_decision_question> server_decision_context::parse_questions(c  static const size_t DECISION_MAX_IMAGES = 8; +// any media, mtmd tells an audio clip from an image by its content static void decision_load_image(const json & url, std::vector<raw_buffer> & files) {-    if (!url.is_string() || !string_starts_with(url.get<std::string>(), "data:image/")) {-        throw std::invalid_argument("images must be data URLs (data:image/...;base64,...)");+    if (!url.is_string() || !string_starts_with(url.get<std::string>(), "data:")) {+        throw std::invalid_argument("images must be data URLs (data:image/...;base64,... or data:audio/...;base64,...)");     }     if (files.size() >= DECISION_MAX_IMAGES) {         throw std::invalid_argument(string_format("too many images, the maximum is %zu", DECISION_MAX_IMAGES));     }     handle_media(files, url.get<std::string>(), ""); } +// audio is base64 data, as a data URL or not (OpenAI input_audio)+static void decision_load_audio(const json & data, std::vector<raw_buffer> & files) {+    if (!data.is_string() || string_starts_with(data.get<std::string>(), "http") || string_starts_with(data.get<std::string>(), "file://")) {+        throw std::invalid_argument("audio must be base64 data");+    }+    if (files.size() >= DECISION_MAX_IMAGES) {+        throw std::invalid_argument(string_format("too many media files, the maximum is %zu", DECISION_MAX_IMAGES));+    }+    handle_media(files, data.get<std::string>(), "");+}+ json server_decision_context::parse_state(const json & body, std::vector<raw_buffer> & files) const {     if (body.contains("videos") && !body.at("videos").is_null() && !body.at("videos").empty()) {         throw std::invalid_argument("\"videos\" is not supported");     }-    if (body.contains("images") && !body.at("images").is_null()) {-        if (!body.at("images").is_array()) {-            throw std::invalid_argument("\"images\" must be an array");+    // "images" is an alias of "files"+    for (const char * key : {"files", "images"}) {+        if (!body.contains(key) || body.at(key).is_null()) {+            continue;+        }+        if (!body.at(key).is_array()) {+            throw std::invalid_argument(string_format("\"%s\" must be an array", key));         }-        for (const auto & url : body.at("images")) {+        for (const auto & url : body.at(key)) {             decision_load_image(url, files);         }     }@@ -247,7 +277,7 @@ json server_decision_context::parse_state(const json & body, std::vector<raw_buf         return state;     } -    // chat messages: take the image parts out of the content+    // chat messages: take the image and audio parts out of the content     json messages_out = json::array();     for (const auto & msg : messages) {         if (!msg.is_object() || !msg.contains("content") || !msg.at("content").is_array()) {@@ -259,6 +289,9 @@ json server_decision_context::parse_state(const json & body, std::vector<raw_buf             if (part.is_object() && json_value(part, "type", std::string()) == "image_url" && part.contains("image_url")) {                 const json & image_url = part.at("image_url");                 decision_load_image(image_url.is_object() && image_url.contains("url") ? image_url.at("url") : image_url, files);+            } else if (part.is_object() && json_value(part, "type", std::string()) == "input_audio" && part.contains("input_audio")) {+                const json input_audio = json_value(part, "input_audio", json::object());+                decision_load_audio(input_audio.contains("data") ? input_audio.at("data") : json_value(input_audio, "url", json()), files);             } else {                 content.push_back(part);             }@@ -364,6 +397,41 @@ static std::string decision_kev_text(const json & val) {     return std::regex_replace(decision_kev_render(val), re_special, "<\xC2\xA6$1\xC2\xA6>"); } +// 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) {+    static const std::regex re_special("<\\|([A-Za-z0-9_]+)\\|>");+    if (val.is_string()) {+        return std::regex_replace(val.get<std::string>(), re_special, "<\xC2\xA6$1\xC2\xA6>");+    }+    if (val.is_array()) {+        json out = json::array();+        for (const auto & item : val) {+            out.push_back(decision_d1omni_escape(item));+        }+        return out;+    }+    if (val.is_object()) {+        json out = json::object();+        for (const auto & [key, item] : val.items()) {+            out[std::regex_replace(key, re_special, "<\xC2\xA6$1\xC2\xA6>")] = decision_d1omni_escape(item);+        }+        return out;+    }+    return val;+}++// given to the lfm2-d1-omni template: text between the pieces of the prompt, and at the start of the pieces that are cut to a token budget+static const std::string D1OMNI_MARKER        = "<<d1omni:";+static const std::string D1OMNI_SEP           = "<<d1omni:sep>>";+static const std::string D1OMNI_MARK_STATE    = "<<d1omni:state>>";+static const std::string D1OMNI_MARK_QUESTION = "<<d1omni:question>>";+static const std::string D1OMNI_MARK_OPTION   = "<<d1omni:option>>";++// max_length, image_text_length and audio_text_length of the model config, the converter checks them+static const size_t D1OMNI_MAX_TOKENS       = 16384;+static const size_t D1OMNI_MAX_TOKENS_IMAGE = 896;+static const size_t D1OMNI_MAX_TOKENS_AUDIO = 15360;+ size_t server_decision_context::n_variants(const server_decision_question & question) const {     // lev shows the options of a choice in 2 orders, to cancel the preference for the first label     if (type == COMMON_DECISION_TYPE_LEV && question.type == SERVER_DECISION_QUESTION_CHOICE && question.options.size() > 1) {@@ -515,7 +583,8 @@ std::string server_decision_context::render(         const std::vector<server_decision_question> & questions,         const server_decision_question & question,         size_t variant,-        size_t n_images) const {+        size_t n_images,+        bool is_audio) const {     // the template is given raw JSON values, it serializes the ones that are not strings     json inp = json{         {"id",           question.id},@@ -554,6 +623,15 @@ std::string server_decision_context::render(         inp = decision_replace_text(inp, text_marker, " ");     } +    if (type == COMMON_DECISION_TYPE_LFM2_D1_OMNI) {+        inp = decision_replace_text(decision_d1omni_escape(inp), D1OMNI_MARKER, "<<d1omni ");+        inp["audio"]         = is_audio;+        inp["sep"]           = D1OMNI_SEP;+        inp["mark_state"]    = D1OMNI_MARK_STATE;+        inp["mark_question"] = D1OMNI_MARK_QUESTION;+        inp["mark_option"]   = D1OMNI_MARK_OPTION;+    }+     // the template puts one media marker per image     json images = json::array();     if (n_images > 0) {@@ -580,6 +658,11 @@ void server_decision_context::fill_task(         mtmd_context * mctx,         const mtmd_helper_init_opt & init_opt,         server_task & task) const {+    if (type == COMMON_DECISION_TYPE_LFM2_D1_OMNI) {+        fill_task_d1omni(state, questions, question, variant, files, mctx, init_opt, task);+        return;+    }+     const std::string prompt = render(state, questions, question, variant, files.size());      if (type == COMMON_DECISION_TYPE_OPENJEV || type == COMMON_DECISION_TYPE_LEV || type == COMMON_DECISION_TYPE_NIMBLE || type == COMMON_DECISION_TYPE_PPLX_DECIDER) {@@ -678,6 +761,129 @@ void server_decision_context::fill_task_laya(llama_tokens & tokens, const server     task.decision.column = question.type; } +// each piece is cut to its budget as the model was trained (d1-omni prompt.py: encode), the state gets the room that is left+// the media come first, the text after them has a budget of its own+void server_decision_context::fill_task_d1omni(+        const json & state,+        const std::vector<server_decision_question> & questions,+        const server_decision_question & question,+        size_t variant,+        const std::vector<raw_buffer> & files,+        mtmd_context * mctx,+        const mtmd_helper_init_opt & init_opt,+        server_task & task) const {+    const auto invalid = std::runtime_error("unexpected layout of the decision prompt");++    // the prompt depends on the kind of media, mtmd tells an audio clip from an image by its content+    std::string markers;+    server_tokens media(llama_tokens(), false);+    bool is_audio = false;+    if (!files.empty()) {+        for (size_t i = 0; i < files.size(); i++) {+            markers += get_media_marker();+        }+        media = process_mtmd_prompt(mctx, markers, files, init_opt);+        for (size_t i = 0; i < media.size(); i++) {+            if (media[i] == LLAMA_TOKEN_NULL) {+                const auto & chunk = media.find_chunk(i);+                is_audio = is_audio || mtmd_input_chunk_get_type(chunk.get()) == MTMD_INPUT_CHUNK_TYPE_AUDIO;+                i += mtmd_input_chunk_get_n_tokens(chunk.get()) - 1;+            }+        }+    }+    if (is_audio && files.size() > 1) {+        throw std::invalid_argument("a request has images or one audio clip, not both");+    }+    const size_t n_media = media.size();++    const std::string prompt = render(state, questions, question, variant, files.size(), is_audio);+    std::vector<std::string> pieces = string_split(prompt, D1OMNI_SEP);+    if (pieces.empty() || pieces[0] != markers) {+        throw invalid;+    }++    size_t n_max = D1OMNI_MAX_TOKENS;+    if (!files.empty()) {+        n_max = std::min(is_audio ? D1OMNI_MAX_TOKENS_AUDIO : D1OMNI_MAX_TOKENS_IMAGE, D1OMNI_MAX_TOKENS - std::min(D1OMNI_MAX_TOKENS, n_media));+        if (n_max < 64) {+            throw std::invalid_argument(string_format("the media take %zu of the %zu positions, send fewer images", n_media, D1OMNI_MAX_TOKENS));+        }+    }++    // the options get max(96, min(24 n + 32, max / 2)) tokens, shared evenly+    const int64_t n_options      = question.options.size();+    const int64_t n_budget       = std::max<int64_t>(96, std::min<int64_t>(24 * n_options + 32, n_max / 2));+    const int64_t n_option_max   = std::max<int64_t>(2, (n_budget - 3 * n_options) / n_options);+    const int64_t n_question_max = std::max<int64_t>(16, n_budget);++    llama_tokens head;  // before the state+    llama_tokens body;  // the state+    llama_tokens tail;  // after the state+    bool has_state = false;+    for (size_t i_piece = 1; i_piece < pieces.size(); i_piece++) {+        std::string piece = pieces[i_piece];+        int64_t n_piece_max = -1;+        bool is_state = false;+        if (string_starts_with(piece, D1OMNI_MARK_STATE)) {+            piece    = piece.substr(D1OMNI_MARK_STATE.size());+            is_state = true;+        } else if (string_starts_with(piece, D1OMNI_MARK_QUESTION)) {+            piece = piece.substr(D1OMNI_MARK_QUESTION.size());+            n_piece_max = n_question_max;+        } else if (string_starts_with(piece, D1OMNI_MARK_OPTION)) {+            piece = piece.substr(D1OMNI_MARK_OPTION.size());+            n_piece_max = n_option_max;+        }++        llama_tokens tokens = common_tokenize(vocab, piece, false, true);+        if (n_piece_max >= 0 && (int64_t) tokens.size() > n_piece_max) {+            tokens.resize(n_piece_max);+        }++        if (is_state) {+            if (has_state) {+                throw invalid;+            }+            body      = std::move(tokens);+            has_state = true;+        } else {+            llama_tokens & dst = has_state ? tail : head;+            dst.insert(dst.end(), tokens.begin(), tokens.end());+        }+    }+    if (!has_state) {+        throw invalid;+    }++    const size_t n_room = n_max - std::min(n_max, head.size() + tail.size());+    body.resize(std::min(body.size(), n_room));++    llama_tokens tokens = std::move(head);+    tokens.insert(tokens.end(), body.begin(), body.end());+    tokens.insert(tokens.end(), tail.begin(), tail.end());+    tokens.resize(std::min(tokens.size(), n_max));++    for (size_t i = 0; i < tokens.size(); i++) {+        if (tokens[i] == token_marker) {+            task.decision.markers.push_back(n_media + i);+        }+    }+    if ((int64_t) task.decision.markers.size() != n_options) {+        throw std::invalid_argument("the options do not fit in the context");+    }++    // the output has one score per question type+    task.decision.column = question.type;+    if (files.empty()) {+        task.tokens = server_tokens(tokens, false);+    } else {+        task.tokens = std::move(media);+        for (const llama_token token : tokens) {+            task.tokens.push_back(token);+        }+    }+}+ // // joint prompt (clef) //@@ -850,14 +1056,15 @@ static double decision_confidence_score(const std::vector<double> & probs) {     return std::max(0.0, 1.0 - dist / dist_uniform); } -json server_decision_context::format_answer(const server_decision_question & question, const std::vector<std::vector<float>> & scores) const {+json server_decision_context::format_answer(const server_decision_question & question, const std::vector<std::vector<float>> & scores, bool has_media) const {     const size_t n = n_outputs(question);     if (scores.size() != n_variants(question)) {         throw std::runtime_error("decision result does not match the number of variants");     }      // softmax over the outputs of each variant, then the average of the variants-    const float temperature = get_temperature(question);+    // lfm2-d1-omni: image and audio answers are not calibrated+    const float temperature = has_media && type == COMMON_DECISION_TYPE_LFM2_D1_OMNI ? 1.0f : get_temperature(question);     std::vector<double> probs(n, 0.0);     for (size_t v = 0; v < scores.size(); v++) {         const auto & s = scores[v];
tools/server/server-decision.h15 + / 3 −
@@ -62,6 +62,7 @@ struct server_decision_context {             case COMMON_DECISION_TYPE_CLEF:             case COMMON_DECISION_TYPE_PPLX_DECIDER:             case COMMON_DECISION_TYPE_LFM2_D1:+            case COMMON_DECISION_TYPE_LFM2_D1_OMNI:                 return true;             default:                 return false;@@ -72,7 +73,8 @@ struct server_decision_context {     std::vector<server_decision_question> parse_questions(const json & body) const;      // returns the state without its images, they are appended to files in order-    // images come from "images" and from the image_url parts of a state made of chat messages+    // images come from "files" (alias "images") and from the image_url and input_audio parts of a state made of chat messages+    // an image can be an audio clip if the model supports it     json parse_state(const json & body, std::vector<raw_buffer> & files) const;      // number of prompts that are evaluated to answer this question, each one shows the options in a different order@@ -101,7 +103,7 @@ struct server_decision_context {             server_task & task) const;      // scores: the raw model outputs of each variant-    json format_answer(const server_decision_question & question, const std::vector<std::vector<float>> & scores) const;+    json format_answer(const server_decision_question & question, const std::vector<std::vector<float>> & scores, bool has_media = false) const;  private:     const llama_vocab * vocab = nullptr;@@ -128,12 +130,22 @@ struct server_decision_context {             const std::vector<server_decision_question> & questions,             const server_decision_question & question,             size_t variant,-            size_t n_images) const;+            size_t n_images,+            bool is_audio = false) const;     json render_options(const server_decision_question & question, size_t variant) const;     size_t n_outputs(const server_decision_question & question) const;     // LFM2_D1: label text and tokens of each option     void d1_labels(const server_decision_question & question, std::vector<std::string> & texts, std::vector<llama_tokens> & groups) const;     void fill_task_laya(llama_tokens & tokens, const server_decision_question & question, server_task & task) const;+    void fill_task_d1omni(+            const json & state,+            const std::vector<server_decision_question> & questions,+            const server_decision_question & question,+            size_t variant,+            const std::vector<raw_buffer> & files,+            mtmd_context * mctx,+            const mtmd_helper_init_opt & init_opt,+            server_task & task) const;      float get_temperature(const server_decision_question & question) const; };