# Unsloth Zoo - Utilities for Unsloth # Copyright 2023-present Daniel Han-Chen, Michael Han-Chen & the Unsloth team. All rights reserved. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import torch import torch.nn.functional as F import os import shutil import sys import importlib import importlib.util from typing import Optional, Tuple from torch.autograd import Function from unsloth_zoo.mlx import is_mlx_available UNSLOTH_COMPILE_LOCATION = os.environ.get( "UNSLOTH_COMPILE_LOCATION", "unsloth_compiled_cache" ) try: import bitsandbytes as bnb from bitsandbytes.nn import Params4bit HAS_BNB = True except ImportError: HAS_BNB = False Params4bit = None def _get_compile_location() -> str: return os.path.abspath( os.environ.get("UNSLOTH_COMPILE_LOCATION", UNSLOTH_COMPILE_LOCATION) ) def _log_info(message: str): if os.environ.get("UNSLOTH_ENABLE_LOGGING", "0") == "1": print(message) def install_to_cache(source_path, destination_filename=None): """Copy a file into unsloth_compiled_cache so compiled modules can use it.""" compile_location = _get_compile_location() if not os.path.exists(compile_location): try: os.makedirs(compile_location) except: pass current_file = os.path.abspath(source_path) if destination_filename is None: destination_filename = os.path.basename(current_file) destination = os.path.abspath(os.path.join(compile_location, destination_filename)) if current_file != destination: try: shutil.copy(current_file, destination) except Exception: pass install_to_cache(__file__, "moe_utils.py") _CACHED_FORWARD_MOE_BACKEND = None _CACHED_MOE_UTILS_MODULE = None def _load_cached_moe_utils_module(): global _CACHED_MOE_UTILS_MODULE cache_file = os.path.abspath(os.path.join(_get_compile_location(), "moe_utils.py")) current_file = os.path.abspath(__file__) if not os.path.isfile(cache_file) or cache_file == current_file: return None try: module_name = "unsloth_cached_moe_utils" module = sys.modules.get(module_name, None) if module is not None and os.path.abspath(getattr(module, "__file__", "")) == cache_file: _CACHED_MOE_UTILS_MODULE = module return module spec = importlib.util.spec_from_file_location(module_name, cache_file) if spec is None or spec.loader is None: return None module = importlib.util.module_from_spec(spec) sys.modules[module_name] = module spec.loader.exec_module(module) _CACHED_MOE_UTILS_MODULE = module return module except Exception: return None def get_forward_moe_backend(): """Resolve forward_moe_backend from the compiled cache copy, else the local def.""" global _CACHED_FORWARD_MOE_BACKEND module = _load_cached_moe_utils_module() if module is not None and hasattr(module, "forward_moe_backend"): _CACHED_FORWARD_MOE_BACKEND = module.forward_moe_backend return _CACHED_FORWARD_MOE_BACKEND _CACHED_FORWARD_MOE_BACKEND = forward_moe_backend return _CACHED_FORWARD_MOE_BACKEND # Grouped MM wrapper around torch._grouped_mm; native backward works correctly. def _grouped_mm_with_backward_fix( inputs: torch.Tensor, weight: torch.Tensor, offsets: torch.Tensor ) -> torch.Tensor: """Grouped matmul via torch._grouped_mm with contiguous inputs. Some low-rank LoRA weights are contiguous but still have row strides below the kernel alignment requirement, so keep a narrow fallback for those cases. """ inputs = inputs.contiguous() weight = weight.contiguous() try: return torch._grouped_mm(inputs, weight, offs=offsets) except RuntimeError as exc: message = str(exc) if "strides should be multiple of 16 bytes" not in message: raise return _manual_grouped_mm(inputs, weight, offsets) def _manual_grouped_mm( inputs: torch.Tensor, weight: torch.Tensor, offsets: torch.Tensor ) -> torch.Tensor: """Differentiable grouped matmul fallback for torch._grouped_mm alignment gaps.""" outputs = [] start = 0 for expert_idx, end in enumerate(offsets.detach().cpu().tolist()): if start < end: outputs.append(torch.matmul(inputs[start:end], weight[expert_idx])) start = end if outputs: return torch.cat(outputs, dim=0) return inputs.new_empty((0, weight.shape[-1])) _GROUPED_GEMM_AVAILABLE = None _TORCH_GROUPED_MM_AVAILABLE = hasattr(torch, "_grouped_mm") # GPU support for torch._grouped_mm, verified via runtime probe. _TORCH_GROUPED_MM_SUPPORTED = None def _check_torch_grouped_mm_supported(): """Check torch._grouped_mm support on the current GPU; a runtime probe is the only reliable check.""" global _TORCH_GROUPED_MM_SUPPORTED if _TORCH_GROUPED_MM_SUPPORTED is not None: return _TORCH_GROUPED_MM_SUPPORTED if not _TORCH_GROUPED_MM_AVAILABLE: _TORCH_GROUPED_MM_SUPPORTED = False return False if not torch.cuda.is_available(): _TORCH_GROUPED_MM_SUPPORTED = False return False try: # Dummy call verifies real support (symbol may exist but hardware unsupported, e.g. < H100). device = torch.cuda.current_device() dtype = torch.float16 # 1 expert, 1 token, dim 8 (safe alignment). x = torch.ones((1, 8), device=device, dtype=dtype) w = torch.ones((1, 8, 8), device=device, dtype=dtype) offs = torch.tensor([1], device=device, dtype=torch.int32) torch._grouped_mm(x, w, offs=offs) del x, w, offs _TORCH_GROUPED_MM_SUPPORTED = True except Exception: _TORCH_GROUPED_MM_SUPPORTED = False return _TORCH_GROUPED_MM_SUPPORTED _TRITON_ALLOCATOR_INITIALIZED = False _PERSISTENT_BUFFER = None _original_peft_get_peft_model = None def _init_triton_allocator(): """Initialize a persistent Triton allocator to avoid per-call allocation overhead.""" global _TRITON_ALLOCATOR_INITIALIZED, _PERSISTENT_BUFFER if _TRITON_ALLOCATOR_INITIALIZED: return try: import triton # Persistent buffer that grows as needed, avoiding per-kernel allocations. def persistent_alloc_fn(size: int, alignment: int, stream): global _PERSISTENT_BUFFER # Round up to nearest 128 bytes for alignment / fewer reallocations. rounded_size = ((size + 128 - 1) // 128) * 128 if ( _PERSISTENT_BUFFER is None or _PERSISTENT_BUFFER.numel() * _PERSISTENT_BUFFER.element_size() < rounded_size ): # 10% headroom; uint8 for raw byte storage. _PERSISTENT_BUFFER = torch.empty( int(rounded_size * 1.1), device="cuda", dtype=torch.uint8 ) _PERSISTENT_BUFFER.__hibernate__ = {"type": "ignore"} return _PERSISTENT_BUFFER triton.set_allocator(persistent_alloc_fn) triton._unsloth_allocator_set = True _TRITON_ALLOCATOR_INITIALIZED = True except Exception: pass def _check_grouped_gemm_available(): """Check if Unsloth grouped GEMM kernels are available.""" if os.environ.get("UNSLOTH_DISABLE_MOE_TRITON", "0") == "1": return False if is_mlx_available(): return False global _GROUPED_GEMM_AVAILABLE if _GROUPED_GEMM_AVAILABLE is not None: return _GROUPED_GEMM_AVAILABLE try: from unsloth.kernels.moe.grouped_gemm.interface import grouped_gemm, supports_tma _GROUPED_GEMM_AVAILABLE = True _init_triton_allocator() except (ImportError, ModuleNotFoundError): _GROUPED_GEMM_AVAILABLE = False return _GROUPED_GEMM_AVAILABLE from functools import lru_cache, wraps @lru_cache(maxsize=1) def select_moe_backend(): """Select MoE backend from UNSLOTH_MOE_BACKEND + availability. Choices: "grouped_mm", "unsloth_triton", "native_torch" (default "grouped_mm"). """ # This Unsloth Zoo code section is licensed under AGPL3 requested = os.environ.get("UNSLOTH_MOE_BACKEND") if requested: if requested == "grouped_mm" and _check_torch_grouped_mm_supported(): return "grouped_mm" if requested == "unsloth_triton" and _check_grouped_gemm_available(): return "unsloth_triton" if requested == "native_torch": return "native_torch" _log_info(f"Unsloth: '{requested}' backend requested but is not available. Falling back to next available.") if _check_torch_grouped_mm_supported(): _log_info("Unsloth: Using MoE backend 'grouped_mm'") return "grouped_mm" if _check_grouped_gemm_available(): _log_info("Unsloth: Using MoE backend 'unsloth_triton'") return "unsloth_triton" return "native_torch" def swap_moe_weights_for_call(experts_module, gate_up_proj, down_proj, forward_fn, *args): """Temporarily install dequantized weights for one forward call, then restore. Uses object.__setattr__ to bypass nn.Module Parameter (de)registration (re-registers hooks, unnecessary for read-only temp tensors). Used by the FP8 and bnb4bit MoE dispatchers. """ original_gate_up = experts_module.gate_up_proj original_down = experts_module.down_proj object.__setattr__(experts_module, "gate_up_proj", gate_up_proj) object.__setattr__(experts_module, "down_proj", down_proj) try: return forward_fn(experts_module, *args) finally: object.__setattr__(experts_module, "gate_up_proj", original_gate_up) object.__setattr__(experts_module, "down_proj", original_down) def forward_moe_backend( self, hidden_states: torch.Tensor, top_k_index: torch.Tensor, top_k_weights: torch.Tensor, ) -> torch.Tensor: """Dispatch MoE forward to the selected backend (keeps model-specific patches minimal).""" # This Unsloth Zoo code section is licensed under AGPL3 # Absolute imports: this function is also copied into # unsloth_compiled_cache/moe_utils.py where relative imports of sibling # helpers don't resolve (only the dispatcher is copied). # Keep `except ImportError` around ONLY the import; runtime errors in the # bnb4bit/fp8 path must propagate, not fall through to a crashing backend. _moe_uses_bnb4bit_expert_weights = forward_moe_backend_bnb4bit = None try: from unsloth_zoo.temporary_patches.moe_utils_bnb4bit import ( _moe_uses_bnb4bit_expert_weights, forward_moe_backend_bnb4bit, ) except ImportError: pass if _moe_uses_bnb4bit_expert_weights is not None and _moe_uses_bnb4bit_expert_weights(self): result = forward_moe_backend_bnb4bit(self, hidden_states, top_k_index, top_k_weights) if result is not None: return result _moe_uses_fp8_expert_weights = forward_moe_backend_fp8 = None try: from unsloth_zoo.temporary_patches.moe_utils_fp8 import ( _moe_uses_fp8_expert_weights, forward_moe_backend_fp8, ) except ImportError: pass if _moe_uses_fp8_expert_weights is not None and _moe_uses_fp8_expert_weights(self): return forward_moe_backend_fp8(self, hidden_states, top_k_index, top_k_weights) backend = select_moe_backend() if backend == "grouped_mm": return forward_native_grouped_mm(self, hidden_states, top_k_index, top_k_weights) if backend == "unsloth_triton": return forward_triton_grouped_gemm(self, hidden_states, top_k_index, top_k_weights) return forward_native_moe_loop(self, hidden_states, top_k_index, top_k_weights) @torch.no_grad() def _get_routing_indices(selected_experts, num_experts): """Compute token->expert mapping for grouped GEMM. Returns (token_counts_by_expert (num_experts,), gather_indices (total_tokens,)). """ # This Unsloth Zoo code section is licensed under AGPL3 flat_experts = selected_experts.view(-1) # bincount avoids histc's float conversion overhead. token_counts_by_expert = torch.bincount(flat_experts, minlength=num_experts).to(torch.int32) # stable=True preserves order within each expert. gather_indices = flat_experts.argsort(stable=True) return token_counts_by_expert, gather_indices def _silu_and_mul(x): """Fused SiLU + element-wise multiply for gate/up projections.""" gate, up = x.chunk(2, dim=-1) return F.silu(gate) * up # Separated LoRA helpers. def _has_lora_adapters(param) -> bool: """Check for active LoRA adapters (PEFT ParamWrapper).""" if not hasattr(param, "lora_A") or not hasattr(param, "lora_B"): return False if hasattr(param, "disable_adapters") and param.disable_adapters: return False if hasattr(param, "merged") and param.merged: return False return len(param.lora_A) > 0 def _canonical_lora_weights_for_grouped_mm( weight_A: torch.Tensor, weight_B: torch.Tensor, num_experts: int, rank_per_expert: int, dim_A: int, dim_B: int, ) -> Tuple[torch.Tensor, torch.Tensor]: first_weight = weight_A.view(num_experts, rank_per_expert, dim_A) first_weight = first_weight.permute(0, 2, 1).contiguous() second_weight = weight_B.view(dim_B, num_experts, rank_per_expert) second_weight = second_weight.permute(1, 2, 0).contiguous() return first_weight, second_weight def _reversed_lora_weights_for_grouped_mm( weight_A: torch.Tensor, weight_B: torch.Tensor, num_experts: int, rank_per_expert: int, dim_A: int, dim_B: int, ) -> Tuple[torch.Tensor, torch.Tensor]: first_weight = weight_B.view(dim_B, num_experts, rank_per_expert) first_weight = first_weight.permute(1, 0, 2).contiguous() second_weight = weight_A.view(num_experts, rank_per_expert, dim_A).contiguous() return first_weight, second_weight def _get_param_shape_from_module(module, parameter_name): if module is None or parameter_name is None or not hasattr(module, parameter_name): return None param = getattr(module, parameter_name) if hasattr(param, "get_param"): param = param.get_param() elif hasattr(param, "weight"): param = param.weight return tuple(param.shape) def _get_moe_lora_io_dims(wrapper, experts_module=None): base = None if wrapper is not None and hasattr(wrapper, "get_base_layer"): base = wrapper.get_base_layer() if experts_module is None: experts_module = base if experts_module is None: experts_module = getattr(wrapper, "base_layer", None) parameter_name = getattr(wrapper, "parameter_name", None) source = experts_module if experts_module is not None else base if source is None: return None, None _set_gpt_oss_grouped_mm_format_on_experts(source) shape = _get_param_shape_from_module(source, parameter_name) if shape is not None and len(shape) >= 3: grouped_mm_format = bool(getattr(source, "_unsloth_grouped_mm_format", False)) if grouped_mm_format: return shape[-2], shape[-1] return shape[-1], shape[-2] hidden_dim = getattr(source, "hidden_dim", None) intermediate_dim = getattr(source, "intermediate_dim", None) if hidden_dim is None or intermediate_dim is None: return None, None if parameter_name == "gate_up_proj": return hidden_dim, 2 * intermediate_dim if parameter_name == "down_proj": return intermediate_dim, hidden_dim return None, None def extract_moe_lora_weights_for_grouped_mm( wrapper, weight_A: torch.Tensor, weight_B: torch.Tensor, scaling, num_experts: int, *, experts_module=None, input_dim=None, output_dim=None, model_name: str = "MoE", enable_logging: bool = None, logger_obj=None, ) -> Tuple[torch.Tensor, torch.Tensor, float, int]: total_rank = weight_A.shape[0] rank_per_expert = total_rank // num_experts dim_A = weight_A.shape[1] dim_B = weight_B.shape[0] if num_experts <= 1: return weight_A.T, weight_B.T, scaling, num_experts if input_dim is None or output_dim is None: inferred_input_dim, inferred_output_dim = _get_moe_lora_io_dims( wrapper, experts_module=experts_module, ) if input_dim is None: input_dim = inferred_input_dim if output_dim is None: output_dim = inferred_output_dim canonical_match = ( input_dim is not None and output_dim is not None and dim_A == input_dim and dim_B == output_dim ) reversed_match = ( input_dim is not None and output_dim is not None and dim_A == output_dim and dim_B == input_dim ) if canonical_match and reversed_match: if bool(getattr(wrapper, "_did_swap_in_out_features", False)): first_weight, second_weight = _reversed_lora_weights_for_grouped_mm( weight_A, weight_B, num_experts, rank_per_expert, dim_A, dim_B, ) else: first_weight, second_weight = _canonical_lora_weights_for_grouped_mm( weight_A, weight_B, num_experts, rank_per_expert, dim_A, dim_B, ) return first_weight, second_weight, scaling, num_experts if canonical_match: first_weight, second_weight = _canonical_lora_weights_for_grouped_mm( weight_A, weight_B, num_experts, rank_per_expert, dim_A, dim_B, ) return first_weight, second_weight, scaling, num_experts if reversed_match: first_weight, second_weight = _reversed_lora_weights_for_grouped_mm( weight_A, weight_B, num_experts, rank_per_expert, dim_A, dim_B, ) return first_weight, second_weight, scaling, num_experts if logger_obj is not None: if enable_logging is None: enable_logging = os.environ.get("UNSLOTH_ENABLE_LOGGING", "0") == "1" if enable_logging and (input_dim is not None or output_dim is not None): logger_obj.warning( f"Unsloth: {model_name} LoRA extractor could not match either layout " f"(weight_A={tuple(weight_A.shape)}, weight_B={tuple(weight_B.shape)}, " f"expected input_dim={input_dim}, output_dim={output_dim}, " f"num_experts={num_experts}). Falling back to canonical layout. " "If this is a new PEFT version, the LoRA delta may be wrong." ) first_weight, second_weight = _canonical_lora_weights_for_grouped_mm( weight_A, weight_B, num_experts, rank_per_expert, dim_A, dim_B, ) return first_weight, second_weight, scaling, num_experts def _extract_lora_from_wrapper( wrapper, adapter_name: str = "default", experts_module=None ) -> Optional[Tuple[torch.Tensor, torch.Tensor, float, int]]: """Extract LoRA weights from a PEFT ParamWrapper for MoE separated grouped_mm. PEFT 3D ParamWrapper gives lora_A: (E*R, in_dim), lora_B: (out_dim, E*R); reshaped to first_weight (E, in_dim, R), second_weight (E, R, out_dim) so delta = X @ first @ second. Handles both standard (E, out, in) Qwen3-MoE and transposed (E, in, out) Qwen3-VL-MoE base weight layouts. Returns (first_weight, second_weight, scaling, num_experts) or None. """ # This Unsloth Zoo code section is licensed under AGPL3 try: if not hasattr(wrapper, "lora_A") or not hasattr(wrapper, "lora_B"): return None if hasattr(wrapper, "disable_adapters") and wrapper.disable_adapters: return None if hasattr(wrapper, "merged") and wrapper.merged: return None if not wrapper.lora_A: return None if adapter_name not in wrapper.lora_A: adapter_name = list(wrapper.lora_A.keys())[0] lora_A_module = wrapper.lora_A[adapter_name] lora_B_module = wrapper.lora_B[adapter_name] weight_A = lora_A_module.weight # (E*R, dim1) weight_B = lora_B_module.weight # (dim2, E*R) scaling = wrapper.scaling[adapter_name] num_experts = getattr(wrapper, "num_experts", 1) if experts_module is None: experts_module = wrapper.get_base_layer() if hasattr(wrapper, "get_base_layer") else None # Model-specific LoRA extractor attached to the experts module, if any. extractor_fn = getattr(experts_module, "_unsloth_lora_extractor_fn", None) if extractor_fn is not None: return extractor_fn(wrapper, weight_A, weight_B, scaling, num_experts) return extract_moe_lora_weights_for_grouped_mm( wrapper, weight_A, weight_B, scaling, num_experts, experts_module=experts_module, model_name="MoE", ) except Exception: return None def _extract_lora_weights( param, adapter_name: str = "default", num_experts: int = None, experts_module=None ) -> Optional[Tuple[torch.Tensor, torch.Tensor, float]]: """Compat wrapper around _extract_lora_from_wrapper; returns (first, second, scaling).""" # This Unsloth Zoo code section is licensed under AGPL3 # Pass num_experts through so _extract_lora_from_wrapper can use it. if num_experts is not None and not hasattr(param, "num_experts"): param.num_experts = num_experts result = _extract_lora_from_wrapper(param, adapter_name, experts_module=experts_module) if result is None: return None return result[0], result[1], result[2] def _get_base_weight(param): """Get base weight from a potentially wrapped parameter or module.""" # This Unsloth Zoo code section is licensed under AGPL3 while hasattr(param, "base_layer"): param = param.base_layer if HAS_BNB and isinstance(param, Params4bit): if getattr(param, "quant_state", None) is None: raise RuntimeError( "unsloth: _get_base_weight saw a Params4bit with quant_state=None. " "This usually means the model was used in forward before loading " "completed quantization (meta placeholder still in place), or the " "MoE quantizer patch did not fire for this expert. " f"data.shape={tuple(param.data.shape)}, device={param.device}." ) return bnb.functional.dequantize_4bit(param.data, param.quant_state) if hasattr(param, "get_param"): return param.get_param() if hasattr(param, "weight"): return param.weight return param def _get_lora_wrapper_for_param(experts_module, param_name): """Get the PEFT ParamWrapper for gate_up_proj or down_proj; does not lazily set up wrappers.""" # This Unsloth Zoo code section is licensed under AGPL3 if hasattr(experts_module, f"{param_name}_lora_wrapper"): return getattr(experts_module, f"{param_name}_lora_wrapper") if hasattr(experts_module, param_name): attr = getattr(experts_module, param_name) if hasattr(attr, "lora_A"): # ParamWrapper return attr return None def native_moe_grouped_mm( inputs: torch.Tensor, weight: torch.Tensor, offsets: torch.Tensor ) -> torch.Tensor: """Grouped_mm with backward fix for PyTorch's grouped_mm backward stride bug.""" return _grouped_mm_with_backward_fix(inputs, weight, offsets) def _apply_lora_grouped_mm( inputs: torch.Tensor, lora_B: torch.Tensor, lora_A: torch.Tensor, offsets: torch.Tensor, scaling: float, grouped_mm_func=native_moe_grouped_mm, ) -> torch.Tensor: """Apply LoRA via grouped GEMM: result = ((X @ B) @ A) * scaling. inputs (total_tokens, in_dim); lora_B (E, in_dim, R); lora_A (E, R, out_dim). """ # This Unsloth Zoo code section is licensed under AGPL3 # X @ B then result @ A; both already in native (E, ...) layout, no transpose. lora_intermediate = grouped_mm_func(inputs, lora_B.contiguous(), offsets) lora_delta = grouped_mm_func(lora_intermediate, lora_A.contiguous(), offsets) return lora_delta * scaling def _should_use_separated_lora() -> bool: """Use separated LoRA (default True); UNSLOTH_MOE_LORA_MERGED=1 forces the merged path.""" return os.environ.get("UNSLOTH_MOE_LORA_MERGED", "0") != "1" # Model-specific weight preprocessing hooks: each model registers a transposition # function so the generic backend works across weight layouts. _WEIGHT_PREPROCESSORS = {} def register_weight_preprocessor(model_type: str, preprocessor_fn): """Register a weight preprocessor (weight, proj_type, hidden_dim) -> weight for a model type.""" _WEIGHT_PREPROCESSORS[model_type] = preprocessor_fn def get_weight_preprocessor(model_type: str): """Get registered weight preprocessor for model type.""" return _WEIGHT_PREPROCESSORS.get(model_type) def preprocess_weight( weight: torch.Tensor, proj_type: str, hidden_dim: int, model_type=None ): """Preprocess a weight into (E, in_dim, out_dim) for grouped_mm. Uses a registered model-specific preprocessor if present, else transposes by shape. proj_type is "gate_up" or "down". """ # This Unsloth Zoo code section is licensed under AGPL3 if model_type and model_type in _WEIGHT_PREPROCESSORS: return _WEIGHT_PREPROCESSORS[model_type](weight, proj_type, hidden_dim) if proj_type == "gate_up": # Want (E, hidden_dim, 2*intermediate). if weight.shape[1] == hidden_dim: return weight else: return weight.transpose(-2, -1) else: # down # Want (E, intermediate, hidden_dim). if weight.shape[2] == hidden_dim: return weight else: return weight.transpose(-2, -1) # Generic MoE detection and ParamWrapper patching. def _normalize_model_type(value) -> str: if value is None: return "" return str(value).lower().replace("-", "_") def _iter_model_configs(model): seen = set() queue = [model] while queue and len(seen) < 8: current = queue.pop(0) if current is None: continue current_id = id(current) if current_id in seen: continue seen.add(current_id) config = getattr(current, "config", None) if config is not None: yield config for attr in ("base_model", "model"): nested = getattr(current, attr, None) if nested is not None and nested is not current: queue.append(nested) def _is_gpt_oss_model(model) -> bool: for config in _iter_model_configs(model): model_type = _normalize_model_type(getattr(config, "model_type", None)) if model_type == "gpt_oss": return True for attr in ("_name_or_path", "name_or_path"): name = getattr(config, attr, None) if name is None: continue # Match only the final path component so parent directories like # /data/gpt-oss-tests/qwen3-7b do not count as gpt-oss. base = str(name).replace("\\", "/").rstrip("/").rsplit("/", 1)[-1] if "gpt_oss" in _normalize_model_type(base): return True return False def _set_gpt_oss_grouped_mm_format_on_experts(module) -> bool: if module is None: return False if module.__class__.__name__ != "GptOssExperts": return False if bool(getattr(module, "_unsloth_grouped_mm_format", False)): return False # Require the gpt-oss (E, in, out) weight signature: gate_up's out dim is # twice down's in dim. Same-named classes with other layouts stay unflagged. gate_shape = _get_param_shape_from_module(module, "gate_up_proj") down_shape = _get_param_shape_from_module(module, "down_proj") if gate_shape is None or down_shape is None: return False if len(gate_shape) < 3 or len(down_shape) < 3: return False if gate_shape[0] != down_shape[0]: return False if gate_shape[-2] != down_shape[-1] or gate_shape[-1] != 2 * down_shape[-2]: return False module._unsloth_grouped_mm_format = True return True def patch_gpt_oss_grouped_mm_format(model) -> int: """ Mark GPT-OSS experts as storing weights in grouped_mm format. Stock transformers GPT-OSS experts use (E, in_dim, out_dim) tensors but do not carry Unsloth's `_unsloth_grouped_mm_format` instance flag. Set it on live expert modules so the shared MoE LoRA extractor chooses GPT-OSS ordering instead of the Qwen-style fallback. """ # This Unsloth Zoo code section is licensed under AGPL3 if model is None or not _is_gpt_oss_model(model): return 0 modules = getattr(model, "modules", None) if not callable(modules): return 0 updated = 0 for module in modules(): if _set_gpt_oss_grouped_mm_format_on_experts(module): updated += 1 return updated def _patch_peft_get_peft_model_for_moe(): # This Unsloth Zoo code section is licensed under AGPL3 global _original_peft_get_peft_model if _original_peft_get_peft_model is not None: return try: import peft except Exception: return original_get_peft_model = getattr(peft, "get_peft_model", None) if original_get_peft_model is None: return if getattr(original_get_peft_model, "_unsloth_moe_patched", False): return _original_peft_get_peft_model = original_get_peft_model @wraps(original_get_peft_model) def patched_get_peft_model(model, *args, **kwargs): peft_model = original_get_peft_model(model, *args, **kwargs) try: patch_gpt_oss_grouped_mm_format(model) if peft_model is not model: patch_gpt_oss_grouped_mm_format(peft_model) except Exception: pass return peft_model patched_get_peft_model._unsloth_moe_patched = True peft.get_peft_model = patched_get_peft_model for module_name in ("peft.mapping_func", "peft.mapping"): try: module = importlib.import_module(module_name) except Exception: continue if getattr(module, "get_peft_model", None) is original_get_peft_model: module.get_peft_model = patched_get_peft_model def _is_moe_experts_module(module) -> bool: """Generic check for an MoE experts layer with stacked 3D expert weights. Matches gate_up_proj/down_proj (Qwen3-MoE etc.) or w1/w2/w3 (older models). """ # This Unsloth Zoo code section is licensed under AGPL3 import torch.nn as nn # After PEFT's parametrize wrapping, gate_up_proj is a Tensor (not Parameter), # so accept both. if hasattr(module, "gate_up_proj"): param = module.gate_up_proj # 4-bit params are packed into 2D tensors. if HAS_BNB and isinstance(param, Params4bit) and param.ndim == 2: return True # Standard MoE weights are 3D (num_experts, in, out). if isinstance(param, (nn.Parameter, torch.Tensor)) and param.ndim in (2, 3): return True # w1/w2 pattern (separate gate/up projections). if hasattr(module, "w1") and hasattr(module, "w2"): w1 = module.w1 if isinstance(w1, (nn.Parameter, torch.Tensor)) and w1.ndim in (2, 3): return True return False # Aliases for compatibility with gpt_oss.py _get_moe_lora_weights = _extract_lora_from_wrapper # Store original ParamWrapper.forward for fallback _original_param_wrapper_forward = None def _patched_param_wrapper_forward( self, x: torch.Tensor, *args, **kwargs ) -> torch.Tensor: """Patched ParamWrapper.forward for MoE separated LoRA. For MoE experts: bypass PEFT's _activate_lora and stash LoRA data by parameter_name for forward_native_grouped_mm. For non-MoE: original forward. """ # This Unsloth Zoo code section is licensed under AGPL3 # Use self.base_layer (immediate parent), NOT get_base_layer() which recurses # to the deepest layer; the wrapper chain down_proj -> gate_up_proj -> # Qwen3MoeExperts must be preserved. immediate_base_layer = self.base_layer # For stashing LoRA data we need the actual experts module (recursive lookup). experts_module = self.get_base_layer() use_separated = _should_use_separated_lora() param_name = getattr(self, "parameter_name", None) if ( use_separated and param_name in ("gate_up_proj", "down_proj") and _is_moe_experts_module(experts_module) ): # MoE experts: bypass PEFT's _activate_lora, use separated computation. if self.disable_adapters: if self.merged: self.unmerge() return immediate_base_layer(x, *args, **kwargs) if self.merged: return immediate_base_layer(x, *args, **kwargs) # Ensure wrapper.num_experts is set for LoRA weight reshaping. if not hasattr(self, "num_experts"): if hasattr(experts_module, "num_experts"): self.num_experts = experts_module.num_experts elif hasattr(experts_module, param_name): p = getattr(experts_module, param_name) if hasattr(p, "shape") and len(p.shape) >= 1: self.num_experts = p.shape[0] # Extract LoRA for this parameter and stash on the experts module # (not base_layer): _unsloth_lora_gate_up_proj / _unsloth_lora_down_proj. lora_data = _extract_lora_from_wrapper(self) if lora_data is not None and param_name: lora_attr = f"_unsloth_lora_{param_name}" setattr(experts_module, lora_attr, lora_data) try: # Immediate base_layer preserves the wrapper chain. result = immediate_base_layer(x, *args, **kwargs) finally: if param_name: lora_attr = f"_unsloth_lora_{param_name}" if hasattr(experts_module, lora_attr): delattr(experts_module, lora_attr) return result # Non-MoE: original PEFT forward with _activate_lora. return _original_param_wrapper_forward(self, x, *args, **kwargs) def patch_param_wrapper_for_moe(): """Patch PEFT's ParamWrapper.forward for MoE separated LoRA (call after PEFT import).""" # This Unsloth Zoo code section is licensed under AGPL3 global _original_param_wrapper_forward module = _load_cached_moe_utils_module() if module is not None and hasattr(module, "patch_param_wrapper_for_moe"): try: return module.patch_param_wrapper_for_moe() except Exception: pass try: from peft.tuners.lora.layer import ParamWrapper if _original_param_wrapper_forward is None: _original_param_wrapper_forward = ParamWrapper.forward ParamWrapper.forward = _patched_param_wrapper_forward _patch_peft_get_peft_model_for_moe() return True except ImportError: return False def forward_native_grouped_mm( self, hidden_states: torch.Tensor, top_k_index: torch.Tensor, top_k_weights: torch.Tensor, ) -> torch.Tensor: """Native PyTorch grouped-GEMM MoE forward via torch._grouped_mm (no Triton; needs runtime support).""" # This Unsloth Zoo code section is licensed under AGPL3 # Runtime safety check (defense in depth). if not _check_torch_grouped_mm_supported(): major, minor = torch.cuda.get_device_capability(torch.cuda.current_device()) raise RuntimeError( f"torch._grouped_mm is not supported on this device (Compute Capability {major}.{minor}). " f"Set UNSLOTH_MOE_BACKEND='unsloth_triton' or 'native_torch' to use a compatible backend." ) is_2d_input = hidden_states.dim() == 2 if is_2d_input: sequence_length, hidden_dim = hidden_states.shape batch_size = 1 else: batch_size, sequence_length, hidden_dim = hidden_states.shape hidden_states = hidden_states.view(-1, hidden_dim) # Routing: count tokens per expert, sort to group by expert, gather inputs. flat_top_k = top_k_index.view(-1) num_tokens_per_expert = torch.bincount(flat_top_k, minlength=self.num_experts).int() sorted_indices = torch.argsort(flat_top_k, stable=True) token_indices = sorted_indices // top_k_index.shape[-1] permuted_input = hidden_states[token_indices] offsets = torch.cumsum(num_tokens_per_expert, dim=0, dtype=torch.int32) # Gate + Up projection with optional separated LoRA (default). use_separated_lora = _should_use_separated_lora() gate_up_lora = None # Prefer LoRA injected by the patched ParamWrapper; fall back to the parameter. if getattr(self, "_unsloth_lora_gate_up_proj", None) is not None: gate_up_lora = self._unsloth_lora_gate_up_proj[:3] # (first, second, scaling) elif ( use_separated_lora and hasattr(self, "gate_up_proj") and _has_lora_adapters(self.gate_up_proj) ): gate_up_lora = _extract_lora_weights( self.gate_up_proj, num_experts=self.num_experts, experts_module=self ) if hasattr(self, "gate_up_proj"): gate_up_base = _get_base_weight(self.gate_up_proj) model_type = getattr(self, "_unsloth_model_type", None) # grouped_mm backward needs contiguous weights; preprocess may return a transposed view. w1 = preprocess_weight(gate_up_base, "gate_up", hidden_dim, model_type) mm1_out = _grouped_mm_with_backward_fix(permuted_input, w1, offsets) # Separated LoRA: + ((X @ first) @ second) * scaling. if gate_up_lora is not None: first_weight, second_weight, scaling = gate_up_lora # Cast to input dtype (LoRA is float32) and make contiguous for grouped_mm. first_weight = first_weight.to(permuted_input.dtype).contiguous() second_weight = second_weight.to(permuted_input.dtype).contiguous() try: lora_out = _grouped_mm_with_backward_fix(permuted_input, first_weight, offsets) lora_out = lora_out.contiguous() except RuntimeError as e: raise e # Second matmul; pad an unaligned output dim or fall back on failure. try: if second_weight.shape[-1] % 8 != 0: pad_size = 8 - (second_weight.shape[-1] % 8) second_weight_padded = F.pad( second_weight, (0, pad_size) ).contiguous() lora_delta = _grouped_mm_with_backward_fix( lora_out, second_weight_padded, offsets ) lora_delta = lora_delta[:, :-pad_size] else: lora_delta = _grouped_mm_with_backward_fix( lora_out, second_weight, offsets ) except RuntimeError: # Manual loop fallback on grouped_mm failure (e.g. stride alignment). lora_delta = torch.empty( (lora_out.shape[0], second_weight.shape[-1]), dtype=lora_out.dtype, device=lora_out.device, ) cpu_offsets = offsets.cpu().tolist() prev_offset = 0 for i, end in enumerate(cpu_offsets): if prev_offset < end: lora_delta[prev_offset:end] = torch.matmul( lora_out[prev_offset:end], second_weight[i] ) prev_offset = end mm1_out = mm1_out + lora_delta * scaling if hasattr(self, "gate_up_proj_bias") and self.gate_up_proj_bias is not None: num_repeats = num_tokens_per_expert.to(self.gate_up_proj_bias.device) bias_expanded = self.gate_up_proj_bias.repeat_interleave(num_repeats, dim=0) mm1_out = mm1_out + bias_expanded.to(mm1_out.dtype) if "GptOssExperts" in self.__class__.__name__: gate = mm1_out[..., ::2] up = mm1_out[..., 1::2] else: gate, up = mm1_out.chunk(2, dim=-1) elif hasattr(self, "w1") and hasattr(self, "w3"): # Separate w1/w3 weights (older models). w1_base = _get_base_weight(self.w1) w3_base = _get_base_weight(self.w3) w1 = w1_base.transpose(-2, -1) w3 = w3_base.transpose(-2, -1) gate = _grouped_mm_with_backward_fix(permuted_input, w1, offsets) up = _grouped_mm_with_backward_fix(permuted_input, w3, offsets) # Add LoRA for w1 and w3 separately if present. if use_separated_lora: if _has_lora_adapters(self.w1): w1_lora = _extract_lora_weights(self.w1, experts_module=self) if w1_lora is not None: lora_A, lora_B, scaling = w1_lora lora_A_t = lora_A.transpose(-2, -1) lora_A_out = _grouped_mm_with_backward_fix( permuted_input, lora_A_t, offsets ) lora_B_t = lora_B.transpose(-2, -1) lora_B_out = _grouped_mm_with_backward_fix(lora_A_out, lora_B_t, offsets) gate = gate + lora_B_out * scaling if _has_lora_adapters(self.w3): w3_lora = _extract_lora_weights(self.w3, experts_module=self) if w3_lora is not None: lora_A, lora_B, scaling = w3_lora lora_A_t = lora_A.transpose(-2, -1) lora_A_out = _grouped_mm_with_backward_fix( permuted_input, lora_A_t, offsets ) lora_B_t = lora_B.transpose(-2, -1) lora_B_out = _grouped_mm_with_backward_fix(lora_A_out, lora_B_t, offsets) up = up + lora_B_out * scaling else: raise AttributeError("MoE layer must have 'gate_up_proj' or 'w1'/'w3'.") # Activation if "GptOssExperts" in self.__class__.__name__: # Custom GptOss activation. limit = getattr(self, "limit", 7.0) alpha = getattr(self, "alpha", 1.702) gate = gate.clamp(min=None, max=limit) up = up.clamp(min=-limit, max=limit) glu = gate * torch.sigmoid(gate * alpha) inter = (up + 1.0) * glu elif hasattr(self, 'act_fn') and callable(self.act_fn): inter = self.act_fn(gate) * up else: inter = F.silu(gate) * up # Down projection with optional separated LoRA (default). down_lora = None # Prefer LoRA injected by the patched ParamWrapper; fall back to the parameter. if getattr(self, "_unsloth_lora_down_proj", None) is not None: down_lora = self._unsloth_lora_down_proj[:3] # (first, second, scaling) elif ( use_separated_lora and hasattr(self, "down_proj") and _has_lora_adapters(self.down_proj) ): down_lora = _extract_lora_weights(self.down_proj, num_experts=self.num_experts, experts_module=self) if hasattr(self, "down_proj"): down_base = _get_base_weight(self.down_proj) model_type = getattr(self, "_unsloth_model_type", None) w2 = preprocess_weight(down_base, "down", hidden_dim, model_type) mm2_out = _grouped_mm_with_backward_fix(inter, w2, offsets) if down_lora is not None: first_weight, second_weight, scaling = down_lora # Cast to input dtype (LoRA is float32) and make contiguous for grouped_mm. first_weight = first_weight.to(inter.dtype).contiguous() second_weight = second_weight.to(inter.dtype).contiguous() lora_out = _grouped_mm_with_backward_fix(inter, first_weight, offsets) lora_out = lora_out.contiguous() try: lora_delta = _grouped_mm_with_backward_fix(lora_out, second_weight, offsets) except RuntimeError: # Manual loop fallback. lora_delta = torch.empty( (lora_out.shape[0], second_weight.shape[-1]), dtype=lora_out.dtype, device=lora_out.device, ) cpu_offsets = offsets.cpu().tolist() prev_offset = 0 for i, end in enumerate(cpu_offsets): if prev_offset < end: lora_delta[prev_offset:end] = torch.matmul( lora_out[prev_offset:end], second_weight[i] ) prev_offset = end mm2_out = mm2_out + lora_delta * scaling if hasattr(self, "down_proj_bias") and self.down_proj_bias is not None: bias_expanded = self.down_proj_bias.repeat_interleave( num_tokens_per_expert.to(self.down_proj_bias.device), dim=0 ).to(mm2_out.device) mm2_out = mm2_out + bias_expanded.to(mm2_out.dtype) elif hasattr(self, "w2"): w2_base = _get_base_weight(self.w2) w2 = w2_base.transpose(-2, -1) mm2_out = _grouped_mm_with_backward_fix(inter, w2, offsets) if use_separated_lora and _has_lora_adapters(self.w2): w2_lora = _extract_lora_weights(self.w2, experts_module=self) if w2_lora is not None: lora_A, lora_B, scaling = w2_lora lora_A_t = lora_A.transpose(-2, -1).contiguous() lora_A_out = _grouped_mm_with_backward_fix(inter, lora_A_t, offsets) lora_B_t = lora_B.transpose(-2, -1).contiguous() lora_B_out = _grouped_mm_with_backward_fix(lora_A_out, lora_B_t, offsets) mm2_out = mm2_out + lora_B_out * scaling else: raise AttributeError("MoE layer must have 'down_proj' or 'w2'.") # Apply routing weights and scatter-add (reduce). flat_weights = top_k_weights.view(-1) permuted_weights = flat_weights[sorted_indices] mm2_out = mm2_out * permuted_weights.unsqueeze(-1) final_hidden_states = torch.zeros( (batch_size * sequence_length, hidden_dim), dtype=hidden_states.dtype, device=hidden_states.device, ) final_hidden_states.index_add_(0, token_indices, mm2_out.to(hidden_states.dtype)) if is_2d_input: return final_hidden_states return final_hidden_states.view(batch_size, sequence_length, hidden_dim) def forward_triton_grouped_gemm( self, hidden_states: torch.Tensor, top_k_index: torch.Tensor, top_k_weights: torch.Tensor, ) -> torch.Tensor: """Grouped-GEMM MoE forward via Triton kernels (torch.compile-compatible, mode="max-autotune").""" # This Unsloth Zoo code section is licensed under AGPL3 from unsloth.kernels.moe.grouped_gemm.interface import grouped_gemm from unsloth.kernels.moe.autotune_cache import get_or_autotune_moe_kernels if not hasattr(self, "_unsloth_moe_configs"): self._unsloth_moe_configs = None use_separated_lora = _should_use_separated_lora() # gate_up LoRA from the patched ParamWrapper (mirrors the down block below). gate_up_lora = None if getattr(self, "_unsloth_lora_gate_up_proj", None) is not None: gate_up_lora = self._unsloth_lora_gate_up_proj[:3] elif ( use_separated_lora and hasattr(self, "gate_up_proj") and _has_lora_adapters(self.gate_up_proj) ): gate_up_lora = _extract_lora_weights( self.gate_up_proj, num_experts=self.num_experts ) # Flatten 3D inputs (batch_size, seq_len, hidden_dim). is_3d = hidden_states.dim() == 3 if is_3d: batch_size, seq_len, hidden_dim = hidden_states.shape hidden_states = hidden_states.view(-1, hidden_dim) num_tokens = batch_size * seq_len if top_k_index.dim() == 3: top_k_index = top_k_index.view(-1, top_k_index.shape[-1]) if top_k_weights.dim() == 3: top_k_weights = top_k_weights.view(-1, top_k_weights.shape[-1]) else: num_tokens, hidden_dim = hidden_states.shape top_k = top_k_index.shape[1] # Cache model dims and kernel configs on first call. if self._unsloth_moe_configs is None: intermediate_dim = self.gate_up_proj.shape[1] // 2 # Autotune first GEMM. gemm1_configs = get_or_autotune_moe_kernels( num_experts=self.num_experts, hidden_dim=hidden_dim, intermediate_dim=intermediate_dim * 2, top_k=top_k, dtype=hidden_states.dtype, ) # Autotune second GEMM (output dim is hidden_dim). gemm2_configs = get_or_autotune_moe_kernels( num_experts=self.num_experts, hidden_dim=intermediate_dim, intermediate_dim=hidden_dim, top_k=top_k, dtype=hidden_states.dtype, ) self._unsloth_moe_configs = (intermediate_dim, gemm1_configs, gemm2_configs) torch.cuda.empty_cache() intermediate_dim, gemm1_configs, gemm2_configs = self._unsloth_moe_configs fwd_config_1, bwd_dX_config_1, bwd_dW_config_1 = gemm1_configs fwd_config_2, bwd_dX_config_2, bwd_dW_config_2 = gemm2_configs token_counts_by_expert, gather_indices = _get_routing_indices( top_k_index, self.num_experts ) offsets = torch.cumsum(token_counts_by_expert, dim=0, dtype=torch.int32) if self.gate_up_proj.shape[-1] == hidden_dim: w1 = self.gate_up_proj else: w1 = self.gate_up_proj.transpose(-2, -1).contiguous() # First grouped GEMM: gate_up projection. first_gemm_output = grouped_gemm( X=hidden_states, W=w1, m_sizes=token_counts_by_expert, topk=top_k, gather_indices=gather_indices, permute_x=True, permute_y=False, autotune=False, # cached configs kernel_config_fwd=fwd_config_1, kernel_config_bwd_dX=bwd_dX_config_1, kernel_config_bwd_dW=bwd_dW_config_1, is_first_gemm=True, ) # Separated LoRA for gate_up. grouped_gemm ran permute_x=True so first_gemm_output # is expert-sorted; _apply_lora_grouped_mm wants pre-permuted input, so gather via # gather_indices // top_k (expert-sorted row -> originating token row). if gate_up_lora is not None: first_weight, second_weight, scaling = gate_up_lora first_weight = first_weight.to(hidden_states.dtype) second_weight = second_weight.to(hidden_states.dtype) permuted_hidden = hidden_states[gather_indices // top_k] gate_up_lora_delta = _apply_lora_grouped_mm( permuted_hidden, first_weight, second_weight, offsets, scaling, grouped_mm_func=native_moe_grouped_mm, ) first_gemm_output = first_gemm_output + gate_up_lora_delta # Activation + gate*up. if hasattr(self, 'act_fn') and callable(self.act_fn): gate, up = first_gemm_output.chunk(2, dim=-1) intermediate = self.act_fn(gate) * up else: intermediate = _silu_and_mul(first_gemm_output) # Grouped GEMM 2: down projection. down_lora = None if getattr(self, "_unsloth_lora_down_proj", None) is not None: down_lora = self._unsloth_lora_down_proj[:3] elif ( use_separated_lora and hasattr(self, "down_proj") and _has_lora_adapters(self.down_proj) ): down_lora = _extract_lora_weights(self.down_proj, num_experts=self.num_experts) if self.down_proj.shape[-1] == intermediate.shape[-1]: w2 = self.down_proj else: w2 = self.down_proj.transpose(-2, -1).contiguous() second_gemm_output = grouped_gemm( X=intermediate, W=w2, m_sizes=token_counts_by_expert, topk=top_k, gather_indices=gather_indices, permute_x=False, permute_y=True, autotune=False, # cached configs kernel_config_fwd=fwd_config_2, kernel_config_bwd_dX=bwd_dX_config_2, kernel_config_bwd_dW=bwd_dW_config_2, is_first_gemm=False, ) # Separated LoRA for down (intermediate already permuted from step 1, same offsets). if down_lora is not None: first_weight, second_weight, scaling = down_lora first_weight = first_weight.to(intermediate.dtype) second_weight = second_weight.to(intermediate.dtype) lora_delta = _apply_lora_grouped_mm( intermediate, first_weight, second_weight, offsets, scaling, grouped_mm_func=native_moe_grouped_mm ) second_gemm_output = second_gemm_output + lora_delta # Apply routing weights and sum across top_k: (num_tokens, top_k, hidden) -> (num_tokens, hidden). top_k_weights_casted = top_k_weights.to(hidden_states.dtype) final_hidden_states = ( second_gemm_output.view(num_tokens, top_k, hidden_dim) * top_k_weights_casted[..., None] ) final_hidden_states = final_hidden_states.sum(dim=1) if is_3d: final_hidden_states = final_hidden_states.view(batch_size, seq_len, hidden_dim) return final_hidden_states @torch.compiler.disable def forward_native_moe_loop( self, hidden_states: torch.Tensor, top_k_index: torch.Tensor, top_k_weights: torch.Tensor, ) -> torch.Tensor: """Loop over experts with routed tokens; torch.compile-disabled to avoid graph breaks on dynamic control flow.""" # This Unsloth Zoo code section is licensed under AGPL3 final_hidden_states = torch.zeros_like(hidden_states) use_separated_lora = _should_use_separated_lora() gate_up_lora = getattr(self, "_unsloth_lora_gate_up_proj", None) if gate_up_lora is not None: gate_up_lora = gate_up_lora[:3] elif ( use_separated_lora and hasattr(self, "gate_up_proj") and _has_lora_adapters(self.gate_up_proj) ): gate_up_lora = _extract_lora_weights( self.gate_up_proj, num_experts=self.num_experts, experts_module=self ) # Pre-cast LoRA factors to the activation dtype once (avoid per-expert .to()). # `scaling` is left alone: a Python float is a no-op, a tensor broadcasts. if gate_up_lora is not None: _gate_up_first, _gate_up_second, _gate_up_scaling = gate_up_lora gate_up_lora = ( _gate_up_first.to(hidden_states.dtype), _gate_up_second.to(hidden_states.dtype), _gate_up_scaling, ) down_lora = getattr(self, "_unsloth_lora_down_proj", None) if down_lora is not None: down_lora = down_lora[:3] elif ( use_separated_lora and hasattr(self, "down_proj") and _has_lora_adapters(self.down_proj) ): down_lora = _extract_lora_weights( self.down_proj, num_experts=self.num_experts, experts_module=self ) if down_lora is not None: _down_first, _down_second, _down_scaling = down_lora down_lora = ( _down_first.to(hidden_states.dtype), _down_second.to(hidden_states.dtype), _down_scaling, ) # Expert mask -> which experts have tokens. with torch.no_grad(): expert_mask = F.one_hot(top_k_index, num_classes=self.num_experts) expert_mask = expert_mask.permute(2, 1, 0) # (num_experts, top_k, n_tokens) expert_hit = torch.greater(expert_mask.sum(dim=(-1, -2)), 0).nonzero() # Some patches (Qwen3-VL-MoE) store experts in grouped_mm layout (E, in, out) # rather than F.linear's (E, out, in) and set _unsloth_grouped_mm_format=True. # Prefer it over the shape check, which is unsafe when intermediate_dim == hidden_dim. grouped_mm_format = bool(getattr(self, "_unsloth_grouped_mm_format", False)) # GPT-OSS uses interleaved gate/up, clamped swiglu, and per-expert biases. is_gpt_oss = "GptOssExperts" in self.__class__.__name__ for expert_idx_t in expert_hit: expert_idx = expert_idx_t.item() top_k_pos, token_idx = torch.where(expert_mask[expert_idx]) current_state = hidden_states[token_idx] # gate_up projection for this expert ('gate_up_proj' or 'w1'/'w3'). if hasattr(self, "gate_up_proj"): gate_up_weight = self.gate_up_proj[expert_idx] if grouped_mm_format or gate_up_weight.shape[-1] != current_state.shape[-1]: gate_up_weight = gate_up_weight.T gate_up = F.linear(current_state, gate_up_weight) if gate_up_lora is not None: first_weight, second_weight, scaling = gate_up_lora lora_delta = current_state @ first_weight[expert_idx] lora_delta = lora_delta @ second_weight[expert_idx] gate_up = gate_up + lora_delta * scaling if is_gpt_oss: gate_up_bias = getattr(self, "gate_up_proj_bias", None) if gate_up_bias is not None: gate_up = gate_up + gate_up_bias[expert_idx].to(gate_up.dtype) gate = gate_up[..., ::2] up = gate_up[..., 1::2] else: gate, up = gate_up.chunk(2, dim=-1) else: gate = F.linear(current_state, self.w1[expert_idx]) up = F.linear(current_state, self.w3[expert_idx]) if is_gpt_oss: limit = getattr(self, "limit", 7.0) alpha = getattr(self, "alpha", 1.702) gate = gate.clamp(min=None, max=limit) up = up.clamp(min=-limit, max=limit) current_hidden_states = (up + 1.0) * (gate * torch.sigmoid(gate * alpha)) elif hasattr(self, "act_fn") and callable(self.act_fn): current_hidden_states = self.act_fn(gate) * up else: current_hidden_states = F.silu(gate) * up # down projection for this expert. if hasattr(self, "down_proj"): down_weight = self.down_proj[expert_idx] # Mirror gate_up: prefer the flag over the shape heuristic (unsafe at square dims). if grouped_mm_format or down_weight.shape[-1] != current_hidden_states.shape[-1]: down_weight = down_weight.T down = F.linear(current_hidden_states, down_weight) if down_lora is not None: first_weight, second_weight, scaling = down_lora lora_delta = current_hidden_states @ first_weight[expert_idx] lora_delta = lora_delta @ second_weight[expert_idx] down = down + lora_delta * scaling if is_gpt_oss: down_bias = getattr(self, "down_proj_bias", None) if down_bias is not None: down = down + down_bias[expert_idx].to(down.dtype) current_hidden_states = down else: current_hidden_states = F.linear(current_hidden_states, self.w2[expert_idx]) current_hidden_states = ( current_hidden_states * top_k_weights[token_idx, top_k_pos, None] ) final_hidden_states.index_add_( 0, token_idx, current_hidden_states.to(final_hidden_states.dtype) ) return final_hidden_states