|
5 | 5 | import torch |
6 | 6 |
|
7 | 7 | if TYPE_CHECKING: |
| 8 | + from pathlib import Path |
8 | 9 | from torch import Tensor |
9 | 10 |
|
10 | 11 | from .base import MmprojModel, ModelBase, TextModel, gguf, logger |
@@ -201,6 +202,7 @@ class NemotronHModel(GraniteHybridModel): |
201 | 202 | model_arch = gguf.MODEL_ARCH.NEMOTRON_H |
202 | 203 | is_moe: bool = False |
203 | 204 | supports_mtp_export = True |
| 205 | + _experts: list[dict[str, Tensor]] | None = None |
204 | 206 |
|
205 | 207 | _SSM_LAYER_TYPES = {"mamba", "linear_attention"} |
206 | 208 | _ATTN_LAYER_TYPES = {"attention", "full_attention"} |
@@ -513,3 +515,88 @@ def prepare_tensors(self): |
513 | 515 | experts = [k for d in self._experts for k in d.keys()] |
514 | 516 | if len(experts) > 0: |
515 | 517 | raise ValueError(f"Unprocessed experts: {experts}") |
| 518 | + |
| 519 | + |
| 520 | +@ModelBase.register("NemotronHPuzzleForCausalLM") |
| 521 | +@ModelBase.example("nvidia/NVIDIA-Nemotron-Labs-3-Puzzle-75B-A9B-BF16") |
| 522 | +class NemotronHPuzzleModel(NemotronHModel): |
| 523 | + """NVIDIA Puzzle: NemotronH with a per-block MoE config (block_configs). |
| 524 | +
|
| 525 | + The checkpoint also ships an MTP draft head (mtp.safetensors). It is skipped |
| 526 | + here: there is no Puzzle MTP inference path in tree, and the head is laid out |
| 527 | + by mtp_block_configs rather than the mtp.layers.* form NemotronHModel maps.""" |
| 528 | + |
| 529 | + model_arch = gguf.MODEL_ARCH.NEMOTRON_H_MOE |
| 530 | + is_moe: bool = True |
| 531 | + supports_mtp_export = False |
| 532 | + |
| 533 | + def __init__(self, dir_model: "Path", *args, **kwargs): |
| 534 | + hparams = dict(kwargs.pop("hparams", None) or ModelBase.load_hparams(dir_model, self.is_mistral_format)) |
| 535 | + |
| 536 | + self.block_configs: list[dict] = hparams["block_configs"] |
| 537 | + self.n_layer_trunk = len(self.block_configs) |
| 538 | + |
| 539 | + # block_configs carries the per-block MoE shape, and is the authority on the |
| 540 | + # block pattern too: the layers_block_type the HF config wrapper computes is |
| 541 | + # not sized to it. |
| 542 | + hparams["num_hidden_layers"] = self.n_layer_trunk |
| 543 | + hparams["layers_block_type"] = [bc["block_type"] for bc in self.block_configs] |
| 544 | + |
| 545 | + self.model_arch = gguf.MODEL_ARCH.NEMOTRON_H_MOE |
| 546 | + |
| 547 | + # Bypass NemotronHModel.__init__: it assumes a flat num_experts_per_tok / |
| 548 | + # moe_intermediate_size and a layers_block_type sized to block_count, neither |
| 549 | + # of which hold for Puzzle's per-block config. |
| 550 | + GraniteHybridModel.__init__(self, dir_model, *args, hparams=hparams, **kwargs) |
| 551 | + |
| 552 | + self.head_dim = self.find_hparam(["head_dim", "attention_head_dim"]) |
| 553 | + self.d_inner = self.find_hparam(["num_heads"]) * self.d_model |
| 554 | + |
| 555 | + # NemotronHModel.__init__ folds an MTP block into block_count when the |
| 556 | + # config carries num_nextn_predict_layers; Puzzle's config does, but its |
| 557 | + # head has a different layout and no inference path, so stay opted out. |
| 558 | + self._mtp_bid = None |
| 559 | + |
| 560 | + def set_gguf_parameters(self): |
| 561 | + GraniteHybridModel.set_gguf_parameters(self) |
| 562 | + |
| 563 | + head_dim = self.head_dim |
| 564 | + if head_dim is None: |
| 565 | + raise ValueError("Could not find the attention head dim in config") |
| 566 | + self.gguf_writer.add_key_length(head_dim) |
| 567 | + self.gguf_writer.add_value_length(head_dim) |
| 568 | + |
| 569 | + ffn_lengths = [bc.get("moe_intermediate_size") or 0 for bc in self.block_configs] |
| 570 | + experts_used = [bc.get("num_experts_per_tok") or 0 for bc in self.block_configs] |
| 571 | + |
| 572 | + self.gguf_writer.add_feed_forward_length(ffn_lengths) |
| 573 | + self.gguf_writer.add_expert_feed_forward_length(ffn_lengths) |
| 574 | + self.gguf_writer.add_expert_used_count(experts_used) |
| 575 | + |
| 576 | + self.gguf_writer.add_expert_shared_feed_forward_length(self.hparams["moe_shared_expert_intermediate_size"]) |
| 577 | + self.gguf_writer.add_expert_count(self.hparams["n_routed_experts"]) |
| 578 | + self.gguf_writer.add_expert_shared_count(self.hparams["n_shared_experts"]) |
| 579 | + self.gguf_writer.add_expert_weights_norm(self.hparams["norm_topk_prob"]) |
| 580 | + self.gguf_writer.add_expert_weights_scale(self.hparams["routed_scaling_factor"]) |
| 581 | + self.gguf_writer.add_expert_group_count(self.hparams["n_group"]) |
| 582 | + self.gguf_writer.add_moe_latent_size(self.hparams["moe_latent_size"]) |
| 583 | + |
| 584 | + def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]: |
| 585 | + # The official BF16 checkpoint (NVIDIA-Nemotron-Labs-3-Puzzle-75B-A9B-BF16) |
| 586 | + # names the trunk "model.*" (model.layers.*, model.embeddings, model.norm_f) |
| 587 | + # where the original release used the NemotronH-style "backbone.*", and spells |
| 588 | + # the router bias "e_score_correction_bias" instead of "e_score_correction.bias"; |
| 589 | + # normalize so both convert identically. |
| 590 | + if name.startswith("model."): |
| 591 | + name = "backbone." + name[len("model."):] |
| 592 | + if name.endswith("mixer.gate.e_score_correction_bias"): |
| 593 | + name = name[: -len("e_score_correction_bias")] + "e_score_correction.bias" |
| 594 | + |
| 595 | + yield from super().modify_tensors(data_torch, name, bid) |
| 596 | + |
| 597 | + @classmethod |
| 598 | + def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None: |
| 599 | + # Drop the MTP head unconditionally; see the class docstring. |
| 600 | + if item[0].startswith("mtp."): |
| 601 | + return None |
| 602 | + return super().filter_tensors(item) |
0 commit comments