# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved.
#
# 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.

import torch
from transformers import (
    BitsAndBytesConfig,
    AutoProcessor,
    AutoTokenizer,
    AutoModelForCausalLM,
)

try:
    from transformers import AutoModelForImageTextToText
    AutoModelForVision2Seq = AutoModelForImageTextToText
except:
    from transformers import AutoModelForVision2Seq
from ..kernels import (
    post_patch_loss_function,
)
from ._utils import (
    __version__,
    importlib_version,
    _prepare_model_for_qat,
    resolve_model_class,
    resolve_attention_implementation,
    _get_text_only_config,
    _is_family_text_decoder,
    _config_get,
    _is_flash_attention_requested,
    _apply_text_only_key_mapping,
    _select_moe_detection_targets,
    set_task_config_attr,
)
from ._utils import *
from .loader_utils import (
    _exclude_rope_inv_freq_from_ddp,
    _get_fp8_mode_and_check_settings,
    _restore_dropped_fp8_scales,
)
from ..save import patch_saving_functions
from ..models.loader_utils import is_distributed
from unsloth_zoo.gradient_checkpointing import (
    unpatch_unsloth_gradient_checkpointing,
    unpatch_unsloth_smart_gradient_checkpointing,
)
import torch.utils.checkpoint as torch_checkpoint
import transformers.modeling_utils as hf_modeling_utils
from peft import LoraConfig, TaskType, get_peft_model as _get_peft_model
from peft import PeftModelForCausalLM
from transformers import set_seed as transformers_set_seed
from unsloth_zoo.peft_utils import (
    get_peft_regex,
    SKIP_QUANTIZATION_MODULES,
    requires_grad_for_gradient_checkpointing,
)
from transformers.models.llama.modeling_llama import logger
from transformers import __version__ as transformers_version
from triton import __version__ as triton_version
from unsloth_zoo.utils import _get_dtype
from unsloth_zoo.hf_utils import (
    dtype_from_config,
    add_dtype_kwargs,
    fix_lora_auto_mapping,
    get_auto_processor,
)
from unsloth_zoo.patching_utils import patch_model_and_tokenizer
from unsloth_zoo.training_utils import prepare_model_for_training

from unsloth_zoo.utils import Version
from transformers import __version__ as transformers_version

import types
import functools
import os
import gc
import math
import warnings
from typing import Optional, Tuple, List, Union
import re, inspect, sys
import contextlib

try:
    from huggingface_hub.utils import get_token
except:
    # Old HF Hub versions <= 0.0.25
    from huggingface_hub.utils._token import get_token
from ..device_type import (
    get_device_stats,
    clean_gpu_cache,
    is_hip,
    get_device_type,
    DEVICE_TYPE,
    DEVICE_TYPE_TORCH,
    DEVICE_COUNT,
    ALLOW_PREQUANTIZED_MODELS,
)

__all__ = [
    "FastBaseModel",
]


def _infer_device_map_from_loaded_model(model):
    """Build a compact device_map by inspecting actual parameter placements."""
    device_map = {}

    def _assign(module, prefix):
        params = list(module.named_parameters(remove_duplicate = False))
        if not params:
            bufs = list(module.named_buffers())
            if bufs:
                device_map[prefix] = bufs[0][1].device
            return
        devices = {p.device for _, p in params}
        if len(devices) == 1:
            device_map[prefix] = next(iter(devices))
        else:
            for child_name, child in module.named_children():
                child_prefix = f"{prefix}.{child_name}" if prefix else child_name
                _assign(child, child_prefix)
            for pname, param in module.named_parameters(remove_duplicate = False):
                if "." not in pname:
                    full = f"{prefix}.{pname}" if prefix else pname
                    if not any(full == k or full.startswith(k + ".") for k in device_map):
                        device_map[full] = param.device

    _assign(model, "")
    if "" in device_map and len(device_map) > 1:
        device_map.pop("")
    return device_map


def _attach_bnb_multidevice_hooks(
    model, load_in_4bit, load_in_8bit, offload_embedding, fast_inference
):
    """
    Attach accelerate AlignDevicesHook on a bnb model loaded across multiple
    devices (or a non-default device).  No-op for single-GPU cuda:0, non-bnb,
    vLLM, or already-dispatched models.
    """
    if fast_inference:
        return
    is_bnb = (
        load_in_4bit
        or load_in_8bit
        or getattr(model, "is_loaded_in_4bit", False)
        or getattr(model, "is_loaded_in_8bit", False)
        or getattr(model, "quantization_method", None) == "bitsandbytes"
    )
    if not is_bnb:
        return
    if offload_embedding:
        return
    if getattr(model, "hf_device_map", None) is not None:
        return  # already dispatched

    try:
        all_devs = {p.device for p in model.parameters()}
    except Exception as exc:
        warnings.warn(
            "Unsloth: Failed to determine device placement from model parameters, "
            f"so multi-GPU hooks cannot be attached. ({type(exc).__name__}: {exc})",
            RuntimeWarning,
            stacklevel = 2,
        )
        return

    cuda_devs = {d for d in all_devs if d.type == "cuda"}
    if not cuda_devs:
        return

    default_cuda = torch.device("cuda", 0)
    if all_devs == {default_cuda}:
        return

    try:
        from accelerate import dispatch_model
    except ImportError:
        return  # accelerate not available

    try:
        inferred_map = _infer_device_map_from_loaded_model(model)
        if not inferred_map:
            return

        # bnb constructors reject _is_hf_initialized; strip before dispatch.
        _extra_keys = ("_is_hf_initialized",)
        _stripped = []
        for _, param in model.named_parameters():
            for key in _extra_keys:
                if key in param.__dict__:
                    _stripped.append((param, key, param.__dict__.pop(key)))

        try:
            # CUDA -> int index, non-CUDA -> type string ("cpu", "meta").
            device_map_int = {
                k: (v.index if v.type == "cuda" else v.type) if isinstance(v, torch.device) else v
                for k, v in inferred_map.items()
            }

            # force_hooks=True: install hooks even for single-device maps.
            main_device = device_map_int.get("")
            if main_device in (None, "cpu", "disk"):
                main_device = next(
                    (d for d in device_map_int.values() if d not in ("cpu", "disk")),
                    None,
                )
            dispatch_model(
                model,
                device_map = device_map_int,
                main_device = main_device,
                skip_keys = getattr(model, "_skip_keys_device_placement", None),
                force_hooks = True,
            )
            desc = f"{len(inferred_map)} block(s) across {len(cuda_devs)} device(s)"
        finally:
            # Restore stripped keys.
            for param, key, val in _stripped:
                param.__dict__[key] = val

        logger.info(
            f"Unsloth: Attached accelerate AlignDevicesHook ({desc}) for bnb multi-GPU inference."
        )
    except Exception as exc:
        warnings.warn(
            f"Unsloth: Could not attach multi-device dispatch hooks automatically "
            f"({type(exc).__name__}: {exc}). "
            "Cross-device inference may fail. Consider using a single GPU or "
            "calling accelerate.dispatch_model() manually.",
            RuntimeWarning,
            stacklevel = 2,
        )


global NUM_LOGITS_TO_KEEP
NUM_LOGITS_TO_KEEP = dict()


def _unsloth_generate_accepts_kwarg(model, key):
    # True if the top level accepts this generate kwarg (some models expose it on an inner forward only).
    try:
        model_args = set(inspect.signature(model.prepare_inputs_for_generation).parameters)
    except (TypeError, ValueError, AttributeError):
        model_args = set()
    if "kwargs" in model_args or "model_kwargs" in model_args:
        try:
            model_args |= set(inspect.signature(model.forward).parameters)
        except (TypeError, ValueError, AttributeError):
            pass
    return key in model_args


def _install_offload_embedding_hooks(embed_tokens, output_embeddings, return_device):
    # Lookup runs on the weight's current device (CPU when offloaded); the output returns to the
    # decoder device read live from output_embeddings (lm_head, untied here) so it tracks
    # model.to() moves. A meta (disk-offloaded) or missing lm_head falls back to return_device.
    if embed_tokens is None:
        return False
    if getattr(embed_tokens, "_unsloth_offload_hooks_installed", False):
        return True

    def _decoder_device():
        weight = getattr(output_embeddings, "weight", None)
        if weight is not None and weight.device.type != "meta":
            return weight.device
        return return_device

    def _unsloth_offload_pre_hook(module, args):
        if not args:
            return args
        inp = args[0]
        if not hasattr(inp, "device"):
            return args
        weight = getattr(module, "weight", None)
        target = weight.device if weight is not None else _decoder_device()
        if target is None or inp.device == target:
            return args
        return (inp.to(target),) + tuple(args[1:])

    def _unsloth_offload_post_hook(module, args, output):
        target = _decoder_device()
        if target is not None and hasattr(output, "device") and output.device != target:
            return output.to(target)
        return output

    embed_tokens.register_forward_pre_hook(_unsloth_offload_pre_hook, prepend = True)
    embed_tokens.register_forward_hook(_unsloth_offload_post_hook, prepend = True)
    embed_tokens._unsloth_offload_hooks_installed = True
    return True


def _embeddings_are_tied(input_embeddings, output_embeddings):
    # Tied lm_head reuses this weight; offloading to CPU would strand the output projection.
    if input_embeddings is None or output_embeddings is None:
        return False
    w_in = getattr(input_embeddings, "weight", None)
    w_out = getattr(output_embeddings, "weight", None)
    if w_in is None or w_out is None:
        return False
    return w_in is w_out or w_in.data_ptr() == w_out.data_ptr()


def _offload_embedding_unsupported_platform():
    # Offloaded embeddings do not work on Windows or WSL.
    if bool(os.environ.get("WSL_DISTRO_NAME") or os.environ.get("WSL_INTEROP")):
        return "WSL"
    if os.name == "nt":
        return "Windows"
    return None


def _embedding_dispatch_device(input_embeddings):
    # accelerate's hook wraps forward and re-sends the ids to the device it recorded, after
    # our offload pre-hook already sent them to the CPU weight. Returns that device, or None.
    hook = getattr(input_embeddings, "_hf_hook", None)
    return None if hook is None else getattr(hook, "execution_device", None)


def _resolve_offload_embedding(model, offload_embedding):
    """Report `offload_embedding` as True only when the offload will really run.

    It is a VRAM optimisation, not a correctness switch, so turn it off where it
    cannot help instead of failing the load. It also gates
    `_attach_bnb_multidevice_hooks`, which must still run whenever no offload
    happens, so every "no offload" case has to answer False.
    """
    if not offload_embedding:
        return False
    platform_name = _offload_embedding_unsupported_platform()
    if platform_name is not None:
        print(f"Unsloth: Not offloading embeddings; offloading is unsupported on {platform_name}.")
        return False
    try:
        in_embed = model.get_input_embeddings()
        out_embed = (
            model.get_output_embeddings() if hasattr(model, "get_output_embeddings") else None
        )
    except Exception:
        # Cannot inspect it, so leave the caller's request alone.
        return offload_embedding
    if _embeddings_are_tied(in_embed, out_embed):
        print(
            "Unsloth: Not offloading embeddings; this model ties embed_tokens "
            "to lm_head, so offloading saves no VRAM."
        )
        return False
    if _embedding_dispatch_device(in_embed) is not None:
        print(
            "Unsloth: Not offloading embeddings; this model is dispatched across devices, "
            "which overrides the offload."
        )
        return False
    return True


VLLM_SUPPORTED_VLM = [
    "qwen2_5_vl",
    "gemma3",
    "mistral3",
    "qwen3_vl",
    "qwen3_vl_moe",
]
VLLM_NON_LORA_VLM = [
    "mllama",
]
PRE_COMPILE_INFERENCE = [
    "gpt_oss",
]

from transformers import GenerationConfig, CompileConfig, AutoConfig

try:
    from transformers import PreTrainedConfig
    PretrainedConfig = PreTrainedConfig
except:
    from transformers import PretrainedConfig

HAS_TORCH_DTYPE = "torch_dtype" in PretrainedConfig.__doc__

_compile_config = CompileConfig(
    fullgraph = False,
    dynamic = None,
    mode = "reduce-overhead",
)
_compile_config.disable = True  # Must set manually

try:
    torch_compiler_set_stance = torch.compiler.set_stance
except:
    torch_compiler_set_stance = None


def _uses_flash_attention_for_generation(config):
    language_config_names = (
        "text_config",
        "llm_config",
        "decoder_config",
        "language_config",
        "thinker_config",
        "talker_config",
        "decoder",
        "generator",
    )
    non_language_config_names = (
        "vision_config",
        "audio_config",
        "vision_encoder_config",
        "audio_encoder_config",
        "encoder_config",
        "text_encoder",
    )

    def _mapping_uses_flash_attention(attn_implementation):
        if not isinstance(attn_implementation, dict):
            return _is_flash_attention_requested(attn_implementation)
        language_implementations = [
            implementation
            for config_name, implementation in attn_implementation.items()
            if config_name not in ("", *non_language_config_names) and implementation is not None
        ]
        if language_implementations:
            return any(map(_is_flash_attention_requested, language_implementations))
        return _is_flash_attention_requested(attn_implementation.get(""))

    def _get_text_config(current_config):
        get_text_config = _config_get(current_config, "get_text_config", None)
        if not callable(get_text_config):
            return None
        try:
            return get_text_config()
        except Exception:
            return None

    language_configs = []
    pending_configs = [config]
    visited_config_ids = set()
    while pending_configs:
        current_config = pending_configs.pop()
        if id(current_config) in visited_config_ids:
            continue
        visited_config_ids.add(id(current_config))

        text_config = _get_text_config(current_config)
        if (
            text_config is not None
            and text_config is not current_config
            and all(text_config is not item for item in language_configs)
        ):
            language_configs.append(text_config)

        nested_config_names = list(language_config_names)
        declared_sub_configs = _config_get(current_config, "sub_configs", None)
        if isinstance(declared_sub_configs, dict):
            nested_config_names.extend(
                config_name
                for config_name in declared_sub_configs
                if config_name not in nested_config_names
            )
        for config_name in nested_config_names:
            nested_config = _config_get(current_config, config_name, None)
            if nested_config is None or nested_config is current_config:
                continue
            pending_configs.append(nested_config)
            nested_text_config = _get_text_config(nested_config)
            if (
                config_name in language_config_names
                and (nested_text_config is None or nested_text_config is nested_config)
                and all(nested_config is not item for item in language_configs)
            ):
                language_configs.append(nested_config)

    language_implementations = [
        _config_get(language_config, config_field, None)
        for language_config in language_configs
        for config_field in ("_attn_implementation", "attn_implementation")
    ]
    language_implementations = [
        implementation for implementation in language_implementations if implementation is not None
    ]
    if language_implementations:
        return any(map(_mapping_uses_flash_attention, language_implementations))

    return any(
        _mapping_uses_flash_attention(_config_get(config, config_field, None))
        for config_field in ("_attn_implementation", "attn_implementation")
    )


def _clear_generation_caches(model):
    for name, module in model.named_modules():
        if hasattr(module, "_flex_attention_cache"):
            try:
                del module._flex_attention_cache
            except:
                pass
        # Solves AttributeError: 'SlidingWindowLayer' object has no attribute 'max_batch_size'
        if hasattr(module, "_cache") and "cache_utils" in str(module._cache.__class__):
            try:
                del module._cache
            except:
                pass


def unsloth_base_fast_generate(self, *args, **kwargs):
    if len(args) != 0:
        input_ids = args[0]
    elif "input_ids" in kwargs:
        input_ids = kwargs["input_ids"]
    elif "input" in kwargs:
        input_ids = kwargs["input"]
    elif "input_features" in kwargs:
        input_ids = kwargs["input_features"]
    elif "inputs_embeds" in kwargs:
        # canonical HF name for embedding inputs (e.g. multimodal generate)
        input_ids = kwargs["inputs_embeds"]
    elif "input_embeds" in kwargs:
        input_ids = kwargs["input_embeds"]
    elif "inputs" in kwargs:
        input_ids = kwargs["inputs"]
    else:
        key = next(iter(kwargs.keys()))
        if type(kwargs[key]) is not torch.Tensor:
            raise TypeError("Unsloth: You need to pass in input_ids to .generate!")
        input_ids = kwargs[key]
    assert type(input_ids) is torch.Tensor
    bsz = input_ids.shape[0]

    FastBaseModel.for_inference(self)
    dtype = _get_dtype(dtype_from_config(self.config))
    # Handle full float32 cases as config.dtype == torch.float32!
    do_bfloat16_mixed_precision = os.environ.get("UNSLOTH_BFLOAT16_MIXED_PRECISION", "0") == "1"
    if do_bfloat16_mixed_precision:
        dtype = torch.bfloat16

    is_vlm = any(
        x.endswith(("ForConditionalGeneration", "ForVisionText2Text"))
        for x in self.config.architectures
    )
    is_vlm = is_vlm or hasattr(self.config, "vision_config")
    arch = self.config.architectures[0]

    # Remove token_type_ids - WRONG for Gemma 3 since bidirectional attention
    if hasattr(self, "generate") and hasattr(self, "forward"):
        # did not combine with below since self might not have model
        keys = inspect.signature(self.forward).parameters.keys()
        if "token_type_ids" not in keys:
            kwargs.pop("token_type_ids", None)
    # kwargs.pop("token_type_ids", None)

    # Vision processors emit mm_token_type_ids that generate() rejects (Qwen3-VL); unlike
    # logits_to_keep it is an incoming kwarg, so drop it when generate does not accept it.
    if "mm_token_type_ids" in kwargs and not _unsloth_generate_accepts_kwarg(
        self, "mm_token_type_ids"
    ):
        kwargs.pop("mm_token_type_ids", None)

    # VLMs do not allow logits_to_keep
    global NUM_LOGITS_TO_KEEP
    if arch not in NUM_LOGITS_TO_KEEP:
        m = self
        # Find which is used: num_logits_to_keep or logits_to_keep
        while hasattr(m, "model"):
            if hasattr(m, "forward"):
                keys = inspect.signature(m.forward).parameters.keys()
                if "num_logits_to_keep" in keys:
                    NUM_LOGITS_TO_KEEP[arch] = "num_logits_to_keep"
                    break
                elif "logits_to_keep" in keys:
                    NUM_LOGITS_TO_KEEP[arch] = "logits_to_keep"
                    break
            m = m.model
        if arch not in NUM_LOGITS_TO_KEEP:
            NUM_LOGITS_TO_KEEP[arch] = None
    key = NUM_LOGITS_TO_KEEP[arch]
    if key is not None and key not in kwargs and _unsloth_generate_accepts_kwarg(self, key):
        kwargs[key] = 1

    model_eos_token_id = getattr(self.config, "eos_token_id", None)
    if model_eos_token_id is not None and hasattr(model_eos_token_id, "__iter__"):
        model_eos_token_id = model_eos_token_id[0]

    kwargs["pad_token_id"] = kwargs.pop("pad_token_id", model_eos_token_id)

    # Get pixel values for VLMs
    try:
        kwargs["pixel_values"] = kwargs["pixel_values"].to(dtype)
    except:
        pass
    try:
        kwargs["pixel_values_videos"] = kwargs["pixel_values_videos"].to(dtype)
    except:
        pass

    # Mixed precision autocast. Stamped by from_pretrained: UNSLOTH_FORCE_FLOAT32 is
    # process wide, so a later load would otherwise decide this model's rollouts.
    forced_float32 = getattr(self, "_unsloth_forced_float32", None)
    if forced_float32 is None:
        forced_float32 = os.environ.get("UNSLOTH_FORCE_FLOAT32", "0") == "1"
    if forced_float32:
        autocaster = torch.autocast(device_type = DEVICE_TYPE_TORCH, dtype = torch.float16)
        dtype = torch.float16
    else:
        # CUDA autocast does not validate its dtype the way the CPU/XPU/MPS paths do,
        # so autocast(dtype = torch.float32) enters ENABLED rather than turning into a
        # no-op, and the compiled graph then returns inf/NaN logits. A float32 model has
        # nothing to autocast to; disable instead of asking for a dtype that is not one.
        autocaster = torch.autocast(
            device_type = DEVICE_TYPE_TORCH,
            dtype = dtype,
            enabled = dtype in (torch.float16, torch.bfloat16),
        )
    # Prepare LoRA
    # state_dict = convert_lora_modules(self, dtype = dtype)

    # FlashAttention breaks on the forced static cache below (unfilled slots stay
    # unmasked while decoding), so delegate after normalization but before it.
    _clear_generation_caches(self)
    if _uses_flash_attention_for_generation(self.config):
        # Pin the literal "dynamic": None is merged back to the model default, and a
        # static cache still arrives via kwargs / the caller's generation_config (TRL).
        # The kwarg wins (update runs last); skip it when the caller passed a cache.
        if kwargs.get("past_key_values") is None:
            kwargs["cache_implementation"] = "dynamic"
        try:
            with torch.inference_mode(), autocaster:
                return self._old_generate(*args, **kwargs)
        finally:
            _clear_generation_caches(self)

    # Set compile dynamic shapes
    torch._dynamo.mark_static(input_ids, 0)
    torch._dynamo.mark_dynamic(input_ids, 1)
    if "attention_mask" in kwargs:
        torch._dynamo.mark_static(kwargs["attention_mask"], 0)
        torch._dynamo.mark_dynamic(kwargs["attention_mask"], 1)
    if "token_type_ids" in kwargs:
        torch._dynamo.mark_static(kwargs["token_type_ids"], 0)
        torch._dynamo.mark_dynamic(kwargs["token_type_ids"], 1)

    # Fix generation_config
    # Use hybrid if sliding window seen, otherwise try static
    cache_implementation = getattr(self.config, "cache_implementation", None)
    if getattr(self, "_supports_static_cache", getattr(self, "_can_compile_fullgraph", True)):
        if os.environ.get("UNSLOTH_DISABLE_STATIC_GENERATION", "0") == "0":
            cache_implementation = "static"
        elif Version(transformers_version) < Version("4.56.0.dev0"):
            cache_implementation = None
        else:
            # Should work in latest transformers!
            cache_implementation = "static"
    else:
        cache_implementation = None
    if cache_implementation is not None:
        swa = getattr(getattr(self.config, "text_config", self.config), "sliding_window", None)
        if (swa == 0 or type(swa) is not int) and (
            getattr(self, "_can_compile_fullgraph", True) is True
        ):
            cache_implementation = "static"
        else:
            if Version(transformers_version) < Version("4.56.0.dev0"):
                cache_implementation = "hybrid"
            else:
                cache_implementation = "static"
    # [TODO] Unsure why static fails
    if do_bfloat16_mixed_precision:
        cache_implementation = None

    if "generation_config" in kwargs:
        kwargs["generation_config"].cache_implementation = cache_implementation
        if cache_implementation is not None:
            kwargs["generation_config"].compile_config = _compile_config
    else:
        kwargs["cache_implementation"] = cache_implementation
        if cache_implementation is not None:
            kwargs["compile_config"] = _compile_config

    try:
        with torch.inference_mode(), autocaster:
            output = self._old_generate(*args, **kwargs)
    finally:
        _clear_generation_caches(self)

    # FastBaseModel.for_training(self)
    return output


# Offline helpers live in loader_utils.py (shared canonical source).
from .loader_utils import (
    _get_effective_local_files_only,
    _hub_repo_or_local_path,
    _is_offline_related_error,
    _load_pretrained_tokenizer_fast,
    _mark_loaded_revision,
    _offline_aware_load,
)


def _missing_torchvision_error(error = None):
    """True if a VLM processor failed to load due to missing torchvision (#4202).

    Checks availability directly first, then only the specific torchvision-required
    error text (not any incidental "torchvision" substring like a model path)."""
    import importlib.util

    if importlib.util.find_spec("torchvision") is None:
        return True
    if error is not None:
        error_str = str(error).lower()
        return (
            "requires the torchvision" in error_str or "no module named 'torchvision'" in error_str
        )
    return False


def _construct_vlm_processor_fallback(
    tokenizer_name,
    model_type,
    token,
    trust_remote_code,
    cache_dir = None,
    local_files_only = False,
    revision = None,
):
    """Build a VLM processor manually when AutoProcessor.from_pretrained fails (some VLMs
    have unresolvable tokenizer_class entries): load the image processor + tokenizer
    separately and combine. Returns (processor_or_None, error_or_None) so the caller can
    tell an offline failure (retry from cache) from a genuine one."""
    _fb_err = None
    try:
        from transformers import AutoImageProcessor, AutoConfig
        from transformers.models.auto.processing_auto import PROCESSOR_MAPPING_NAMES
        import json

        load_path = _hub_repo_or_local_path(
            tokenizer_name,
            token = token,
            cache_dir = cache_dir,
            local_files_only = local_files_only,
            revision = revision,
        )
        # Load image processor
        image_processor = AutoImageProcessor.from_pretrained(
            load_path,
            token = token,
            trust_remote_code = trust_remote_code,
            cache_dir = cache_dir,
            local_files_only = local_files_only,
            revision = revision,
        )
        # Load tokenizer via PreTrainedTokenizerFast (bypasses tokenizer_class check).
        # Resolve the cached snapshot first so transformers does not call model_info (#7481).
        tok = _load_pretrained_tokenizer_fast(
            tokenizer_name,
            padding_side = "left",
            token = token,
            trust_remote_code = trust_remote_code,
            cache_dir = cache_dir,
            local_files_only = local_files_only,
            revision = revision,
        )
        # Read tokenizer_config.json for special tokens: prefer the local file (offline
        # / local checkpoint dir), else hf_hub_download with local_files_only forwarded.
        try:
            import json as _json

            tok_config = None
            _local_cfg = os.path.join(tokenizer_name, "tokenizer_config.json")
            if os.path.isdir(tokenizer_name):
                # Local dir: read directly. A missing file raises a clear FileNotFoundError
                # rather than letting hf_hub_download treat the path as a repo id.
                if os.path.exists(_local_cfg):
                    with open(_local_cfg, "r", encoding = "utf-8") as f:
                        tok_config = _json.load(f)
                else:
                    raise FileNotFoundError(
                        f"tokenizer_config.json not found in local directory: {tokenizer_name}"
                    )
            else:
                from huggingface_hub import hf_hub_download
                config_path = hf_hub_download(
                    tokenizer_name,
                    "tokenizer_config.json",
                    token = token,
                    cache_dir = cache_dir,
                    revision = revision,
                    local_files_only = local_files_only,
                )
                with open(config_path, "r", encoding = "utf-8") as f:
                    tok_config = _json.load(f)
            # Set model-specific special tokens and their IDs
            for key in (
                "image_token",
                "image_start_token",
                "image_end_token",
                "image_thumbnail",
                "video_token",
            ):
                if key in tok_config and not hasattr(tok, key):
                    setattr(tok, key, tok_config[key])
                    id_key = key + "_id" if not key.endswith("_id") else key
                    token_id = tok.convert_tokens_to_ids(tok_config[key])
                    if not hasattr(tok, id_key):
                        setattr(tok, id_key, token_id)
        except Exception as _e:
            _fb_err = _e  # remember (non-fatal here); surfaced only if no processor is built

        # Find the processor class - try model_type first, then top-level config model_type
        proc_class_name = PROCESSOR_MAPPING_NAMES.get(model_type)
        if proc_class_name is None:
            # model_type might be a sub-model type (e.g. "lfm2" instead of "lfm2_vl").
            # Try the top-level config.model_type which often has the processor mapping.
            try:
                config = AutoConfig.from_pretrained(
                    load_path,
                    token = token,
                    trust_remote_code = trust_remote_code,
                    cache_dir = cache_dir,
                    local_files_only = local_files_only,
                    revision = revision,
                )
                proc_class_name = PROCESSOR_MAPPING_NAMES.get(config.model_type)
            except Exception as _e:
                _fb_err = _e  # surface a network/cache miss so the offline retry can fire

        if proc_class_name is not None:
            import transformers
            proc_class = getattr(transformers, proc_class_name, None)
            if proc_class is not None:
                processor = proc_class(image_processor = image_processor, tokenizer = tok)
                # Copy chat_template from tokenizer to processor if needed
                if not getattr(processor, "chat_template", None) and getattr(
                    tok, "chat_template", None
                ):
                    processor.chat_template = tok.chat_template
                return processor, None
    except Exception as _e:
        _fb_err = _e
    return None, _fb_err


def _get_total_transformer_layers(model):
    """Best-effort total transformer block count across HF model shapes.
    Returns None if not determinable; caller should skip the conversion."""
    cfg = getattr(model, "config", None)
    if cfg is None:
        return None
    for name in (
        "num_hidden_layers",
        "n_layer",
        "n_layers",
        "num_layers",
    ):
        v = getattr(cfg, name, None)
        if isinstance(v, int) and v > 0:
            return v
    text_cfg = getattr(cfg, "text_config", None)
    if text_cfg is not None:
        for name in (
            "num_hidden_layers",
            "n_layer",
            "n_layers",
            "num_layers",
        ):
            v = getattr(text_cfg, name, None)
            if isinstance(v, int) and v > 0:
                return v
    return None


class FastBaseModel:
    @staticmethod
    @_offline_aware_load
    def from_pretrained(
        model_name = "unsloth/Llama-3.2-1B-Instruct",
        max_seq_length = 2048,
        dtype = None,
        load_in_4bit = True,
        load_in_8bit = False,
        load_in_16bit = False,
        full_finetuning = False,
        token = None,
        device_map = "sequential",
        trust_remote_code = False,
        model_types = None,
        tokenizer_name = None,
        auto_model = AutoModelForVision2Seq,
        use_gradient_checkpointing = "unsloth",
        supports_sdpa = True,
        whisper_language = None,
        whisper_task = None,
        auto_config = None,
        offload_embedding = False,
        float32_mixed_precision = None,  # Forces float32 mixed precision
        # vLLM parameters
        fast_inference = False,
        gpu_memory_utilization = 0.5,
        float8_kv_cache = False,
        random_state = 3407,
        max_lora_rank = 64,
        disable_log_stats = False,
        unsloth_vllm_standby = False,
        load_in_fp8 = False,  # fp8 LoRA (True, False, 'block')
        text_only = False,
        **kwargs,
    ):
        user_config = kwargs.pop("config", None)
        if auto_config is None and user_config is not None:
            auto_config = user_config

        # Offline snapshot for the loads below; not popped, so the weight load still
        # reads local_files_only from **kwargs. See _get_effective_local_files_only.
        local_files_only = _get_effective_local_files_only(kwargs)

        if unsloth_vllm_standby and os.environ.get("UNSLOTH_VLLM_STANDBY", "0") != "1":
            raise RuntimeError(
                "Unsloth: `unsloth_vllm_standby` is True, but environment variable `UNSLOTH_VLLM_STANDBY` is not set to 1!"
            )

        if model_types is None:
            raise RuntimeError(
                "Unsloth: Please use FastModel or FastVisionModel and not use FastBaseModel directly!"
            )
        if os.environ.get("UNSLOTH_MODEL_NAME", "") == "":
            os.environ["UNSLOTH_MODEL_NAME"] = model_name.lower()

        # Read from kwargs, not the signature: the weight load below forwards **kwargs, so
        # binding it would drop it there.
        _revision = kwargs.get("revision")
        _tokenizer_revision_arg = kwargs.pop("tokenizer_revision", None)
        if _revision is not None and fast_inference and is_vLLM_available():
            # vLLM fetches the default branch, so pinning only the config and tokenizer
            # would mix two refs in one model.
            logger.warning_once(
                f"Unsloth: Ignoring revision = `{_revision}` since vLLM loads weights from "
                "the default branch. Use `fast_inference = False` to load a pinned revision."
            )
            _revision = None
            _tokenizer_revision_arg = None
            kwargs.pop("revision", None)
        _revision_repo = model_name  # The repo the pin names, captured before any remap.

        # Resolve text-only before the is_vlm / vLLM checks so is_vlm stays consistent;
        # skip the vision tower only for families with their own text decoder (Gemma 3). #5816
        if text_only and auto_config is None:
            auto_config = AutoConfig.from_pretrained(
                model_name,
                token = token,
                trust_remote_code = trust_remote_code,
                local_files_only = local_files_only,
                revision = _revision,
            )
        if text_only and hasattr(auto_config, "vision_config"):
            parent_config = auto_config
            text_config = _get_text_only_config(parent_config, model_name)
            text_class = resolve_model_class(AutoModelForCausalLM, text_config)
            if text_class is not None and _is_family_text_decoder(
                getattr(parent_config, "model_type", ""),
                getattr(text_config, "model_type", ""),
            ):
                auto_config = text_config
                auto_model = AutoModelForCausalLM
                _apply_text_only_key_mapping(kwargs, parent_config, text_config)
        elif text_only and auto_model in [
            AutoModelForVision2Seq,
            AutoModelForImageTextToText,
        ]:
            # Pure text model requested text-only with a VLM auto class.
            auto_model = AutoModelForCausalLM
        is_vlm = auto_model in [AutoModelForVision2Seq, AutoModelForImageTextToText]
        # A repo-code VLM may register only AutoModel / AutoModelForCausalLM (e.g.
        # DeepSeek-OCR, Nemotron-VL), so auto_model is not a VLM class even though the
        # config is a vision model. Keep is_vlm (auto-class derived) for processor
        # selection below -- these repos ship no AutoProcessor -- but treat the model as a
        # VLM on the vLLM path so a vision_config model is never silently loaded/converted
        # as text-only. text_only resolves the tower away first, so honour that here.
        is_vlm_config = is_vlm or (not text_only and hasattr(auto_config, "vision_config"))
        is_whisper = whisper_language is not None and whisper_task is not None
        auto_processor = AutoProcessor if (is_vlm or is_whisper) else AutoTokenizer

        model_type_arch = model_types[0]
        if model_type_arch == "siglip":
            for model_type_arch in model_types:
                if model_type_arch != "siglip":
                    break

        vllm_enable_lora = True

        if is_vlm_config and fast_inference:
            if not any(arch in VLLM_SUPPORTED_VLM for arch in model_types):
                raise RuntimeError(
                    f"Unsloth: Fast inference is only supported for Language models and Qwen2.5-VL, Gemma3 among vision models. "
                    f"Found architectures: {', '.join(model_types)}!"
                )

        if any(arch in VLLM_NON_LORA_VLM for arch in model_types):
            # mllama is still only in vllm v0 https://arc.net/l/quote/llwkfgmu
            # https://docs.vllm.ai/en/stable/models/supported_models.html#text-generation_1
            # vLLM V0 does not support LoRA on multi modal models.
            # TODO: Update this once vLLM V1 supports Llama 3.2 aka mllama
            vllm_enable_lora = False

        os.environ["UNSLOTH_USE_NEW_MODEL"] = "1"
        if trust_remote_code:
            print(
                "Unsloth: WARNING `trust_remote_code` is True.\n"
                "Are you certain you want to do remote code execution?"
            )
        token = hf_login(token)
        SUPPORTS_BFLOAT16 = is_bfloat16_supported()

        gpu_stats_name, gpu_stats_snippet, max_memory = get_device_stats()
        if DEVICE_TYPE == "xpu":
            vllm_version = ""
        else:
            try:
                vllm_version = f" vLLM: {importlib_version('vllm')}."
            except:
                vllm_version = ""

        arch_name = model_type_arch.title()
        arch_name = arch_name.replace("_Vl_", "_VL_").replace("_Moe", "_MoE")
        statistics = (
            f"==((====))==  Unsloth {__version__}: Fast {arch_name} patching. Transformers: {transformers_version}.{vllm_version}\n"
            f"   {chr(92)}{chr(92)}   /|    {gpu_stats_name}Num GPUs = {DEVICE_COUNT}. Max memory: {max_memory} GB. Platform: {platform_system}.\n"
            f"O^O/ {chr(92)}_/ {chr(92)}    Torch: {torch.__version__}. {gpu_stats_snippet} Triton: {triton_version}\n"
            f"{chr(92)}        /    Bfloat16 = {str(SUPPORTS_BFLOAT16).upper()}. FA [Xformers = {xformers_version}. FA2 = {HAS_FLASH_ATTENTION}]\n"
            f' "-____-"     Free license: http://github.com/unslothai/unsloth'
        )

        print(statistics)

        # Warn about fast transfers
        if "HF_HUB_ENABLE_HF_TRANSFER" in os.environ:
            old_hf_transfer = os.environ["HF_HUB_ENABLE_HF_TRANSFER"]
            if old_hf_transfer in ("False", "false"):
                old_hf_transfer = "0"
            if old_hf_transfer in ("True", "true"):
                old_hf_transfer = "1"
        else:
            old_hf_transfer = "0"
        if old_hf_transfer == "1":
            print(
                "Unsloth: Fast downloading is enabled - ignore downloading bars which are red colored!"
            )
        if old_hf_transfer != "0":
            os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1"

        # For debugging - we use a download counter to see if environments are not breaking or if HF is down
        get_statistics(kwargs.get("local_files_only", False))

        # The base + tokenizer prefetch runs AFTER the load-mode validation below, so an invalid
        # load_in_* combination fails without first downloading a snapshot.

        # Whether float32 was asked for rather than arrived at by upcasting. Only an
        # explicit request may suppress the V100/T4 float16 autocast (see #4082).
        user_float32 = _requested_float32(dtype)
        if dtype is None:
            dtype = torch.float16 if not SUPPORTS_BFLOAT16 else torch.bfloat16
        elif os.environ.get("UNSLOTH_FORCE_FLOAT32", "0") == "1":
            if dtype == torch.float16:
                dtype = torch.bfloat16
        elif dtype == torch.bfloat16 and not SUPPORTS_BFLOAT16:
            logger.warning_once("Device does not support bfloat16. Will change to float16.")
            dtype = torch.float16
        assert dtype in (torch.float16, torch.bfloat16, torch.float32)

        bnb_compute_dtype = dtype
        do_forced_float32 = False
        if os.environ.get("UNSLOTH_FORCE_FLOAT32", "0") == "1":
            print(
                f"Unsloth: Using float16 precision for {model_type_arch} won't work! Using float32."
            )
            bnb_compute_dtype = torch.float16
            do_forced_float32 = True

        # Check for custom data-types
        custom_datatype = None
        correct_dtype = None
        if os.environ.get("UNSLOTH_FORCE_CUSTOM_DTYPE", "") != "":
            custom_datatype = os.environ["UNSLOTH_FORCE_CUSTOM_DTYPE"]
            assert custom_datatype.count(";") >= 4
            checker, _dtype, _bnb_compute_dtype, _custom_datatype, execute_code = (
                custom_datatype.split(";", 4)
            )
            # Allow custom dtypes on all runs
            allow_all_runs = checker == "all"
            # Allow only on float16 datatypes
            allow_float16_runs = (checker == "float16" or checker == "torch.float16") and (
                dtype == torch.float16 or os.environ.get("UNSLOTH_FORCE_FLOAT32", "0") == "1"
            )
            if allow_all_runs or allow_float16_runs:
                if eval(_dtype) is not None:
                    dtype = eval(_dtype)
                if eval(_bnb_compute_dtype) is not None:
                    bnb_compute_dtype = eval(_bnb_compute_dtype)
                correct_dtype = bnb_compute_dtype
                custom_datatype = _custom_datatype
                # Execute code as well
                if len(execute_code.strip()) != 0:
                    exec(execute_code)
            else:
                custom_datatype = None
                correct_dtype = None

        if auto_config is None:
            auto_config = AutoConfig.from_pretrained(
                model_name,
                token = token,
                trust_remote_code = trust_remote_code,
                local_files_only = local_files_only,
                revision = _revision,
            )
        model_class = resolve_model_class(auto_model, auto_config)
        attn_impl = resolve_attention_implementation(
            model_class,
            auto_config,
            requested_attn_implementation = kwargs.get("attn_implementation", None),
            supports_sdpa = supports_sdpa,
        )

        # Handle FP8 models: get_model_name has already redirected this to BF16 sibling if the model ships with
        # FP8 weights. We just need to update it here for sanity.
        auto_config.model_name = model_name
        kwargs["attn_implementation"] = attn_impl

        bnb_config = None
        user_quantization_config = kwargs.get("quantization_config", None)

        # Check if model already has a non-bitsandbytes quantization config (e.g. compressed-tensors/NVFP4)
        from .loader_utils import (
            check_and_disable_bitsandbytes_loading,
            sync_unsloth_model_name_bnb_flags,
        )

        load_in_4bit, load_in_8bit, _ = check_and_disable_bitsandbytes_loading(
            auto_config, load_in_4bit = load_in_4bit, load_in_8bit = load_in_8bit
        )
        # Correct UNSLOTH_MODEL_NAME's bnb tokens now that the effective bnb state is known
        # (the per-load env was built before remap/disable). gpt-oss only; no-op otherwise.
        sync_unsloth_model_name_bnb_flags(load_in_4bit, load_in_8bit)

        if full_finetuning and (load_in_4bit or load_in_8bit):
            print(
                "Unsloth: You selected full finetuning support, but 4bit / 8bit is enabled - disabling LoRA / QLoRA."
            )
            load_in_4bit = False
            load_in_8bit = False
            load_in_16bit = False

        if int(load_in_4bit) + int(load_in_8bit) + int(load_in_16bit) >= 2:
            raise RuntimeError(
                "Unsloth: Can only load in 4bit or 8bit or 16bit, not a combination!"
            )

        # Prefetch the repo (killable child) so the in-process load below is a cache hit. vLLM owns the
        # weight download only when actually available; if fast_inference was requested but vLLM is
        # missing, the load falls through in-process, so weights must still be warmed here.
        _vllm_owns_weights = fast_inference and is_vLLM_available()
        _prefetched = maybe_prefetch_hf_snapshot(
            model_name,
            token = token,
            revision = kwargs.get("revision"),
            cache_dir = kwargs.get("cache_dir"),
            local_files_only = kwargs.get("local_files_only", False),
            fast_inference = _vllm_owns_weights,
            subfolder = kwargs.get("subfolder"),
            force_download = kwargs.get("force_download", False),
            use_safetensors = kwargs.get("use_safetensors"),
            from_tf = kwargs.get("from_tf", False),
            from_flax = kwargs.get("from_flax", False),
            # Bare load reads only ROOT weights; skip subdir weights. Ignored when a subfolder is set.
            weights_at_root = True,
            variant = kwargs.get("variant"),  # forward so the warm keeps the variant .bin
            gguf_file = kwargs.get(
                "gguf_file"
            ),  # forward so the warm fetches the GGUF (else ignored)
        )
        # Child did the forced download; clear the flag so the load reuses the warm cache.
        if _prefetched and kwargs.get("force_download", False):
            kwargs["force_download"] = False

        # Warm a SEPARATE tokenizer repo only (model_name is covered above). Not model_name here: this
        # runs before fast_inference_setup may remap the repo, so it would warm the wrong one.
        _tokenizer_repo = (
            tokenizer_name if (isinstance(tokenizer_name, str) and tokenizer_name) else model_name
        )
        _warm_tokenizer_repo = (
            isinstance(_tokenizer_repo, str)
            and bool(_tokenizer_repo)
            and _tokenizer_repo != model_name
        )
        if _warm_tokenizer_repo:
            # No revision: this only runs when the repo differs from the pinned one.
            maybe_prefetch_hf_snapshot(
                _tokenizer_repo,
                token = token,
                cache_dir = kwargs.get("cache_dir"),
                local_files_only = kwargs.get("local_files_only", False),
                tokenizer_only = True,
            )

        _skip_modules = SKIP_QUANTIZATION_MODULES.copy()
        # Nemotron-H uses 'mixer' (not 'mamba') for Mamba layers.
        # Mamba fused kernels pass out_proj.weight directly to F.linear,
        # which fails with quantized Params4bit. Skip out_proj from quantization.
        if any(mt == "nemotron_h" for mt in (model_types or [])):
            _skip_modules.append("out_proj")

        if load_in_4bit:
            bnb_config = BitsAndBytesConfig(
                load_in_4bit = True,
                bnb_4bit_use_double_quant = True,
                bnb_4bit_quant_type = "nf4",
                bnb_4bit_compute_dtype = bnb_compute_dtype,
                llm_int8_skip_modules = _skip_modules,
            )
        elif load_in_8bit:
            bnb_config = BitsAndBytesConfig(
                load_in_8bit = True,
                llm_int8_skip_modules = _skip_modules,
            )
        elif load_in_16bit:
            bnb_config = None
        elif not load_in_4bit and not load_in_8bit and not full_finetuning:
            print("Unsloth: QLoRA and full finetuning all not selected. Switching to 16bit LoRA.")

        if full_finetuning:
            os.environ["UNSLOTH_ENABLE_FULL_FINETUNING"] = "1"
            if dtype == torch.bfloat16:
                if float32_mixed_precision != True:
                    print(
                        f"Unsloth: Using bfloat16 full finetuning which cuts memory usage by 50%.\n"
                        f"To enable float32 training, use `float32_mixed_precision = True` during FastLanguageModel.from_pretrained"
                    )
                else:
                    print(
                        f"Unsloth: Using full float32 full finetuning. "
                        f"To enable bfloat16 training to reduce VRAM usage by 50% albeit with a slightly higher loss, do:\n"
                        "use `float32_mixed_precision = False` during FastLanguageModel.from_pretrained"
                    )
                    os.environ["UNSLOTH_BFLOAT16_MIXED_PRECISION"] = "1"
            elif dtype == torch.float32:
                print("Unsloth: Using float32 full finetuning.")
            else:
                print(
                    "Unsloth: Float16 full finetuning uses more memory since we upcast weights to float32."
                )
        else:
            os.environ["UNSLOTH_ENABLE_FULL_FINETUNING"] = "0"

        # Fix AttributeError: 'BitsAndBytesConfig' object has no attribute 'get_loading_attributes'
        if bnb_config is not None and not hasattr(bnb_config, "get_loading_attributes"):
            bnb_config.get_loading_attributes = lambda *args, **kwargs: {}

        # Cannot be None, since HF now checks for the config
        if load_in_4bit or load_in_8bit:
            # Ignore load_in_4bit / load_in_8bit for MXFP4 - best to get config file
            if "gpt-oss-20b" in model_name.lower() or "gpt-oss-120b" in model_name.lower():
                pass
            else:
                if user_quantization_config is None:
                    kwargs["quantization_config"] = bnb_config
        else:
            if auto_config is None:
                auto_config = AutoConfig.from_pretrained(
                    model_name,
                    token = token,
                    trust_remote_code = trust_remote_code,
                    local_files_only = local_files_only,
                    revision = _revision,
                )
            if hasattr(auto_config, "quantization_config"):
                from transformers.quantizers.auto import (
                    AUTO_QUANTIZATION_CONFIG_MAPPING,
                )

                quantization_config = auto_config.quantization_config
                quant_method = quantization_config["quant_method"]
                # Sometimes bitsandbytes_4bit + bitsandbytes_8bit is provided
                if (
                    quant_method == "bitsandbytes"
                    and "bitsandbytes" not in AUTO_QUANTIZATION_CONFIG_MAPPING
                ):
                    if "bitsandbytes_4bit" not in AUTO_QUANTIZATION_CONFIG_MAPPING:
                        raise KeyError(
                            "Unsloth: AUTO_QUANTIZATION_CONFIG_MAPPING does not have `bitsandbytes_4bit`"
                        )
                    quantizer = AUTO_QUANTIZATION_CONFIG_MAPPING["bitsandbytes_4bit"]
                else:
                    quantizer = AUTO_QUANTIZATION_CONFIG_MAPPING[quant_method]
                quantizer_kwargs = {}
                if quant_method == "compressed-tensors":
                    # Ignore these
                    pass
                else:
                    # We cannot dequantize since gpt-oss-20b MXFP4 will now be gpt-oss-20b-BF16
                    if load_in_16bit and "dequantize" in inspect.signature(quantizer).parameters:
                        quantizer_kwargs["dequantize"] = True
                    try:
                        # Sometimes this fails so we wrap it in a try except
                        quantization_config = quantizer.from_dict(
                            quantization_config, **quantizer_kwargs
                        )
                    except:
                        pass
                    if user_quantization_config is None:
                        kwargs["quantization_config"] = quantization_config

        # Check if using forced float32 - we load it in bfloat16, then cast to float16!
        torch_dtype = dtype
        if do_forced_float32:
            torch_dtype = torch.bfloat16

        kwargs = add_dtype_kwargs(torch_dtype, kwargs)

        config_attn_impl = kwargs.get("attn_implementation", None)
        if config_attn_impl is None:
            config_attn_impl = "sdpa" if supports_sdpa else "eager"
        if auto_config is None:
            auto_config = AutoConfig.from_pretrained(
                model_name,
                token = token,
                trust_remote_code = trust_remote_code,
                local_files_only = local_files_only,
                revision = _revision,
            )
        _set_attn_impl(auto_config, config_attn_impl)
        model_config = auto_config

        verify_fp8_support_if_applicable(model_config)

        raise_handler = RaiseUninitialized()
        try:
            if offload_embedding and fast_inference:
                # vLLM manages its own weights; embedding offload does not apply.
                print(
                    "Unsloth: Not offloading embeddings; incompatible with fast_inference (vLLM)."
                )
                offload_embedding = False
            if not fast_inference:
                # Prevent load_in_fp8 from being forwarded into HF internal model loading
                load_in_fp8 = kwargs.pop("load_in_fp8", None)
                # Transformers 5.x @strict config classes reject unexpected kwargs.
                # Move config-level attributes onto the config object directly.
                _num_labels = kwargs.pop("num_labels", None)
                if _num_labels is not None:
                    set_task_config_attr(model_config, "num_labels", _num_labels)
                for _cfg_key in ("id2label", "label2id", "problem_type"):
                    _cfg_val = kwargs.pop(_cfg_key, None)
                    if _cfg_val is not None:
                        set_task_config_attr(model_config, _cfg_key, _cfg_val)
                _cfg_val = kwargs.pop("max_position_embeddings", None)
                if _cfg_val is not None:
                    setattr(model_config, "max_position_embeddings", _cfg_val)
                model = auto_model.from_pretrained(
                    model_name,
                    config = model_config,
                    device_map = device_map,
                    # torch_dtype           = torch_dtype, # Transformers removed torch_dtype
                    # quantization_config   = bnb_config,
                    token = token,
                    trust_remote_code = trust_remote_code,
                    # attn_implementation   = attn_implementation,
                    **kwargs,
                )
                # Must precede _attach_bnb_multidevice_hooks: it returns early
                # while offload_embedding is True.
                offload_embedding = _resolve_offload_embedding(
                    model,
                    offload_embedding,
                )

                # Attach dispatch hooks for bnb multi-device loads.
                _attach_bnb_multidevice_hooks(
                    model,
                    load_in_4bit = load_in_4bit,
                    load_in_8bit = load_in_8bit,
                    offload_embedding = offload_embedding,
                    fast_inference = fast_inference,
                )
                # Re-apply block-fp8 weight_scale_inv tensors transformers dropped on load (#6200).
                _restore_dropped_fp8_scales(
                    model,
                    model_name,
                    local_files_only = local_files_only,
                    token = token,
                    revision = kwargs.get("revision"),
                    subfolder = kwargs.get("subfolder"),
                    cache_dir = kwargs.get("cache_dir"),
                    variant = kwargs.get("variant"),
                )
                if hasattr(model, "generate"):
                    model.fast_generate = make_fast_generate_wrapper(model.generate)
                    model.fast_generate_batches = error_out_no_vllm
                if offload_embedding:
                    # Unsupported platforms and tied embeddings were screened out above.
                    embed_tokens = model.get_input_embeddings()
                    out_embed = (
                        model.get_output_embeddings()
                        if hasattr(model, "get_output_embeddings")
                        else None
                    )
                    nbytes = embed_tokens.weight.numel() * embed_tokens.weight.itemsize
                    ngb = round(nbytes / 1024 / 1024 / 1024, 2)
                    print(f"Unsloth: Offloading embeddings to RAM to save {ngb} GB.")
                    _embed_device = embed_tokens.weight.device  # decoder device, before offload
                    embed_tokens.to("cpu")

                    # Device-safe embedding offload.
                    _install_offload_embedding_hooks(embed_tokens, out_embed, _embed_device)
                    # Must free GPU memory otherwise will not free!
                    clean_gpu_cache()
                    gc.collect()
            else:
                from unsloth_zoo.vllm_utils import (
                    load_vllm,
                    get_vllm_state_dict,
                    convert_vllm_to_huggingface,
                    generate_batches,
                    get_lora_supported_ranks,
                )

                if full_finetuning:
                    max_lora_rank = max(get_lora_supported_ranks())
                    raise NotImplementedError(
                        "Unsloth: `fast_inference=True` cannot be used together with `full_finetuning=True`.\n"
                        "Reason: fast_inference is optimized for inference-only workflows and "
                        "does not currently support full fine-tuning.\n"
                        "Workaround: disable fast_inference, or use parameter-efficient fine-tuning "
                        f"(e.g. LoRA with rank r={max_lora_rank})."
                    )

                model_config.model_name = model_name

                if fast_inference:
                    fast_inference, model_name = fast_inference_setup(model_name, model_config)

                fp8_mode = None
                if load_in_fp8 != False:
                    fp8_mode = _get_fp8_mode_and_check_settings(
                        load_in_fp8,
                        fast_inference,
                        full_finetuning,
                        load_in_4bit,
                        load_in_8bit,
                        load_in_16bit,
                    )

                allowed_args = inspect.getfullargspec(load_vllm).args
                load_vllm_kwargs = dict(
                    model_name = model_name,
                    config = model_config,
                    gpu_memory_utilization = gpu_memory_utilization,
                    max_seq_length = max_seq_length,
                    dtype = dtype,
                    float8_kv_cache = float8_kv_cache,
                    enable_lora = vllm_enable_lora,
                    max_lora_rank = max_lora_rank,
                    disable_log_stats = disable_log_stats,
                    use_bitsandbytes = load_in_4bit,
                    unsloth_vllm_standby = unsloth_vllm_standby,
                    is_vision_model = is_vlm_config,
                    fp8_mode = fp8_mode,
                )
                for allowed_arg in allowed_args:
                    if allowed_arg not in load_vllm_kwargs and allowed_arg in kwargs:
                        load_vllm_kwargs[allowed_arg] = kwargs[allowed_arg]

                # Load vLLM first
                llm = load_vllm(**load_vllm_kwargs)

                # Convert to HF format
                _, quant_state_dict = get_vllm_state_dict(
                    llm,
                    config = model_config,
                    is_vision_model = is_vlm_config,
                    load_in_fp8 = load_in_fp8,
                )
                model = convert_vllm_to_huggingface(
                    quant_state_dict,
                    model_config,
                    dtype,
                    bnb_config,
                    is_vision_model = is_vlm_config,
                )
                model.vllm_engine = llm
                llm.shared_weights = True
                model.fast_generate = model.vllm_engine.generate
                model.fast_generate_batches = functools.partial(generate_batches, model.vllm_engine)

        finally:
            raise_handler.remove()
            # Return old flag
            os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = old_hf_transfer

        # Check float32 norm weights
        if os.environ.get("UNSLOTH_HIGH_PRECISION_LAYERNORM", "0") == "1":
            for jj, (name, module) in enumerate(model.named_modules()):
                if (
                    name.endswith(("norm", "norm1", "norm2", "norm3", "norm4"))
                    or "layernorm" in name
                    or "layer_norm" in name
                ) and hasattr(module, "weight"):
                    module._pre_set_compute_dtype = torch.float32
        # Edit data-types
        if custom_datatype is not None:
            with torch.no_grad():
                for jj, (name, module) in enumerate(model.named_modules()):
                    exec(custom_datatype)
        # Clear deleted GPU items
        for _ in range(3):
            gc.collect()
            clean_gpu_cache()

        # Counteract saved tokenizers
        tokenizer_name = model_name if tokenizer_name is None else tokenizer_name
        # The loader resolves this (caller's adapter repo vs resolved base model); the
        # fallback below covers a direct call.
        _tokenizer_revision = _tokenizer_revision_arg
        if _tokenizer_revision is None and tokenizer_name == _revision_repo:
            _tokenizer_revision = _revision

        # On the vLLM path the tokenizer warm was deferred (fast_inference_setup may remap model_name).
        # Warm the now-final tokenizer repo so the load below hits the cache (a cached/local repo is a no-op).
        if _vllm_owns_weights and isinstance(tokenizer_name, str) and tokenizer_name:
            maybe_prefetch_hf_snapshot(
                tokenizer_name,
                token = token,
                # Match the tokenizer load below, which only pins its own repo.
                revision = _tokenizer_revision,
                cache_dir = kwargs.get("cache_dir"),
                local_files_only = kwargs.get("local_files_only", False),
                tokenizer_only = True,
            )

        # Fix _Unsloth_Patched_ prefix in local config files from old saves (issue #4085)
        if os.path.isdir(tokenizer_name):
            import json as _json
            for _cfg_name in (
                "processor_config.json",
                "preprocessor_config.json",
                "tokenizer_config.json",
            ):
                _cfg_path = os.path.join(tokenizer_name, _cfg_name)
                if os.path.exists(_cfg_path):
                    try:
                        with open(_cfg_path, "r", encoding = "utf-8") as _f:
                            _cfg = _json.load(_f)
                        if _cfg.get("processor_class", "").startswith("_Unsloth_Patched_"):
                            _cfg["processor_class"] = _cfg["processor_class"][
                                len("_Unsloth_Patched_") :
                            ]
                            with open(_cfg_path, "w", encoding = "utf-8") as _f:
                                _json.dump(_cfg, _f, indent = 2, ensure_ascii = False)
                    except Exception:
                        pass

        # Functional load chain (AutoProcessor -> get_auto_processor -> manual VLM
        # fallback); offline is already forced upstream. Surfaces the error for the retry.
        def _acquire_processor(lfo):
            _err = None  # underlying load failure (used by the entry-point retry)
            if (whisper_language and whisper_task) or auto_model.__name__.endswith(
                "ForConditionalGeneration"
            ):
                try:
                    _tok = auto_processor.from_pretrained(
                        tokenizer_name,
                        padding_side = "left",
                        token = token,
                        language = whisper_language,
                        task = whisper_task,
                        trust_remote_code = trust_remote_code,
                        cache_dir = kwargs.get("cache_dir"),
                        local_files_only = lfo,
                        revision = _tokenizer_revision,
                    )
                except Exception as _e:
                    _tok = None
                    _err = _e
            else:
                try:
                    _tok = auto_processor.from_pretrained(
                        tokenizer_name,
                        padding_side = "left",
                        token = token,
                        trust_remote_code = trust_remote_code,
                        cache_dir = kwargs.get("cache_dir"),
                        local_files_only = lfo,
                        revision = _tokenizer_revision,
                    )
                except Exception as _e:
                    _err = _e
                    try:
                        _tok = get_auto_processor(
                            tokenizer_name,
                            padding_side = "left",
                            token = token,
                            trust_remote_code = trust_remote_code,
                            cache_dir = kwargs.get("cache_dir"),
                            local_files_only = lfo,
                            revision = _tokenizer_revision,
                        )
                    except Exception:
                        # Swallow so the manual fallback / entry-point retry can run.
                        _tok = None

            # Build the processor manually if it failed to load or silently degraded to
            # a text-only tokenizer (no image_processor) for a VLM (issue #4085).
            _processor_is_degraded = (
                is_vlm and _tok is not None and not hasattr(_tok, "image_processor")
            )
            if (_tok is None or _processor_is_degraded) and is_vlm:
                try:
                    _fallback, _fb_err = _construct_vlm_processor_fallback(
                        tokenizer_name,
                        model_type_arch,
                        token,
                        trust_remote_code,
                        cache_dir = kwargs.get("cache_dir"),
                        local_files_only = lfo,
                        revision = _tokenizer_revision,
                    )
                except Exception as _fe:
                    _fallback, _fb_err = None, _fe
                if _fallback is not None:
                    _tok = _fallback
                elif _err is None or (_fb_err is not None and _is_offline_related_error(_fb_err)):
                    # Prefer a network fallback error over a permanent primary one so the
                    # offline retry still fires.
                    _err = _fb_err
            return _tok, _err

        def _is_degraded_vlm(_t):
            # VLM that loaded only a text-only tokenizer (no image_processor).
            return is_vlm and _t is not None and not hasattr(_t, "image_processor")

        tokenizer, _primary_err = _acquire_processor(local_files_only)
        # Online network failure/degrade: raise so @_offline_aware_load retries from cache.
        # Permanent / missing-file errors propagate; when already offline keep what we got.
        if (
            (tokenizer is None or _is_degraded_vlm(tokenizer))
            and not local_files_only
            and _is_offline_related_error(_primary_err)
        ):
            raise _primary_err
        # Missing torchvision silently degrades a VLM processor to text-only; surface the
        # real cause instead of a later collator error (#4202), incl. on a silent degrade.
        if is_vlm and (tokenizer is None or not hasattr(tokenizer, "image_processor")):
            if _missing_torchvision_error(_primary_err):
                raise ImportError(
                    f"Unsloth: Could not load the vision processor for `{tokenizer_name}` "
                    "because torchvision is not installed. transformers requires torchvision "
                    "for this model's vision (image/video) processors. Please install it, "
                    "e.g. `pip install torchvision`."
                )
            import sys
            print(
                f"Unsloth: Warning - VLM processor fallback returned None for model_type={model_type_arch}",
                file = sys.stderr,
            )
        # Backwards compat: if processor has no chat_template (e.g. old saves without
        # chat_template.jinja) but the inner tokenizer does, copy it to the processor.
        if (
            hasattr(tokenizer, "tokenizer")
            and getattr(tokenizer, "chat_template", None) is None
            and getattr(tokenizer.tokenizer, "chat_template", None) is not None
        ):
            tokenizer.chat_template = tokenizer.tokenizer.chat_template

        if hasattr(tokenizer, "tokenizer"):
            __tokenizer = tokenizer.tokenizer
            # Add padding side as well
            __tokenizer.padding_side = "left"
            # Check bos, eos, pad tokens
            if hasattr(__tokenizer, "bos_token"):
                tokenizer.bos_token = __tokenizer.bos_token
                tokenizer.bos_token_id = __tokenizer.bos_token_id
            if hasattr(__tokenizer, "eos_token"):
                tokenizer.eos_token = __tokenizer.eos_token
                tokenizer.eos_token_id = __tokenizer.eos_token_id
            if hasattr(__tokenizer, "pad_token"):
                tokenizer.pad_token = __tokenizer.pad_token
                tokenizer.pad_token_id = __tokenizer.pad_token_id
        # Fix other stuff like BnB compute data types
        model, tokenizer = patch_model_and_tokenizer(
            model,
            tokenizer,
            downcast_rope = False,
            fix_embeddings = False,
            do_forced_float32 = do_forced_float32,
            correct_dtype = correct_dtype,
        )

        try:
            model, tokenizer = patch_tokenizer(model, tokenizer)
        except Exception as _patch_err:
            # Some VLM processors (e.g. ERNIE VL) fail patching; fall back to AutoTokenizer.
            try:
                from transformers import AutoTokenizer as _AutoTokenizer

                _fallback_tok = _AutoTokenizer.from_pretrained(
                    tokenizer_name,
                    padding_side = "left",
                    token = token,
                    trust_remote_code = trust_remote_code,
                    cache_dir = kwargs.get("cache_dir"),
                    local_files_only = local_files_only,
                    revision = _tokenizer_revision,
                )
                model, _fallback_tok = patch_tokenizer(model, _fallback_tok)
                # Re-attach as processor wrapper if original was a processor
                if hasattr(tokenizer, "image_processor"):
                    tokenizer.tokenizer = _fallback_tok
                else:
                    tokenizer = _fallback_tok
            except Exception as _fb_err:
                # Online network failure: propagate for the offline retry; else raise the patch error.
                if not local_files_only and _is_offline_related_error(_fb_err):
                    raise
                raise _patch_err
        model = post_patch_loss_function(model)

        # Log Unsloth version for future fastpaths for inference
        if hasattr(model, "config"):
            model.config.update({"unsloth_version": __version__})
        patch_saving_functions(model, vision = True)
        if tokenizer is None:
            # Last resort: AutoTokenizer, then PreTrainedTokenizerFast (raise on network failure to retry).
            def _last_resort_tokenizer(lfo):
                from transformers import AutoTokenizer as _AutoTokenizer
                load_path = _hub_repo_or_local_path(
                    tokenizer_name,
                    token = token,
                    cache_dir = kwargs.get("cache_dir"),
                    local_files_only = lfo,
                    revision = _tokenizer_revision,
                )
                try:
                    return _AutoTokenizer.from_pretrained(
                        load_path,
                        padding_side = "left",
                        token = token,
                        trust_remote_code = trust_remote_code,
                        cache_dir = kwargs.get("cache_dir"),
                        local_files_only = lfo,
                        revision = _tokenizer_revision,
                    )
                except Exception:
                    return _load_pretrained_tokenizer_fast(
                        tokenizer_name,
                        padding_side = "left",
                        token = token,
                        trust_remote_code = trust_remote_code,
                        cache_dir = kwargs.get("cache_dir"),
                        local_files_only = lfo,
                        revision = _tokenizer_revision,
                    )

            _last_resort_err = None
            try:
                tokenizer = _last_resort_tokenizer(local_files_only)
            except Exception as _e:
                _last_resort_err = _e
                # Online network failure: let the entry point retry forced-offline.
                if not local_files_only and _is_offline_related_error(_e):
                    raise
            if tokenizer is None:
                del model
                raise RuntimeError(
                    "Unsloth: Could not load the tokenizer/processor. If you are "
                    "offline, make sure the tokenizer files exist in the checkpoint "
                    "folder or were previously downloaded to the Hugging Face cache, "
                    "or set HF_HUB_OFFLINE=1 to force local loading. "
                    "Otherwise please check that the model has a tokenizer."
                ) from _last_resort_err
        patch_saving_functions(tokenizer, vision = True)

        # Fix gradient accumulation. See issue #4982.
        from transformers.trainer import Trainer

        apply_accepts_loss_kwargs_fix(model)
        patch_gradient_accumulation_fix(Trainer)

        # Save tokenizer for inference purposes
        tokenizer.padding_side = "left"  # Force inference
        if hasattr(tokenizer, "tokenizer"):
            tokenizer.tokenizer.padding_side = "left"  # Force inference
        # Audio feature extractors must stay right padded: left (a text setting,
        # forwarded by from_pretrained) shifts Whisper mels and desyncs Gemma 4
        # audio token counts (crash on transformers < 5.10).
        feature_extractor = getattr(tokenizer, "feature_extractor", None)
        if (
            feature_extractor is not None
            and getattr(feature_extractor, "padding_side", None) == "left"
        ):
            feature_extractor.padding_side = "right"
        m = model
        while hasattr(m, "model"):
            m.max_seq_length = max_seq_length
            m._saved_temp_tokenizer = tokenizer
            m = m.model
        m.max_seq_length = max_seq_length
        # Save to modules as well
        for module in model.modules():
            module.max_seq_length = max_seq_length
        m._saved_temp_tokenizer = tokenizer
        # Prevent Transformers Trainer from auto-wrapping Unsloth LoRA models in DP.
        _mark_unsloth_disable_data_parallel(model, disable = not full_finetuning)

        # Patch generate
        if os.environ.get("UNSLOTH_DISABLE_FAST_GENERATION", "0") == "0" and hasattr(
            model, "generate"
        ):
            if model.generate.__name__ != "unsloth_base_fast_generate":
                model._old_generate = model.generate
                unsloth_base_fast_generate.__doc__ = model._old_generate.__doc__
                model.generate = types.MethodType(unsloth_base_fast_generate, model)
        model._unsloth_trust_remote_code = trust_remote_code
        # Post patches
        model = FastBaseModel.post_patch_model(
            model,
            use_gradient_checkpointing = use_gradient_checkpointing,
            trust_remote_code = trust_remote_code,
            model_type = model_type_arch,
            tokenizer = tokenizer,
            float32_mixed_precision = float32_mixed_precision,
        )
        # Clear deleted GPU items
        for _ in range(3):
            gc.collect()
            clean_gpu_cache()
        # Saving restores sentencepiece assets from the repo name alone, which carries no
        # branch. Stamped here, not per processor branch, so a fallback cannot lose it.
        _mark_loaded_revision(tokenizer, _tokenizer_revision)
        model = _mark_forced_float32(model, do_forced_float32)
        model = _mark_full_finetuning(model, full_finetuning)
        return _mark_requested_float32(model, user_float32), tokenizer

    @staticmethod
    def get_peft_model(
        model,
        r = 16,
        target_modules = None,
        lora_alpha = 16,
        lora_dropout = 0.0,
        bias = "none",
        finetune_vision_layers = True,
        finetune_language_layers = True,
        finetune_attention_modules = True,
        finetune_mlp_modules = True,
        finetune_last_n_layers = None,
        layers_to_transform = None,
        layers_pattern = None,
        use_gradient_checkpointing = "unsloth",
        random_state = 3407,
        max_seq_length = 2048,  # not used anymore
        use_rslora = False,
        modules_to_save = None,
        init_lora_weights = True,
        loftq_config = {},
        task_type = TaskType.CAUSAL_LM,
        temporary_location = "_unsloth_temporary_saved_buffers",
        qat_scheme = None,
        target_parameters = None,  # For MoE expert layers (nn.Parameter)
        ensure_weight_tying = False,  # [TODO] Add `ensure_weight_tying` for `modules_to_save` for vision models
        finetune_audio_layers = False,  # placed last to preserve existing positional argument order
        **kwargs,
    ):
        if os.environ.get("UNSLOTH_ENABLE_FULL_FINETUNING", "0") == "1":
            print("Unsloth: Full finetuning is enabled, so .get_peft_model has no effect")
            # Full finetuning still compiles, so a stray pre-train forward can poison the
            # cache; install the detector here too (it is idempotent).
            _unsloth_install_pretrain_detector(model)
            return model
        transformers_set_seed(random_state)

        if type(r) is not int:
            raise TypeError(f"Unsloth: Rank of {str(r)} must be an integer.")
        if r <= 0:
            raise TypeError(f"Unsloth: Rank of {str(r)} must be larger than 0.")

        if isinstance(model, PeftModelForCausalLM):
            raise RuntimeError("Unsloth: You already added LoRA adapters to your model!")

        # Remember whether the CALLER explicitly opted into audio. "all-linear" turns
        # the flag on implicitly below, but an old unsloth_zoo that cannot do audio
        # must not make a plain all-linear (text/vision) run fail.
        _audio_explicitly_requested = bool(finetune_audio_layers)
        if target_modules == "all-linear":
            finetune_vision_layers = True
            finetune_language_layers = True
            finetune_attention_modules = True
            finetune_mlp_modules = True
            finetune_audio_layers = True
        # Older unsloth_zoo (before get_peft_regex gained finetune_audio_layers) does
        # not accept the kwarg. Pass it only when supported; if the caller EXPLICITLY
        # asked for audio but it is unsupported, fail loudly rather than silently
        # training a language-only adapter. (all-linear's implicit opt-in degrades
        # gracefully instead of raising.)
        if "finetune_audio_layers" in inspect.signature(get_peft_regex).parameters:
            _audio_kwargs = {"finetune_audio_layers": finetune_audio_layers}
        elif _audio_explicitly_requested:
            raise RuntimeError(
                "Unsloth: finetune_audio_layers=True requires a newer unsloth_zoo. "
                "Please upgrade with `pip install --upgrade --no-deps unsloth_zoo`."
            )
        else:
            _audio_kwargs = {}
        # Remember the caller's ORIGINAL explicit leaf list for MoE expert
        # detection. When an explicit list is routed through get_peft_regex for
        # family scoping below, the generated regex carries get_peft_regex's full
        # "mlp|feed_forward|ffn|dense" component block even when the caller named
        # only attention leaves (q/k/v/o_proj). Keying expert detection on that
        # regex would train the experts for an attention-only request. The
        # original list carries the true leaf intent, so use it for MoE detection;
        # only the auto (None / "all-linear") path relies on the regex, whose mlp
        # block is the sole remaining MLP-intent signal on fused-expert models.
        _moe_detect_target = target_modules if type(target_modules) in (list, tuple) else None
        if target_modules is None or target_modules == "all-linear":
            target_modules = get_peft_regex(
                model,
                finetune_vision_layers = finetune_vision_layers,
                finetune_language_layers = finetune_language_layers,
                finetune_attention_modules = finetune_attention_modules,
                finetune_mlp_modules = finetune_mlp_modules,
                **_audio_kwargs,
            )
        else:
            assert type(target_modules) in (list, tuple, str)
            # Route an explicit list through get_peft_regex when the caller scoped a
            # layer family (one of the finetune_* below is off) OR opted into audio (so
            # the new audio/embedder branches are considered). finetune_audio_layers is
            # a POSITIVE term here: using `not finetune_audio_layers` would -- since it
            # defaults False -- force every explicit list through the filter.
            _scoping = (
                not finetune_vision_layers
                or not finetune_language_layers
                or not finetune_attention_modules
                or not finetune_mlp_modules
            )
            if type(target_modules) in (list, tuple) and (_scoping or finetune_audio_layers):
                if _scoping:
                    print(
                        "Unsloth: Explicit target_modules are constrained by the "
                        "finetune_(vision|language|attention|mlp) filters; adapters "
                        "attach only where both select."
                    )
                target_modules = get_peft_regex(
                    model,
                    finetune_vision_layers = finetune_vision_layers,
                    finetune_language_layers = finetune_language_layers,
                    finetune_attention_modules = finetune_attention_modules,
                    finetune_mlp_modules = finetune_mlp_modules,
                    target_modules = list(target_modules),
                    **_audio_kwargs,
                )

        if hasattr(model, "vllm_engine"):
            if (
                hasattr(model.vllm_engine, "llm_engine")
                and hasattr(model.vllm_engine.llm_engine, "vllm_config")
                and getattr(model.vllm_engine.llm_engine.vllm_config, "lora_config", None) is None
            ):
                # If vLLM is being used but lora is not enabled, throw an error
                # Ref https://github.com/vllm-project/vllm/blob/51ba839555a5d122eadd91e9c16463ac288f5fa1/vllm/v1/engine/processor.py#L148-L151
                raise RuntimeError("Unsloth: LoRA is not enabled for this model!")
            if finetune_vision_layers:
                # vLLM does not support LoRA on vision layers
                # https://github.com/vllm-project/vllm/blob/main/vllm/lora/models.py#L471-L477
                # TODO: Update this once vLLM V1 supports LoRA on vision layers (possibly not happening)
                raise RuntimeError(
                    "Unsloth: Finetuning vision layers is not supported for fast_inference. Only text layers are supported!"
                )
            if model.config.model_type in VLLM_NON_LORA_VLM:
                # mllama is still only in vllm v0 https://arc.net/l/quote/llwkfgmu
                # https://docs.vllm.ai/en/stable/models/supported_models.html#text-generation_1
                # vLLM V0 does not support LoRA on multi modal models.
                # TODO: Update this once vLLM V1 supports Llama 3.2 aka mllama
                raise RuntimeError(
                    "Unsloth: LoRA finetuning for Llama 3.2 aka mllama models is not supported with fast_inference!"
                )

        # Clear deleted GPU items
        for _ in range(3):
            gc.collect()
            clean_gpu_cache()
        max_seq_length = model.max_seq_length
        # If we pass loftq_config = None we will get an error
        loftq_config = validate_loftq_config(
            loftq_config, lora_dropout, bias, init_lora_weights, model
        )

        # Auto-detect MoE models and populate target_parameters for expert layers.
        # Prefer the caller's ORIGINAL explicit leaf list over the scoped regex so an
        # attention-only request does not train experts via get_peft_regex's mlp block,
        # but only when MLP and language families are both still in scope. If the caller
        # scoped MLP or language OFF (finetune_mlp_modules / finetune_language_layers
        # False), the scoped regex already dropped the experts, so honor it instead of
        # re-introducing the original list's gate/up/down leaves.
        if target_parameters is None:
            _moe_targets = _select_moe_detection_targets(
                _moe_detect_target,
                target_modules,
                finetune_mlp_modules = finetune_mlp_modules,
                finetune_language_layers = finetune_language_layers,
            )
            target_parameters = get_moe_target_parameters(model, _moe_targets)

        # Per-expert Linear expert layouts (e.g. gpt-oss bnb-4bit) target experts via
        # target_modules, not fused Parameters. Extend either form PEFT accepts: a leaf
        # list (explicit) or a regex string (auto / all-linear / scoped). No-op otherwise.
        _moe_module_detect = _select_moe_detection_targets(
            _moe_detect_target,
            target_modules,
            finetune_mlp_modules = finetune_mlp_modules,
            finetune_language_layers = finetune_language_layers,
        )
        _moe_module_targets = get_moe_target_modules(model, _moe_module_detect)
        if _moe_module_targets:
            if isinstance(target_modules, (list, tuple)):
                target_modules = list(target_modules) + [
                    target for target in _moe_module_targets if target not in target_modules
                ]
            elif isinstance(target_modules, str):
                _expert_leaves = sorted({t.rsplit(".", 1)[0] for t in _moe_module_targets})
                _expert_alt = (
                    r".*\.experts\.(?:"
                    + "|".join(re.escape(leaf) for leaf in _expert_leaves)
                    + r")\.\d+"
                )
                target_modules = f"(?:{target_modules})|(?:{_expert_alt})"
            print(
                f"Unsloth: Detected MoE model with per-expert Linear experts. "
                f"Enabling LoRA on {len(_moe_module_targets)} expert projection modules."
            )
            warn_if_zoo_cannot_merge_moe_experts()

        if finetune_last_n_layers is not None and layers_to_transform is None:
            _total_layers = _get_total_transformer_layers(model)
            if _total_layers is not None and _total_layers > 0:
                n = max(1, min(int(finetune_last_n_layers), _total_layers))
                layers_to_transform = list(range(_total_layers - n, _total_layers))

        # Get only allowed parameters for LoraConfig
        local_variables = {
            **locals(),
            **kwargs,
        }
        del local_variables["kwargs"]
        allowed_parameters = inspect.signature(LoraConfig).parameters.keys()
        lora_config = LoraConfig(
            **{k: v for k, v in local_variables.items() if k in allowed_parameters},
        )
        model = prepare_model_for_kbit_training(
            model,
            use_gradient_checkpointing = use_gradient_checkpointing,
        )
        # Gemma4 ClippableLinear wraps nn.Linear -- PEFT can't inject LoRA on it directly.
        # Monkey-patch PEFT to target the inner .linear child instead.
        _clippable_linear_cls = None
        try:
            from transformers.models.gemma4.modeling_gemma4 import (
                Gemma4ClippableLinear as _clippable_linear_cls,
            )
        except ImportError:
            pass
        if _clippable_linear_cls is not None:
            from peft.tuners.lora.model import LoraModel as _LoraModel

            _original_car = _LoraModel._create_and_replace

            def _patched_car(
                self,
                peft_config,
                adapter_name,
                target,
                target_name,
                parent,
                current_key = None,
                **kwargs,
            ):
                if isinstance(target, _clippable_linear_cls):
                    return _original_car(
                        self,
                        peft_config,
                        adapter_name,
                        target.linear,
                        "linear",
                        target,
                        current_key = current_key,
                        **kwargs,
                    )
                return _original_car(
                    self,
                    peft_config,
                    adapter_name,
                    target,
                    target_name,
                    parent,
                    current_key = current_key,
                    **kwargs,
                )

            _LoraModel._create_and_replace = _patched_car

        model = _get_peft_model(model, lora_config)

        # Restore original PEFT method
        if _clippable_linear_cls is not None:
            _LoraModel._create_and_replace = _original_car
        # Apply QAT + LoRA if specified
        if qat_scheme is not None:
            print("Unsloth: Applying QAT to mitigate quantization degradation")
            model = _prepare_model_for_qat(model, qat_scheme)
        # Fix LoraConfig.auto_mapping is None
        fix_lora_auto_mapping(model)
        # Enable gradients on modules which are trainable
        requires_grad_for_gradient_checkpointing(model)
        trust_remote_code = getattr(model, "_unsloth_trust_remote_code", False)
        model = FastBaseModel.post_patch_model(
            model,
            use_gradient_checkpointing = use_gradient_checkpointing,
            trust_remote_code = trust_remote_code,
        )
        model.max_seq_length = max_seq_length
        # Save to modules as well
        for module in model.modules():
            module.max_seq_length = max_seq_length
        # Clear deleted GPU items
        for _ in range(3):
            gc.collect()
            clean_gpu_cache()
        patch_saving_functions(model, vision = True)
        patch_peft_fast_inference(model)

        # Add for_inference and for_training
        model.for_training = functools.partial(FastBaseModel.for_training, model)
        model.for_inference = functools.partial(FastBaseModel.for_inference, model)
        m = model
        while hasattr(m, "model"):
            m.for_training = functools.partial(FastBaseModel.for_training, m)
            m.for_inference = functools.partial(FastBaseModel.for_inference, m)
            m = m.model
        # Detect a stray pre-train forward so train() can drop the torch.compile
        # graph cache it would otherwise poison (see prepare_for_training_mode).
        _unsloth_install_pretrain_detector(model)
        model = _exclude_rope_inv_freq_from_ddp(model)
        return model

    @staticmethod
    def post_patch_model(
        model,
        use_gradient_checkpointing = True,
        trust_remote_code = False,
        model_type = None,
        tokenizer = None,
        float32_mixed_precision = None,
    ):
        full_finetuning = os.environ.get("UNSLOTH_ENABLE_FULL_FINETUNING", "0") == "1"

        if type(float32_mixed_precision) is bool:
            # Respect whatever it was set before
            pass
        else:
            float32_mixed_precision = True
            if _get_dtype(dtype_from_config(model.config)) == torch.bfloat16 and full_finetuning:
                # Use bfloat16 precision for full finetuning
                float32_mixed_precision = False

        # VLMs can hit DDP "marked ready twice" with re-entrant checkpointing.
        # See: https://github.com/unslothai/unsloth/issues/3713.
        use_reentrant = not is_distributed()
        if not use_reentrant:
            # Under DDP, avoid the offloaded/re-entrant checkpoint patch.
            unpatch_unsloth_gradient_checkpointing()
            unpatch_unsloth_smart_gradient_checkpointing()
            # Force native checkpoint to default to non-reentrant for downstream calls.
            _orig_checkpoint = torch_checkpoint.checkpoint

            def _nonre_checkpoint(function, *args, **kwargs):
                kwargs["use_reentrant"] = False
                return _orig_checkpoint(function, *args, **kwargs)

            torch_checkpoint.checkpoint = _nonre_checkpoint
            hf_modeling_utils.checkpoint = _nonre_checkpoint

        model = prepare_model_for_training(
            model,
            use_gradient_checkpointing = use_gradient_checkpointing,
            use_reentrant = use_reentrant,
            full_finetuning = full_finetuning,
            train_layernorms = full_finetuning,
            train_embedding = full_finetuning,
            train_lm_head = full_finetuning,
            float32_mixed_precision = float32_mixed_precision,
            patch_modules_to_save = True,
        )
        # Persist the configured GC mode so the trainer restores it verbatim.
        # for_inference() clears the module flags (GRPO does this every generation
        # step), and a plain TrainingArguments defaults gradient_checkpointing=False,
        # which would otherwise silently disable this setting at train time (#4735).
        model._unsloth_gradient_checkpointing = use_gradient_checkpointing

        # Gemma3N audio conformer processes variable-length audio tensors
        # that cause stride mismatches in AOT autograd compiled backward
        # when non-reentrant checkpointing is used. The notebook or TRL
        # may override gradient_checkpointing_kwargs with use_reentrant=False
        # after this point, so we intercept gradient_checkpointing_enable
        # to always force use_reentrant=True for Gemma3N.
        _model_type = getattr(getattr(model, "config", None), "model_type", "") or ""
        if "gemma3n" in _model_type.lower() or "gemma4" in _model_type.lower():
            _original_gc_enable = model.gradient_checkpointing_enable

            def _gc_enable_reentrant(**kwargs):
                gc_kwargs = kwargs.get("gradient_checkpointing_kwargs", {}) or {}
                gc_kwargs["use_reentrant"] = True
                kwargs["gradient_checkpointing_kwargs"] = gc_kwargs
                return _original_gc_enable(**kwargs)

            model.gradient_checkpointing_enable = _gc_enable_reentrant

        from transformers.trainer import Trainer

        if (
            Trainer._inner_training_loop.__name__ != "_fast_inner_training_loop"
            and trust_remote_code == False
        ):
            raise RuntimeError("Unsloth: Unsuccessfully patched inner_training_loop")
        patch_saving_functions(model, vision = True)

        # Patch tokenizer to pad to the left
        m = model
        while hasattr(m, "model"):
            if hasattr(m, "_saved_temp_tokenizer"):
                if hasattr(m._saved_temp_tokenizer, "tokenizer"):
                    m._saved_temp_tokenizer.tokenizer.padding_side = "left"
            m = m.model
        if hasattr(m, "_saved_temp_tokenizer"):
            if hasattr(m._saved_temp_tokenizer, "tokenizer"):
                m._saved_temp_tokenizer.tokenizer.padding_side = "left"
        # Prevent Transformers Trainer from auto-wrapping Unsloth LoRA models in DP.
        _mark_unsloth_disable_data_parallel(model, disable = not full_finetuning)

        # Clear deleted GPU items
        for _ in range(3):
            gc.collect()
            clean_gpu_cache()
        # Add for_inference and for_training
        model.for_training = functools.partial(FastBaseModel.for_training, model)
        model.for_inference = functools.partial(FastBaseModel.for_inference, model)
        m = model
        while hasattr(m, "model"):
            m.for_training = functools.partial(FastBaseModel.for_training, m)
            m.for_inference = functools.partial(FastBaseModel.for_inference, m)
            m = m.model
        # Set weight[padding_idx] = 0 for embeddings that are NOT tied with the
        # lm_head. When weights are tied, zeroing the padding row also zeros
        # the corresponding lm_head row, forcing logit = 0 for the pad token.
        # Only do this if tokenizer is defined since eos_token == pad_token sometimes!
        pad_token_id = getattr(tokenizer, "pad_token_id", None)
        lm_head = getattr(model, "lm_head", None)
        lm_head_weight = getattr(lm_head, "weight", None) if lm_head is not None else None
        if tokenizer is not None and getattr(tokenizer, "eos_token_id", None) != pad_token_id:
            with torch.no_grad():
                for name, module in model.named_modules():
                    if type(module) is torch.nn.Embedding:
                        if (
                            getattr(module, "weight", None) is not None
                            and getattr(module, "padding_idx", None) is not None
                        ):
                            if (
                                module.padding_idx == pad_token_id
                                and module.padding_idx < module.weight.shape[0]
                            ):
                                # Skip if tied to lm_head
                                if (
                                    lm_head_weight is not None
                                    and module.weight.data_ptr() == lm_head_weight.data_ptr()
                                ):
                                    continue
                                module.weight[module.padding_idx] = 0
        return model

    @staticmethod
    def for_inference(model):
        if not hasattr(model, "parameters"):
            raise TypeError(
                "Unsloth: I think you're passing a tokenizer, not the model to for_inference!"
            )

        def _for_inference(m):
            if hasattr(m, "gradient_checkpointing"):
                m.gradient_checkpointing = False
            if hasattr(m, "training"):
                m.training = False
            # Pad tokenizer to the left
            if hasattr(m, "_saved_temp_tokenizer"):
                m._saved_temp_tokenizer.padding_side = "left"
            # Set a flag for generation!
            m._flag_for_generation = True

        m = model
        while hasattr(m, "model"):
            _for_inference(m)
            m = m.model
        _for_inference(m)
        model.eval()  # to turn off training on modules deeper in

        # Since transformers 4.53, must turn off explicitly
        for module in model.modules():
            if hasattr(module, "gradient_checkpointing"):
                module.gradient_checkpointing = False

        # Also disable training for embeddings for NEFTune
        if hasattr(model, "get_input_embeddings"):
            embeddings = model.get_input_embeddings()
            if hasattr(embeddings, "training"):
                embeddings.training = False
        if hasattr(model, "get_output_embeddings"):
            embeddings = model.get_output_embeddings()
            if hasattr(embeddings, "training"):
                embeddings.training = False
        # Restore use_cache values that prepare_model_for_training disabled
        # for gradient checkpointing (older unsloth_zoo has no restore helper)
        try:
            from unsloth_zoo.training_utils import restore_use_cache
            restore_use_cache(model)
        except ImportError:
            pass

        # Must disable returning hidden states in the case for GRPO
        os.environ["UNSLOTH_RETURN_HIDDEN_STATES"] = "0"
        # Must enable returning logits
        os.environ["UNSLOTH_RETURN_LOGITS"] = "1"
        # Turn off skip guards and set stance to default
        if torch_compiler_set_stance is not None:
            torch_compiler_set_stance(stance = "default", skip_guard_eval_unsafe = False)
        return model

    @staticmethod
    def for_training(model, use_gradient_checkpointing = True):
        if not hasattr(model, "parameters"):
            raise TypeError(
                "Unsloth: I think you're passing a tokenizer, not the model to for_training!"
            )

        # Delete all fast inference loras
        for param in model.parameters():
            if hasattr(param, "_fast_lora"):
                del param._fast_lora

        def _for_training(m):
            if hasattr(m, "gradient_checkpointing"):
                m.gradient_checkpointing = use_gradient_checkpointing
            if hasattr(m, "training"):
                m.training = True
            # Pad tokenizer to the left
            if hasattr(m, "_saved_temp_tokenizer"):
                m._saved_temp_tokenizer.padding_side = "right"
            # Set a flag for generation!
            if hasattr(m, "_flag_for_generation"):
                try:
                    # Weirdly sometimes cannot succeed so do a try except
                    del m._flag_for_generation
                except:
                    pass

        m = model
        while hasattr(m, "model"):
            _for_training(m)
            m = m.model
        _for_training(m)
        model.train()  # to turn on training on modules deeper in

        # Since transformers 4.53, must turn on explicitly
        for module in model.modules():
            if hasattr(module, "gradient_checkpointing"):
                module.gradient_checkpointing = use_gradient_checkpointing

        # Also re-enable training for embeddings for NEFTune
        if hasattr(model, "get_input_embeddings"):
            embeddings = model.get_input_embeddings()
            if hasattr(embeddings, "training"):
                embeddings.training = True
        if hasattr(model, "get_output_embeddings"):
            embeddings = model.get_output_embeddings()
            if hasattr(embeddings, "training"):
                embeddings.training = True
        # Re-disable use_cache if prepare_model_for_training had disabled it
        # and for_inference restored it (record only exists after a disable)
        if (
            use_gradient_checkpointing
            and getattr(model, "_unsloth_use_cache_originals", None) is not None
        ):
            try:
                from unsloth_zoo.training_utils import disable_use_cache
                disable_use_cache(model)
            except ImportError:
                pass

        # Can re-enable not returning logits
        os.environ["UNSLOTH_RETURN_LOGITS"] = "0"
        # Turn off skip guards and set stance to default
        if torch_compiler_set_stance is not None:
            torch_compiler_set_stance(stance = "default", skip_guard_eval_unsafe = False)
        return model


def _looks_like_message_list(value):
    return isinstance(value, list) and (len(value) == 0 or isinstance(value[0], dict))


def _iter_message_lists(example, column):
    if _looks_like_message_list(example):
        yield example
        return
    if not isinstance(example, dict):
        return
    seen_keys = set()
    for key in (column, "messages", "conversations", "prompt", "completion"):
        if key in seen_keys:
            continue
        seen_keys.add(key)
        value = example.get(key)
        if _looks_like_message_list(value):
            yield value


def _local_path_from_video_value(video_path):
    # data: URIs are inline payloads, not files, and contain no "://"
    if video_path.startswith("data:"):
        return None
    if "://" not in video_path:
        return video_path
    if not video_path.startswith("file://"):
        return None
    from urllib.parse import urlparse
    from urllib.request import url2pathname

    parsed = urlparse(video_path)
    # RFC 8089: only an empty authority or "localhost" is the local machine
    if parsed.netloc and parsed.netloc != "localhost":
        return None
    path = url2pathname(parsed.path)
    return path or None


def check_dataset_for_missing_videos(
    dataset,
    column = "messages",
    raise_error = True,
    checked = None,
):
    """
    Validate that local video paths referenced in a dataset exist, catching
    missing files before training (torchvision otherwise returns an empty
    tensor and the model silently receives no video signal).

    Args:
        dataset:     Map-style Dataset, list of dicts, or iterable of examples
                     (not a streaming IterableDataset - iterating consumes it).
        column:      Chat-messages column, default "messages"; "conversations",
                     "prompt" and "completion" are also scanned.
        raise_error: True (default) raises FileNotFoundError listing missing
                     files; False warns and returns them.
        checked:     Optional set of known-good paths for cross-call dedup.

    Returns:
        List[str]: Missing file paths (empty when all exist).
    """
    try:
        from datasets import IterableDataset as _IterableDataset
        if isinstance(dataset, _IterableDataset):
            warnings.warn(
                "Unsloth: check_dataset_for_missing_videos received a streaming "
                "IterableDataset; iterating would exhaust it and training would "
                "see zero samples. Skipping validation - pass a map-style Dataset "
                "or rely on the UnslothVisionDataCollator's per-batch check.",
                stacklevel = 2,
            )
            return []
    except ImportError:
        pass

    missing = []
    # Report each missing path once; only confirmed-existing paths enter
    # `checked`, so retries after an error re-check previously missing files.
    seen_missing = set()
    if checked is None:
        checked = set()

    for example in dataset:
        for messages in _iter_message_lists(example, column):
            for msg in messages:
                if not isinstance(msg, dict):
                    continue
                content = msg.get("content", [])
                if not isinstance(content, (list, tuple)):
                    continue
                for item in content:
                    if not isinstance(item, dict) or item.get("type") != "video":
                        continue
                    video_path = item.get("video", "")
                    if not isinstance(video_path, str) or not video_path:
                        continue
                    path = _local_path_from_video_value(video_path)
                    if path is None or path in checked or path in seen_missing:
                        continue
                    if not os.path.isfile(path):
                        seen_missing.add(path)
                        missing.append(path)
                    else:
                        checked.add(path)

    if missing:
        missing_list = "\n".join(f"  - {p}" for p in missing)
        error_msg = (
            f"Unsloth: {len(missing)} video file(s) referenced in your dataset could not be found.\n"
            "Training would silently continue with empty video tensors - the model would receive\n"
            "no actual video signal while loss still appears to decrease.\n\n"
            f"Missing files:\n{missing_list}\n\n"
            "Fix: verify the video file paths in your dataset before calling the trainer."
        )
        if raise_error:
            raise FileNotFoundError(error_msg)
        warnings.warn(error_msg, stacklevel = 2)

    return missing


# Auto-enable grouped-GEMM MoE (transformers<5 ModuleList experts); see llama.py.
try:
    from unsloth_zoo.temporary_patches.moe_grouped_modulelist import wrap_loader_for_grouped_moe
    FastBaseModel.from_pretrained = staticmethod(
        wrap_loader_for_grouped_moe(FastBaseModel.from_pretrained)
    )
    FastBaseModel.get_peft_model = staticmethod(
        wrap_loader_for_grouped_moe(FastBaseModel.get_peft_model)
    )
except Exception:
    pass
