Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 47 additions & 60 deletions examples/hf_ptq/example_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import json
import logging
import os
import shutil
import warnings
from collections.abc import Callable, Iterable
from dataclasses import dataclass
Expand All @@ -44,6 +43,7 @@
)

from modelopt.torch.export.model_utils import is_multimodal_model
from modelopt.torch.export.plugins.hf_checkpoint_utils import copy_non_safetensor_files_from_ckpt

try:
from huggingface_hub import snapshot_download
Expand All @@ -56,6 +56,27 @@

SPECULATIVE_MODEL_LIST = ["Eagle", "Medusa"]

_HF_SIDECAR_DOWNLOAD_ALLOW_PATTERNS = [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot comment.

The local-dir path is now blanket-copy-minus-exclusions, but the HF-model-ID path is still bounded by this extension allow-list, so the two paths disagree about what counts as a sidecar. Notably missing: chat_template.jinja (transformers ≥ 4.51 saves chat templates as a standalone file, and many current hub repos ship one) and extensionless LICENSE/NOTICE — the shared helper's own docstring names LICENSE as something it exists to preserve. Note the new test's accuracy_chart.png assertion can only ever hold for a local source dir, so it reads as broader coverage than it gives.

At minimum add "*.jinja", "LICENSE*", "NOTICE*"; ideally derive both paths from one source of truth (e.g. ignore_patterns=HF_CHECKPOINT_WEIGHT_FILE_PATTERNS for the download too, which already covers the bulk-artifact over-download concern) and pin download patterns vs. copy exclusions in a test so they can't drift.

"*.jinja",
"*.json",
"*.md",
"*.model",
"*.py",
"*.tiktoken",
"*.txt",
"LICENSE*",
"NOTICE*",
]
Comment on lines +59 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[IMPORTANT Compatibility] The HF-ID path still uses an extension allowlist, so it reintroduces exactly the brittleness this PR removes from the local-dir path — and the two paths now disagree about what a "sidecar" is.

For a local --pyt_ckpt_path, copy_custom_model_files copies everything that isn't a weight/index/export-owned file. For an HF model ID, the set is bounded by what snapshot_download pulled, i.e. only *.json, *.md, *.model, *.py, *.tiktoken, *.txt. Files a real hub repo ships that never reach the export:

  • chat_template.jinja — transformers ≥4.51 saves chat templates as a standalone .jinja sidecar, and many current hub repos ship one. hf_ptq.py:930-933 states the explicit intent that source tokenizer files win over regenerated ones "which may differ in format due to newer transformers versions". For .jinja that intent silently does not hold on the HF-ID path: the regenerated template is kept instead.
  • LICENSE / NOTICE — extensionless, so never downloaded, even though the shared helper's own docstring names LICENSE as a file it exists to preserve.
  • README assets (*.png, *.svg). Note the new test asserts accuracy_chart.png is copied — that assertion can only ever hold for a local source dir, so the test reads as broader coverage than it provides.

Why it matters: the reported bug (dropped reasoning parsers) is fixed for *.py, but the same class of bug remains for any sidecar whose extension isn't enumerated here — and it's invisible, since copy_custom_model_files reports success over an already-truncated directory.

Suggested fix: keep the allowlist only as a size guard and make it complete for non-weight text sidecars, e.g. add "*.jinja", "LICENSE*", "NOTICE*". Alternatively, invert to ignore_patterns=HF_CHECKPOINT_WEIGHT_FILE_PATTERNS on the download too, so both paths derive from the single weight-pattern source of truth (that list already covers *.gguf/*.npz/*.tar*/*.zip, which was the over-download concern from the earlier review round). Either way, worth a test that pins the download patterns and the copy exclusions against each other so they can't drift.

_HF_PTQ_EXPORT_OWNED_FILES = {
"config.json",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot comment.

hf_ptq.py explicitly supports already-quantized sources (pack-quantized/compressed-tensors and MXFP4 gpt-oss). Those checkpoints often carry stale quant metadata sidecars — recipe.yaml (llm-compressor), quantize_config.json (AutoAWQ/GPTQ), quant_config.json — which the new blanket copy will drop next to the freshly written hf_quant_config.json, potentially confusing vLLM/SGLang scheme detection. Consider adding those to _EXPORT_OWNED_CHECKPOINT_FILES (or a separate "stale quant metadata" skip set).

"hf_quant_config.json",
"quant_config.json",
"quantization_config.json",
"quantize_config.json",
"recipe.yaml",
"recipe.yml",
}
Comment on lines +70 to +78

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[IMPORTANT Compatibility] generation_config.json is excluded unconditionally, but only one of the two export paths that call copy_custom_model_files actually writes one — so on the TRT-LLM path this silently drops a file that used to be copied.

Tracing both call sites in hf_ptq.py:

  • Unified HF path (hf_ptq.py:935): export_hf_checkpointmodel.save_pretrained writes generation_config.json. Excluding the source copy is correct here.
  • TRT-LLM path (hf_ptq.py:884): export_tensorrt_llm_checkpoint writes only config.json and (for auto-quant) quant_cfg.json — grep for generation_config in modelopt/torch/export/model_config_export.py and tensorrt_llm_utils.py returns nothing. Before this PR, generation_config.json was copied here via the "*.json" code pattern whenever --trust_remote_code was set. Now it is never copied and never written, so the export loses the source's sampling defaults (temperature, top_p, eos_token_id overrides) outright.

Compare unified_export_megatron.py:319-330, which calls the same helper without exclude_files and then explicitly re-saves GenerationConfig.from_pretrained(...) — i.e. that path guarantees the file exists before excluding the source copy. The TRT-LLM path here does neither.

Same reasoning applies to config.json on the TRT-LLM path, but there the exclusion is right: the TRT-LLM config.json schema is unrelated to the HF one and copying the source over it would corrupt the export. generation_config.json has no such conflict.

Suggested fix: don't hardcode one exclusion set for both paths. Either pass the exclusions in from the caller so the TRT-LLM branch omits generation_config.json, or drop it from _HF_PTQ_EXPORT_OWNED_FILES and rely on ordering (the unified HF path runs save_pretrained before the copy, so the export version already exists — but note that means source-wins if you copy after, which is the opposite of today's behavior; pick one deliberately and say so in the docstring).

While here: the docstring at line 967-971 claims "Source processor files intentionally still win" — with preprocessor_config.json no longer in the exclusion set that is true, but the shared helper's docstring (hf_checkpoint_utils.py:298-299) lists preprocessor_config.json as modelopt-owned. Those two statements now contradict each other for readers of the shared helper.



@dataclass
class DistributedState:
Expand Down Expand Up @@ -895,11 +916,13 @@ def _resolve_model_path(model_name_or_path: str, trust_remote_code: bool = False
try:
local_path = snapshot_download(
repo_id=model_name_or_path,
allow_patterns=["*.py", "*.json"], # Only download Python files and config
allow_patterns=_HF_SIDECAR_DOWNLOAD_ALLOW_PATTERNS,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot comment.

This changes an intentionally tiny fetch (allow_patterns=["*.py", "*.json"]) into "download every file in the repo that isn't a recognized weight file". For HF-ID sources that ship GGUF conversions, images/videos, or archives, this can be many GB downloaded at export time (and *.gguf isn't even in the ignore list). Suggest keeping an allow-list here (or adding a size/pattern guard) and broadening only the local copy step, which is what the bug is actually about.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

return local_path
except Exception as e:
print(f"Warning: Could not download model files using snapshot_download: {e}")
print(
f"Warning: Could not download checkpoint sidecars using snapshot_download: {e}"
)

# Fallback: try to find in HuggingFace cache
from transformers.utils import TRANSFORMERS_CACHE
Expand Down Expand Up @@ -935,48 +958,23 @@ def _resolve_model_path(model_name_or_path: str, trust_remote_code: bool = False


def copy_custom_model_files(source_path: str, export_path: str, trust_remote_code: bool = False):
"""Copy processor/tokenizer artifacts (and, with trust_remote_code, custom code) to export.

Processor and tokenizer *data* artifacts -- e.g. a VLM's ``preprocessor_config.json``,
``merges.txt``/``vocab.json``, and the processor helper modules -- are needed by the
deployment stack (vLLM/SGLang) even when the model itself runs on native (non-remote)
transformers code. transformers 5.x restructured many VLM configs and no longer
re-saves these on ``save_pretrained`` for models loaded natively, so without copying
them a native-path export is missing e.g. ``preprocessor_config.json`` and fails to
load (``Can't load image processor``). These are copied regardless of
``trust_remote_code``. Executable model/config code (``modeling*.py``,
``configuration_*.py``, ``tokenization_*.py``, and other custom JSON) is only meaningful
with ``trust_remote_code`` and is copied only then. ``config.json`` and
``model.safetensors.index.json`` are always skipped (handled by the export itself).
"""Copy source checkpoint sidecar files to an HF PTQ export.

The HF PTQ script writes ModelOpt-owned metadata and quantized weights first, then
copies source checkpoint sidecars so tokenizer/processor files, remote-code modules,
README assets, parser plugins, and similar deployment files are preserved for both
native and ``trust_remote_code`` loads. Weight and weight-index files are skipped
to avoid copying the unquantized source weights. Export-owned metadata (``config.json``,
``hf_quant_config.json``) and stale source quantization metadata are also skipped.
Source tokenizer, processor, and generation files intentionally still win because
Transformers may not regenerate all metadata in the source format.

Args:
source_path: Path to the original model directory or HuggingFace model ID
export_path: Path to the exported model directory
trust_remote_code: Whether trust_remote_code was used (gates the executable code files)
trust_remote_code: Whether trust_remote_code was used when resolving HuggingFace model
IDs
"""
# Deployment-critical processor/tokenizer artifacts: safe to copy regardless of
# trust_remote_code (data + processor helpers, not model code).
always_copy_patterns = [
"preprocessor_config.json",
"processor_config.json",
"image_processing*.py",
"processing_*.py",
"video_processing*.py",
"feature_extraction_*.py",
"added_tokens.json",
"special_tokens_map.json",
"vocab.json",
"merges.txt",
"tokenizer.model",
]
# Executable custom model/config code + other custom JSON: only used with trust_remote_code.
code_patterns = [
"configuration_*.py",
"modeling*.py",
"tokenization_*.py",
"*.json",
]

# Resolve the source path (handles both local paths and HF model IDs)
resolved_source_path = _resolve_model_path(source_path, trust_remote_code)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot comment.

The old docstring deliberately gated executable code (modeling*.py, configuration_*.py, tokenization_*.py) behind trust_remote_code; after this change those files are copied into the export unconditionally and trust_remote_code only affects HF-ID resolution. That's likely intentional given the goal, but please call it out in the PR body (it's a behavior change relevant to SECURITY.md guidance) and add a test pinning the new parity between trust_remote_code=True/False.


Expand All @@ -997,29 +995,18 @@ def copy_custom_model_files(source_path: str, export_path: str, trust_remote_cod
print(f"Warning: Export directory {export_path} does not exist")
return

patterns = [*always_copy_patterns, *(code_patterns if trust_remote_code else [])]

copied_files: list[str] = []
for pattern in patterns:
for file_path in source_dir.glob(pattern):
if file_path.is_file():
# Skip config.json and model.safetensors.index.json as they're handled separately
if file_path.name in ["config.json", "model.safetensors.index.json"]:
continue
if file_path.name in copied_files: # e.g. matched by both pattern lists
continue
dest_path = export_dir / file_path.name
try:
shutil.copy2(file_path, dest_path)
copied_files.append(file_path.name)
print(f"Copied custom model file: {file_path.name}")
except Exception as e:
print(f"Warning: Failed to copy {file_path.name}: {e}")
copied_files = copy_non_safetensor_files_from_ckpt(
source_dir,
export_dir,
exclude_files=_HF_PTQ_EXPORT_OWNED_FILES,
)

if copied_files:
print(f"Successfully copied {len(copied_files)} custom model files to {export_path}")
for file_name in copied_files:
print(f"Copied checkpoint sidecar file: {file_name}")
print(f"Successfully copied {len(copied_files)} checkpoint sidecar files to {export_path}")
else:
print("No custom model files found to copy")
print("No checkpoint sidecar files found to copy")


def _layerwise_checkpoint_dir_location(algorithm) -> tuple[str, str] | None:
Expand Down
61 changes: 53 additions & 8 deletions modelopt/torch/export/plugins/hf_checkpoint_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@

"""Hugging Face checkpoint utility."""

import fnmatch
import json
import os
import shutil
import warnings
from collections.abc import Iterable
from pathlib import Path
from typing import Any

Expand All @@ -29,6 +31,31 @@
from tqdm import tqdm

_HF_HUB_OFFLINE_TRUE_VALUES = {"1", "ON", "YES", "TRUE"}
_HF_CHECKPOINT_WEIGHT_FILE_PATTERNS = (
"*.safetensors",
"*.safetensors.index.json",
"*.bin",
"*.bin.index.json",
"*.ckpt",
"*.gguf",
"*.h5",
"*.msgpack",
"*.npy",
"*.npz",
"*.onnx",
"*.pb",
"*.pickle",
"*.pkl",
"*.pt",
"*.pth",
"*.tar",
"*.tar.bz2",
"*.tar.gz",
"*.tar.xz",
"*.tflite",
"*.tgz",
"*.zip",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot comment.

HF_CHECKPOINT_WEIGHT_FILE_PATTERNS is a new public name in a module star-exported via plugins/__init__.py, and this module has no __all__ (so the star-import also re-exports fnmatch/json/os/shutil/torch/...). Per CONTRIBUTING.md, declare the public API with __all__, or make the constant private (_HF_...) if examples/hf_ptq is the only intended consumer — otherwise it becomes an API surface with a compat obligation.

Minor naming nit: *.npy/*.npz/*.pkl/*.zip/*.tar* aren't HF weight formats; some legitimate sidecars are .npy (precomputed stats) and will now be silently dropped. Something like ..._LARGE_ARTIFACT_PATTERNS would describe the intent more honestly.

)


def _as_nonnegative_int(value: Any) -> int | None:
Expand Down Expand Up @@ -253,25 +280,43 @@ def load_multimodal_components(
return multimodal_state_dict


def copy_non_safetensor_files_from_ckpt(src: str | os.PathLike, dst: str | os.PathLike):
"""Copy every non-safetensors file from a local HF checkpoint dir verbatim.
def _matches_any_pattern(file_name: str, patterns: tuple[str, ...]) -> bool:
return any(fnmatch.fnmatchcase(file_name, pattern) for pattern in patterns)


def copy_non_safetensor_files_from_ckpt(
src: str | os.PathLike,
dst: str | os.PathLike,
*,
exclude_files: Iterable[str] | None = None,
) -> list[str]:
"""Copy every non-weight sidecar file from a local HF checkpoint dir verbatim.

Use as a baseline so tokenizer files, remote_code ``*.py``, README, LICENSE, etc.
are preserved from the source. The caller is expected to overwrite the files
modelopt owns (``config.json``, ``generation_config.json``, ``hf_quant_config.json``,
``preprocessor_config.json``) after this step.
are preserved from the source. Callers can pass files through ``exclude_files`` when
copying after export-owned metadata has already been written.

Args:
src: Source HF checkpoint directory. Must be a local path.
dst: Destination directory; created if missing.
exclude_files: Exact file names to skip in addition to weights and weight indexes.

Returns:
File names copied into ``dst``.
"""
if not os.path.isdir(src):
raise ValueError(f"Invalid source path: {src}. It should be a directory.")
exclude_files = set(exclude_files or ())
copied_files = []
os.makedirs(dst, exist_ok=True)
for entry in os.listdir(src):
for entry in sorted(os.listdir(src)):
if entry in exclude_files or _matches_any_pattern(
entry, _HF_CHECKPOINT_WEIGHT_FILE_PATTERNS
):
continue
sp = os.path.join(src, entry)
if not os.path.isfile(sp):
continue
if entry.endswith(".safetensors") or entry == "model.safetensors.index.json":
continue
shutil.copy2(sp, dst)
Comment on lines 318 to 320

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

sed -n '280,330p' modelopt/torch/export/plugins/hf_checkpoint_utils.py
printf '\nRelevant callers:\n'
rg -n -A12 -B4 'copy_non_safetensor_files_from_ckpt|copy_custom_model_files' \
  modelopt examples tests

printf '\nPython standard-library behavior for the exact operations:\n'
python3 - <<'PY'
import os
import shutil
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as td:
    root = Path(td)
    source = root / "source"
    destination = root / "destination"
    source.mkdir()
    destination.mkdir()

    secret = root / "host-readable-secret.txt"
    secret.write_text("sensitive\n")
    sidecar = source / "tokenizer_config.json"
    sidecar.symlink_to(secret)

    print("isfile_symlink:", os.path.isfile(sidecar))
    shutil.copy2(sidecar, destination)
    copied = destination / sidecar.name
    print("copied_exists:", copied.exists())
    print("copied_is_symlink:", copied.is_symlink())
    print("copied_contents:", copied.read_text())
PY

Repository: NVIDIA/Model-Optimizer

Length of output: 19748


Sensitive Data Exposure (CWE-59)

Reachability: External

Reachability path
● Entry
  examples/hf_ptq/example_utils.py:961
  copy_custom_model_files: Copy source checkpoint sidecar files to an HF PTQ export. The HF PTQ script writes ModelOpt-owned metadata and quantized weights first, t…
│
▼
● Sink
  modelopt/torch/export/plugins/hf_checkpoint_utils.py

Reject source symlinks before copying checkpoint sidecars.

os.path.isfile(sp) and shutil.copy2(sp, dst) follow symlinks. A sidecar link can copy a process-readable host file into the export. Reject symlinks or restrict targets to trusted snapshot roots, and add a regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modelopt/torch/export/plugins/hf_checkpoint_utils.py` around lines 319 - 325,
Update the sidecar-copy loop around the os.path.isfile and shutil.copy2 calls to
reject symbolic-link sources before copying, using os.path.islink(sp) (or an
equivalent trusted-root validation) so only regular files from the snapshot are
exported. Preserve the existing exclusions and copy behavior for valid
non-symlink sidecars, and add a regression test covering a symlinked sidecar.

Source: Path instructions

copied_files.append(entry)
Comment on lines +312 to +321

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] Two small things in the copy loop:

  1. The hardcoded safetensors check at line 321-322 is now redundant on the new path but inconsistent on the old one. HF_CHECKPOINT_WEIGHT_FILE_PATTERNS already covers *.safetensors and *.safetensors.index.json (which is a superset of the model.safetensors.index.json equality check — it also catches e.g. adapter_model.safetensors.index.json). For the Megatron caller, which passes no exclude_patterns, line 321 remains the only guard and still misses sharded index files not named exactly model.safetensors.index.json. Consider making HF_CHECKPOINT_WEIGHT_FILE_PATTERNS the always-applied baseline and letting exclude_patterns be purely additive — that removes the double source of truth and fixes the Megatron gap in one move.

  2. Silent failures. The old copy_custom_model_files wrapped each shutil.copy2 in try/except and warned per file. The new loop does not, so a single unreadable file (bad permissions, broken symlink, dangling HF-cache blob) now aborts the whole sidecar copy — and because this runs after the weights are written, the user gets a traceback on an otherwise-complete export. A per-file try/except with a warning would preserve the previous resilience.

Both are non-blocking, but (2) is a real behavior regression for the HF-cache source case, where dangling blob symlinks do occur.

return copied_files
77 changes: 77 additions & 0 deletions tests/examples/hf_ptq/test_example_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,83 @@ def _write_safetensors(path, tensors):
save_file(tensors, str(path), metadata={"format": "pt"})


def test_copy_custom_model_files_preserves_non_weight_sidecars(tmp_path):
source_dir = tmp_path / "source"
export_dir = tmp_path / "export"
source_dir.mkdir()
export_dir.mkdir()

source_files = {
"super_v3_reasoning_parser.py": "class Parser: pass\n",
"modeling_custom.py": "class Model: pass\n",
"README.md": "# Source model\n",
"LICENSE": "license text\n",
"chat_template.jinja": "{{ messages }}\n",
"generation_config.json": '{"source": "generation"}\n',
"config.json": '{"source": "config"}\n',
"hf_quant_config.json": '{"source": "quant"}\n',
"quant_config.json": '{"source": "stale quant"}\n',
"quantize_config.json": '{"source": "stale quant"}\n',
"recipe.yaml": "quantize: {}\n",
"model.safetensors.index.json": '{"weight_map": {}}\n',
"model-00001-of-00001.safetensors": "source weights\n",
"model.gguf": "source weights\n",
}
for file_name, contents in source_files.items():
(source_dir / file_name).write_text(contents)

(export_dir / "config.json").write_text('{"export": "config"}\n')
(export_dir / "generation_config.json").write_text('{"export": "generation"}\n')
(export_dir / "hf_quant_config.json").write_text('{"export": "quant"}\n')

example_utils.copy_custom_model_files(str(source_dir), str(export_dir), trust_remote_code=False)

for file_name in [
"super_v3_reasoning_parser.py",
"modeling_custom.py",
"README.md",
"LICENSE",
"chat_template.jinja",
"generation_config.json",
]:
assert (export_dir / file_name).read_text() == source_files[file_name]

assert (export_dir / "config.json").read_text() == '{"export": "config"}\n'
assert (export_dir / "hf_quant_config.json").read_text() == '{"export": "quant"}\n'
assert not (export_dir / "quant_config.json").exists()
assert not (export_dir / "quantize_config.json").exists()
assert not (export_dir / "recipe.yaml").exists()
assert not (export_dir / "model.safetensors.index.json").exists()
assert not (export_dir / "model-00001-of-00001.safetensors").exists()
assert not (export_dir / "model.gguf").exists()


def test_resolve_model_path_snapshot_download_stays_allowlisted(monkeypatch, tmp_path):
snapshot_dir = tmp_path / "snapshot"

def fake_snapshot_download(**kwargs):
assert kwargs == {
"repo_id": "org/model",
"allow_patterns": example_utils._HF_SIDECAR_DOWNLOAD_ALLOW_PATTERNS,
}
return str(snapshot_dir)

def fake_from_pretrained(*args, **kwargs):
assert (args, kwargs) == (("org/model",), {"trust_remote_code": False})
return SimpleNamespace(_name_or_path="org/model")

monkeypatch.setattr(
example_utils.AutoConfig,
"from_pretrained",
fake_from_pretrained,
)
monkeypatch.setattr(example_utils, "snapshot_download", fake_snapshot_download)

assert example_utils._resolve_model_path("org/model", trust_remote_code=False) == str(
snapshot_dir
)


def test_load_mtp_weights_inlined_orphaned(tmp_path):
# GLM-5.1: HF builds only num_hidden decoders → MTP keys orphaned.
main_keys = ["model.embed_tokens.weight", "model.layers.0.x.weight"]
Expand Down
Loading