From 023405c76a1a993ea6ff14b5fcbe438c5c7c8021 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Mon, 2 Feb 2026 05:48:09 +0000 Subject: [PATCH] backend restructuring and housekeeping --- .gitignore | 33 + backend/backend/model_config.py | 704 ------------------ backend/{backend => core}/__init__.py | 22 +- backend/core/export/__init__.py | 9 + backend/{backend => core/export}/export.py | 4 +- backend/core/inference/__init__.py | 9 + .../{backend => core/inference}/inference.py | 8 +- backend/core/training/__init__.py | 14 + backend/{backend => core/training}/trainer.py | 2 +- .../{backend => core/training}/training.py | 2 +- backend/routes/models.py | 8 +- backend/routes/training.py | 4 +- backend/utils/paths/__init__.py | 11 + .../{backend => utils/paths}/path_utils.py | 0 14 files changed, 103 insertions(+), 727 deletions(-) create mode 100755 .gitignore delete mode 100644 backend/backend/model_config.py rename backend/{backend => core}/__init__.py (52%) create mode 100644 backend/core/export/__init__.py rename backend/{backend => core/export}/export.py (99%) create mode 100644 backend/core/inference/__init__.py rename backend/{backend => core/inference}/inference.py (99%) create mode 100644 backend/core/training/__init__.py rename backend/{backend => core/training}/trainer.py (99%) rename backend/{backend => core/training}/training.py (99%) create mode 100644 backend/utils/paths/__init__.py rename backend/{backend => utils/paths}/path_utils.py (100%) diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000000..11b0a5d20b --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# Virtual environments +.venv/ +venv/ +env/ + +# Unsloth cache +unsloth_compiled_cache/ + +# ML artifacts (large files) +outputs/ +*.gguf +*.safetensors +models/ + +# IDE / Editors +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Other +resources/ diff --git a/backend/backend/model_config.py b/backend/backend/model_config.py deleted file mode 100644 index 85bdd2cf42..0000000000 --- a/backend/backend/model_config.py +++ /dev/null @@ -1,704 +0,0 @@ -""" -Model and LoRA configuration handling -""" -from transformers import AutoConfig -from dataclasses import dataclass -from typing import Optional, Dict, Any -from .path_utils import normalize_path, is_local_path, is_model_cached -from utils.utils import without_hf_auth -import logging -from pathlib import Path -from typing import List, Tuple -import json -import yaml - - -logger = logging.getLogger(__name__) - -# Model name mapping: maps all equivalent model names to their canonical YAML config file -# Format: "canonical_model_name.yaml": [list of all equivalent model names] -# Based on the model mapper provided - canonical filename is based on the first model name in the mapper -MODEL_NAME_MAPPING = { - "unsloth_answerdotai_ModernBERT-large.yaml": [ - "answerdotai/ModernBERT-large", - ], - "unsloth_Qwen2.5-Coder-7B-Instruct-bnb-4bit.yaml": [ - "unsloth/Qwen2.5-Coder-7B-Instruct-bnb-4bit", - "unsloth/Qwen2.5-Coder-7B-Instruct", - "Qwen/Qwen2.5-Coder-7B-Instruct", - ], - "unsloth_codegemma-7b-bnb-4bit.yaml": [ - "unsloth/codegemma-7b-bnb-4bit", - "unsloth/codegemma-7b", - "google/codegemma-7b", - ], - "unsloth_ERNIE-4.5-21B-A3B-PT.yaml": [ - "unsloth/ERNIE-4.5-21B-A3B-PT", - ], - "unsloth_ERNIE-4.5-VL-28B-A3B-PT.yaml": [ - "unsloth/ERNIE-4.5-VL-28B-A3B-PT", - ], - - "tiiuae_Falcon-H1-0.5B-Instruct.yaml": [ - "tiiuae/Falcon-H1-0.5B-Instruct", - "unsloth/Falcon-H1-0.5B-Instruct", - ], - "unsloth_functiongemma-270m-it.yaml": [ - "unsloth/functiongemma-270m-it-unsloth-bnb-4bit", - "google/functiongemma-270m-it", - "unsloth/functiongemma-270m-it-unsloth-bnb-4bit", - ], - "unsloth_gemma-2-2b.yaml": [ - "unsloth/gemma-2-2b-bnb-4bit", - "google/gemma-2-2b", - ], - "unsloth_gemma-2-27b-bnb-4bit.yaml": [ - "unsloth/gemma-2-9b-bnb-4bit", - "unsloth/gemma-2-9b", - "google/gemma-2-9b", - "unsloth/gemma-2-27b", - "google/gemma-2-27b", - ], - "unsloth_gemma-3-4b-pt.yaml": [ - "unsloth/gemma-3-4b-pt-unsloth-bnb-4bit", - "google/gemma-3-4b-pt", - "unsloth/gemma-3-4b-pt-bnb-4bit", - ], - "unsloth_gemma-3-4b-it.yaml": [ - "unsloth/gemma-3-4b-it-unsloth-bnb-4bit", - "google/gemma-3-4b-it", - "unsloth/gemma-3-4b-it-bnb-4bit", - ], - "unsloth_gemma-3-27b-it.yaml": [ - "unsloth/gemma-3-27b-it-unsloth-bnb-4bit", - "google/gemma-3-27b-it", - "unsloth/gemma-3-27b-it-bnb-4bit", - ], - "unsloth_gemma-3-270m-it.yaml": [ - "unsloth/gemma-3-270m-it-unsloth-bnb-4bit", - "google/gemma-3-270m-it", - "unsloth/gemma-3-270m-it-bnb-4bit", - ], - "unsloth_gemma-3n-E4B-it.yaml": [ - "unsloth/gemma-3n-E4B-it-unsloth-bnb-4bit", - "google/gemma-3n-E4B-it", - "unsloth/gemma-3n-E4B-it-unsloth-bnb-4bit", - ], - "unsloth_gemma-3n-E4B.yaml": [ - "unsloth/gemma-3n-E4B-unsloth-bnb-4bit", - "google/gemma-3n-E4B", - ], - "unsloth_gpt-oss-20b.yaml": [ - "openai/gpt-oss-20b", - "unsloth/gpt-oss-20b-unsloth-bnb-4bit", - "unsloth/gpt-oss-20b-BF16", - ], - - "unsloth_gpt-oss-120b.yaml": [ - "openai/gpt-oss-120b", - "unsloth/gpt-oss-120b-unsloth-bnb-4bit", - ], - "unsloth_granite-4.0-350m-unsloth-bnb-4bit.yaml": [ - "unsloth/granite-4.0-350m", - "ibm-granite/granite-4.0-350m", - "unsloth/granite-4.0-350m-bnb-4bit", - ], - "unsloth_granite-4.0-h-micro.yaml": [ - "ibm-granite/granite-4.0-h-micro", - "unsloth/granite-4.0-h-micro-bnb-4bit", - "unsloth/granite-4.0-h-micro-unsloth-bnb-4bit", - ], - "unsloth_LFM2-1.2B.yaml": [ - "unsloth/LFM2-1.2B", - ], - "unsloth_llama-3-8b-bnb-4bit.yaml": [ - "unsloth/llama-3-8b", - "meta-llama/Meta-Llama-3-8B", - ], - "unsloth_llama-3-8b-Instruct-bnb-4bit.yaml": [ - "unsloth/llama-3-8b-Instruct", - "meta-llama/Meta-Llama-3-8B-Instruct", - ], - "unsloth_Meta-Llama-3.1-70B-bnb-4bit.yaml": [ - "unsloth/Meta-Llama-3.1-8B-bnb-4bit", - "unsloth/Meta-Llama-3.1-8B-unsloth-bnb-4bit", - "meta-llama/Meta-Llama-3.1-8B", - "unsloth/Meta-Llama-3.1-70B-bnb-4bit", - "unsloth/Meta-Llama-3.1-8B", - "unsloth/Meta-Llama-3.1-70B", - "meta-llama/Meta-Llama-3.1-70B", - "unsloth/Meta-Llama-3.1-405B-bnb-4bit", - "meta-llama/Meta-Llama-3.1-405B", - ], - - "unsloth_Meta-Llama-3.1-8B-Instruct-bnb-4bit.yaml": [ - "unsloth/Meta-Llama-3.1-8B-Instruct-unsloth-bnb-4bit", - "unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit", - "meta-llama/Meta-Llama-3.1-8B-Instruct", - "unsloth/Meta-Llama-3.1-8B-Instruct", - "RedHatAI/Llama-3.1-8B-Instruct-FP8", - "unsloth/Llama-3.1-8B-Instruct-FP8-Block", - "unsloth/Llama-3.1-8B-Instruct-FP8-Dynamic", - ], - "unsloth_Llama-3.2-3B-Instruct.yaml": [ - "unsloth/Llama-3.2-3B-Instruct-unsloth-bnb-4bit", - "meta-llama/Llama-3.2-3B-Instruct", - "unsloth/Llama-3.2-3B-Instruct-bnb-4bit", - "RedHatAI/Llama-3.2-3B-Instruct-FP8", - "unsloth/Llama-3.2-3B-Instruct-FP8-Block", - "unsloth/Llama-3.2-3B-Instruct-FP8-Dynamic", - ], - "unsloth_Llama-3.2-1B-Instruct.yaml": [ - "unsloth/Llama-3.2-1B-Instruct-unsloth-bnb-4bit", - "meta-llama/Llama-3.2-1B-Instruct", - "unsloth/Llama-3.2-1B-Instruct-bnb-4bit", - "RedHatAI/Llama-3.2-1B-Instruct-FP8", - "unsloth/Llama-3.2-1B-Instruct-FP8-Block", - "unsloth/Llama-3.2-1B-Instruct-FP8-Dynamic", - ], - "unsloth_Llama-3.2-11B-Vision-Instruct.yaml": [ - "unsloth/Llama-3.2-11B-Vision-Instruct-unsloth-bnb-4bit", - "meta-llama/Llama-3.2-11B-Vision-Instruct", - "unsloth/Llama-3.2-11B-Vision-Instruct-bnb-4bit", - ], - "unsloth_Llama-3.3-70B-Instruct.yaml": [ - "unsloth/Llama-3.3-70B-Instruct-unsloth-bnb-4bit", - "meta-llama/Llama-3.3-70B-Instruct", - "unsloth/Llama-3.3-70B-Instruct-bnb-4bit", - "RedHatAI/Llama-3.3-70B-Instruct-FP8", - "unsloth/Llama-3.3-70B-Instruct-FP8-Block", - "unsloth/Llama-3.3-70B-Instruct-FP8-Dynamic", - ], - "unsloth_Llasa-3B.yaml": [ - "HKUSTAudio/Llasa-1B", - "unsloth/Llasa-3B", - ], - "unsloth_Magistral-Small-2509-unsloth-bnb-4bit.yaml": [ - "unsloth/Magistral-Small-2509", - "mistralai/Magistral-Small-2509", - "unsloth/Magistral-Small-2509-bnb-4bit", - ], - "unsloth_Ministral-3-3B-Instruct-2512.yaml": [ - "unsloth/Ministral-3-3B-Instruct-2512", - ], - "unsloth_mistral-7b-v0.3-bnb-4bit.yaml": [ - "unsloth/mistral-7b-v0.3-bnb-4bit" - "unsloth/mistral-7b-v0.3", - "mistralai/Mistral-7B-v0.3", - ], - "unsloth_Mistral-Nemo-Base-2407-bnb-4bit.yaml": [ - "unsloth/Mistral-Nemo-Base-2407-bnb-4bit", - "unsloth/Mistral-Nemo-Base-2407", - "mistralai/Mistral-Nemo-Base-2407", - "unsloth/Mistral-Nemo-Instruct-2407-bnb-4bit", - "unsloth/Mistral-Nemo-Instruct-2407", - "mistralai/Mistral-Nemo-Instruct-2407", - - ], - "unsloth_Mistral-Small-Instruct-2409.yaml": [ - "unsloth/Mistral-Small-Instruct-2409-bnb-4bit", - "mistralai/Mistral-Small-Instruct-2409", - ], - "unsloth_mistral-7b-instruct-v0.3-bnb-4bit.yaml": [ - "unsloth/mistral-7b-instruct-v0.3-bnb-4bit", - "unsloth/mistral-7b-instruct-v0.3", - "mistralai/Mistral-7B-Instruct-v0.3", - ], - "unsloth_Qwen2.5-1.5B-Instruct.yaml": [ - "unsloth/Qwen2.5-1.5B-Instruct-unsloth-bnb-4bit", - "Qwen/Qwen2.5-1.5B-Instruct", - "unsloth/Qwen2.5-1.5B-Instruct-bnb-4bit", - ], - "unsloth_Nemotron-3-Nano-30B-A3B.yaml": [ - "unsloth/Nemotron-3-Nano-30B-A3B", - ], - "unsloth_orpheus-3b-0.1-ft.yaml": [ - "unsloth/orpheus-3b-0.1-ft-unsloth-bnb-4bit", - "canopylabs/orpheus-3b-0.1-ft", - "unsloth/orpheus-3b-0.1-ft-bnb-4bit", - ], - "OuteAI_Llama-OuteTTS-1.0-1B.yaml": [ - "OuteAI/Llama-OuteTTS-1.0-1B", - ], - "unsloth_PaddleOCR-VL.yaml": [ - "unsloth/PaddleOCR-VL", - ], - "unsloth_Phi-3-medium-4k-instruct.yaml": [ - "unsloth/Phi-3-medium-4k-instruct-bnb-4bit", - "microsoft/Phi-3-medium-4k-instruct", - ], - "unsloth_Phi-3.5-mini-instruct.yaml": [ - "unsloth/Phi-3.5-mini-instruct-bnb-4bit", - "microsoft/Phi-3.5-mini-instruct", - ], - "unsloth_Phi-4.yaml": [ - "unsloth/phi-4-unsloth-bnb-4bit", - "microsoft/phi-4", - "unsloth/phi-4-bnb-4bit", - ], - "unsloth_Pixtral-12B-2409.yaml": [ - "unsloth/Pixtral-12B-2409-unsloth-bnb-4bit", - "mistralai/Pixtral-12B-2409", - "unsloth/Pixtral-12B-2409-bnb-4bit", - ], - "unsloth_Qwen2-7B.yaml": [ - "unsloth/Qwen2-7B-bnb-4bit", - "Qwen/Qwen2-7B", - ], - "unsloth_Qwen2-VL-7B-Instruct.yaml": [ - "unsloth/Qwen2-VL-7B-Instruct-unsloth-bnb-4bit", - "Qwen/Qwen2-VL-7B-Instruct", - "unsloth/Qwen2-VL-7B-Instruct-bnb-4bit", - ], - "unsloth_Qwen2.5-7B.yaml": [ - "unsloth/Qwen2.5-7B-unsloth-bnb-4bit", - "Qwen/Qwen2.5-7B", - "unsloth/Qwen2.5-7B-bnb-4bit", - ], - "unsloth_Qwen2.5-Coder-1.5B-Instruct.yaml": [ - "unsloth/Qwen2.5-Coder-1.5B-Instruct-bnb-4bit", - "Qwen/Qwen2.5-Coder-1.5B-Instruct", - ], - "unsloth_Qwen2.5-Coder-14B-Instruct.yaml": [ - "unsloth/Qwen2.5-Coder-14B-Instruct-bnb-4bit", - "Qwen/Qwen2.5-Coder-14B-Instruct", - ], - "unsloth_Qwen2.5-VL-7B-Instruct-bnb-4bit.yaml": [ - "unsloth/Qwen2.5-VL-7B-Instruct", - "Qwen/Qwen2.5-VL-7B-Instruct", - "unsloth/Qwen2.5-VL-7B-Instruct-unsloth-bnb-4bit", - ], - "unsloth_Qwen3-0.6B.yaml": [ - "unsloth/Qwen3-0.6B-unsloth-bnb-4bit", - "Qwen/Qwen3-0.6B", - "unsloth/Qwen3-0.6B-bnb-4bit", - "Qwen/Qwen3-0.6B-FP8", - "unsloth/Qwen3-0.6B-FP8", - ], - "unsloth_Qwen3-4B-Instruct-2507.yaml": [ - "unsloth/Qwen3-4B-Instruct-2507-unsloth-bnb-4bit", - "Qwen/Qwen3-4B-Instruct-2507", - "unsloth/Qwen3-4B-Instruct-2507-bnb-4bit", - "Qwen/Qwen3-4B-Instruct-2507-FP8", - "unsloth/Qwen3-4B-Instruct-2507-FP8", - ], - "unsloth_Qwen3-4B-Thinking-2507.yaml": [ - "unsloth/Qwen3-4B-Thinking-2507-unsloth-bnb-4bit", - "Qwen/Qwen3-4B-Thinking-2507", - "unsloth/Qwen3-4B-Thinking-2507-bnb-4bit", - "Qwen/Qwen3-4B-Thinking-2507-FP8", - "unsloth/Qwen3-4B-Thinking-2507-FP8", - ], - "unsloth_Qwen3-14B-Base-unsloth-bnb-4bit.yaml": [ - "unsloth/Qwen3-14B-Base", - "Qwen/Qwen3-14B-Base", - "unsloth/Qwen3-14B-Base-bnb-4bit", - ], - "unsloth_Qwen3-14B.yaml": [ - "unsloth/Qwen3-14B-unsloth-bnb-4bit", - "Qwen/Qwen3-14B", - "unsloth/Qwen3-14B-bnb-4bit", - "Qwen/Qwen3-14B-FP8", - "unsloth/Qwen3-14B-FP8", - ], - "unsloth_Qwen3-32B.yaml": [ - "unsloth/Qwen3-32B-unsloth-bnb-4bit", - "Qwen/Qwen3-32B", - "unsloth/Qwen3-32B-bnb-4bit", - "Qwen/Qwen3-32B-FP8", - "unsloth/Qwen3-32B-FP8", - ], - "unsloth_Qwen3-VL-8B-Instruct-unsloth-bnb-4bit.yaml": [ - "Qwen/Qwen3-VL-8B-Instruct-FP8", - "unsloth/Qwen3-VL-8B-Instruct-FP8", - "unsloth/Qwen3-VL-8B-Instruct", - "Qwen/Qwen3-VL-8B-Instruct", - "unsloth/Qwen3-VL-8B-Instruct-bnb-4bit", - ], - "sesame_csm-1b.yaml": [ - "sesame/csm-1b", - ], - "Spark-TTS-0.5B_LLM.yaml": [ - "Spark-TTS-0.5B/LLM", - ], - "unsloth_tinyllama-bnb-4bit.yaml": [ - "unsloth/tinyllama", - "TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T", - ], - "unsloth_whisper-large-v3.yaml": [ - "unsloth/whisper-large-v3", - "openai/whisper-large-v3", - ], -} - -# Reverse mapping for quick lookup: model_name -> canonical_filename -_REVERSE_MODEL_MAPPING = {} -for canonical_file, model_names in MODEL_NAME_MAPPING.items(): - for model_name in model_names: - _REVERSE_MODEL_MAPPING[model_name] = canonical_file - -def load_model_config(model_name: str, use_auth: bool = False, token: Optional[str] = None): - """ - Load model config with optional authentication control. - """ - - if token: - # Explicit token provided - use it - return AutoConfig.from_pretrained( - model_name, - trust_remote_code=True, - token=token - ) - - if not use_auth: - # Load without any authentication (for public model checks) - with without_hf_auth(): - return AutoConfig.from_pretrained( - model_name, - trust_remote_code=True, - token=None - ) - - # Use default authentication (cached tokens) - return AutoConfig.from_pretrained( - model_name, - trust_remote_code=True - ) -pass - - -def is_vision_model(model_name: str, hf_token: Optional[str] = None) -> bool: - """ - Detect vision models by checking architecture in config. - Works for fine-tuned models since they inherit the base architecture. - - Args: - model_name: Model identifier (HF repo or local path) - hf_token: Optional HF token for accessing gated/private models - """ - try: - config = load_model_config(model_name, token=hf_token) - - # Check vision arch - if hasattr(config, 'architectures'): - is_vlm = any( - x.endswith(("ForConditionalGeneration", "ForVisionText2Text")) - for x in config.architectures - ) - if is_vlm: - logger.info(f"Model {model_name} detected as vision model: architecture {config.architectures}") - return True - - # Quick check for vision config as backup - if hasattr(config, 'vision_config'): - logger.info(f"Model {model_name} detected as vision model: has vision_config") - return True - - return False - - except Exception as e: - logger.warning(f"Could not determine if {model_name} is vision model: {e}") - return False -pass - - -def scan_trained_loras(outputs_dir: str = "./outputs") -> List[Tuple[str, str]]: - """ - Scan outputs folder for trained LoRA adapters. - - Returns: - List of tuples: [(display_name, adapter_path), ...] - - Example: - [ - ("unsloth_Meta-Llama-3.1_...", "./outputs/unsloth_Meta-Llama-3.1_.../"), - ("my_finetuned_model", "./outputs/my_finetuned_model/"), - ] - """ - trained_loras = [] - outputs_path = Path(outputs_dir) - - if not outputs_path.exists(): - logger.warning(f"Outputs directory not found: {outputs_dir}") - return trained_loras - - try: - for item in outputs_path.iterdir(): - if item.is_dir(): - # Check if this directory contains a LoRA adapter - adapter_config = item / "adapter_config.json" - adapter_model = item / "adapter_model.safetensors" - - if adapter_config.exists() or adapter_model.exists(): - display_name = item.name - adapter_path = str(item) - trained_loras.append((display_name, adapter_path)) - logger.debug(f"Found trained LoRA: {display_name}") - - # Sort by modification time (newest first) - trained_loras.sort(key=lambda x: Path(x[1]).stat().st_mtime, reverse=True) - - logger.info(f"Found {len(trained_loras)} trained LoRA adapters in {outputs_dir}") - return trained_loras - - except Exception as e: - logger.error(f"Error scanning outputs folder: {e}") - return [] - -def get_base_model_from_lora(lora_path: str) -> Optional[str]: - """ - Read the base model name from a LoRA adapter's config. - - Args: - lora_path: Path to the LoRA adapter directory - - Returns: - Base model identifier (e.g., "unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit") - or None if not found - - Example: - >>> get_base_model_from_lora("./outputs/unsloth_Meta-Llama-3.1_.../") - "unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit" - """ - try: - lora_path_obj = Path(lora_path) - - # Try adapter_config.json first - adapter_config_path = lora_path_obj / "adapter_config.json" - if adapter_config_path.exists(): - with open(adapter_config_path, 'r') as f: - config = json.load(f) - base_model = config.get("base_model_name_or_path") - if base_model: - logger.info(f"Detected base model from adapter_config.json: {base_model}") - return base_model - - # Fallback: try training_args.bin (requires torch) - training_args_path = lora_path_obj / "training_args.bin" - if training_args_path.exists(): - try: - import torch - training_args = torch.load(training_args_path) - if hasattr(training_args, 'model_name_or_path'): - base_model = training_args.model_name_or_path - logger.info(f"Detected base model from training_args.bin: {base_model}") - return base_model - except Exception as e: - logger.warning(f"Could not load training_args.bin: {e}") - - # Last resort: parse from directory name - # Format: unsloth_Meta-Llama-3.1-8B-Instruct-bnb-4bit_timestamp - dir_name = lora_path_obj.name - if dir_name.startswith("unsloth_"): - # Remove timestamp suffix (usually _1234567890) - parts = dir_name.split("_") - # Reconstruct model name - if len(parts) >= 2: - model_parts = parts[1:-1] # Skip "unsloth" and timestamp - base_model = "unsloth/" + "_".join(model_parts) - logger.info(f"Detected base model from directory name: {base_model}") - return base_model - - logger.warning(f"Could not detect base model for LoRA: {lora_path}") - return None - - except Exception as e: - logger.error(f"Error reading base model from LoRA config: {e}") - return None -pass - -# Status indicators that appear in UI dropdowns -UI_STATUS_INDICATORS = [" (Ready)", " (Loading...)", " (Active)", "↓ "] - -def load_model_defaults(model_name: str) -> Dict[str, Any]: - """ - Load default training parameters for a model from YAML file. - - Args: - model_name: Model identifier (e.g., "unsloth/Meta-Llama-3.1-8B-bnb-4bit") - - Returns: - Dictionary with default parameters from YAML file, or empty dict if not found - - The function looks for a YAML file in configs/model_defaults/ (including subfolders) - based on the model name or its aliases from MODEL_NAME_MAPPING. - If no specific file exists, it falls back to default.yaml. - """ - try: - # Get the script directory to locate configs - script_dir = Path(__file__).parent.parent - defaults_dir = script_dir / "configs" / "model_defaults" - - # First, check if model is in the mapping - if model_name in _REVERSE_MODEL_MAPPING: - canonical_file = _REVERSE_MODEL_MAPPING[model_name] - # Search in subfolders and root - for config_path in defaults_dir.rglob(canonical_file): - if config_path.is_file(): - with open(config_path, 'r', encoding='utf-8') as f: - config = yaml.safe_load(f) or {} - logger.info(f"Loaded model defaults from {config_path} (via mapping)") - return config - - # Try exact model name match (for backward compatibility) - model_filename = model_name.replace("/", "_") + ".yaml" - # Search in subfolders and root - for config_path in defaults_dir.rglob(model_filename): - if config_path.is_file(): - with open(config_path, 'r', encoding='utf-8') as f: - config = yaml.safe_load(f) or {} - logger.info(f"Loaded model defaults from {config_path}") - return config - - # Fall back to default.yaml - default_config_path = defaults_dir / "default.yaml" - if default_config_path.exists(): - with open(default_config_path, 'r', encoding='utf-8') as f: - config = yaml.safe_load(f) or {} - logger.info(f"Loaded default model defaults from {default_config_path}") - return config - - logger.warning(f"No default config found for model {model_name}") - return {} - - except Exception as e: - logger.error(f"Error loading model defaults for {model_name}: {e}") - return {} - -@dataclass -class ModelConfig: - """Configuration for a model to load""" - identifier: str # Clean model identifier (org/name or path) - display_name: str # Original UI display name - path: str # Normalized filesystem path - is_local: bool # Is this a local file vs HF model? - is_cached: bool # Is this already in HF cache? - is_vision: bool # Is this a vision model? - is_lora: bool # Is this a lora adapter? - base_model: Optional[str] = None # Base model (for LoRAs) - - @classmethod - def from_lora_path(cls, lora_path: str, hf_token: Optional[str] = None) -> Optional['ModelConfig']: - """ - Create ModelConfig from a local LoRA adapter path. - - Automatically detects the base model from adapter config. - - Args: - lora_path: Path to LoRA adapter (e.g., "./outputs/unsloth_Meta-Llama-3.1_.../") - hf_token: HF token for vision detection - - Returns: - ModelConfig for the LoRA adapter - """ - try: - lora_path_obj = Path(lora_path) - - if not lora_path_obj.exists(): - logger.error(f"LoRA path does not exist: {lora_path}") - return None - - # Get base model - base_model = get_base_model_from_lora(lora_path) - if not base_model: - logger.error(f"Could not determine base model for LoRA: {lora_path}") - return None - - # Check if base model is vision - is_vision = is_vision_model(base_model, hf_token=hf_token) - - display_name = lora_path_obj.name - identifier = lora_path # Use path as identifier for local LoRAs - - return cls( - identifier=identifier, - display_name=display_name, - path=lora_path, - is_local=True, - is_cached=True, # Local LoRAs are always "cached" - is_vision=is_vision, - is_lora=True, - base_model=base_model, - ) - - except Exception as e: - logger.error(f"Error creating ModelConfig from LoRA path: {e}") - return None - - - @classmethod - def from_ui_selection(cls, - dropdown_value: Optional[str], - search_value: Optional[str], - local_models: list = None, - hf_token: Optional[str] = None, - is_lora: bool = False) -> Optional['ModelConfig']: - """ - Create a universal ModelConfig from UI dropdown/search selections. - Handles base models and LoRA adapters. - """ - selected = None - if search_value and search_value.strip(): - selected = search_value.strip() - elif dropdown_value: - selected = dropdown_value - - if not selected: - return None - - display_name = selected - - # Use the correct 'local_models' parameter to resolve display names - if " (Active)" in selected or " (Ready)" in selected: - clean_display_name = selected.replace(" (Active)", "").replace(" (Ready)", "") - if local_models: - for local_display, local_path in local_models: - if local_display == clean_display_name: - selected = local_path - break - - # Clean all UI status indicators to get the final identifier - identifier = selected - for status in UI_STATUS_INDICATORS: - identifier = identifier.replace(status, "") - identifier = identifier.strip() - - is_local = is_local_path(identifier) - path = normalize_path(identifier) if is_local else identifier - - # Add unsloth/ prefix for shorthand HF models - if not is_local and "/" not in identifier: - identifier = f"unsloth/{identifier}" - path = identifier - - # --- Logic for Base Model and Vision Detection --- - base_model = None - is_vision = False - - if is_lora: - # For a LoRA, we MUST find its base model. - base_model = get_base_model_from_lora(path) - if not base_model: - logger.warning(f"Could not determine base model for LoRA '{path}'. Cannot create config.") - return None # Cannot proceed without a base model - - # A LoRA's vision capability is determined by its base model. - is_vision = is_vision_model(base_model, hf_token=hf_token) - else: - # For a base model, just check its own vision status. - is_vision = is_vision_model(identifier, hf_token=hf_token) - - from .path_utils import is_model_cached - is_cached = is_model_cached(identifier) if not is_local else True - - return cls( - identifier=identifier, - display_name=display_name, - path=path, - is_local=is_local, - is_cached=is_cached, - is_vision=is_vision, - is_lora=is_lora, - base_model=base_model, # This will be None for base models, and populated for LoRAs - ) - pass diff --git a/backend/backend/__init__.py b/backend/core/__init__.py similarity index 52% rename from backend/backend/__init__.py rename to backend/core/__init__.py index 33a28c425c..864227d49d 100644 --- a/backend/backend/__init__.py +++ b/backend/core/__init__.py @@ -1,36 +1,40 @@ """ -Unified backend module for Unsloth +Unified core module for Unsloth backend """ # Inference -from .inference import InferenceBackend +from .inference import InferenceBackend, get_inference_backend # Training -from .trainer import UnslothTrainer, get_trainer -from .training import TrainingBackend, get_training_backend, create_training_handlers +from .training import UnslothTrainer, get_trainer, TrainingBackend, get_training_backend, create_training_handlers, TrainingProgress -# Configuration -from .model_config import is_vision_model, ModelConfig, scan_trained_loras -# Utilities -from .path_utils import normalize_path, is_local_path, is_model_cached +# Configuration (from utils) +from utils.models import is_vision_model, ModelConfig, scan_trained_loras, load_model_defaults, get_base_model_from_lora + +# Utilities (from utils) +from utils.paths import normalize_path, is_local_path, is_model_cached from utils.utils import without_hf_auth, format_error_message, get_gpu_memory_info, search_hf_models from utils.datasets.dataset_utils import format_and_template_dataset __all__ = [ # Inference 'InferenceBackend', + 'get_inference_backend', # Training 'UnslothTrainer', 'get_trainer', 'get_training_backend', 'TrainingBackend', - "create_training_handlers", + 'create_training_handlers', + 'TrainingProgress', # Config 'ModelConfig', 'is_vision_model', 'scan_trained_loras', + 'load_model_defaults', + 'get_base_model_from_lora', # Utils 'search_hf_models', diff --git a/backend/core/export/__init__.py b/backend/core/export/__init__.py new file mode 100644 index 0000000000..66154f48eb --- /dev/null +++ b/backend/core/export/__init__.py @@ -0,0 +1,9 @@ +""" +Export submodule - Model export operations +""" +from .export import ExportBackend, get_export_backend + +__all__ = [ + 'ExportBackend', + 'get_export_backend', +] diff --git a/backend/backend/export.py b/backend/core/export/export.py similarity index 99% rename from backend/backend/export.py rename to backend/core/export/export.py index b1557624ab..11662c9e5f 100644 --- a/backend/backend/export.py +++ b/backend/core/export/export.py @@ -12,8 +12,8 @@ from huggingface_hub import HfApi, ModelCard from transformers.modeling_utils import PushToHubMixin import torch -from .model_config import is_vision_model, get_base_model_from_lora -from .inference import get_inference_backend +from utils.models import is_vision_model, get_base_model_from_lora +from core.inference import get_inference_backend logger = logging.getLogger(__name__) diff --git a/backend/core/inference/__init__.py b/backend/core/inference/__init__.py new file mode 100644 index 0000000000..494229a087 --- /dev/null +++ b/backend/core/inference/__init__.py @@ -0,0 +1,9 @@ +""" +Inference submodule - Inference backend for model loading and generation +""" +from .inference import InferenceBackend, get_inference_backend + +__all__ = [ + 'InferenceBackend', + 'get_inference_backend', +] diff --git a/backend/backend/inference.py b/backend/core/inference/inference.py similarity index 99% rename from backend/backend/inference.py rename to backend/core/inference/inference.py index 117487cb79..5d4817f3f4 100644 --- a/backend/backend/inference.py +++ b/backend/core/inference/inference.py @@ -9,8 +9,8 @@ from peft import PeftModel, PeftModelForCausalLM import sys import torch from typing import Optional, Generator, Tuple -from .model_config import ModelConfig, get_base_model_from_lora -from .path_utils import is_model_cached +from utils.models import ModelConfig, get_base_model_from_lora +from utils.paths import is_model_cached from utils.utils import format_error_message, log_gpu_memory from io import StringIO import logging @@ -319,7 +319,7 @@ class InferenceBackend: Prepare for eval: ensure base model and the specified adapter are loaded. """ try: - from .model_config import ModelConfig + from utils.models import ModelConfig lora_config = ModelConfig.from_lora_path(lora_path, hf_token) if not lora_config: return False, None, None @@ -368,7 +368,7 @@ class InferenceBackend: This function is idempotent and handles all states correctly. """ try: - from .model_config import ModelConfig + from utils.models import ModelConfig lora_config = ModelConfig.from_lora_path(lora_path, hf_token) if not lora_config: return False, None, None diff --git a/backend/core/training/__init__.py b/backend/core/training/__init__.py new file mode 100644 index 0000000000..65bf4c3501 --- /dev/null +++ b/backend/core/training/__init__.py @@ -0,0 +1,14 @@ +""" +Training submodule - Training backends and trainer classes +""" +from .trainer import UnslothTrainer, get_trainer, TrainingProgress +from .training import TrainingBackend, get_training_backend, create_training_handlers + +__all__ = [ + 'UnslothTrainer', + 'get_trainer', + 'TrainingProgress', + 'TrainingBackend', + 'get_training_backend', + 'create_training_handlers', +] diff --git a/backend/backend/trainer.py b/backend/core/training/trainer.py similarity index 99% rename from backend/backend/trainer.py rename to backend/core/training/trainer.py index ce66e8ea6f..80cd5f9ef3 100644 --- a/backend/backend/trainer.py +++ b/backend/core/training/trainer.py @@ -19,7 +19,7 @@ from datasets import Dataset, load_dataset # Add the parent directory to sys.path to import unsloth modules #sys.path.append(os.path.join(os.path.dirname(__file__), '..')) -from .model_config import is_vision_model +from utils.models import is_vision_model from utils.datasets.dataset_utils import format_and_template_dataset from utils.datasets.dataset_utils import MODEL_TO_TEMPLATE_MAPPER, TEMPLATE_TO_RESPONSES_MAPPER from trl import SFTTrainer, SFTConfig diff --git a/backend/backend/training.py b/backend/core/training/training.py similarity index 99% rename from backend/backend/training.py rename to backend/core/training/training.py index 02e96128eb..9bd44400ac 100644 --- a/backend/backend/training.py +++ b/backend/core/training/training.py @@ -494,7 +494,7 @@ class TrainingBackend: print("=" * 60) try: - from .inference import get_inference_backend + from ..inference import get_inference_backend session = self.current_training_session diff --git a/backend/routes/models.py b/backend/routes/models.py index c58bf0a138..40db17eb04 100644 --- a/backend/routes/models.py +++ b/backend/routes/models.py @@ -15,28 +15,28 @@ if str(backend_path) not in sys.path: # Import backend functions try: from utils.utils import search_hf_models - from backend.model_config import ( + from utils.models import ( scan_trained_loras, load_model_defaults, get_base_model_from_lora, is_vision_model, ModelConfig, ) - from backend.inference import get_inference_backend + from core.inference import get_inference_backend except ImportError: # Fallback: try to import from parent directory parent_backend = backend_path.parent / "backend" if str(parent_backend) not in sys.path: sys.path.insert(0, str(parent_backend)) from utils.utils import search_hf_models - from backend.model_config import ( + from utils.models import ( scan_trained_loras, load_model_defaults, get_base_model_from_lora, is_vision_model, ModelConfig, ) - from backend.inference import get_inference_backend + from core.inference import get_inference_backend from models.models import ( ModelSearchRequest, diff --git a/backend/routes/training.py b/backend/routes/training.py index 361e5a7b6b..4eaa0643d1 100644 --- a/backend/routes/training.py +++ b/backend/routes/training.py @@ -19,13 +19,13 @@ if str(backend_path) not in sys.path: # Import backend functions try: - from backend.training import get_training_backend + from core.training import get_training_backend except ImportError: # Fallback: try to import from parent directory parent_backend = backend_path.parent / "backend" if str(parent_backend) not in sys.path: sys.path.insert(0, str(parent_backend)) - from backend.training import get_training_backend + from core.training import get_training_backend from models.training import ( TrainingStartRequest, diff --git a/backend/utils/paths/__init__.py b/backend/utils/paths/__init__.py new file mode 100644 index 0000000000..ffc16dfb3d --- /dev/null +++ b/backend/utils/paths/__init__.py @@ -0,0 +1,11 @@ +""" +Path utilities for model and dataset handling +""" +from .path_utils import normalize_path, is_local_path, is_model_cached, get_cache_path + +__all__ = [ + 'normalize_path', + 'is_local_path', + 'is_model_cached', + 'get_cache_path', +] diff --git a/backend/backend/path_utils.py b/backend/utils/paths/path_utils.py similarity index 100% rename from backend/backend/path_utils.py rename to backend/utils/paths/path_utils.py