# 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 . """ Auto-tuning cache system for MoE kernels to ensure tuning runs only once at training start. """ import hashlib import json import logging import os import time from typing import Dict, List, Optional, Tuple, Any import torch import triton logger = logging.getLogger(__name__) _kernel_config_cache: Dict[str, Any] = {} _autotune_completed: Dict[str, bool] = {} def _get_cache_key( num_experts: int, hidden_dim: int, intermediate_dim: int, top_k: int, dtype: torch.dtype, device_capability: Tuple[int, int], seq_len: int = 8192, # Default sequence length for tuning ) -> str: """Generate a unique cache key based on model configuration.""" key_data = { "num_experts": num_experts, "hidden_dim": hidden_dim, "intermediate_dim": intermediate_dim, "top_k": top_k, "dtype": str(dtype), "device_capability": device_capability, "seq_len": seq_len, } key_str = json.dumps(key_data, sort_keys = True) return hashlib.md5(key_str.encode()).hexdigest() def _get_cache_file_path(cache_key: str) -> str: """Get the file path for the cache file.""" cache_dir = os.path.expanduser("~/.cache/unsloth/moe_autotune") os.makedirs(cache_dir, exist_ok = True) return os.path.join(cache_dir, f"{cache_key}.json") def load_cached_config(cache_key: str) -> Optional[Dict[str, Any]]: """Load cached kernel configuration from disk.""" cache_file = _get_cache_file_path(cache_key) if not os.path.exists(cache_file): return None try: with open(cache_file, "r", encoding = "utf-8") as f: cached_data = json.load(f) # Invalidate if device capability changed current_device_capability = torch.cuda.get_device_capability() if cached_data.get("device_capability") != current_device_capability: logger.info("Device capability changed, invalidating cache") os.remove(cache_file) return None logger.info(f"Loaded cached MoE kernel config: {cache_key}") return cached_data except Exception as e: logger.warning(f"Failed to load cache file {cache_file}: {e}") try: os.remove(cache_file) except: pass return None def save_cached_config( cache_key: str, config_fwd: Any, config_bwd_dx: Any, config_bwd_dw: Any, metadata: Dict[str, Any] = None, ) -> None: """Save kernel configuration to disk cache.""" cache_file = _get_cache_file_path(cache_key) cache_data = { "timestamp": time.time(), "device_capability": torch.cuda.get_device_capability(), "config_fwd": config_fwd.__dict__ if hasattr(config_fwd, "__dict__") else str(config_fwd), "config_bwd_dx": config_bwd_dx.__dict__ if hasattr(config_bwd_dx, "__dict__") else str(config_bwd_dx), "config_bwd_dw": config_bwd_dw.__dict__ if hasattr(config_bwd_dw, "__dict__") else str(config_bwd_dw), "metadata": metadata or {}, } try: with open(cache_file, "w", encoding = "utf-8") as f: json.dump(cache_data, f, indent = 2) logger.info(f"Saved MoE kernel config cache: {cache_key}") except Exception as e: logger.warning(f"Failed to save cache file {cache_file}: {e}") def get_or_autotune_moe_kernels( num_experts: int, hidden_dim: int, intermediate_dim: int, top_k: int, dtype: torch.dtype, force_autotune: bool = False, seq_len: int = 8192, ) -> Tuple[Any, Any, Any]: """ Get cached kernel configurations or run auto-tuning. Args: num_experts: Number of experts in the MoE layer hidden_dim: Hidden dimension of the model intermediate_dim: Intermediate dimension for MoE MLP top_k: Number of experts to route to dtype: Data type for computation force_autotune: Force re-running autotuning even if cache exists seq_len: Sequence length to use for tuning benchmarks Returns: Tuple of (config_fwd, config_bwd_dx, config_bwd_dw) """ device_capability = torch.cuda.get_device_capability() cache_key = _get_cache_key( num_experts, hidden_dim, intermediate_dim, top_k, dtype, device_capability, seq_len, ) # Env override to disable autotuning if os.environ.get("UNSLOTH_MOE_DISABLE_AUTOTUNE", "0") == "1": logger.info( f"UNSLOTH_MOE_DISABLE_AUTOTUNE=1: Using Heuristic (Safe) MoE kernel configs for SM{device_capability[0]}{device_capability[1]}" ) return _get_heuristic_configs() if not force_autotune and cache_key in _kernel_config_cache: logger.info(f"Using in-memory cached MoE kernel configs: {cache_key}") return _kernel_config_cache[cache_key] # Try to load from disk if not force_autotune: cached_data = load_cached_config(cache_key) if cached_data is not None: # Reconstruct config objects from cache try: from .grouped_gemm.kernels.tuning import ( KernelConfigForward, KernelConfigBackward_dX, KernelConfigBackward_dW, ) config_fwd = KernelConfigForward(**cached_data["config_fwd"]) config_bwd_dx = KernelConfigBackward_dX(**cached_data["config_bwd_dx"]) config_bwd_dw = KernelConfigBackward_dW(**cached_data["config_bwd_dw"]) configs = (config_fwd, config_bwd_dx, config_bwd_dw) _kernel_config_cache[cache_key] = configs return configs except Exception as e: logger.warning(f"Failed to reconstruct cached configs: {e}") if cache_key in _autotune_completed and not force_autotune: logger.info(f"Autotuning already completed for: {cache_key}") return _kernel_config_cache[cache_key] logger.info(f"Running MoE kernel auto-tuning for: {cache_key}") logger.info( f"Configuration: {num_experts} experts, {hidden_dim} hidden, {intermediate_dim} intermediate, top_k={top_k}" ) try: configs = _run_moe_autotuning( num_experts, hidden_dim, intermediate_dim, top_k, dtype, seq_len ) _kernel_config_cache[cache_key] = configs _autotune_completed[cache_key] = True # Save to disk config_fwd, config_bwd_dx, config_bwd_dw = configs save_cached_config( cache_key, config_fwd, config_bwd_dx, config_bwd_dw, { "num_experts": num_experts, "hidden_dim": hidden_dim, "intermediate_dim": intermediate_dim, }, ) logger.info(f"MoE kernel auto-tuning completed: {cache_key}") return configs except Exception as e: logger.error(f"MoE kernel auto-tuning failed: {e}") if "AttributeError" in str(e) and "_experimental_make_tensor_descriptor" in str(e): logger.warning( "Unsloth: Your Triton version might be incompatible with TMA features. Falling back to default configs." ) logger.info("Falling back to default kernel configurations") return _get_default_configs() def _run_moe_autotuning( num_experts: int, hidden_dim: int, intermediate_dim: int, top_k: int, dtype: torch.dtype, seq_len: int, ) -> Tuple[Any, Any, Any]: """Run the actual auto-tuning for MoE kernels.""" device = "cuda" # Fixed token count avoids OOMs and seq_len dependency; we ignore the passed seq_len here num_tokens = 4096 total_tokens = num_tokens * top_k hidden_states = torch.randn(num_tokens, hidden_dim, device = device, dtype = dtype) gate_up_weights = torch.randn( num_experts, 2 * intermediate_dim, hidden_dim, device = device, dtype = dtype ) down_weights = torch.randn( num_experts, hidden_dim, intermediate_dim, device = device, dtype = dtype ) # Dummy routing data m_sizes = torch.randint(1, total_tokens // num_experts + 1, (num_experts,), device = device) m_sizes = m_sizes * (total_tokens // m_sizes.sum().item()) # Adjust to exact total diff = total_tokens - m_sizes.sum().item() if diff != 0: m_sizes[0] += diff gather_indices = torch.arange(total_tokens, device = device) torch.randperm(total_tokens, out = gather_indices) # Autotune via the interface function with autotune=True (lets triton tune) from .grouped_gemm.interface import ( grouped_gemm_forward, grouped_gemm_dX, grouped_gemm_dW, ) from .grouped_gemm.kernels.forward import _autotuned_grouped_gemm_forward_kernel from .grouped_gemm.kernels.backward import ( _autotuned_grouped_gemm_dX_kernel, _autotuned_grouped_gemm_dW_kernel, ) from .grouped_gemm.kernels.tuning import ( KernelConfigForward, KernelConfigBackward_dX, KernelConfigBackward_dW, ) logger.info("Autotuning forward kernel (first GEMM)...") _ = grouped_gemm_forward( X = hidden_states, W = gate_up_weights, topk = top_k, m_sizes = m_sizes, gather_indices = gather_indices, permute_x = True, permute_y = False, autotune = True, ) triton_config_fwd = _autotuned_grouped_gemm_forward_kernel.best_config config_fwd = KernelConfigForward( BLOCK_SIZE_M = triton_config_fwd.kwargs["BLOCK_SIZE_M"], BLOCK_SIZE_N = triton_config_fwd.kwargs["BLOCK_SIZE_N"], BLOCK_SIZE_K = triton_config_fwd.kwargs["BLOCK_SIZE_K"], num_warps = triton_config_fwd.num_warps, num_stages = triton_config_fwd.num_stages, use_tma_load_x = triton_config_fwd.kwargs.get("USE_TMA_LOAD_X", False), use_tma_load_w = triton_config_fwd.kwargs.get("USE_TMA_LOAD_W", False), use_tma_store = triton_config_fwd.kwargs.get("USE_TMA_STORE", False), ) # Autotune backward dX kernel logger.info("Autotuning backward dX kernel...") dummy_grad = torch.randn(total_tokens, 2 * intermediate_dim, device = device, dtype = dtype) _ = grouped_gemm_dX( dY = dummy_grad, W = gate_up_weights, gather_indices = gather_indices, m_sizes = m_sizes, topk = top_k, permute_x = True, permute_y = False, autotune = True, ) triton_config_bwd_dx = _autotuned_grouped_gemm_dX_kernel.best_config config_bwd_dx = KernelConfigBackward_dX( BLOCK_SIZE_M = triton_config_bwd_dx.kwargs["BLOCK_SIZE_M"], BLOCK_SIZE_N = triton_config_bwd_dx.kwargs["BLOCK_SIZE_N"], BLOCK_SIZE_K = triton_config_bwd_dx.kwargs["BLOCK_SIZE_K"], num_warps = triton_config_bwd_dx.num_warps, num_stages = triton_config_bwd_dx.num_stages, use_tma_load_dy = triton_config_bwd_dx.kwargs.get("USE_TMA_LOAD_dY", False), use_tma_load_w = triton_config_bwd_dx.kwargs.get("USE_TMA_LOAD_W", False), use_tma_store = triton_config_bwd_dx.kwargs.get("USE_TMA_STORE", False), ) # Autotune backward dW kernel logger.info("Autotuning backward dW kernel...") _ = grouped_gemm_dW( X = hidden_states, dY = dummy_grad, m_sizes = m_sizes, gather_indices = gather_indices, topk = top_k, permute_x = True, permute_y = False, autotune = True, ) triton_config_bwd_dw = _autotuned_grouped_gemm_dW_kernel.best_config config_bwd_dw = KernelConfigBackward_dW( BLOCK_SIZE_M = triton_config_bwd_dw.kwargs["BLOCK_SIZE_M"], BLOCK_SIZE_N = triton_config_bwd_dw.kwargs["BLOCK_SIZE_N"], BLOCK_SIZE_K = triton_config_bwd_dw.kwargs["BLOCK_SIZE_K"], num_warps = triton_config_bwd_dw.num_warps, num_stages = triton_config_bwd_dw.num_stages, use_tma_load_dy = triton_config_bwd_dw.kwargs.get("USE_TMA_LOAD_dY", False), use_tma_load_x = triton_config_bwd_dw.kwargs.get("USE_TMA_LOAD_X", False), use_tma_store = triton_config_bwd_dw.kwargs.get("USE_TMA_STORE", False), ) return config_fwd, config_bwd_dx, config_bwd_dw return config_fwd, config_bwd_dx, config_bwd_dw def _get_heuristic_configs() -> Tuple[Any, Any, Any]: """ Get 'Safe Heuristic' kernel configurations. These are verified to be safe on A100 (SM80) and provide ~9x speedup on H100/B200. """ from .grouped_gemm.kernels.tuning import ( KernelConfigForward, KernelConfigBackward_dX, KernelConfigBackward_dW, ) # Safe Forward Config: 64x128x128 (Fits A100 SMEM) config_fwd = KernelConfigForward( BLOCK_SIZE_M = 64, BLOCK_SIZE_N = 128, BLOCK_SIZE_K = 128, num_warps = 8, num_stages = 3, permute_x = True, permute_y = True, use_tma_load_x = False, use_tma_load_w = False, # TMA loads might need alignment checks, safer to disable for heuristic use_tma_store = False, ) # Safe Backward Configs: 64x64x256 config_bwd_dx = KernelConfigBackward_dX( BLOCK_SIZE_M = 64, BLOCK_SIZE_N = 64, BLOCK_SIZE_K = 256, num_warps = 8, num_stages = 4, permute_x = True, permute_y = True, use_tma_load_dy = False, use_tma_load_w = False, use_tma_store = False, ) config_bwd_dw = KernelConfigBackward_dW( BLOCK_SIZE_M = 64, BLOCK_SIZE_N = 64, BLOCK_SIZE_K = 256, num_warps = 8, num_stages = 4, permute_x = True, permute_y = True, use_tma_load_dy = False, use_tma_load_x = False, use_tma_store = False, ) return config_fwd, config_bwd_dx, config_bwd_dw def _get_default_configs() -> Tuple[Any, Any, Any]: """Get default kernel configurations as fallback.""" from .grouped_gemm.kernels.tuning import ( KernelConfigForward, KernelConfigBackward_dX, KernelConfigBackward_dW, ) logger.warning("Using default MoE kernel configurations (not optimal)") config_fwd = KernelConfigForward( BLOCK_SIZE_M = 128, BLOCK_SIZE_N = 128, BLOCK_SIZE_K = 64, num_warps = 8, num_stages = 3, use_tma_load_x = False, use_tma_load_w = False, use_tma_store = False, ) config_bwd_dx = KernelConfigBackward_dX( BLOCK_SIZE_M = 128, BLOCK_SIZE_N = 128, BLOCK_SIZE_K = 64, num_warps = 8, num_stages = 3, use_tma_load_dy = False, use_tma_load_w = False, use_tma_store = False, ) config_bwd_dw = KernelConfigBackward_dW( BLOCK_SIZE_M = 128, BLOCK_SIZE_N = 128, BLOCK_SIZE_K = 64, num_warps = 8, num_stages = 3, use_tma_load_dy = False, use_tma_load_x = False, use_tma_store = False, ) return config_fwd, config_bwd_dx, config_bwd_dw def clear_cache() -> None: """Clear all cached kernel configurations.""" global _kernel_config_cache, _autotune_completed _kernel_config_cache.clear() _autotune_completed.clear() logger.info("Cleared MoE kernel cache") def is_autotuning_completed(cache_key: str) -> bool: """Check if autotuning has been completed for a given cache key.""" return cache_key in _autotune_completed