Source code for tensorrt_edgellm.checkpoint.loader

# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Weight loader for safetensors checkpoints.

Supports both single-file (``model.safetensors``) and multi-shard
(``model.safetensors.index.json`` -> multiple shard files) layouts.

Key design
----------
Rather than ``module.load_state_dict()``, weights are assigned directly to
module buffers/parameters via :func:`_set_tensor`.  This preserves the
original tensor dtype (fp8, uint8, bfloat16, ...) without any silent cast that
PyTorch's state-dict mechanism might introduce, and handles keys not present
in the module (logged as warnings rather than hard errors).

Usage
-----
    model = AutoModel.from_pretrained(model_dir)   # from model.py
    load_weights(model, model_dir)                 # fills all buffers in-place
"""

import json
import logging
import os
import pathlib
from typing import Callable, Dict, Iterator, Optional, Tuple

import torch
import torch.nn as nn
from safetensors import safe_open

from ..config import Mapping
from ..models.linear import LinearBase, TPMode
from .repacking import apply_all_repacking

logger = logging.getLogger(__name__)

__all__ = ["load_weights", "load_submodule_weights"]

_FUSED_INPUT_CHANNEL_ATTRS = {"pre_quant_scale"}


def _is_awq_prepacked_weight(tensor: torch.Tensor, attr_suffix: str,
                             expected_N: int) -> bool:
    """True when *tensor* is the AWQ ModelOpt prepacked ``weight`` buffer.

    The prepacked layout is ``uint8 [N//2, K]`` with two int4 nibbles per byte
    (even N rows in the low nibble, odd N rows in the high nibble).  Any
    per-head split has to unpack, slice, and re-pack instead of a plain
    ``dim=0`` slice.
    """
    return (attr_suffix == "weight" and tensor.dtype == torch.uint8
            and tensor.dim() == 2 and tensor.shape[0] * 2 == expected_N)


def _unpack_awq_prepacked(w_u8: torch.Tensor, N: int) -> torch.Tensor:
    """Unpack ``[N//2, K] uint8`` AWQ ModelOpt weight to ``[N, K] int16``."""
    K = w_u8.shape[1]
    w_u16 = w_u8.to(torch.int16) & 0xFF
    unpacked = torch.zeros(N, K, dtype=torch.int16)
    unpacked[0::2] = w_u16 & 0xF
    unpacked[1::2] = (w_u16 >> 4) & 0xF
    return unpacked


def _repack_awq_prepacked(w_int16: torch.Tensor) -> torch.Tensor:
    """Repack ``[N, K] int16`` nibbles back to AWQ ``[N//2, K] uint8``."""
    N = w_int16.shape[0]
    assert N % 2 == 0, f"AWQ repack requires even N, got {N}"
    low = w_int16[0::2] & 0xF
    high = w_int16[1::2] & 0xF
    return ((high << 4) | low).to(torch.uint8)


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


[docs] def load_weights( model: nn.Module, model_dir: str, device: str = "cpu", key_remap: "Optional[Callable[[str], Optional[str]]]" = None, key_prefix: Optional[str] = None, pre_repack_hook: Optional[Callable[[nn.Module], None]] = None, mapping: Optional[Mapping] = None, ) -> None: """Load all safetensors weights from *model_dir* into *model* in-place. Args: model: Module built by :meth:`~tensorrt_edgellm.model.AutoModel.from_pretrained`. model_dir: Checkpoint directory that contains safetensors files. device: Target device (e.g. ``"cpu"``, ``"cuda:0"``). Tensors are moved here after loading. key_remap: Optional callable ``(key: str) -> Optional[str]``. Called on each checkpoint key *after* stripping the prefix. Return a new key to remap, the original key unchanged, or ``None`` to skip the tensor entirely. key_prefix: Explicit checkpoint key prefix to strip (e.g. ``"talker."`` or ``"talker.code_predictor."``). When provided, only keys starting with this prefix are loaded and auto-detection via :func:`_detect_key_prefix` is skipped. pre_repack_hook: Optional callback invoked after raw checkpoint tensors are loaded and before quantized weights are repacked. mapping: Parallel-placement config (default = no TP). Drives :func:`_shard_for_module` to slice each NVFP4 weight/scale to its per-rank shard before assignment. Must match ``ModelConfig.mapping`` used to build *model*. """ mapping = mapping or Mapping() shard_map = _build_shard_map(model_dir) # Group keys by shard path to open each shard only once path_to_keys: Dict[str, list] = {} for key, path in shard_map.items(): path_to_keys.setdefault(path, []).append(key) if key_prefix is not None: strip_prefix, insert_prefix = key_prefix, "" else: # Auto-detect a common VL wrapper prefix (e.g. "language_model." for # InternVL) and strip it so the LLM weights map to our module tree. all_keys = list(shard_map.keys()) strip_prefix, insert_prefix = _detect_key_prefix(all_keys) if strip_prefix: logger.info( "Stripping key prefix %r from checkpoint keys (inserting %r)", strip_prefix, insert_prefix) def _apply_prefix(key: str) -> Optional[str]: if strip_prefix and key.startswith(strip_prefix): return insert_prefix + key[len(strip_prefix):] # When an explicit key_prefix was given, skip keys outside the prefix. if key_prefix is not None: return None return key loaded = skipped = 0 for shard_path, keys in path_to_keys.items(): if shard_path.endswith(".bin"): # PyTorch pickle shard -- load all at once, then iterate keys. bin_state = torch.load(shard_path, map_location=device, weights_only=True) for key in keys: tensor = bin_state.get(key) if tensor is None: logger.debug("Key not found in .bin shard: %s", key) skipped += 1 continue mapped_key = _apply_prefix(key) if mapped_key is None: skipped += 1 continue if key_remap is not None: mapped_key = key_remap(mapped_key) if mapped_key is None: skipped += 1 continue if _set_tensor(model, mapped_key, tensor, mapping=mapping): loaded += 1 elif _try_split_fused_tensor(model, mapped_key, tensor, mapping=mapping): loaded += 1 else: logger.debug("Key not found in model: %s", key) skipped += 1 else: with safe_open(shard_path, framework="pt", device=device) as f: for key in keys: tensor = f.get_tensor(key) mapped_key = _apply_prefix(key) if mapped_key is None: skipped += 1 continue if key_remap is not None: mapped_key = key_remap(mapped_key) if mapped_key is None: skipped += 1 continue if _set_tensor(model, mapped_key, tensor, mapping=mapping): loaded += 1 elif _try_split_fused_tensor(model, mapped_key, tensor, mapping=mapping): loaded += 1 else: logger.debug("Key not found in model: %s", key) skipped += 1 logger.info("Loaded %d tensors, skipped %d from %s", loaded, skipped, model_dir) if pre_repack_hook is not None: pre_repack_hook(model) apply_all_repacking(model) # Post-process: apply tied embeddings (HF tie_word_embeddings=True models # omit lm_head.weight from the checkpoint; tie_weights() restores the share). config = getattr(model, "config", None) if (hasattr(model, "tie_weights") and config is not None and getattr(config, "tie_word_embeddings", False)): from ..models.linear import FP16Linear if isinstance(getattr(model, "lm_head", None), FP16Linear): model.tie_weights() logger.info("Tied lm_head.weight to embed_tokens.weight") else: logger.debug( "Skipping tied lm_head.weight for non-FP16 lm_head type %s", type(getattr(model, "lm_head", None)).__name__)
# --------------------------------------------------------------------------- # Internal helpers # --------------------------------------------------------------------------- def _detect_key_prefix(keys: list) -> Tuple[str, str]: """Return ``(strip_prefix, insert_prefix)`` for remapping checkpoint keys. Detects VL wrapper prefixes so that LLM weights map into our module tree. After stripping ``strip_prefix`` from each key, ``insert_prefix`` is prepended before setting the tensor. Examples: InternVL3 keys: ``language_model.model.embed_tokens.weight`` → strip ``"language_model."`` → ``"model.embed_tokens.weight"`` ✓ Qwen3-VL-2B keys: ``model.language_model.embed_tokens.weight`` → strip ``"model.language_model."`` + insert ``"model."`` → ``"model.embed_tokens.weight"`` ✓ """ key_set = set(keys) for prefix in ("language_model.", "text_model.", "llm."): if (f"{prefix}model.embed_tokens.weight" in key_set or any(k.startswith(f"{prefix}model.layers.0.") for k in keys) or any( k.startswith(f"{prefix}backbone.layers.0.") for k in keys)): return prefix, "" # Qwen3-VL-2B: text decoder is under "model.language_model.*". # Strip the outer "model.language_model." and prepend "model." so that # the stripped key (e.g. "embed_tokens.weight") becomes # "model.embed_tokens.weight" matching the CausalLM parameter tree. if (any(k.startswith("model.language_model.") for k in keys) and "model.embed_tokens.weight" not in key_set): return "model.language_model.", "model." # Alpamayo-R1: LLM text decoder is under "vlm.model.language_model.*". # lm_head at "vlm.lm_head.*" falls through and requires key_remap. if any(k.startswith("vlm.model.language_model.") for k in keys): return "vlm.model.language_model.", "model." # Qwen3-ASR / Qwen3-Omni: LLM weights are under thinker.model.* if (any(k.startswith("thinker.model.layers.0.") for k in keys) and not any(k.startswith("model.layers.0.") for k in keys)): return "thinker.", "" # Qwen3-TTS: all weights are under talker.* (talker is the LLM backbone) if (any(k.startswith("talker.model.layers.0.") for k in keys) and not any(k.startswith("model.layers.0.") for k in keys)): return "talker.", "" return "", "" def _resolve_shard(model_dir: str, shard: str) -> str: """Return the absolute shard path, asserting it stays inside model_dir.""" base = pathlib.Path(model_dir).resolve() resolved = (base / shard).resolve() try: resolved.relative_to(base) except ValueError: raise ValueError( f"Shard path {shard!r} in checkpoint index escapes model_dir " f"{model_dir!r}. This may indicate a malformed checkpoint.") return str(resolved) def _build_shard_map(model_dir: str) -> Dict[str, str]: """Return a mapping of weight-key -> absolute shard file path. Handles: • ``model.safetensors`` (single file, no index) • ``model.safetensors.index.json`` (multi-shard, weight_map inside) • ``pytorch_model.bin`` (single PyTorch pickle file) • ``pytorch_model.bin.index.json`` (multi-shard PyTorch pickle) """ # ---- safetensors (preferred) ---------------------------------------- index_path = os.path.join(model_dir, "model.safetensors.index.json") single_path = os.path.join(model_dir, "model.safetensors") if os.path.exists(index_path): with open(index_path) as f: index = json.load(f) weight_map: Dict[str, str] = index["weight_map"] missing_shards = { shard for shard in set(weight_map.values()) if not os.path.exists(_resolve_shard(model_dir, shard)) } if missing_shards and os.path.exists(single_path): logger.warning( "Ignoring stale %s because shard file(s) are missing and " "single-file model.safetensors exists: %s", index_path, ", ".join(sorted(missing_shards)), ) else: return { key: _resolve_shard(model_dir, shard) for key, shard in weight_map.items() } if os.path.exists(single_path): keys: Dict[str, str] = {} with safe_open(single_path, framework="pt") as f: for key in f.keys(): keys[key] = single_path return keys if os.path.exists(index_path): return { key: _resolve_shard(model_dir, shard) for key, shard in weight_map.items() } # ---- PyTorch pickle (.bin) fallback --------------------------------- bin_index_path = os.path.join(model_dir, "pytorch_model.bin.index.json") bin_single_path = os.path.join(model_dir, "pytorch_model.bin") if os.path.exists(bin_index_path): with open(bin_index_path) as f: index = json.load(f) weight_map = index["weight_map"] return { key: _resolve_shard(model_dir, shard) for key, shard in weight_map.items() } if os.path.exists(bin_single_path): # Load with map_location="meta" to get key names without loading data. meta = torch.load(bin_single_path, map_location="meta", weights_only=True) return {key: bin_single_path for key in meta.keys()} raise FileNotFoundError( f"No checkpoint files found in {model_dir!r}. " "Expected 'model.safetensors', 'model.safetensors.index.json', " "'pytorch_model.bin', or 'pytorch_model.bin.index.json'.") def _navigate(model: nn.Module, parts: list) -> Tuple[nn.Module, str]: """Walk the module tree following *parts* and return (leaf_module, attr). Integer parts are interpreted as ``nn.ModuleList`` indices. Raises: AttributeError: if any part of the path is not found. """ module = model for part in parts[:-1]: if part.isdigit(): module = module[int(part)] else: module = getattr(module, part) if module is None: raise AttributeError(f"None encountered at '{part}' in path") return module, parts[-1] def load_weight_shard(tensor: torch.Tensor, dim: int, mapping: Optional[Mapping] = None) -> torch.Tensor: """Slice *tensor* along *dim* to the per-rank shard given by *mapping*. Free function so any caller (``_shard_for_module``, future ``MoEMethodBase.load_weights``) can reuse the same primitive. Supports lazy partial reads when *tensor* is a safetensors slice (has ``get_shape``). """ mapping = mapping or Mapping() tp_size, tp_rank = mapping.tp_size, mapping.tp_rank if tp_size <= 1: return tensor # safetensors PySafeSlice path: read only the slice from disk. if hasattr(tensor, "get_shape"): shape = tensor.get_shape() assert shape[dim] % tp_size == 0, ( f"TP shard: dim-{dim} {shape[dim]} not divisible by tp_size={tp_size}" ) shard = shape[dim] // tp_size sl = [slice(None)] * len(shape) sl[dim] = slice(tp_rank * shard, (tp_rank + 1) * shard) return tensor[tuple(sl)] # In-memory torch.Tensor path. assert tensor.shape[dim] % tp_size == 0, ( f"TP shard: dim-{dim} {tensor.shape[dim]} not divisible by tp_size={tp_size}" ) shard = tensor.shape[dim] // tp_size idx = [slice(None)] * tensor.dim() idx[dim] = slice(tp_rank * shard, (tp_rank + 1) * shard) return tensor[tuple(idx)].contiguous() def _shard_for_module(module: nn.Module, attr: str, tensor: torch.Tensor, mapping: Optional[Mapping] = None) -> torch.Tensor: """Slice *tensor* to the per-rank shard declared by *module* for *attr*. Dispatches on :meth:`LinearBase.tp_split_dim` so each Linear subclass owns its TP rule. The loader stays uniform across quant formats. Wraps the shared :func:`load_weight_shard` primitive. """ mapping = mapping or Mapping() if mapping.tp_size == 1: return tensor dim = module.tp_split_dim(attr) if isinstance(module, LinearBase) else None tp_mode = getattr(module, "tp_mode", TPMode.REPLICATED) if dim is None: if tp_mode != TPMode.REPLICATED and tensor.dim() >= 2: raise NotImplementedError( f"TP sharding not declared for {type(module).__name__}.{attr} " f"under tp_mode={tp_mode!r}.") return tensor return load_weight_shard(tensor, dim, mapping=mapping) def _set_tensor(model: nn.Module, key: str, tensor: torch.Tensor, *, mapping: Optional[Mapping] = None) -> bool: """Assign *tensor* to the buffer or parameter at *key* inside *model*. Bfloat16 tensors are cast to float16 on the fly. The export pipeline assumes FP16 activations and the C++ runtime requires FP16 (or FP8) weight files. Doing the cast here avoids a separate post-loading sweep. Returns True on success, False if the key does not resolve to a known buffer or parameter. """ mapping = mapping or Mapping() parts = key.split(".") try: module, attr = _navigate(model, parts) except (AttributeError, IndexError, TypeError): return False if tensor.dtype == torch.bfloat16: tensor = tensor.to(torch.float16) tensor = _shard_for_module(module, attr, tensor, mapping) if attr in module._buffers: module._buffers[attr] = tensor return True if attr in module._parameters: module._parameters[attr] = nn.Parameter(tensor, requires_grad=False) return True # Key resolves to a module-level attribute that is neither buffer nor # parameter (e.g. 'weight' on nn.Embedding before load). if hasattr(module, attr): setattr(module, attr, tensor) return True return False def _try_split_fused_tensor(model: nn.Module, key: str, tensor: torch.Tensor, *, mapping: Optional[Mapping] = None) -> bool: """Handle fused-weight checkpoint patterns not matched by ``_set_tensor``. Rules (first match wins): 1. ``.base_layer.`` PEFT prefix removal. 2. ``self_attn.qkv_proj`` → ``q_proj`` / ``k_proj`` / ``v_proj``. 3. ``mlp.gate_up_proj`` → ``gate_proj`` / ``up_proj``. 4. GDN ``in_proj_qkvz`` → ``in_proj_qkv`` / ``in_proj_z`` (Qwen3-Next family; per-k-head ``[q | k | v | z]`` layout reshape). 5. GDN ``in_proj_ba`` → ``in_proj_b`` / ``in_proj_a`` (Qwen3-Next family; per-k-head ``[b | a]`` layout with width ``2 * gqa``). Returns True if at least one split sub-tensor was set successfully. """ mapping = mapping or Mapping() world = mapping.tp_size # --- 1. Strip PEFT base_layer prefix ------------------------------------ if ".base_layer." in key: stripped = key.replace(".base_layer.", ".") if _set_tensor(model, stripped, tensor, mapping=mapping): return True # Still failed: fall through to fused-split checks with the stripped key key = stripped # --- 2. Fused QKV split ------------------------------------------------- if ".self_attn.qkv_proj." in key: config = getattr(model, "config", None) if config is None: return False # Extract layer prefix and attribute suffix (e.g. "weight", "weight_scale") qkv_idx = key.index(".self_attn.qkv_proj.") prefix = key[:qkv_idx] attr_suffix = key[qkv_idx + len(".self_attn.qkv_proj."):] # When TP>1 the config carries per-rank head counts. Multiply back up # so the split aligns with the full checkpoint tensor. Each split slice # is then re-sharded inside _set_tensor by _shard_for_module. num_q = config.num_attention_heads * config.head_dim * world num_kv = config.num_key_value_heads * config.head_dim * world # Scalar, per-tensor, or per-input-channel attributes: # copy the same value to all three projections. if (tensor.dim() == 0 or (tensor.dim() == 1 and tensor.shape[0] <= 1) or attr_suffix in _FUSED_INPUT_CHANNEL_ATTRS): ok = _set_tensor(model, f"{prefix}.self_attn.q_proj.{attr_suffix}", tensor, mapping=mapping) ok |= _set_tensor(model, f"{prefix}.self_attn.k_proj.{attr_suffix}", tensor, mapping=mapping) ok |= _set_tensor(model, f"{prefix}.self_attn.v_proj.{attr_suffix}", tensor, mapping=mapping) if ok: logger.debug("Broadcast qkv_proj.%s -> q/k/v for prefix %r", attr_suffix, prefix) return ok # Per-output-channel attributes (weight, weight_scale): split dim 0. split_sizes = [num_q, num_kv, num_kv] expected = num_q + 2 * num_kv if attr_suffix == "weight" and tensor.dim( ) >= 2 and tensor.shape[0] * 2 == expected: # ModelOpt W4A16 stores packed int4 weights as [out/2, in]. split_sizes = [s // 2 for s in split_sizes] expected //= 2 if tensor.shape[0] != expected: logger.warning( "qkv_proj.%s shape %s doesn't match expected (%d, %d, %d), " "skipping split", attr_suffix, tensor.shape, *split_sizes) return False q, k, v = tensor.split(split_sizes, dim=0) ok = _set_tensor(model, f"{prefix}.self_attn.q_proj.{attr_suffix}", q, mapping=mapping) ok |= _set_tensor(model, f"{prefix}.self_attn.k_proj.{attr_suffix}", k, mapping=mapping) ok |= _set_tensor(model, f"{prefix}.self_attn.v_proj.{attr_suffix}", v, mapping=mapping) if ok: logger.debug("Split qkv_proj.%s -> q/k/v for prefix %r", attr_suffix, prefix) return ok # --- 3. Fused gate+up split -------------------------------------------- if ".mlp.gate_up_proj." in key: gate_up_idx = key.index(".mlp.gate_up_proj.") prefix = key[:gate_up_idx] attr_suffix = key[gate_up_idx + len(".mlp.gate_up_proj."):] # Scalar, per-tensor, or per-input-channel attributes: copy to both. if (tensor.dim() == 0 or (tensor.dim() == 1 and tensor.shape[0] <= 1) or attr_suffix in _FUSED_INPUT_CHANNEL_ATTRS): ok = _set_tensor(model, f"{prefix}.mlp.gate_proj.{attr_suffix}", tensor, mapping=mapping) ok |= _set_tensor(model, f"{prefix}.mlp.up_proj.{attr_suffix}", tensor, mapping=mapping) if ok: logger.debug( "Broadcast gate_up_proj.%s -> gate/up for prefix %r", attr_suffix, prefix) return ok # Per-output-channel attributes: split in half on dim 0. half = tensor.shape[0] // 2 gate, up = tensor[:half], tensor[half:] ok = _set_tensor(model, f"{prefix}.mlp.gate_proj.{attr_suffix}", gate, mapping=mapping) ok |= _set_tensor(model, f"{prefix}.mlp.up_proj.{attr_suffix}", up, mapping=mapping) if ok: logger.debug("Split gate_up_proj.%s -> gate/up for prefix %r", attr_suffix, prefix) return ok # --- 4. GDN fused in_proj_qkvz split (Qwen3-Next family) ---------------- # HF layout is per-k-head ``[q | k | v | z]``; our GdnMixer keeps q/k/v # fused (in_proj_qkv, head-major contiguous) and z separate (in_proj_z), # so reshape-per-head → slice → reshape-back is required. AWQ ModelOpt # prepacked ``weight`` (uint8 [N//2, K]) is unpacked/split/repacked; # ``pre_quant_scale`` broadcasts unchanged to both splits. if ".linear_attn.in_proj_qkvz." in key: idx = key.index(".linear_attn.in_proj_qkvz.") prefix = key[:idx] attr_suffix = key[idx + len(".linear_attn.in_proj_qkvz."):] mixer = _resolve_module(model, f"{prefix}.linear_attn") if mixer is None: return False num_k_heads = mixer.num_k_heads num_v_heads = mixer.num_v_heads head_k_dim = mixer.k_dim head_v_dim = mixer.v_dim gqa = num_v_heads // num_k_heads per_head_qkvz = 2 * head_k_dim + 2 * head_v_dim * gqa expected_N = num_k_heads * per_head_qkvz if attr_suffix in _FUSED_INPUT_CHANNEL_ATTRS: ok = _set_tensor(model, f"{prefix}.linear_attn.in_proj_qkv.{attr_suffix}", tensor, mapping=mapping) ok |= _set_tensor(model, f"{prefix}.linear_attn.in_proj_z.{attr_suffix}", tensor, mapping=mapping) return ok is_awq_packed = _is_awq_prepacked_weight(tensor, attr_suffix, expected_N) if is_awq_packed: per_head = _unpack_awq_prepacked(tensor, expected_N).reshape( num_k_heads, per_head_qkvz, tensor.shape[1]) else: if tensor.dim() < 1 or tensor.shape[0] != expected_N: logger.warning( "in_proj_qkvz.%s shape %s incompatible with num_k_heads=%d " "per_head_qkvz=%d", attr_suffix, tensor.shape, num_k_heads, per_head_qkvz) return False rest = tensor.shape[1:] per_head = tensor.reshape(num_k_heads, per_head_qkvz, *rest) rest_full = per_head.shape[2:] off = 0 q_part = per_head[:, off:off + head_k_dim] off += head_k_dim k_part = per_head[:, off:off + head_k_dim] off += head_k_dim v_part = per_head[:, off:off + head_v_dim * gqa] off += head_v_dim * gqa z_part = per_head[:, off:off + head_v_dim * gqa] key_dim = num_k_heads * head_k_dim value_dim = num_v_heads * head_v_dim qkv_full = torch.cat([ q_part.reshape(key_dim, *rest_full), k_part.reshape(key_dim, *rest_full), v_part.reshape(value_dim, *rest_full), ], dim=0) z_full = z_part.reshape(value_dim, *rest_full) if is_awq_packed: qkv_weight = _repack_awq_prepacked(qkv_full) z_weight = _repack_awq_prepacked(z_full) else: qkv_weight = qkv_full z_weight = z_full ok = _set_tensor(model, f"{prefix}.linear_attn.in_proj_qkv.{attr_suffix}", qkv_weight, mapping=mapping) ok |= _set_tensor(model, f"{prefix}.linear_attn.in_proj_z.{attr_suffix}", z_weight, mapping=mapping) if ok: logger.debug( "Split in_proj_qkvz.%s -> in_proj_qkv / in_proj_z for prefix %r", attr_suffix, prefix) return ok # --- 5. GDN fused in_proj_ba split (Qwen3-Next family) ------------------ # HF Qwen3-Next stores [b | a] per-k-head interleaved with width # ``2 * (num_v_heads // num_k_heads)``. Our ``GdnMixer`` keeps them as # separate Linears of ``num_v_heads`` rows each — extract by strided # slicing. AWQ ModelOpt prepacked ``weight`` goes through the same # unpack / slice / repack path as ``in_proj_qkvz`` above. if ".linear_attn.in_proj_ba." in key: idx = key.index(".linear_attn.in_proj_ba.") prefix = key[:idx] attr_suffix = key[idx + len(".linear_attn.in_proj_ba."):] mixer = _resolve_module(model, f"{prefix}.linear_attn") if mixer is None: return False num_k_heads = mixer.num_k_heads num_v_heads = mixer.num_v_heads gqa = num_v_heads // num_k_heads per_head_ba = 2 * gqa expected_N = num_k_heads * per_head_ba if attr_suffix in _FUSED_INPUT_CHANNEL_ATTRS: ok = _set_tensor(model, f"{prefix}.linear_attn.in_proj_b.{attr_suffix}", tensor, mapping=mapping) ok |= _set_tensor(model, f"{prefix}.linear_attn.in_proj_a.{attr_suffix}", tensor, mapping=mapping) return ok is_awq_packed = _is_awq_prepacked_weight(tensor, attr_suffix, expected_N) if is_awq_packed: per_head = _unpack_awq_prepacked(tensor, expected_N).reshape( num_k_heads, per_head_ba, tensor.shape[1]) else: if tensor.dim() < 1 or tensor.shape[0] != expected_N: logger.warning( "in_proj_ba.%s shape %s incompatible with num_k_heads=%d " "per_head_ba=%d", attr_suffix, tensor.shape, num_k_heads, per_head_ba) return False rest = tensor.shape[1:] per_head = tensor.reshape(num_k_heads, per_head_ba, *rest) rest_full = per_head.shape[2:] b_part = per_head[:, 0:gqa] a_part = per_head[:, gqa:2 * gqa] b_full = b_part.reshape(num_v_heads, *rest_full) a_full = a_part.reshape(num_v_heads, *rest_full) if is_awq_packed: b_weight = _repack_awq_prepacked(b_full) a_weight = _repack_awq_prepacked(a_full) else: b_weight = b_full a_weight = a_full ok = _set_tensor(model, f"{prefix}.linear_attn.in_proj_b.{attr_suffix}", b_weight, mapping=mapping) ok |= _set_tensor(model, f"{prefix}.linear_attn.in_proj_a.{attr_suffix}", a_weight, mapping=mapping) if ok: logger.debug( "Split in_proj_ba.%s -> in_proj_b / in_proj_a for prefix %r", attr_suffix, prefix) return ok # --- 6. Fused MoE expert split ------------------------------------------- # Fused 3-D expert tensors (gate rows first, then up): # mlp.experts.gate_up_proj [E, 2*I, H] / mlp.experts.down_proj [E, H, I] # Split into the per-expert Linear weights held by Qwen3MoEExperts. if key.endswith(".mlp.experts.gate_up_proj") and tensor.dim() == 3: prefix = key[:-len("gate_up_proj")] inter = tensor.shape[1] // 2 ok = True for expert in range(tensor.shape[0]): ok &= _set_tensor(model, f"{prefix}{expert}.gate_proj.weight", tensor[expert, :inter, :], mapping=mapping) ok &= _set_tensor(model, f"{prefix}{expert}.up_proj.weight", tensor[expert, inter:, :], mapping=mapping) if ok: logger.debug( "Split fused experts.gate_up_proj -> %d gate/up pairs " "for prefix %r", tensor.shape[0], prefix) return ok if key.endswith(".mlp.experts.down_proj") and tensor.dim() == 3: prefix = key[:-len("down_proj")] ok = True for expert in range(tensor.shape[0]): ok &= _set_tensor(model, f"{prefix}{expert}.down_proj.weight", tensor[expert], mapping=mapping) if ok: logger.debug( "Split fused experts.down_proj -> %d down weights " "for prefix %r", tensor.shape[0], prefix) return ok return False def _resolve_module(model: nn.Module, dotted: str) -> Optional[nn.Module]: """Walk attribute path on *model*. Returns None on any miss.""" mod = model for part in dotted.split("."): if not part: continue if not hasattr(mod, part): return None mod = getattr(mod, part) return mod def iter_checkpoint_keys(model_dir: str) -> Iterator[str]: """Yield all weight keys present in a checkpoint directory (no data).""" shard_map = _build_shard_map(model_dir) yield from shard_map.keys() def load_submodule_weights( model: nn.Module, weights: Dict[str, torch.Tensor], key_remap: Callable[[str], Optional[str]], *, transform: Optional[Callable[[str, torch.Tensor], torch.Tensor]] = None, label: str = "model", log: Optional[logging.Logger] = None, do_repack: bool = True, ) -> None: """Load a sliced ``{key: tensor}`` dict into a sub-encoder via ``_set_tensor``. Used by visual / audio modeling files that receive a flat weights dict (already loaded from safetensors by the orchestrator) and need to filter and rename keys before assignment. Sharing this helper avoids duplicating the iterate-remap-set-tensor-track-missing-log pattern across families. ``_set_tensor`` is used (not ``load_state_dict``) so that: - ``bfloat16 -> float16`` cast happens automatically; - quantized weights (``float8_e4m3fn`` / packed int8 / ...) keep their original dtype rather than being silently cast. :param model: Target sub-encoder module. :param weights: Flat ``{key: tensor}`` dict from safetensors. :param key_remap: Called per checkpoint key. Return the new path inside ``model`` (e.g. ``"encoder.blocks.0.attn.weight"``), or ``None`` to skip the key. Use this to strip / rewrite checkpoint prefixes. :param transform: Optional ``(remapped_key, tensor) -> tensor`` hook for per-tensor reshaping (e.g. flat → conv2d) or interpolation. :param label: Used in the missing-keys warning ("<label>: keys not loaded"). :param log: Logger used for the missing-keys warning. Defaults to this module's logger. :param do_repack: When ``True`` (default), call ``apply_all_repacking`` after assignment so type-aware fixups (FP8 scale cast, NVFP4 view-cast, AWQ/GPTQ swizzle, ...) run. Safe to leave on for non-quantized sub-encoders — each fixup is gated by an ``isinstance`` check. """ if log is None: log = logger missing: list[str] = [] for k, v in weights.items(): new_key = key_remap(k) if new_key is None: continue if transform is not None: v = transform(new_key, v) if not _set_tensor(model, new_key, v): missing.append(new_key) if do_repack: apply_all_repacking(model) if missing: log.warning( "%s: keys not loaded: %s%s", label, missing[:10], " ..." if len(missing) > 10 else "", )