diff --git a/conversion/bert.py b/conversion/bert.py index 2b82853994..32b71278fb 100644 --- a/conversion/bert.py +++ b/conversion/bert.py @@ -641,6 +641,11 @@ class ModernBertModel(BertModel): yield from super().modify_tensors(data_torch, name, bid) +def _jinja_str(name: str) -> str: + # non-string values are rendered as JSON + return "{{ " + name + " if " + name + " is string else " + name + " | tojson }}" + + def _is_decision_checkpoint(dir_model: Path) -> bool: if not (dir_model / "encoder" / "config.json").is_file(): return False @@ -702,21 +707,22 @@ class ModernBertDecisionModel(ModernBertModel): with open(self.dir_model / "tokenizer" / "tokenizer_config.json", encoding="utf-8") as f: tokenizer_config = json.load(f) tok_cls, tok_sep, tok_mask = (tokenizer_config[k] for k in ("cls_token", "sep_token", "mask_token")) + description = _jinja_str("o.description") if self.hparams["decision"].get("architecture") == "JuliaDecisionModel": - option = "{% if o.description %}{{ o.description }}{% else %}{{ o.key }}{% endif %}" + option = "{% if o.description %}" + description + "{% else %}{{ o.key }}{% endif %}" else: option = ( - "{% if type == 'choice' %}{{ o.key }}{% if o.description %}: {{ o.description }}{% endif %}" - "{% elif type == 'score' %}level {{ o.key }}: {{ o.description }}" - "{% else %}{{ o.key }}: {% if o.description %}{{ o.description }}" - "{% elif o.key == 'true' %}yes, the statement holds" + "{% if type == 'choice' %}{{ o.key }}{% if o.description %}: " + description + "{% endif %}" + "{% elif type == 'score' %}level {{ o.key }}: " + description + + "{% else %}{{ o.key }}: {% if o.description %}" + description + + "{% elif o.key == 'true' %}yes, the statement holds" "{% else %}no, the statement does not hold{% endif %}{% endif %}" ) # one marker token per option return ( - tok_cls + "{{ type }} question: {{ instructions }}" + tok_sep + tok_cls + "{{ type }} question: " + _jinja_str("instructions") + tok_sep + "{% for o in options %}" + tok_mask + " " + option + "{% endfor %}" - + tok_sep + "{{ state }}" + tok_sep + + tok_sep + _jinja_str("state") + tok_sep ) def set_gguf_parameters(self): @@ -725,9 +731,11 @@ class ModernBertDecisionModel(ModernBertModel): self.gguf_writer.add_decision_type(gguf.DecisionType.LAYA) self.gguf_writer.add_decision_block_count(decision["head_layers"]) self.gguf_writer.add_decision_max_head_tokens(decision.get("head_max_len", 256)) - temperatures = dict(zip(("choice", "score", "noul"), decision.get("temperature", []))) - temperatures.update(decision.get("temperature_by_options", {})) - self.gguf_writer.add_decision_temperatures(temperatures) + for name, value in zip(("choice", "score", "noul"), decision.get("temperature", [])): + self.gguf_writer.add_decision_temperature(name, value) + # "choice:3-5" -> "choice.3_5", "choice:11+" -> "choice.11" + for name, value in decision.get("temperature_by_options", {}).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: diff --git a/conversion/qwen.py b/conversion/qwen.py index 79b56e7b0a..17b31aef22 100644 --- a/conversion/qwen.py +++ b/conversion/qwen.py @@ -656,6 +656,11 @@ class Qwen3_5TextModel(_Qwen35MRopeMixin, _LinearAttentionVReorderBase): model_arch = gguf.MODEL_ARCH.QWEN35 +def _jinja_str(name: str) -> str: + # non-string values are rendered as JSON + return "{{ " + name + " if " + name + " is string else " + name + " | tojson }}" + + def _is_openjev_checkpoint(dir_model: Path) -> bool: return (dir_model / "helper" / "shim.py").is_file() and (dir_model / "config.json").is_file() @@ -673,6 +678,7 @@ def _load_openjev_hparams(dir_model: Path) -> dict[str, Any]: @ModelBase.example("openjev/openjev") class OpenJevModel(Qwen3_5TextModel): model_arch = gguf.MODEL_ARCH.QWEN35 + no_mtp = True # the checkpoint has no MTP head # prompt and calibration follow helper/shim.py of the model repo (text lane) _LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" @@ -684,17 +690,17 @@ class OpenJevModel(Qwen3_5TextModel): self.gguf_writer.add_chat_template([{"name": "systemone", "template": self._systemone_template()}]) def _systemone_template(self) -> str: + description = _jinja_str("o.description") option = ( - "{% if type == 'noul' %}" - "{% if o.key == 'true' %}yes: {{ o.description or 'The statement is true.' }}" - "{% else %}no: {{ o.description or 'The statement is false.' }}{% endif %}" - "{% else %}{{ o.key }}: {{ o.description or '' }}{% endif %}" + "{% if type != 'noul' %}{{ o.key }}: {% if o.description %}" + description + "{% endif %}" + "{% elif o.key == 'true' %}yes: {% if o.description %}" + description + "{% else %}The statement is true.{% endif %}" + "{% else %}no: {% if o.description %}" + description + "{% else %}The statement is false.{% endif %}{% endif %}" ) # newlines next to a block tag are emitted as expressions, so that trim_blocks cannot drop them return ( "{% set letters = '" + self._LETTERS + "' %}" - "<|im_start|>user\nState:\n{{ state }}\n\nQuestion: {{ instructions }}" - "{% if type == 'score' %} Rate along the ordered levels below (lowest first).{% endif %}" + "<|im_start|>user\nState:\n" + _jinja_str("state") + "\n\nQuestion: " + _jinja_str("instructions") + + "{% if type == 'score' %} Rate along the ordered levels below (lowest first).{% endif %}" "{{ '\\nOptions:\\n' }}" "{% for o in options %}[{{ letters[loop.index0] }}] " + option + "{{ '\\n' }}{% endfor %}" "{{ '\\nAnswer with the letter of the best option only.<|im_end|>\\n<|im_start|>assistant\\n\\n\\n\\n\\n' }}" @@ -703,11 +709,9 @@ class OpenJevModel(Qwen3_5TextModel): def set_gguf_parameters(self): super().set_gguf_parameters() self.gguf_writer.add_decision_type(gguf.DecisionType.OPENJEV) - self.gguf_writer.add_decision_temperatures({ - "choice": self._TEMPERATURE, - "score": self._TEMPERATURE, - "noul": self._TEMPERATURE * self._TEMPERATURE_NOUL, - }) + self.gguf_writer.add_decision_temperature("choice", self._TEMPERATURE) + self.gguf_writer.add_decision_temperature("score", self._TEMPERATURE) + self.gguf_writer.add_decision_temperature("noul", self._TEMPERATURE * self._TEMPERATURE_NOUL) @ModelBase.register("Qwen3_5MoeForConditionalGeneration", "Qwen3_5MoeForCausalLM") diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py index 9ce78cf2f9..dab8445a9a 100644 --- a/gguf-py/gguf/constants.py +++ b/gguf-py/gguf/constants.py @@ -325,8 +325,7 @@ class Keys: # note: single-use-case keys can be hard-coded in cpp code BLOCK_COUNT = "{arch}.decision.block_count" MAX_HEAD_TOKENS = "{arch}.decision.max_head_tokens" - TEMPERATURE_KEYS = "{arch}.decision.temperature.keys" # "" or ":" - TEMPERATURE_VALUES = "{arch}.decision.temperature.values" + TEMPERATURE = "{arch}.decision.temperature.{name}" # name: "" or "." class Tokenizer: MODEL = "tokenizer.ggml.model" diff --git a/gguf-py/gguf/gguf_writer.py b/gguf-py/gguf/gguf_writer.py index 5dd585a0b6..1dee3fe115 100644 --- a/gguf-py/gguf/gguf_writer.py +++ b/gguf-py/gguf/gguf_writer.py @@ -1349,9 +1349,8 @@ class GGUFWriter: def add_decision_max_head_tokens(self, value: int) -> None: self.add_uint32(Keys.Decision.MAX_HEAD_TOKENS.format(arch=self.arch), value) - def add_decision_temperatures(self, value: Mapping[str, float]) -> None: - self.add_array(Keys.Decision.TEMPERATURE_KEYS.format(arch=self.arch), list(value.keys())) - self.add_array(Keys.Decision.TEMPERATURE_VALUES.format(arch=self.arch), list(value.values())) + def add_decision_temperature(self, name: str, value: float) -> None: + self.add_float32(Keys.Decision.TEMPERATURE.format(arch=self.arch, name=name), value) # for vision models