Commit 9c9074e14 for llama.cpp

commit 9c9074e14e029a16fc3298e6bc6a3ef00a0b9c11
Author: Daniel Bevenius <daniel.bevenius@gmail.com>
Date:   Thu Nov 5 01:05:06 2026 -0800

    conversion : resolve quantized_layers from source tensor names (#31611)

    * conversion : resolve quantized_layers from source tensor names

    This commit changes how the per layer NVFP4 quant_algo are resolved from
    hf_quant_config.json quantized_layers.

    The keys in quantized_layers are module paths in the source checkpoint,
    and these can be either be modules like 'lm_head' or parent modules
    like 'layers.0.ffn.experts'. Currently these keys are passed through
    filter_tensors, but they are not tensor names. So a filter_tensors that
    keeps an explicit list of tensor names, like Nemotron's with --mtp
    would drop them. That means there was no `_nvfp4_layer_algo entry`, and
    `_tag_prec_a4` fell back to `_nvfp4_global_algo`.

    This commit records the mapping from source tensor name to filtered
    tensor name in index_tensors. Then in prepare_tensors each source
    tensor is resolved to its closest quantized_layers parent, and
    the algo is stored under the filtered tensor name, which is what
    _tag_prec_a4 later looks up.

    Converting Nemotron 3.5 Lightning NVFP4 with --mtp now produces:
    ```console
        general.tensor_extra.name = ['output.weight']
        general.tensor_extra.prec_a4 = [False]
    ```
    whereas on master no general.tensor_extra fields are written. For the
    non-MTP conversion, the general.tensor_extra fields are identical to
    master.

    Also manually verified (using pdb) that this works for
    DeepSeek-V4.1-Flash-1017-NVFP4.

    Refs: https://github.com/ggml-org/llama.cpp/pull/31602#issuecomment-6603337403

    * conversion : shorten comment

    * conversion : also remove '.bias' suffixes

diff --git a/conversion/base.py b/conversion/base.py
index 7028d633b..841efe729 100644
--- a/conversion/base.py
+++ b/conversion/base.py
@@ -167,6 +167,8 @@ class ModelBase:
         self._fusable_qkv_weight_layers: set[int] = set()
         self._fusable_qkv_bias_layers: set[int] = set()
         self.hparams = ModelBase.load_hparams(self.dir_model, self.is_mistral_format) if hparams is None else hparams
+        # source tensor name to filtered tensor name mapping.
+        self._source_tensor_names: dict[str, str | None] = {}
         self.model_tensors = self.index_tensors(remote_hf_model_id=remote_hf_model_id)
         self.metadata_override = metadata_override
         self.model_name = model_name
@@ -229,7 +231,9 @@ class ModelBase:
             remote_tensors = gguf.utility.SafetensorRemote.get_list_tensors_hf_model(remote_hf_model_id)
             for name, remote_tensor in remote_tensors.items():
                 data_gen = lambda r=remote_tensor: LazyTorchTensor.from_remote_tensor(r)  # noqa: E731
-                if titem := self.filter_tensors((name, data_gen)):
+                titem = self.filter_tensors((name, data_gen))
+                self._source_tensor_names[name] = titem[0] if titem else None
+                if titem:
                     tname, tgen = titem
                     tensors[tname] = tgen

@@ -294,7 +298,9 @@ class ModelBase:
                             data_gen = lambda data=data_torch: LazyTorchTensor.from_eager(data)  # noqa: E731
                         else:
                             data_gen = lambda data=data_torch: data  # noqa: E731
-                    if titem := self.filter_tensors((name, data_gen)):
+                    titem = self.filter_tensors((name, data_gen))
+                    self._source_tensor_names[name] = titem[0] if titem else None
+                    if titem:
                         tname, tgen = titem
                         tensors[tname] = tgen

@@ -1417,18 +1423,25 @@ class ModelBase:
         # Per-tensor NVFP4 precision.
         self._nvfp4_layer_algo = {}
         if quant_layers:
-            # store all possible module paths and assert if a quantized layer is not in the model
-            modules: set[str] = set()
-            for name in self.model_tensors:
-                while name := name.rpartition(".")[0]:
-                    modules.add(name)
-
-            for layer_name, entry in quant_layers.items():
-                if not isinstance(entry, dict):
+            # quantized_layers keys are module paths in the source checkpoint, not tensor names
+            # resolve each source tensor to the closest quantized_layers parent and record the algo under its filtered name for _tag_prec_a4
+            layer_algo = {k: v.get("quant_algo") for k, v in quant_layers.items() if isinstance(v, dict)}
+            matched: set[str] = set()
+            for src_name, filtered_name in self._source_tensor_names.items():
+                module = src_name
+                while module := module.rpartition(".")[0]:
+                    if module in layer_algo:
+                        break
+                if not module:
                     continue
-                if titem := self.filter_tensors((layer_name, lambda: torch.empty(0))):
-                    assert titem[0] in modules, f"quantized_layers entry {layer_name!r} is not in the model tensors"
-                    self._nvfp4_layer_algo[titem[0]] = entry.get("quant_algo")
+
+                matched.add(module)
+                # filter_tensors may drop a tensors so we need to check for that here.
+                if filtered_name is not None:
+                    self._nvfp4_layer_algo[filtered_name.removesuffix(".weight").removesuffix(".bias")] = layer_algo[module]
+
+            unmatched = layer_algo.keys() - matched
+            assert not unmatched, f"quantized_layers entries not in the model tensors: {sorted(unmatched)}"

         # NVFP4 weights are repacked and written directly to gguf_writer.
         # This must run before dequant_model so NVFP4 tensors are removed