From d593b069e2805bcd4406015efafb6eac0d017596 Mon Sep 17 00:00:00 2001 From: sshah229 Date: Sun, 1 Feb 2026 01:23:16 -0700 Subject: [PATCH] Added the training and models routes --- backend/backend/__init__.py | 44 + backend/backend/dataset_utils.py | 2805 ++++++++++++++++++++ backend/backend/export.py | 506 ++++ backend/backend/inference.py | 1212 +++++++++ backend/backend/model_config.py | 704 +++++ backend/backend/path_utils.py | 78 + backend/backend/trainer.py | 864 ++++++ backend/backend/training.py | 680 +++++ backend/backend/utils.py | 208 ++ backend/main.py | 113 + backend/models/__init__.py | 37 + backend/models/models.py | 56 + backend/models/training.py | 96 + backend/requirements.txt | 7 + backend/routes/__init__.py | 8 + backend/routes/models.py | 310 +++ backend/routes/training.py | 437 +++ backend/run.py | 94 + backend/utils/datasets/alpaca_unsloth.json | 1288 +++++++++ 19 files changed, 9547 insertions(+) create mode 100644 backend/backend/__init__.py create mode 100644 backend/backend/dataset_utils.py create mode 100644 backend/backend/export.py create mode 100644 backend/backend/inference.py create mode 100644 backend/backend/model_config.py create mode 100644 backend/backend/path_utils.py create mode 100644 backend/backend/trainer.py create mode 100644 backend/backend/training.py create mode 100644 backend/backend/utils.py create mode 100644 backend/main.py create mode 100644 backend/models/models.py create mode 100644 backend/models/training.py create mode 100644 backend/requirements.txt create mode 100644 backend/routes/models.py create mode 100644 backend/run.py create mode 100644 backend/utils/datasets/alpaca_unsloth.json diff --git a/backend/backend/__init__.py b/backend/backend/__init__.py new file mode 100644 index 0000000000..55da3dbee7 --- /dev/null +++ b/backend/backend/__init__.py @@ -0,0 +1,44 @@ +""" +Unified backend module for Unsloth +""" + +# Inference +from .inference import InferenceBackend + +# Training +from .trainer import UnslothTrainer, get_trainer +from .training import TrainingBackend, get_training_backend, create_training_handlers + +# 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 +from .utils import without_hf_auth, format_error_message, get_gpu_memory_info, search_hf_models +from .dataset_utils import format_and_template_dataset + +__all__ = [ + # Inference + 'InferenceBackend', + + # Training + 'UnslothTrainer', + 'get_trainer', + 'get_training_backend', + 'TrainingBackend', + "create_training_handlers", + + # Config + 'ModelConfig', + 'is_vision_model', + 'scan_trained_loras', + + # Utils + 'search_hf_models', + 'format_and_template_dataset', + 'normalize_path', + 'is_local_path', + 'is_model_cached', + 'without_hf_auth', + 'format_error_message', + 'get_gpu_memory_info', +] diff --git a/backend/backend/dataset_utils.py b/backend/backend/dataset_utils.py new file mode 100644 index 0000000000..9d42e76041 --- /dev/null +++ b/backend/backend/dataset_utils.py @@ -0,0 +1,2805 @@ +import torch +from torch.utils.data import IterableDataset +try: + from deepseek_ocr.modeling_deepseekocr import ( + format_messages, + text_encode, + BasicImageTransform, + dynamic_preprocess, + ) + DEEPSEEK_OCR_AVAILABLE = True +except ImportError: + DEEPSEEK_OCR_AVAILABLE = False + format_messages = None + text_encode = None + BasicImageTransform = None + dynamic_preprocess = None + import logging + logging.getLogger(__name__).warning( + "DeepSeek OCR module not found. Will auto-install if needed." + ) +import math +from dataclasses import dataclass +from typing import Dict, List, Any, Tuple +from PIL import Image, ImageOps +from torch.nn.utils.rnn import pad_sequence +import io + + + + +DEFAULT_ALPACA_TEMPLATE = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request. + +### Instruction: +{} + +### Input: +{} + +### Response: +{}""" + + + +def standardize_chat_format( + dataset, + tokenizer=None, + aliases_for_system=["system",], + aliases_for_user=["user", "human", "input",], + aliases_for_assistant=["gpt", "assistant", "output",], + batch_size=1000, + num_proc=None, +): + """ + Our own standardization function that handles BOTH messages and conversations. + Converts non-standard role names and keys to standard format. + """ + import collections + import itertools + from datasets import IterableDataset + + # Check if vision tokenizer is used + is_vlm = False + if tokenizer is not None: + if hasattr(tokenizer, "image_processor") or hasattr(tokenizer, "tokenizer"): + is_vlm = True + + column_names = set(next(iter(dataset)).keys()) + + # Check for both 'conversations' and 'messages' + chat_column = None + if "conversations" in column_names: + chat_column = "conversations" + elif "messages" in column_names: + chat_column = "messages" + elif "texts" in column_names: + chat_column = "texts" + else: + return dataset # No chat column found + + # Inspect structure + examples = itertools.islice(dataset, 10) + uniques = collections.defaultdict(list) + for example in examples: + for message in example[chat_column]: + for key, value in message.items(): + if type(value) is not str: + continue # Skip non-string values + uniques[key].append(value) + + if len(uniques.keys()) != 2: + return dataset # Unexpected structure + + keys = list(uniques.keys()) + length_first = len(set(uniques[keys[0]])) + length_second = len(set(uniques[keys[1]])) + + # Determine which is role and which is content + if length_first < length_second: + role_key = keys[0] + content_key = keys[1] + else: + role_key = keys[1] + content_key = keys[0] + + # Mapping for aliases + aliases_mapping = {} + for x in aliases_for_system: aliases_mapping[x] = "system" + for x in aliases_for_user: aliases_mapping[x] = "user" + for x in aliases_for_assistant: aliases_mapping[x] = "assistant" + + def _standardize_dataset(examples): + convos = examples[chat_column] + all_convos = [] + for convo in convos: + new_convo = [] + for message in convo: + # Get original role and content + original_role = message.get(role_key, "") + original_content = message.get(content_key, "") + + # Map to standard role name + standard_role = aliases_mapping.get(original_role, original_role) + + # Handle VLM format + if is_vlm: + original_content = [{"type": "text", "text": original_content}] + + # Create dict with EXPLICIT ORDER + new_message = {"role": standard_role, "content": original_content} + new_convo.append(new_message) + + all_convos.append(new_convo) + + return {chat_column: all_convos} + + + dataset_map_kwargs = { + 'batched': True, + 'batch_size': batch_size, + } + + if not isinstance(dataset, IterableDataset): + from multiprocessing import cpu_count + + if num_proc is None or type(num_proc) is not int: + num_proc = cpu_count() + + dataset_map_kwargs['num_proc'] = num_proc + dataset_map_kwargs['desc'] = "Standardizing chat format" + + return dataset.map(_standardize_dataset, **dataset_map_kwargs) +pass + + +def detect_dataset_format(dataset): + """ + Detects dataset format by inspecting structure. + + Returns: + dict: { + "format": "alpaca" | "sharegpt" | "chatml" | "unknown", + "chat_column": "messages" | "conversations" | None, + "needs_standardization": bool, + "sample_keys": list of keys found in messages (for debugging) + } + """ + column_names = set(next(iter(dataset)).keys()) + + # Check for Alpaca + alpaca_columns = {"instruction", "output"} + if alpaca_columns.issubset(column_names): + return { + "format": "alpaca", + "chat_column": None, + "needs_standardization": False, + "sample_keys": [] + } + + # Check for chat-based formats (messages or conversations) + chat_column = None + if "messages" in column_names: + chat_column = "messages" + elif "conversations" in column_names: + chat_column = "conversations" + elif "texts" in column_names: + chat_column = "texts" + + if chat_column: + # Inspect the structure to determine if ShareGPT or ChatML + try: + sample = next(iter(dataset)) + chat_data = sample[chat_column] + + if chat_data and len(chat_data) > 0: + first_msg = chat_data[0] + msg_keys = set(first_msg.keys()) + + # ShareGPT uses "from" and "value" + if "from" in msg_keys or "value" in msg_keys: + return { + "format": "sharegpt", + "chat_column": chat_column, + "needs_standardization": True, + "sample_keys": list(msg_keys) + } + + # ChatML uses "role" and "content" + elif "role" in msg_keys and "content" in msg_keys: + return { + "format": "chatml", + "chat_column": chat_column, + "needs_standardization": False, + "sample_keys": list(msg_keys) + } + + # Unknown structure but has chat column + else: + return { + "format": "unknown", + "chat_column": chat_column, + "needs_standardization": None, + "sample_keys": list(msg_keys) + } + except Exception as e: + return { + "format": "unknown", + "chat_column": chat_column, + "needs_standardization": None, + "sample_keys": [], + "error": str(e) + } + + # No recognized format + return { + "format": "unknown", + "chat_column": None, + "needs_standardization": None, + "sample_keys": [] + } + + +def format_dataset( + dataset, + format_type = "auto", + tokenizer = None, + aliases_for_system = ["system",], + aliases_for_user = ["user", "human", "input",], + aliases_for_assistant = ["gpt", "assistant", "output",], + batch_size = 1000, + num_proc = None, + auto_detect_custom = True, +): + """ + Formats dataset and returns metadata. + + Returns: + dict: { + "dataset": processed dataset, + "detected_format": original format detected, + "final_format": final format after processing, + "chat_column": column name with chat data, + "is_standardized": whether role names are standardized, + "warnings": list of warning messages + } + """ + + # Detect multimodal first + multimodal_info = detect_multimodal_dataset(dataset) + + # Detect current format + detected = detect_dataset_format(dataset) + warnings = [] + + # Add multimodal warning if detected + if multimodal_info["is_multimodal"]: + warnings.append( + f"Multimodal dataset detected. Found columns: {multimodal_info['multimodal_columns']}" + ) + + # AUTO MODE: Keep format but standardize if needed + if format_type == "auto": + + # Alpaca - keep as is + if detected["format"] == "alpaca": + return { + "dataset": dataset, + "detected_format": "alpaca", + "final_format": "alpaca", + "chat_column": None, + "is_standardized": True, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "warnings": [] + } + + # ShareGPT - needs standardization + elif detected["format"] == "sharegpt": + try: + standardized = standardize_chat_format( + dataset, tokenizer, aliases_for_system, + aliases_for_user, aliases_for_assistant, + batch_size, num_proc + ) + return { + "dataset": standardized, + "detected_format": "sharegpt", + "final_format": f"chatml_{detected['chat_column']}", + "chat_column": detected["chat_column"], + "is_standardized": True, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "warnings": [] + } + except Exception as e: + warnings.append(f"Failed to standardize ShareGPT format: {e}") + return { + "dataset": dataset, + "detected_format": "sharegpt", + "final_format": "sharegpt", + "chat_column": detected["chat_column"], + "is_standardized": False, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "warnings": warnings + } + + elif detected["format"] == "chatml" and detected["chat_column"] in ["conversations", "messages", "texts"]: + return { + "dataset": dataset, + "detected_format": f"chatml_{detected['chat_column']}", + "final_format": f"chatml_{detected['chat_column']}", + "chat_column": detected["chat_column"], + "is_standardized": True, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "warnings": warnings + } + + + # Unknown - try standardization, if fails pass as is + else: + warnings.append(f"Unknown format detected. Keys found: {detected['sample_keys']}") + + # NEW: Try heuristic detection + if auto_detect_custom: + custom_mapping = detect_custom_format_heuristic(dataset) + if custom_mapping: + warnings.append(f"Auto-detected column mapping: {custom_mapping}") + + + def _apply_auto_mapping(examples): + conversations = [] + num_examples = len(examples[list(examples.keys())[0]]) + + # NEW: Check if this is user-provided or auto-detected + is_user_provided = custom_format_mapping is not None # Passed explicitly + + # Preserve non-mapped columns ONLY if auto-detected + preserved_columns = {} + if not is_user_provided: # Only preserve for auto-detection + all_columns = set(examples.keys()) + mapped_columns = set(custom_mapping.keys()) + non_mapped_columns = all_columns - mapped_columns + + for col in non_mapped_columns: + preserved_columns[col] = examples[col] + + for i in range(num_examples): + convo = [] + + # Enforce standard role order + role_order = ['system', 'user', 'assistant'] + + for target_role in role_order: + for col_name, role in custom_mapping.items(): + if role == target_role and col_name in examples: + content = examples[col_name][i] + + # NEW: Different behavior based on mapping source + if is_user_provided: + # User explicitly mapped this - always include even if empty + convo.append({"role": role, "content": str(content) if content else ""}) + else: + # Auto-detected - skip empty (original behavior) + if content and str(content).strip(): + convo.append({"role": role, "content": str(content)}) + + conversations.append(convo) + + result = {"conversations": conversations} + + # Only add preserved columns if auto-detected + if not is_user_provided: + result.update(preserved_columns) + + return result + + + try: + dataset = dataset.map(_apply_auto_mapping, batched=True, batch_size=batch_size) + return { + "dataset": dataset, + "detected_format": "unknown", + "final_format": "chatml_conversations", + "chat_column": "conversations", + "is_standardized": True, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "warnings": warnings + } + except Exception as e: + warnings.append(f"Auto-detection failed: {e}") + + # Try standardization as a last resort + if detected["chat_column"]: + try: + standardized = standardize_chat_format( + dataset, tokenizer, aliases_for_system, + aliases_for_user, aliases_for_assistant, + batch_size, num_proc + ) + warnings.append("Successfully standardized unknown format") + return { + "dataset": standardized, + "detected_format": "unknown", + "final_format": f"chatml_{detected['chat_column']}", + "chat_column": detected["chat_column"], + "is_standardized": True, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "warnings": warnings + } + except Exception as e: + warnings.append(f"Could not standardize: {e}. Passing dataset as-is.") + + # Return as-is with warnings + return { + "dataset": dataset, + "detected_format": "unknown", + "final_format": "unknown", + "chat_column": detected["chat_column"], + "is_standardized": False, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "warnings": warnings + } + + # ALPACA MODE: Convert to Alpaca + elif format_type == "alpaca": + + if detected["format"] == "alpaca": + return { + "dataset": dataset, + "detected_format": "alpaca", + "final_format": "alpaca", + "chat_column": None, + "is_standardized": True, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "warnings": [] + } + + elif detected["format"] in ["sharegpt", "chatml"]: + # First standardize if ShareGPT + if detected["format"] == "sharegpt": + dataset = standardize_chat_format( + dataset, tokenizer, aliases_for_system, + aliases_for_user, aliases_for_assistant, + batch_size, num_proc + ) + + # Then convert to Alpaca + converted = convert_chatml_to_alpaca(dataset, batch_size, num_proc) + return { + "dataset": converted, + "detected_format": detected["format"], + "final_format": "alpaca", + "chat_column": None, + "is_standardized": True, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "warnings": [] + } + + else: + warnings.append(f"Cannot convert unknown format to Alpaca") + return { + "dataset": dataset, + "detected_format": "unknown", + "final_format": "unknown", + "chat_column": detected["chat_column"], + "is_standardized": False, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "warnings": warnings + } + + # CHATML MODE: Convert to ChatML + elif format_type in ["chatml", "conversational"]: + + if detected["format"] == "alpaca": + converted = convert_alpaca_to_chatml(dataset, batch_size, num_proc) + return { + "dataset": converted, + "detected_format": "alpaca", + "final_format": "chatml_conversations", + "chat_column": "conversations", + "is_standardized": True, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "warnings": [] + } + + elif detected["format"] == "sharegpt": + standardized = standardize_chat_format( + dataset, tokenizer, aliases_for_system, + aliases_for_user, aliases_for_assistant, + batch_size, num_proc + ) + return { + "dataset": standardized, + "detected_format": "sharegpt", + "final_format": f"chatml_{detected['chat_column']}", + "chat_column": detected["chat_column"], + "is_standardized": True, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "warnings": [] + } + + elif detected["format"] == "chatml": + return { + "dataset": dataset, + "detected_format": f"chatml_{detected['chat_column']}", + "final_format": f"chatml_{detected['chat_column']}", + "chat_column": detected["chat_column"], + "is_standardized": True, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "warnings": [] + } + + else: + warnings.append(f"Unknown format, attempting standardization") + try: + standardized = standardize_chat_format( + dataset, tokenizer, aliases_for_system, + aliases_for_user, aliases_for_assistant, + batch_size, num_proc + ) + return { + "dataset": standardized, + "detected_format": "unknown", + "final_format": f"chatml_{detected['chat_column']}", + "chat_column": detected["chat_column"], + "is_standardized": True, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "warnings": warnings + } + except Exception as e: + warnings.append(f"Standardization failed: {e}") + return { + "dataset": dataset, + "detected_format": "unknown", + "final_format": "unknown", + "chat_column": detected["chat_column"], + "is_standardized": False, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "warnings": warnings + } + + else: + raise ValueError(f"Unknown format_type: {format_type}") +pass + + +def convert_chatml_to_alpaca(dataset, batch_size=1000, num_proc=None): + """ + Converts ChatML format (messages OR conversations) to Alpaca format. + Handles both standardized and ShareGPT formats. + + Supports: + - "messages" or "conversations" column + - "role"/"content" (standard) or "from"/"value" (ShareGPT) + """ + def _convert(examples): + # Auto-detect which column name is used + chatml_data = examples.get("messages") or examples.get("conversations") or examples.get("texts") + + if chatml_data is None: + raise ValueError("No 'messages' or 'conversations' or 'texts' column found.") + + instructions = [] + outputs = [] + inputs = [] + + for convo in chatml_data: + instruction = "" + output = "" + + for msg in convo: + # Handle both standard and ShareGPT formats + role = msg.get("role") or msg.get("from") + content = msg.get("content") or msg.get("value") + + # Get first user message as instruction + if role in ["user", "human", "input"] and not instruction: + instruction = content + # Get first assistant message as output + elif role in ["assistant", "gpt", "output"] and not output: + output = content + break # Stop after first assistant response + + instructions.append(instruction) + inputs.append("") # Alpaca typically has empty input + outputs.append(output) + + return { + "instruction": instructions, + "input": inputs, + "output": outputs + } + + dataset_map_kwargs = { + 'batched': True, + 'batch_size': batch_size, + } + + if not isinstance(dataset, IterableDataset): + from multiprocessing import cpu_count + + if num_proc is None or type(num_proc) is not int: + num_proc = cpu_count() + + dataset_map_kwargs['num_proc'] = num_proc + dataset_map_kwargs['desc'] = "Converting ChatML to Alpaca format" + + return dataset.map(_convert, **dataset_map_kwargs) + + +def convert_alpaca_to_chatml(dataset, batch_size=1000, num_proc=None): + """ + Converts Alpaca format to ChatML format. + + Output format: Uses 'conversations' column with standard 'role'/'content' structure. + """ + def _convert(examples): + conversations = [] + + for i in range(len(examples["instruction"])): + instruction = examples["instruction"][i] + input_text = examples.get("input", [""] * len(examples["instruction"]))[i] + output = examples["output"][i] + + # Combine instruction and input (if exists) for user message + if input_text and input_text.strip(): + user_content = f"{instruction}\n\n{input_text}".strip() + else: + user_content = instruction + + # Build conversation in standard ChatML format + convo = [ + {"role": "user", "content": user_content}, + {"role": "assistant", "content": output} + ] + conversations.append(convo) + + return {"conversations": conversations} + + dataset_map_kwargs = { + 'batched': True, + 'batch_size': batch_size, + } + + if not isinstance(dataset, IterableDataset): + from multiprocessing import cpu_count + + if num_proc is None or type(num_proc) is not int: + num_proc = cpu_count() + + dataset_map_kwargs['num_proc'] = num_proc + dataset_map_kwargs['desc'] = "Converting Alpaca to ChatML format" + + return dataset.map(_convert, **dataset_map_kwargs) + + +def apply_chat_template_to_dataset( + dataset_info, + tokenizer, + model_name = None, + custom_prompt_template=None, + add_eos_token=False, + remove_bos_prefix=False, + custom_format_mapping=None, + auto_detect_mapping=True, + batch_size=1000, + num_proc=None, +): + """ + Applies chat template to dataset based on its format. + + Args: + dataset_info: Output from format_dataset() with metadata + tokenizer: Tokenizer with chat template + custom_prompt_template: Optional string template for custom formatting + add_eos_token: If True, appends tokenizer.eos_token to each text + remove_bos_prefix: If True, removes '' prefix (for Gemma, etc.) + custom_format_mapping: Dict mapping custom columns to standard format + batch_size: Batch size for processing + num_proc: Number of processes + + Returns: + dict with dataset, success status, warnings, and errors + """ + dataset = dataset_info["dataset"] + final_format = dataset_info["final_format"] + chat_column = dataset_info["chat_column"] + is_standardized = dataset_info["is_standardized"] + + warnings = list(dataset_info.get("warnings", [])) + errors = [] + + # Get EOS token if needed + eos_token = "" + if add_eos_token: + if hasattr(tokenizer, 'eos_token') and tokenizer.eos_token: + eos_token = tokenizer.eos_token + else: + warnings.append("add_eos_token=True but tokenizer has no eos_token") + + # CUSTOM FORMAT MAPPING (for non-standard datasets) + if final_format == "unknown": + # NEW: Try auto-detection if no custom mapping provided + if custom_format_mapping is None and auto_detect_mapping: + # Check if format_dataset already tried and failed + if not dataset_info.get("auto_detection_attempted", False): + custom_format_mapping = detect_custom_format_heuristic(dataset) + if custom_format_mapping: + warnings.append(f"Auto-detected column mapping: {custom_format_mapping}") + else: + errors.append("Could not auto-detect format mapping") + return { + "dataset": dataset, + "success": False, + "warnings": warnings, + "errors": errors + } + else: + # Already failed once in format_dataset, don't retry + errors.append( + "Format remains unknown after detection attempts. " + "Please provide custom_format_mapping to specify column roles manually." + ) + return { + "dataset": dataset, + "success": False, + "warnings": warnings, + "errors": errors + } + + if custom_format_mapping: + warnings.append(f"Applying custom format mapping: {custom_format_mapping}") + is_user_provided = dataset_info.get("custom_format_mapping") is not None + + + def _apply_custom_mapping(examples): + conversations = [] + num_examples = len(examples[list(examples.keys())[0]]) + + # Only preserve unmapped columns if auto-detected + preserved_columns = {} + if not is_user_provided: + all_columns = set(examples.keys()) + mapped_columns = set(custom_format_mapping.keys()) + non_mapped_columns = all_columns - mapped_columns + + for col in non_mapped_columns: + preserved_columns[col] = examples[col] + + for i in range(num_examples): + convo = [] + role_order = ['system', 'user', 'assistant'] + + for target_role in role_order: + for col_name, role in custom_format_mapping.items(): + if role == target_role and col_name in examples: + content = examples[col_name][i] + + if is_user_provided: + # User explicitly mapped - include even if empty + convo.append({"role": role, "content": str(content) if content else ""}) + else: + # Auto-detected - skip empty + if content and str(content).strip(): + convo.append({"role": role, "content": str(content)}) + + conversations.append(convo) + + result = {"conversations": conversations} + if not is_user_provided: + result.update(preserved_columns) + return result + + try: + dataset = dataset.map(_apply_custom_mapping, batched=True, batch_size=batch_size) + # Update to use conversations format + final_format = "chatml_conversations" + chat_column = "conversations" + is_standardized = True + warnings.append("Successfully converted to ChatML format via custom mapping") + except Exception as e: + errors.append(f"Custom format mapping failed: {e}") + return { + "dataset": dataset, + "success": False, + "warnings": warnings, + "errors": errors + } + + # ALPACA FORMAT + if final_format == "alpaca": + + # Use custom template if provided + def _format_alpaca_custom(examples): + texts = [] + for i in range(len(examples["instruction"])): + fields = { + "instruction": examples["instruction"][i], + "input": examples.get("input", [""] * len(examples["instruction"]))[i], + "output": examples["output"][i] + } + + try: + text = DEFAULT_ALPACA_TEMPLATE.format(fields["instruction"],fields["input"],fields["output"]) + text += eos_token + texts.append(text) + except KeyError as e: + errors.append(f"Custom template missing field: {e}") + texts.append("") + + return {"text": texts} + + formatted_fn = _format_alpaca_custom + + try: + dataset_map_kwargs = { + 'batched': True, + 'batch_size': batch_size, + } + + if not isinstance(dataset, IterableDataset): + from multiprocessing import cpu_count + if num_proc is None or type(num_proc) is not int: + num_proc = cpu_count() + dataset_map_kwargs['num_proc'] = num_proc + dataset_map_kwargs['desc'] = "Applying template to Alpaca format" + + formatted_dataset = dataset.map(formatted_fn, **dataset_map_kwargs) + + return { + "dataset": formatted_dataset, + "success": True, + "warnings": warnings, + "errors": errors + } + except Exception as e: + errors.append(f"Failed to format Alpaca dataset: {e}") + return { + "dataset": dataset, + "success": False, + "warnings": warnings, + "errors": errors + } + + # CHATML FORMATS + elif final_format in ["chatml_messages", "chatml_conversations"]: + + if not is_standardized: + warnings.append("Dataset may not be fully standardized") + + # Apply Unsloth chat template if model matches + if model_name: + tokenizer = get_tokenizer_chat_template(tokenizer, model_name) + + def _format_chatml(examples): + convos = examples[chat_column] + texts = [] + + for convo in convos: + try: + text = tokenizer.apply_chat_template( + convo, + tokenize=False, + add_generation_prompt=False + ) + + if remove_bos_prefix: + text = text.removeprefix('') + text += eos_token + + texts.append(text) + except Exception as e: + if len(texts) == 0: + warnings.append(f"Chat template failed: {e}") + texts.append("") + + return {"text": texts} + + try: + dataset_map_kwargs = { + 'batched': True, + 'batch_size': batch_size, + } + + if not isinstance(dataset, IterableDataset): + from multiprocessing import cpu_count + if num_proc is None or type(num_proc) is not int: + num_proc = cpu_count() + dataset_map_kwargs['num_proc'] = num_proc + dataset_map_kwargs['desc'] = f"Applying chat template to {final_format}" + + formatted_dataset = dataset.map(_format_chatml, **dataset_map_kwargs) + + return { + "dataset": formatted_dataset, + "success": True, + "warnings": warnings, + "errors": errors + } + except Exception as e: + errors.append(f"Failed to format ChatML dataset: {e}") + return { + "dataset": dataset, + "success": False, + "warnings": warnings, + "errors": errors + } + + # UNKNOWN FORMAT + else: + errors.append( + f"Cannot apply chat template to format: {final_format}. " + f"This should not happen after custom mapping." + ) + return { + "dataset": dataset, + "success": False, + "warnings": warnings, + "errors": errors + } + + +def format_and_template_dataset( + dataset, + model_name, + tokenizer, + is_vlm = False, + format_type="auto", + # VLM-specific parameters + vlm_instruction=None, # Now optional - will auto-generate + vlm_text_column=None, + vlm_image_column=None, + dataset_name=None, + + custom_prompt_template=None, + add_eos_token=False, + remove_bos_prefix=False, + custom_format_mapping=None, + auto_detect_custom=True, # NEW + auto_detect_mapping=True, # NEW + aliases_for_system=["system",], + aliases_for_user=["user", "human", "input",], + aliases_for_assistant=["gpt", "assistant", "output",], + batch_size=1000, + num_proc=None, +): + """ + Convenience function that combines format_dataset and apply_chat_template_to_dataset. + Perfect for UI workflows - one function does everything! + + Returns: + dict: { + "dataset": Final dataset with 'text' column, + "detected_format": Original format, + "final_format": Format after processing, + "success": Whether template application succeeded, + "warnings": List of warnings, + "errors": List of errors, + "summary": Human-readable summary + } + """ + + # VLM FLOW + if is_vlm: + warnings = [] + errors = [] + + multimodal_info = detect_multimodal_dataset(dataset) + vlm_structure = detect_vlm_dataset_structure(dataset) + + # Handle Llava format + if vlm_structure["format"] == "vlm_messages_llava": + try: + dataset = convert_llava_to_vlm_format(dataset) + warnings.append("Converted from Llava format (image indices) to standard VLM format") + except Exception as e: + errors.append(f"Failed to convert Llava format: {e}") + import traceback + traceback.print_exc() + + return { + "dataset": dataset, + "detected_format": "vlm_messages_llava", + "final_format": "vlm_conversion_failed", + "is_vlm": True, + "success": False, + "warnings": warnings, + "errors": errors, + } + + # Handle simple format + elif vlm_structure["needs_conversion"]: + # ... existing simple conversion code + if vlm_text_column is None: + vlm_text_column = vlm_structure["text_column"] + if vlm_image_column is None: + vlm_image_column = vlm_structure["image_column"] + + if vlm_text_column is None or vlm_image_column is None: + errors.append( + f"Could not auto-detect image/text columns. Found: {vlm_structure}. " + ) + return { + "dataset": dataset, + "detected_format": "vlm_unknown", + "final_format": "vlm_unknown", + "is_vlm": True, + "success": False, + "warnings": warnings, + "errors": errors, + } + + try: + dataset = convert_to_vlm_format( + dataset, + instruction=vlm_instruction, + text_column=vlm_text_column, + image_column=vlm_image_column, + dataset_name=dataset_name, + ) + + if vlm_instruction: + warnings.append(f"Using user-provided instruction: '{vlm_instruction}'") + else: + warnings.append("Auto-generated instruction based on dataset analysis") + + except Exception as e: + errors.append(f"Failed to convert to VLM format: {e}") + import traceback + traceback.print_exc() + + return { + "dataset": dataset, + "detected_format": vlm_structure["format"], + "final_format": "vlm_conversion_failed", + "is_vlm": True, + "success": False, + "warnings": warnings, + "errors": errors, + } + + # Already in standard VLM format + elif vlm_structure["format"] == "vlm_messages": + dataset = [sample for sample in dataset] + warnings.append("Dataset already in standard VLM messages format") + + # Return as list + return { + "dataset": dataset, + "detected_format": vlm_structure["format"], + "final_format": "vlm_messages", + "chat_column": "messages", + "is_vlm": True, + "is_multimodal": multimodal_info["is_multimodal"], + "multimodal_info": multimodal_info, + "vlm_structure": vlm_structure, + "success": True, + "warnings": warnings, + "errors": errors, + } + + # LLM FLOW (Existing code) + else: + # Step 1: Format the dataset + dataset_info = format_dataset( + dataset, + format_type=format_type, + tokenizer=tokenizer, + auto_detect_custom = auto_detect_custom, + aliases_for_system=aliases_for_system, + aliases_for_user=aliases_for_user, + aliases_for_assistant=aliases_for_assistant, + batch_size=batch_size, + num_proc=num_proc, + ) + + # Step 2: Apply chat template + if "gemma" in model_name.lower() and not dataset_info["is_multimodal"] and (format_type != "alpaca" or (format_type == "auto" and dataset_info["detected_format"] != "alpaca")): + print("remove_bos_prefix is true") + remove_bos_prefix = True + template_result = apply_chat_template_to_dataset( + dataset_info=dataset_info, + tokenizer=tokenizer, + model_name = model_name, + custom_prompt_template=custom_prompt_template, + add_eos_token=add_eos_token, + remove_bos_prefix=remove_bos_prefix, + custom_format_mapping=custom_format_mapping, + auto_detect_mapping = auto_detect_mapping, + batch_size=batch_size, + num_proc=num_proc, + ) + + # Step 3: Generate summary + summary = get_dataset_info_summary(dataset_info) + + # Combine results + all_warnings = dataset_info.get("warnings", []) + template_result.get("warnings", []) + all_errors = template_result.get("errors", []) + + return { + "dataset": template_result["dataset"], + "detected_format": dataset_info["detected_format"], + "final_format": dataset_info["final_format"], + "chat_column": dataset_info.get("chat_column"), + "is_vlm": False, # This is LLM flow + "success": template_result["success"], + "warnings": all_warnings, + "errors": all_errors, + "summary": summary, + } + + +def get_dataset_info_summary(dataset_info): + """ + Returns a human-readable summary for UI display. + """ + detected_format = dataset_info["detected_format"] + final_format = dataset_info["final_format"] + + format_descriptions = { + "alpaca": "Alpaca format (instruction/input/output)", + "sharegpt": "ShareGPT format (needs standardization)", + "chatml_messages": "ChatML format (messages column) - OpenAI compatible", + "chatml_conversations": "ChatML format (conversations column) - HuggingFace standard", + "unknown": "Unknown format" + } + + return { + "detected_format": detected_format, + "final_format": final_format, + "detected_description": format_descriptions.get(detected_format, "Unknown"), + "final_description": format_descriptions.get(final_format, "Unknown"), + "chat_column": dataset_info["chat_column"], + "is_standardized": dataset_info["is_standardized"], + "warnings": dataset_info.get("warnings", []), + "ready_for_training": dataset_info["is_standardized"] and final_format != "unknown" + } + + + +def detect_custom_format_heuristic(dataset): + """ + Smart detection with priority scoring. + + Strategy for ambiguous keywords like 'task': + 1. Detect assistant first (unambiguous) + 2. Detect user using high-priority keywords first + 3. Check REMAINING columns for system keywords (including 'task') + 4. Only if no system match, use 'task' as fallback user + """ + sample = next(iter(dataset)) + all_columns = list(sample.keys()) + + mapping = {} + + # Keywords + assistant_words = [ + 'output', 'answer', 'response', 'assistant', 'completion', + 'expected', 'recommendation', 'reply', 'result', 'target', + 'solution', 'explanation', 'solve' + ] + + # Split into high/low priority + user_words_high_priority = [ + 'input', 'question', 'query', 'prompt', 'instruction', + 'request', 'snippet', 'user', 'text', + 'problem', 'exercise' + ] + user_words_low_priority = ['task'] # Ambiguous - can be user OR system + user_words = user_words_high_priority + user_words_low_priority + + system_words = [ + 'system', 'context', 'description', 'persona', 'role', + 'template', 'task' # Also in system + ] + + # Metadata columns to ignore + metadata_exact_match = { + 'id', 'idx', 'index', 'key', 'timestamp', 'date', + 'metadata', 'source', 'kind', 'type', 'category', + 'score', 'label', 'tag', 'inference_mode' + } + + metadata_prefix_patterns = [ + 'problem_type', 'problem_source', + 'generation_model', 'pass_rate', + ] + + priority_patterns = { + 'generated': 100, + 'gen_': 90, + 'model_': 80, + 'predicted': 70, + 'completion': 60, + } + + def has_keyword(col_name, keywords): + """Check if any keyword appears in column name.""" + col_lower = col_name.lower() + col_normalized = col_lower.replace('_', '').replace('-', '').replace(' ', '') + + for keyword in keywords: + if keyword in col_lower or keyword in col_normalized: + return True + return False + + def is_metadata(col_name): + """Check if column is likely metadata.""" + col_lower = col_name.lower() + + if col_lower in metadata_exact_match: + return True + + if col_lower in metadata_prefix_patterns: + return True + + for pattern in metadata_prefix_patterns: + if col_lower.startswith(pattern.split('_')[0] + '_') and col_lower != pattern: + if '_' in col_lower: + prefix = col_lower.split('_')[0] + if prefix in ['generation', 'pass', 'inference']: + return True + + if len(col_lower) <= 2 and not col_lower in ['qa', 'q', 'a']: + return True + + return False + + def get_priority_score(col_name): + """Calculate priority score based on column name patterns.""" + col_lower = col_name.lower() + score = 0 + + for pattern, pattern_score in priority_patterns.items(): + if pattern in col_lower: + score += pattern_score + + return score + + def get_content_length(col_name): + """Get average content length for this column.""" + try: + if col_name in sample and sample[col_name]: + content = str(sample[col_name]) + return len(content) + return 0 + except: + return 0 + + def score_column(col_name, keywords, role_type, num_candidates): + """Score a column for how likely it is to be a particular role.""" + if not has_keyword(col_name, keywords): + return 0 + + score = 0 + score += 10 + + # NEW: Penalize ambiguous keywords when scoring for user + if role_type == 'user': + col_lower = col_name.lower() + # If column is ONLY "task" (or task_xxx), give it lower priority for user role + if 'task' in col_lower and not any(kw in col_lower for kw in user_words_high_priority): + score -= 15 # Significant penalty so other user columns win + + priority_bonus = get_priority_score(col_name) + score += priority_bonus + + if role_type in ['assistant', 'user']: + avg_length = get_content_length(col_name) + + if num_candidates > 1: + if avg_length > 1000: + score += 50 + elif avg_length > 200: + score += 30 + elif avg_length > 50: + score += 10 + elif avg_length < 50: + score -= 20 + else: + if avg_length > 1000: + score += 50 + elif avg_length > 200: + score += 30 + elif avg_length > 50: + score += 10 + + return score + + # Filter out metadata columns + content_columns = [col for col in all_columns if not is_metadata(col)] + + # Count candidates first + assistant_potential = [col for col in content_columns if has_keyword(col, assistant_words)] + user_potential = [col for col in content_columns if has_keyword(col, user_words)] + + # STEP 1: Find best ASSISTANT column + assistant_candidates = [] + for col in assistant_potential: + score = score_column(col, assistant_words, 'assistant', len(assistant_potential)) + if score > 0: + assistant_candidates.append((col, score)) + + if assistant_candidates: + assistant_candidates.sort(key=lambda x: x[1], reverse=True) + assistant_col = assistant_candidates[0][0] + mapping[assistant_col] = 'assistant' + else: + assistant_col = None + + # STEP 2: Find best USER column (with penalty for ambiguous keywords) + user_candidates = [] + for col in user_potential: + if col == assistant_col: + continue + score = score_column(col, user_words, 'user', len(user_potential)) + if score > 0: + user_candidates.append((col, score)) + + if user_candidates: + user_candidates.sort(key=lambda x: x[1], reverse=True) + user_col = user_candidates[0][0] + mapping[user_col] = 'user' + else: + user_col = None + + # STEP 3: Check ALL remaining columns for SYSTEM matches (priority check) + remaining_columns = [col for col in content_columns if col not in mapping] + + system_col = None + for col in remaining_columns: + if has_keyword(col, system_words): + # Found a system match in remaining columns + mapping[col] = 'system' + system_col = col + break + + # STEP 4: Handle any additional remaining columns + if system_col: + remaining_columns = [col for col in remaining_columns if col != system_col] + + if len(remaining_columns) >= 1: + remaining_col = remaining_columns[0] + + # If no strong keyword match, decide based on what's missing + if not has_keyword(remaining_col, user_words + assistant_words): + mapping[remaining_col] = 'system' + elif user_col is None: + # No user column yet, assign this as user + mapping[remaining_col] = 'user' + else: + # Already have user + assistant, treat as system context + mapping[remaining_col] = 'system' + + # VALIDATION: Ensure we have at least user + assistant + has_user = any(role == 'user' for role in mapping.values()) + has_assistant = any(role == 'assistant' for role in mapping.values()) + + if not has_user and len(remaining_columns) > 0: + for col in remaining_columns: + if col not in mapping: + mapping[col] = 'user' + has_user = True + break + + if has_user and has_assistant: + return mapping + + return None + +def detect_multimodal_dataset(dataset): + """ + Detects if dataset contains multimodal data (images/vision). + + Returns: + dict: { + "is_multimodal": bool, + "multimodal_columns": list of column names containing image data, + "modality_types": list of detected types (e.g., ["image", "pixel"]) + } + """ + sample = next(iter(dataset)) + column_names = list(sample.keys()) + + # Keywords that indicate multimodal/image data + multimodal_keywords = ['image', 'img', 'pixel'] + + multimodal_columns = [] + modality_types = set() + + for col_name in column_names: + col_lower = col_name.lower() + + for keyword in multimodal_keywords: + if keyword in col_lower: + multimodal_columns.append(col_name) + modality_types.add(keyword) + break # Don't check other keywords for this column + + return { + "is_multimodal": len(multimodal_columns) > 0, + "multimodal_columns": multimodal_columns, + "modality_types": list(modality_types) + } +pass + +def detect_vlm_dataset_structure(dataset): + """ + Detects if VLM dataset is: + - Standard VLM messages format (image objects in content) + - Llava format (image indices + separate images column) + - Simple format needing conversion (image + text columns) + """ + try: + sample = next(iter(dataset)) + except StopIteration: + return { + "format": "unknown", + "needs_conversion": None, + "image_column": None, + "text_column": None, + "messages_column": None, + } + + column_names = set(sample.keys()) + + # Check if has messages column + if "messages" in column_names: + messages = sample["messages"] + + if messages and len(messages) > 0: + first_msg = messages[0] + if "content" in first_msg: + content = first_msg["content"] + + if isinstance(content, list) and len(content) > 0: + if isinstance(content[0], dict) and "type" in content[0]: + + # Check for llava format + has_index = any('index' in item for item in content if isinstance(item, dict)) + has_images_column = 'images' in column_names + + if has_index and has_images_column: + return { + "format": "vlm_messages_llava", + "needs_conversion": True, + "messages_column": "messages", + "image_column": "images", + "text_column": None, + } + + # Standard VLM format + has_image = any('image' in item for item in content if isinstance(item, dict)) + if has_image: + return { + "format": "vlm_messages", + "needs_conversion": False, + "messages_column": "messages", + "image_column": None, + "text_column": None, + } + + # Find image and text columns using metadata filtering + + # Define metadata patterns to EXCLUDE + metadata_patterns = { + 'suffixes': ['_id', '_url', '_name', '_filename', '_uri', '_link', '_key', '_index'], + 'prefixes': ['id_', 'url_', 'name_', 'filename_', 'uri_', 'link_', 'key_', 'index_'], + } + + # Image-related keywords + image_keywords = ['image', 'img', 'photo', 'picture', 'pic', 'visual', 'scan'] + + # Text-related keywords + text_keywords = ['text', 'caption', 'description', 'answer', 'output', 'response', 'label'] + + def is_metadata_column(col_name): + """Check if column name looks like metadata.""" + col_lower = col_name.lower() + + # Check suffixes + if any(col_lower.endswith(suffix) for suffix in metadata_patterns['suffixes']): + return True + + # Check prefixes + if any(col_lower.startswith(prefix) for prefix in metadata_patterns['prefixes']): + return True + + return False + + def find_image_column(): + """Find image column by filtering out metadata and checking keywords.""" + candidates = [] + + for col in column_names: + col_lower = col.lower() + + # Check if contains image keywords + if any(keyword in col_lower for keyword in image_keywords): + # Verify it actually contains image data + sample_value = sample[col] + + # PIL Image object (highest priority - even if name suggests metadata) + if hasattr(sample_value, 'size') and hasattr(sample_value, 'mode'): + candidates.append((col, 100)) # High priority - actual PIL Image + + # String (could be path) - but lower priority if name is metadata-like + elif isinstance(sample_value, str): + if is_metadata_column(col): + candidates.append((col, 30)) # Lower priority for metadata names + else: + candidates.append((col, 50)) # Medium priority + + # Dict with image data + elif isinstance(sample_value, dict) and ('bytes' in sample_value or 'path' in sample_value): + candidates.append((col, 75)) # High-medium priority + + # Return highest priority candidate + if candidates: + candidates.sort(key=lambda x: x[1], reverse=True) + return candidates[0][0] + + return None + + def find_text_column(): + """Find text column by filtering out metadata and checking keywords.""" + candidates = [] + + for col in column_names: + # Skip metadata columns + if is_metadata_column(col): + continue + + col_lower = col.lower() + + # Check if contains text keywords + if any(keyword in col_lower for keyword in text_keywords): + # Verify it's actually text + sample_value = sample[col] + + if isinstance(sample_value, str) and len(sample_value) > 0: + # Longer text = higher priority (likely content, not just a label) + priority = min(len(sample_value), 1000) # Cap at 1000 + candidates.append((col, priority)) + + # Return highest priority candidate + if candidates: + candidates.sort(key=lambda x: x[1], reverse=True) + return candidates[0][0] + + return None + + found_image = find_image_column() + found_text = find_text_column() + + if found_image and found_text: + return { + "format": "simple_image_text", + "needs_conversion": True, + "image_column": found_image, + "text_column": found_text, + "messages_column": None, + } + + return { + "format": "unknown", + "needs_conversion": None, + "image_column": found_image, + "text_column": found_text, + "messages_column": None, + } +pass + +def convert_to_vlm_format( + dataset, + instruction=None, + text_column="text", + image_column="image", + dataset_name=None, +): + """ + Converts simple {image, text} format to VLM messages format. + + Returns a LIST, not a HuggingFace Dataset (to preserve PIL Images). + + Returns: + list: List of dicts with 'messages' field + """ + from PIL import Image + + # Generate smart instruction if not provided + if instruction is None: + instruction_info = generate_smart_vlm_instruction( + dataset, + text_column=text_column, + image_column=image_column, + dataset_name=dataset_name, + ) + + instruction = instruction_info["instruction"] + instruction_column = instruction_info.get("instruction_column") + uses_dynamic = instruction_info["uses_dynamic_instruction"] + + print(f"📝 Auto-detected instruction type: {instruction_info['instruction_type']}") + print(f"📝 Confidence: {instruction_info['confidence']:.2f}") + if not uses_dynamic: + print(f"📝 Using instruction: '{instruction}'") + else: + print(f"📝 Using dynamic instructions from column: '{instruction_column}'") + else: + instruction_column = None + uses_dynamic = False + + def _convert_single_sample(sample): + """Convert a single sample to VLM format.""" + # Get image (might be PIL Image or path) + image_data = sample[image_column] + + # Handle image paths + if isinstance(image_data, str): + image_data = Image.open(image_data).convert("RGB") + + # Get text + text_data = sample[text_column] + + # Get instruction (static or dynamic) + if uses_dynamic and instruction_column: + current_instruction = sample[instruction_column] + else: + current_instruction = instruction + + # Build VLM messages - simple structure + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": current_instruction}, + {"type": "image", "image": image_data} # PIL object + ] + }, + { + "role": "assistant", + "content": [ + {"type": "text", "text": text_data} + ] + } + ] + + # Return dict with messages + return {"messages": messages} + + # Use list comprehension and return the LIST directly + print(f"🔄 Converting {len(dataset)} samples to VLM format...") + converted_list = [_convert_single_sample(sample) for sample in dataset] + + print(f"Converted {len(converted_list)} samples") + + # Return list, NOT Dataset + return converted_list +pass + +def generate_smart_vlm_instruction( + dataset, + text_column="text", + image_column="image", + dataset_name=None, +): + """ + Generate smart, context-aware instruction for VLM datasets using heuristics. + + Strategy: + 1. Check for explicit question/instruction columns → use that + 2. Infer from text column name + sample content + 3. Analyze dataset name for task hints + 4. Fall back to generic instruction + + Returns: + dict: { + "instruction": str or None, # None means use column content + "instruction_type": "explicit" | "inferred" | "generic", + "uses_dynamic_instruction": bool, # True if instruction varies per sample + "confidence": float, # 0.0 to 1.0 + } + """ + import re + + column_names = set(next(iter(dataset)).keys()) + sample = next(iter(dataset)) + + # ===== LEVEL 1: Explicit Instruction Columns ===== + # Check for columns that contain per-sample instructions + question_columns = ["question", "query", "prompt", "instruction", "user_prompt"] + + for col in question_columns: + if col in column_names: + # Check if this column has varied content (not just empty/same) + sample_content = sample[col] + if sample_content and str(sample_content).strip(): + return { + "instruction": None, # Signal to use column content + "instruction_column": col, + "instruction_type": "explicit", + "uses_dynamic_instruction": True, + "confidence": 1.0, + } + + # ===== LEVEL 2: Infer from Column Names + Content ===== + text_col_lower = text_column.lower() + + # Sample the text content to detect patterns + text_sample = str(sample.get(text_column, ""))[:500] # First 500 chars + + # Task-specific keywords and their instructions + task_patterns = { + # OCR / Transcription + "ocr": { + "keywords": ["ocr", "transcribe", "transcript"], + "content_hints": [r"[A-Za-z\u0600-\u06FF]{10,}"], # Long text passages (Latin/Arabic) + "instruction": "Transcribe all the text shown in this image.", + "confidence": 0.9, + }, + + # LaTeX / Math + "latex": { + "keywords": ["latex", "math", "formula", "equation"], + "content_hints": [r"\\[a-z]+\{", r"\^", r"_", r"\\frac"], # LaTeX commands + "instruction": "Convert this image to LaTeX notation.", + "confidence": 0.95, + }, + + # Caption / Description + "caption": { + "keywords": ["caption", "description", "describe"], + "content_hints": [], + "instruction": "Provide a detailed description of this image.", + "confidence": 0.85, + }, + + # Medical / Radiology + "medical": { + "keywords": ["medical", "radiology", "xray", "ct", "mri", "scan", "diagnosis"], + "content_hints": [r"\b(lesion|radiograph|patient|diagnosis|findings)\b"], + "instruction": "Analyze this medical image and describe the key findings.", + "confidence": 0.9, + }, + + # Code / Programming + "code": { + "keywords": ["code", "program", "function", "algorithm"], + "content_hints": [r"def |class |function|import |return "], + "instruction": "Explain what this code visualization shows.", + "confidence": 0.85, + }, + + # Chart / Graph + "chart": { + "keywords": ["chart", "graph", "plot", "visualization", "diagram"], + "content_hints": [r"\b(axis|legend|bar|line|pie|scatter)\b"], + "instruction": "Describe this chart or graph, including key data points and trends.", + "confidence": 0.85, + }, + + # Document / Text Recognition + "document": { + "keywords": ["document", "page", "paragraph", "article"], + "content_hints": [r"\n.*\n.*\n"], # Multi-line text + "instruction": "Extract and transcribe the text from this document image.", + "confidence": 0.85, + }, + } + + # Check column name matches + best_match = None + best_score = 0.0 + + for task_name, task_info in task_patterns.items(): + score = 0.0 + + # Check column name + if any(keyword in text_col_lower for keyword in task_info["keywords"]): + score += 0.5 + + # Check dataset name if provided + if dataset_name and any(keyword in dataset_name.lower() for keyword in task_info["keywords"]): + score += 0.3 + + # Check content patterns + for pattern in task_info["content_hints"]: + if re.search(pattern, text_sample, re.IGNORECASE): + score += 0.4 + break + + if score > best_score: + best_score = score + best_match = task_info + + if best_match and best_score > 0.5: # Confidence threshold + return { + "instruction": best_match["instruction"], + "instruction_column": None, + "instruction_type": "inferred", + "uses_dynamic_instruction": False, + "confidence": min(best_score, best_match["confidence"]), + } + + # ===== LEVEL 3: Analyze Dataset Name ===== + if dataset_name: + name_lower = dataset_name.lower() + + # Common dataset name patterns + if "vqa" in name_lower or "question" in name_lower: + return { + "instruction": "Answer the question about this image.", + "instruction_column": None, + "instruction_type": "inferred", + "uses_dynamic_instruction": False, + "confidence": 0.75, + } + + if "coco" in name_lower or "flickr" in name_lower: + return { + "instruction": "Provide a detailed caption for this image.", + "instruction_column": None, + "instruction_type": "inferred", + "uses_dynamic_instruction": False, + "confidence": 0.75, + } + + # ===== LEVEL 4: Generic Fallback ===== + return { + "instruction": "Describe this image in detail.", + "instruction_column": None, + "instruction_type": "generic", + "uses_dynamic_instruction": False, + "confidence": 0.5, + } +pass + +def convert_llava_to_vlm_format(dataset): + """ + Converts Llava format to standard VLM format. + + Llava format: + - messages: [{'content': [{'type': 'image', 'index': 0}, {'type': 'text', 'text': '...'}]}] + - images: [PIL_Image1, PIL_Image2, ...] + + Standard VLM format: + - messages: [{'content': [{'type': 'image', 'image': PIL_Image}, {'type': 'text', 'text': '...'}]}] + """ + from PIL import Image + + print(f"🔄 Converting {len(dataset)} samples from Llava format to standard VLM format...") + + def _convert_single_sample(sample): + """Convert a single llava sample to standard VLM format.""" + messages = sample["messages"] + images = sample.get("images", []) + + # Process each message + new_messages = [] + for msg in messages: + new_content = [] + + for item in msg["content"]: + if item["type"] == "image": + # Replace index with actual PIL image + if "index" in item and item["index"] is not None: + img_idx = item["index"] + if img_idx < len(images): + pil_image = images[img_idx] + # Ensure it's PIL + if isinstance(pil_image, str): + pil_image = Image.open(pil_image).convert("RGB") + + new_content.append({ + "type": "image", + "image": pil_image # Actual PIL object + }) + else: + # No index, try to use first image + if len(images) > 0: + pil_image = images[0] + if isinstance(pil_image, str): + pil_image = Image.open(pil_image).convert("RGB") + + new_content.append({ + "type": "image", + "image": pil_image + }) + + elif item["type"] == "text": + # Keep text as-is (only type + text) + new_content.append({ + "type": "text", + "text": item.get("text", "") + }) + + new_messages.append({ + "role": msg["role"], + "content": new_content + }) + + return {"messages": new_messages} + + # Convert using list comprehension + converted_list = [_convert_single_sample(sample) for sample in dataset] + + print(f"Converted {len(converted_list)} samples") + return converted_list +pass + +@dataclass +class DeepSeekOCRDataCollator: + """ + Args: + tokenizer: Tokenizer + model: Model + image_size: Size for image patches (default: 640) + base_size: Size for global view (default: 1024) + crop_mode: Whether to use dynamic cropping for large images + train_on_responses_only: If True, only train on assistant responses (mask user prompts) + """ + tokenizer: Any + model: Any + image_size: int = 640 + base_size: int = 1024 + crop_mode: bool = True + image_token_id: int = 128815 + train_on_responses_only: bool = True + + def __init__( + self, + tokenizer, + model, + image_size: int = 640, + base_size: int = 1024, + crop_mode: bool = True, + train_on_responses_only: bool = True, + ): + self.tokenizer = tokenizer + self.model = model + self.image_size = image_size + self.base_size = base_size + self.crop_mode = crop_mode + self.image_token_id = 128815 + self.dtype = model.dtype # Get dtype from model + self.train_on_responses_only = train_on_responses_only + + self.image_transform = BasicImageTransform( + mean=(0.5, 0.5, 0.5), + std=(0.5, 0.5, 0.5), + normalize=True + ) + self.patch_size = 16 + self.downsample_ratio = 4 + + # Get BOS token ID from tokenizer + if hasattr(tokenizer, 'bos_token_id') and tokenizer.bos_token_id is not None: + self.bos_id = tokenizer.bos_token_id + else: + self.bos_id = 0 + print(f"Warning: tokenizer has no bos_token_id, using default: {self.bos_id}") + + def deserialize_image(self, image_data) -> Image.Image: + """Convert image data (bytes dict or PIL Image) to PIL Image in RGB mode""" + if isinstance(image_data, Image.Image): + return image_data.convert("RGB") + elif isinstance(image_data, dict) and 'bytes' in image_data: + image_bytes = image_data['bytes'] + image = Image.open(io.BytesIO(image_bytes)) + return image.convert("RGB") + else: + raise ValueError(f"Unsupported image format: {type(image_data)}") + + def calculate_image_token_count(self, image: Image.Image, crop_ratio: Tuple[int, int]) -> int: + """Calculate the number of tokens this image will generate""" + num_queries = math.ceil((self.image_size // self.patch_size) / self.downsample_ratio) + num_queries_base = math.ceil((self.base_size // self.patch_size) / self.downsample_ratio) + + width_crop_num, height_crop_num = crop_ratio + + if self.crop_mode: + img_tokens = num_queries_base * num_queries_base + 1 + if width_crop_num > 1 or height_crop_num > 1: + img_tokens += (num_queries * width_crop_num + 1) * (num_queries * height_crop_num) + else: + img_tokens = num_queries * num_queries + 1 + + return img_tokens + + def process_image(self, image: Image.Image) -> Tuple[List, List, List, List, Tuple[int, int]]: + """ + Process a single image based on crop_mode and size thresholds + + Returns: + Tuple of (images_list, images_crop_list, images_spatial_crop, tokenized_image, crop_ratio) + """ + images_list = [] + images_crop_list = [] + images_spatial_crop = [] + + if self.crop_mode: + # Determine crop ratio based on image size + if image.size[0] <= 640 and image.size[1] <= 640: + crop_ratio = (1, 1) + images_crop_raw = [] + else: + images_crop_raw, crop_ratio = dynamic_preprocess( + image, min_num=2, max_num=9, + image_size=self.image_size, use_thumbnail=False + ) + + # Process global view with padding + global_view = ImageOps.pad( + image, (self.base_size, self.base_size), + color=tuple(int(x * 255) for x in self.image_transform.mean) + ) + images_list.append(self.image_transform(global_view).to(self.dtype)) + + width_crop_num, height_crop_num = crop_ratio + images_spatial_crop.append([width_crop_num, height_crop_num]) + + # Process local views (crops) if applicable + if width_crop_num > 1 or height_crop_num > 1: + for crop_img in images_crop_raw: + images_crop_list.append( + self.image_transform(crop_img).to(self.dtype) + ) + + # Calculate image tokens + num_queries = math.ceil((self.image_size // self.patch_size) / self.downsample_ratio) + num_queries_base = math.ceil((self.base_size // self.patch_size) / self.downsample_ratio) + + tokenized_image = ([self.image_token_id] * num_queries_base + [self.image_token_id]) * num_queries_base + tokenized_image += [self.image_token_id] + + if width_crop_num > 1 or height_crop_num > 1: + tokenized_image += ([self.image_token_id] * (num_queries * width_crop_num) + [self.image_token_id]) * ( + num_queries * height_crop_num) + + else: # crop_mode = False + crop_ratio = (1, 1) + images_spatial_crop.append([1, 1]) + + # For smaller base sizes, resize; for larger, pad + if self.base_size <= 640: + resized_image = image.resize((self.base_size, self.base_size), Image.Resampling.LANCZOS) + images_list.append(self.image_transform(resized_image).to(self.dtype)) + else: + global_view = ImageOps.pad( + image, (self.base_size, self.base_size), + color=tuple(int(x * 255) for x in self.image_transform.mean) + ) + images_list.append(self.image_transform(global_view).to(self.dtype)) + + num_queries = math.ceil((self.base_size // self.patch_size) / self.downsample_ratio) + tokenized_image = ([self.image_token_id] * num_queries + [self.image_token_id]) * num_queries + tokenized_image += [self.image_token_id] + + return images_list, images_crop_list, images_spatial_crop, tokenized_image, crop_ratio + + def process_single_sample(self, messages: List[Dict]) -> Dict[str, Any]: + """ + Process a single conversation into model inputs. + """ + + # --- 1. Setup --- + images = [] + for message in messages: + if "images" in message and message["images"]: + for img_data in message["images"]: + if img_data is not None: + pil_image = self.deserialize_image(img_data) + images.append(pil_image) + + if not images: + raise ValueError("No images found in sample. Please ensure all samples contain images.") + + tokenized_str = [] + images_seq_mask = [] + images_list, images_crop_list, images_spatial_crop = [], [], [] + + prompt_token_count = -1 # Index to start training + assistant_started = False + image_idx = 0 + + # Add BOS token at the very beginning + tokenized_str.append(self.bos_id) + images_seq_mask.append(False) + + for message in messages: + role = message["role"] + content = message["content"] + + # Check if this is the assistant's turn + if role == "<|Assistant|>": + if not assistant_started: + # This is the split point. All tokens added *so far* + # are part of the prompt. + prompt_token_count = len(tokenized_str) + assistant_started = True + + # Append the EOS token string to the *end* of assistant content + content = f"{content.strip()} {self.tokenizer.eos_token}" + + # Split this message's content by the image token + text_splits = content.split('') + + for i, text_sep in enumerate(text_splits): + # Tokenize the text part + tokenized_sep = text_encode(self.tokenizer, text_sep, bos=False, eos=False) + tokenized_str.extend(tokenized_sep) + images_seq_mask.extend([False] * len(tokenized_sep)) + + # If this text is followed by an tag + if i < len(text_splits) - 1: + if image_idx >= len(images): + raise ValueError( + f"Data mismatch: Found '' token but no corresponding image." + ) + + # Process the image + image = images[image_idx] + img_list, crop_list, spatial_crop, tok_img, _ = self.process_image(image) + + images_list.extend(img_list) + images_crop_list.extend(crop_list) + images_spatial_crop.extend(spatial_crop) + + # Add image placeholder tokens + tokenized_str.extend(tok_img) + images_seq_mask.extend([True] * len(tok_img)) + + image_idx += 1 # Move to the next image + + # --- 3. Validation and Final Prep --- + if image_idx != len(images): + raise ValueError( + f"Data mismatch: Found {len(images)} images but only {image_idx} '' tokens were used." + ) + + # If we never found an assistant message, we're in a weird state + # (e.g., user-only prompt). We mask everything. + if not assistant_started: + print("Warning: No assistant message found in sample. Masking all tokens.") + prompt_token_count = len(tokenized_str) + + # Prepare image tensors + images_ori = torch.stack(images_list, dim=0) + images_spatial_crop_tensor = torch.tensor(images_spatial_crop, dtype=torch.long) + + if images_crop_list: + images_crop = torch.stack(images_crop_list, dim=0) + else: + images_crop = torch.zeros((1, 3, self.base_size, self.base_size), dtype=self.dtype) + + return { + "input_ids": torch.tensor(tokenized_str, dtype=torch.long), + "images_seq_mask": torch.tensor(images_seq_mask, dtype=torch.bool), + "images_ori": images_ori, + "images_crop": images_crop, + "images_spatial_crop": images_spatial_crop_tensor, + "prompt_token_count": prompt_token_count, # This is now accurate + } + + def __call__(self, features: List[Dict[str, Any]]) -> Dict[str, torch.Tensor]: + """Collate batch of samples""" + batch_data = [] + + # Process each sample + for feature in features: + try: + processed = self.process_single_sample(feature['messages']) + batch_data.append(processed) + except Exception as e: + print(f"Error processing sample: {e}") + continue + + if not batch_data: + raise ValueError("No valid samples in batch") + + # Extract lists + input_ids_list = [item['input_ids'] for item in batch_data] + images_seq_mask_list = [item['images_seq_mask'] for item in batch_data] + prompt_token_counts = [item['prompt_token_count'] for item in batch_data] + + # Pad sequences + input_ids = pad_sequence(input_ids_list, batch_first=True, padding_value=self.tokenizer.pad_token_id) + images_seq_mask = pad_sequence(images_seq_mask_list, batch_first=True, padding_value=False) + + # Create labels + labels = input_ids.clone() + + # Mask padding tokens + labels[labels == self.tokenizer.pad_token_id] = -100 + + # Mask image tokens (model shouldn't predict these) + labels[images_seq_mask] = -100 + + # Mask user prompt tokens when train_on_responses_only=True (only train on assistant responses) + if self.train_on_responses_only: + for idx, prompt_count in enumerate(prompt_token_counts): + if prompt_count > 0: + labels[idx, :prompt_count] = -100 + + # Create attention mask + attention_mask = (input_ids != self.tokenizer.pad_token_id).long() + + # Prepare images batch (list of tuples) + images_batch = [] + for item in batch_data: + images_batch.append((item['images_crop'], item['images_ori'])) + + # Stack spatial crop info + images_spatial_crop = torch.cat([item['images_spatial_crop'] for item in batch_data], dim=0) + + return { + "input_ids": input_ids, + "attention_mask": attention_mask, + "labels": labels, + "images": images_batch, + "images_seq_mask": images_seq_mask, + "images_spatial_crop": images_spatial_crop, + } + + + + +def get_tokenizer_chat_template(tokenizer, model_name): + """ + Gets appropriate chat template for tokenizer based on model. + Uses Unsloth's get_chat_template if model is in the mapper. + + Args: + tokenizer: HuggingFace tokenizer + model_name: Model class name (e.g., "Gemma3ForCausalLM") + + Returns: + tokenizer: Tokenizer with appropriate chat template applied + """ + try: + from unsloth.chat_templates import get_chat_template + except ImportError: + # Unsloth not available, return tokenizer as-is + return tokenizer + + # Normalize model_name to lowercase for matching + model_name_lower = model_name.lower() + + # Check if model matches any template in mapper + matched_template = None + + # Direct match in MODEL_TO_TEMPLATE_MAPPER + if model_name_lower in MODEL_TO_TEMPLATE_MAPPER: + matched_template = MODEL_TO_TEMPLATE_MAPPER[model_name_lower] + print(f"📝 Applying Unsloth chat template: {matched_template}") + try: + tokenizer = get_chat_template( + tokenizer, + chat_template=matched_template, + ) + except Exception as e: + print(f"⚠️ Failed to apply Unsloth template '{matched_template}': {e}") + print(f" Falling back to tokenizer's default chat template") + else: + print(f"📝 Using tokenizer's default chat template (no Unsloth template match)") + + return tokenizer +pass + + +TEMPLATE_TO_MODEL_MAPPER = { + "phi-3.5": ( + "unsloth/Phi-3.5-mini-instruct-bnb-4bit", + "unsloth/Phi-3.5-mini-instruct", + "microsoft/Phi-3.5-mini-instruct", + ), + "phi-3": ( + "unsloth/Phi-3-mini-4k-instruct-bnb-4bit", + "unsloth/Phi-3-mini-4k-instruct", + "microsoft/Phi-3-mini-4k-instruct", + "unsloth/Phi-3-medium-4k-instruct-bnb-4bit", + "unsloth/Phi-3-medium-4k-instruct", + "microsoft/Phi-3-medium-4k-instruct", + "unsloth/Phi-3-mini-4k-instruct-v0-bnb-4bit", + "unsloth/Phi-3-mini-4k-instruct-v0", + ), + "phi-4": ( + "unsloth/phi-4-unsloth-bnb-4bit", + "unsloth/phi-4", + "microsoft/phi-4", + "unsloth/phi-4-bnb-4bit", + "unsloth/phi-4-reasoning-unsloth-bnb-4bit", + "unsloth/phi-4-reasoning", + "microsoft/Phi-4-reasoning", + "unsloth/phi-4-reasoning-bnb-4bit", + "unsloth/phi-4-reasoning-plus-unsloth-bnb-4bit", + "unsloth/phi-4-reasoning-plus", + "microsoft/Phi-4-reasoning-plus", + "unsloth/phi-4-reasoning-plus-bnb-4bit", + "unsloth/phi-4-mini-reasoning-unsloth-bnb-4bit", + "unsloth/phi-4-mini-reasoning", + "microsoft/Phi-4-mini-reasoning", + "unsloth/phi-4-mini-reasoning-bnb-4bit", + "unsloth/Phi-4-mini-instruct-unsloth-bnb-4bit", + "unsloth/Phi-4-mini-instruct", + "microsoft/Phi-4-mini-instruct", + "unsloth/Phi-4-mini-instruct-bnb-4bit", + ), + "mistral": ( + "unsloth/mistral-7b-instruct-v0.1-bnb-4bit", + "unsloth/mistral-7b-instruct-v0.1", + "mistralai/Mistral-7B-Instruct-v0.1", + "unsloth/mistral-7b-instruct-v0.2-bnb-4bit", + "unsloth/mistral-7b-instruct-v0.2", + "mistralai/Mistral-7B-Instruct-v0.2", + "unsloth/mistral-7b-instruct-v0.3-bnb-4bit", + "unsloth/mistral-7b-instruct-v0.3", + "mistralai/Mistral-7B-Instruct-v0.3", + "unsloth/Mixtral-8x7B-Instruct-v0.1-unsloth-bnb-4bit", + "unsloth/Mixtral-8x7B-Instruct-v0.1", + "mistralai/Mixtral-8x7B-Instruct-v0.1", + "unsloth/Mixtral-8x7B-Instruct-v0.1-bnb-4bit", + "unsloth/Mistral-Nemo-Instruct-2407-bnb-4bit", + "unsloth/Mistral-Nemo-Instruct-2407", + "mistralai/Mistral-Nemo-Instruct-2407", + "unsloth/Mistral-Large-Instruct-2407-bnb-4bit", + "mistralai/Mistral-Large-Instruct-2407", + "unsloth/Mistral-Small-Instruct-2409-bnb-4bit", + "unsloth/Mistral-Small-Instruct-2409", + "mistralai/Mistral-Small-Instruct-2409", + "unsloth/Mistral-Small-24B-Instruct-2501-unsloth-bnb-4bit", + "unsloth/Mistral-Small-24B-Instruct-2501", + "mistralai/Mistral-Small-24B-Instruct-2501", + "unsloth/Mistral-Small-24B-Instruct-2501-bnb-4bit", + "unsloth/Mistral-Small-3.1-24B-Instruct-2503-unsloth-bnb-4bit", + "unsloth/Mistral-Small-3.1-24B-Instruct-2503", + "mistralai/Mistral-Small-3.1-24B-Instruct-2503", + "unsloth/Mistral-Small-3.1-24B-Instruct-2503-bnb-4bit", + "unsloth/Mistral-Small-3.2-24B-Instruct-2506-unsloth-bnb-4bit", + "unsloth/Mistral-Small-3.2-24B-Instruct-2506", + "mistralai/Mistral-Small-3.2-24B-Instruct-2506", + "unsloth/Mistral-Small-3.2-24B-Instruct-2506-bnb-4bit", + ), + "llama": ( + "meta-llama/Llama-2-13b-chat-hf", + "unsloth/llama-2-7b-chat-bnb-4bit", + "unsloth/llama-2-7b-chat", + "meta-llama/Llama-2-7b-chat-hf", + ), + "llama3": ( + "unsloth/llama-3-8b-Instruct-bnb-4bit", + "unsloth/llama-3-8b-Instruct", + "meta-llama/Meta-Llama-3-8B-Instruct", + "unsloth/llama-3-70b-Instruct-bnb-4bit", + "meta-llama/Meta-Llama-3-70B-Instruct", + ), + "llama-3.1": ( + "unsloth/Meta-Llama-3.1-8B-Instruct-unsloth-bnb-4bit", + "unsloth/Meta-Llama-3.1-8B-Instruct", + "meta-llama/Meta-Llama-3.1-8B-Instruct", + "unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit", + "unsloth/Llama-3.1-8B-Instruct-unsloth-bnb-4bit", + "unsloth/Llama-3.1-8B-Instruct", + "meta-llama/Llama-3.1-8B-Instruct", + "unsloth/Llama-3.1-8B-Instruct-bnb-4bit", + "unsloth/Meta-Llama-3.1-405B-Instruct-bnb-4bit", + "meta-llama/Meta-Llama-3.1-405B-Instruct", + "unsloth/Meta-Llama-3.1-70B-Instruct-bnb-4bit", + "unsloth/Meta-Llama-3.1-70B-Instruct", + "meta-llama/Meta-Llama-3.1-70B-Instruct", + "unsloth/Llama-3.1-Storm-8B-bnb-4bit", + "unsloth/Llama-3.1-Storm-8B", + "akjindal53244/Llama-3.1-Storm-8B", + "unsloth/Hermes-3-Llama-3.1-8B-bnb-4bit", + "unsloth/Hermes-3-Llama-3.1-8B", + "NousResearch/Hermes-3-Llama-3.1-8B", + "unsloth/Hermes-3-Llama-3.1-70B-bnb-4bit", + "unsloth/Hermes-3-Llama-3.1-70B", + "NousResearch/Hermes-3-Llama-3.1-70B", + "unsloth/Hermes-3-Llama-3.1-405B-bnb-4bit", + "NousResearch/Hermes-3-Llama-3.1-405B", + "unsloth/Llama-3.1-Nemotron-70B-Instruct-bnb-4bit", + "unsloth/Llama-3.1-Nemotron-70B-Instruct", + "nvidia/Llama-3.1-Nemotron-70B-Instruct-HF", + "unsloth/Llama-3.1-Tulu-3-8B-bnb-4bit", + "unsloth/Llama-3.1-Tulu-3-8B", + "allenai/Llama-3.1-Tulu-3-8B", + "unsloth/Llama-3.1-Tulu-3-70B-bnb-4bit", + "unsloth/Llama-3.1-Tulu-3-70B", + "allenai/Llama-3.1-Tulu-3-70B", + ), + "llama-3.2": ( + "unsloth/Llama-3.2-1B-Instruct-unsloth-bnb-4bit", + "unsloth/Llama-3.2-1B-Instruct", + "meta-llama/Llama-3.2-1B-Instruct", + "unsloth/Llama-3.2-1B-Instruct-bnb-4bit", + "unsloth/Llama-3.2-3B-Instruct-unsloth-bnb-4bit", + "unsloth/Llama-3.2-3B-Instruct", + "meta-llama/Llama-3.2-3B-Instruct", + "unsloth/Llama-3.2-3B-Instruct-bnb-4bit", + "unsloth/Llama-3.2-11B-Vision-Instruct-unsloth-bnb-4bit", + "unsloth/Llama-3.2-11B-Vision-Instruct", + "meta-llama/Llama-3.2-11B-Vision-Instruct", + "unsloth/Llama-3.2-11B-Vision-Instruct-bnb-4bit", + "unsloth/Llama-3.2-90B-Vision-Instruct-bnb-4bit", + "unsloth/Llama-3.2-90B-Vision-Instruct", + "meta-llama/Llama-3.2-90B-Vision-Instruct", + ), + "llama-3.3": ( + "unsloth/Llama-3.3-70B-Instruct-bnb-4bit", + "unsloth/Llama-3.3-70B-Instruct", + "meta-llama/Llama-3.3-70B-Instruct", + ), + "gemma": ( + "unsloth/gemma-7b-it-bnb-4bit", + "unsloth/gemma-7b-it", + "google/gemma-7b-it", + "google/gemma-2b-it", + "unsloth/gemma-1.1-2b-it-bnb-4bit", + "unsloth/gemma-1.1-2b-it", + "google/gemma-1.1-2b-it", + "unsloth/gemma-1.1-7b-it-bnb-4bit", + "unsloth/gemma-1.1-7b-it", + "google/gemma-1.1-7b-it", + ), + "gemma2": ( + "unsloth/gemma-2-9b-it-bnb-4bit", + "unsloth/gemma-2-9b-it", + "google/gemma-2-9b-it", + "unsloth/gemma-2-27b-it-bnb-4bit", + "unsloth/gemma-2-27b-it", + "google/gemma-2-27b-it", + "unsloth/gemma-2-2b-it-bnb-4bit", + "unsloth/gemma-2-2b-it", + "google/gemma-2-2b-it", + ), + "gemma-3": ( + "unsloth/gemma-3-1b-it-unsloth-bnb-4bit", + "unsloth/gemma-3-1b-it", + "google/gemma-3-1b-it", + "unsloth/gemma-3-1b-it-bnb-4bit", + "unsloth/gemma-3-4b-it-unsloth-bnb-4bit", + "unsloth/gemma-3-4b-it", + "google/gemma-3-4b-it", + "unsloth/gemma-3-4b-it-bnb-4bit", + "unsloth/gemma-3-12b-it-unsloth-bnb-4bit", + "unsloth/gemma-3-12b-it", + "google/gemma-3-12b-it", + "unsloth/gemma-3-12b-it-bnb-4bit", + "unsloth/gemma-3-27b-it-unsloth-bnb-4bit", + "unsloth/gemma-3-27b-it", + "google/gemma-3-27b-it", + "unsloth/gemma-3-27b-it-bnb-4bit", + "unsloth/gemma-3-270m-it-unsloth-bnb-4bit", + "unsloth/gemma-3-270m-it", + "google/gemma-3-270m-it", + "unsloth/gemma-3-270m-it-bnb-4bit", + "unsloth/gemma-3-270m-unsloth-bnb-4bit", + "unsloth/medgemma-4b-it-unsloth-bnb-4bit", + "unsloth/medgemma-4b-it", + "google/medgemma-4b-it", + "unsloth/medgemma-4b-it-bnb-4bit", + "unsloth/medgemma-27b-text-it-unsloth-bnb-4bit", + "unsloth/medgemma-27b-text-it", + "google/medgemma-27b-text-it", + "unsloth/medgemma-27b-text-it-bnb-4bit", + ), + "gemma3n": ( + "unsloth/gemma-3n-E4B-it-unsloth-bnb-4bit", + "unsloth/gemma-3n-E4B-it", + "google/gemma-3n-E4B-it", + "unsloth/gemma-3n-E4B-it-unsloth-bnb-4bit", + "unsloth/gemma-3n-E2B-it-unsloth-bnb-4bit", + "unsloth/gemma-3n-E2B-it", + "google/gemma-3n-E2B-it", + "unsloth/gemma-3n-E2B-it-unsloth-bnb-4bit", + ), + "qwen2.5": ( + "unsloth/Qwen2.5-0.5B-Instruct-unsloth-bnb-4bit", + "unsloth/Qwen2.5-0.5B-Instruct", + "Qwen/Qwen2.5-0.5B-Instruct", + "unsloth/Qwen2.5-0.5B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-1.5B-Instruct-unsloth-bnb-4bit", + "unsloth/Qwen2.5-1.5B-Instruct", + "Qwen/Qwen2.5-1.5B-Instruct", + "unsloth/Qwen2.5-1.5B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-3B-Instruct-unsloth-bnb-4bit", + "unsloth/Qwen2.5-3B-Instruct", + "Qwen/Qwen2.5-3B-Instruct", + "unsloth/Qwen2.5-3B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-7B-Instruct-unsloth-bnb-4bit", + "unsloth/Qwen2.5-7B-Instruct", + "Qwen/Qwen2.5-7B-Instruct", + "unsloth/Qwen2.5-7B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-14B-Instruct-unsloth-bnb-4bit", + "unsloth/Qwen2.5-14B-Instruct", + "Qwen/Qwen2.5-14B-Instruct", + "unsloth/Qwen2.5-14B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-32B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-32B-Instruct", + "Qwen/Qwen2.5-32B-Instruct", + "unsloth/Qwen2.5-72B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-72B-Instruct", + "Qwen/Qwen2.5-72B-Instruct", + "unsloth/Qwen2.5-0.5B-unsloth-bnb-4bit", + "unsloth/Qwen2.5-Math-1.5B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-Math-1.5B-Instruct", + "Qwen/Qwen2.5-Math-1.5B-Instruct", + "unsloth/Qwen2.5-Math-7B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-Math-7B-Instruct", + "Qwen/Qwen2.5-Math-7B-Instruct", + "unsloth/Qwen2.5-Math-72B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-Math-72B-Instruct", + "Qwen/Qwen2.5-Math-72B-Instruct", + "unsloth/Qwen2.5-Coder-0.5B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-Coder-0.5B-Instruct", + "Qwen/Qwen2.5-Coder-0.5B-Instruct", + "unsloth/Qwen2.5-Coder-1.5B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-Coder-1.5B-Instruct", + "Qwen/Qwen2.5-Coder-1.5B-Instruct", + "unsloth/Qwen2.5-Coder-3B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-Coder-3B-Instruct", + "Qwen/Qwen2.5-Coder-3B-Instruct", + "unsloth/Qwen2.5-Coder-7B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-Coder-7B-Instruct", + "Qwen/Qwen2.5-Coder-7B-Instruct", + "unsloth/Qwen2.5-Coder-14B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-Coder-14B-Instruct", + "Qwen/Qwen2.5-Coder-14B-Instruct", + "unsloth/Qwen2.5-Coder-32B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-Coder-32B-Instruct", + "Qwen/Qwen2.5-Coder-32B-Instruct", + "unsloth/Qwen2.5-VL-3B-Instruct-unsloth-bnb-4bit", + "unsloth/Qwen2.5-VL-3B-Instruct", + "Qwen/Qwen2.5-VL-3B-Instruct", + "unsloth/Qwen2.5-VL-3B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-VL-7B-Instruct-unsloth-bnb-4bit", + "unsloth/Qwen2.5-VL-7B-Instruct", + "Qwen/Qwen2.5-VL-7B-Instruct", + "unsloth/Qwen2.5-VL-7B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-VL-32B-Instruct-unsloth-bnb-4bit", + "unsloth/Qwen2.5-VL-32B-Instruct", + "Qwen/Qwen2.5-VL-32B-Instruct", + "unsloth/Qwen2.5-VL-32B-Instruct-bnb-4bit", + "unsloth/Qwen2.5-VL-72B-Instruct-unsloth-bnb-4bit", + "unsloth/Qwen2.5-VL-72B-Instruct", + "Qwen/Qwen2.5-VL-72B-Instruct", + "unsloth/Qwen2.5-VL-72B-Instruct-bnb-4bit", + "unsloth/OpenThinker-7B-unsloth-bnb-4bit", + "unsloth/OpenThinker-7B", + "open-thoughts/OpenThinker-7B", + "unsloth/OpenThinker-7B-bnb-4bit", + ), + "qwen3": ( + "unsloth/Qwen3-0.6B-unsloth-bnb-4bit", + "unsloth/Qwen3-0.6B", + "Qwen/Qwen3-0.6B", + "unsloth/Qwen3-0.6B-bnb-4bit", + "unsloth/Qwen3-1.7B-unsloth-bnb-4bit", + "unsloth/Qwen3-1.7B", + "Qwen/Qwen3-1.7B", + "unsloth/Qwen3-1.7B-bnb-4bit", + "unsloth/Qwen3-4B-unsloth-bnb-4bit", + "unsloth/Qwen3-4B", + "Qwen/Qwen3-4B", + "unsloth/Qwen3-4B-bnb-4bit", + "unsloth/Qwen3-8B-unsloth-bnb-4bit", + "unsloth/Qwen3-8B", + "Qwen/Qwen3-8B", + "unsloth/Qwen3-8B-bnb-4bit", + "unsloth/Qwen3-14B-unsloth-bnb-4bit", + "unsloth/Qwen3-14B", + "Qwen/Qwen3-14B", + "unsloth/Qwen3-14B-bnb-4bit", + "unsloth/Qwen3-32B-unsloth-bnb-4bit", + "unsloth/Qwen3-32B", + "Qwen/Qwen3-32B", + "unsloth/Qwen3-32B-bnb-4bit", + "unsloth/Qwen3-30B-A3B-unsloth-bnb-4bit", + "unsloth/Qwen3-30B-A3B", + "Qwen/Qwen3-30B-A3B", + "unsloth/Qwen3-30B-A3B-bnb-4bit", + ), + "qwen3-instruct": ( + "unsloth/Qwen3-4B-Instruct-2507-unsloth-bnb-4bit", + "unsloth/Qwen3-4B-Instruct-2507", + "Qwen/Qwen3-4B-Instruct-2507", + "unsloth/Qwen3-4B-Instruct-2507-bnb-4bit", + "unsloth/Qwen3-30B-A3B-Instruct-2507", + "Qwen/Qwen3-30B-A3B-Instruct-2507", + "unsloth/Qwen3-Coder-30B-A3B-Instruct", + "Qwen/Qwen3-Coder-30B-A3B-Instruct", + "unsloth/Qwen3-4B-Instruct-2507-unsloth-bnb-4bit", + "unsloth/Qwen3-4B-Instruct-2507", + "Qwen/Qwen3-4B-Instruct-2507", + "unsloth/Qwen3-4B-Instruct-2507-bnb-4bit", + ), + "qwen3-thinking": ( + "unsloth/QwQ-32B-Preview-bnb-4bit", + "unsloth/QwQ-32B-Preview", + "Qwen/QwQ-32B-Preview", + "unsloth/QwQ-32B-unsloth-bnb-4bit", + "unsloth/QwQ-32B", + "Qwen/QwQ-32B", + "unsloth/QwQ-32B-bnb-4bit", + "unsloth/Qwen3-4B-Thinking-2507-unsloth-bnb-4bit", + "unsloth/Qwen3-4B-Thinking-2507", + "Qwen/Qwen3-4B-Thinking-2507", + "unsloth/Qwen3-4B-Thinking-2507-bnb-4bit", + "unsloth/Qwen3-30B-A3B-Thinking-2507", + "Qwen/Qwen3-30B-A3B-Thinking-2507", + ), + "zephyr": ( + "unsloth/zephyr-sft-bnb-4bit", + "unsloth/zephyr-sft", + "HuggingFaceH4/mistral-7b-sft-beta", + ), + "chatml": ( + "unsloth/yi-6b-bnb-4bit", + "unsloth/yi-6b", + "01-ai/Yi-6B", + "unsloth/Hermes-2-Pro-Mistral-7B-bnb-4bit", + "unsloth/Hermes-2-Pro-Mistral-7B", + "NousResearch/Hermes-2-Pro-Mistral-7B", + "unsloth/OpenHermes-2.5-Mistral-7B-bnb-4bit", + "unsloth/OpenHermes-2.5-Mistral-7B", + "teknium/OpenHermes-2.5-Mistral-7B", + ), + "gpt-oss": ( + "unsloth/gpt-oss-20b-unsloth-bnb-4bit", + "unsloth/gpt-oss-20b", + "openai/gpt-oss-20b", + "unsloth/gpt-oss-20b-unsloth-bnb-4bit", + "unsloth/gpt-oss-120b-unsloth-bnb-4bit", + "unsloth/gpt-oss-120b", + "openai/gpt-oss-120b", + "unsloth/gpt-oss-120b-unsloth-bnb-4bit", + ), + "starling": ( + "unsloth/Starling-LM-7B-beta-bnb-4bit", + "unsloth/Starling-LM-7B-beta", + "Nexusflow/Starling-LM-7B-beta", + ), + "yi-chat": ( + "unsloth/yi-34b-chat-bnb-4bit", + "01-ai/Yi-6B-Chat", + "01-ai/Yi-34B-Chat", + ) +} + +MODEL_TO_TEMPLATE_MAPPER = {} + +for key, values in TEMPLATE_TO_MODEL_MAPPER.items(): + for value in values: + MODEL_TO_TEMPLATE_MAPPER[value] = key + pass + + # Get lowercased + lowered_key = key.lower() + for value in values: + MODEL_TO_TEMPLATE_MAPPER[value.lower()] = lowered_key + pass +pass + + +TEMPLATE_TO_RESPONSES_MAPPER = { + "gemma-3": { + "instruction": "user\n", + "response": "model\n", + }, + "gemma3n": { + "instruction": "user\n", + "response": "model\n", + }, + "qwen3-instruct": { + "instruction": "<|im_start|>user\n", + "response": "<|im_start|>assistant\n", + }, + "qwen3-thinking": { + "instruction": "<|im_start|>user\n", + "response": "<|im_start|>assistant\n\n", + }, + "qwen3": { + "instruction": "<|im_start|>user\n", + "response": "<|im_start|>assistant\n", + }, + "qwen2.5": { + "instruction": "<|im_start|>user\n", + "response": "<|im_start|>assistant\n", + }, + "llama-3.2": { + "instruction": "<|start_header_id|>user<|end_header_id|>\n\n", + "response": "<|start_header_id|>assistant<|end_header_id|>\n\n", + }, + "llama-3.3": { + "instruction": "<|start_header_id|>user<|end_header_id|>\n\n", + "response": "<|start_header_id|>assistant<|end_header_id|>\n\n", + }, + "llama-3.1": { + "instruction": "<|start_header_id|>user<|end_header_id|>\n\n", + "response": "<|start_header_id|>assistant<|end_header_id|>\n\n", + }, + "llama3": { + "instruction": "<|start_header_id|>user<|end_header_id|>\n\n", + "response": "<|start_header_id|>assistant<|end_header_id|>\n\n", + }, + "phi-3": { + "instruction": "<|user|>\n", + "response": "<|assistant|>\n", + }, + "phi-3.5": { + "instruction": "<|user|>\n", + "response": "<|assistant|>\n", + }, + "phi-4": { + "instruction": "<|im_start|>user<|im_sep|>", + "response": "<|im_start|>assistant<|im_sep|>", + }, + "mistral": { + "instruction": "[INST] ", + "response": " [/INST]", + }, + "llama": { + "instruction": "[INST] ", + "response": " [/INST]", + }, + "chatml": { + "instruction": "<|im_start|>user\n", + "response": "<|im_start|>assistant\n", + }, + "zephyr": { + "instruction": "<|user|>\n", + "response": "<|assistant|>\n", + }, + "unsloth": { + "instruction": ">>> User: ", + "response": ">>> Assistant: ", + }, + "vicuna": { + "instruction": "USER: ", + "response": "ASSISTANT: ", + }, + "alpaca": { + "instruction": "### Instruction:\n", + "response": "### Response:\n", + }, + "gemma": { + "instruction": "user\n", + "response": "model\n", + }, + "gemma2": { + "instruction": "user\n", + "response": "model\n", + }, + "gpt-oss": { + "instruction": "<|start|>user<|message|>", + "response": "<|start|>assistant<|channel|>final<|message|>", + }, + "lfm-2": { + "instruction": "<|im_start|>user\n", + "response": "<|im_start|>assistant\n", + }, + "starling": { + "instruction": "GPT4 Correct User: ", + "response": "GPT4 Correct Assistant: ", + }, + "yi-chat": { + "instruction": "<|im_start|>user\n", + "response": "<|im_start|>assistant\n", + }, +} diff --git a/backend/backend/export.py b/backend/backend/export.py new file mode 100644 index 0000000000..b1557624ab --- /dev/null +++ b/backend/backend/export.py @@ -0,0 +1,506 @@ +# backend/export.py +""" +Export backend - handles model exporting in various formats +""" +import logging +import os +from pathlib import Path +from typing import Optional, Tuple, List +from peft import PeftModel, PeftModelForCausalLM +from unsloth import FastLanguageModel, FastVisionModel +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 + +logger = logging.getLogger(__name__) + +# Model card template +MODEL_CARD = \ +"""--- +base_model: {base_model} +tags: +- text-generation-inference +- transformers +- unsloth +- {model_type} +- {extra} +license: apache-2.0 +language: +- en +--- + +# Uploaded finetuned {method} model + +- **Developed by:** {username} +- **License:** apache-2.0 +- **Finetuned from model :** {base_model} + +This {model_type} model was trained 2x faster with [Unsloth](https://github.com/unslothai/unsloth) and Huggingface's TRL library. + +[](https://github.com/unslothai/unsloth) +""" + +class ExportBackend: + """Handles model export operations""" + + def __init__(self): + self.inference_backend = get_inference_backend() + self.current_checkpoint = None + self.current_model = None + self.current_tokenizer = None + self.is_vision = False + self.is_peft = False + + def cleanup_memory(self): + """Offload and delete all models from memory""" + try: + logger.info("Starting memory cleanup...") + + # Unload all models from inference backend + model_names = list(self.inference_backend.models.keys()) + for model_name in model_names: + self.inference_backend.unload_model(model_name) + + # Clear current export state + self.current_model = None + self.current_tokenizer = None + self.current_checkpoint = None + + # Force garbage collection + import gc + gc.collect() + + # Clear CUDA cache + if torch.cuda.is_available(): + torch.cuda.empty_cache() + torch.cuda.ipc_collect() + + logger.info("Memory cleanup completed successfully") + return True + + except Exception as e: + logger.error(f"Error during memory cleanup: {e}") + return False + + def scan_checkpoints(self, outputs_dir: str = "./outputs") -> List[Tuple[str, str]]: + """ + Scan outputs folder for model checkpoints. + + Returns: + List of tuples: [(display_name, checkpoint_path), ...] + """ + checkpoints = [] + outputs_path = Path(outputs_dir) + + if not outputs_path.exists(): + logger.warning(f"Outputs directory not found: {outputs_dir}") + return checkpoints + + try: + for item in outputs_path.iterdir(): + if item.is_dir(): + # Check if this directory contains a model + config_file = item / "config.json" + adapter_config = item / "adapter_config.json" + + if config_file.exists() or adapter_config.exists(): + # This is a valid checkpoint + display_name = item.name + checkpoint_path = str(item) + checkpoints.append((display_name, checkpoint_path)) + logger.debug(f"Found checkpoint: {display_name}") + + # Sort by modification time (newest first) + checkpoints.sort(key=lambda x: Path(x[1]).stat().st_mtime, reverse=True) + + logger.info(f"Found {len(checkpoints)} checkpoints in {outputs_dir}") + return checkpoints + + except Exception as e: + logger.error(f"Error scanning checkpoints: {e}") + return [] + + def load_checkpoint(self, + checkpoint_path: str, + max_seq_length: int = 2048, + load_in_4bit: bool = True) -> Tuple[bool, str]: + """ + Load a checkpoint for export. + + Returns: + Tuple of (success: bool, message: str) + """ + try: + logger.info(f"Loading checkpoint: {checkpoint_path}") + + # First, cleanup existing models + self.cleanup_memory() + + # Detect if vision model + checkpoint_path_obj = Path(checkpoint_path) + + # Check if it's a LoRA adapter + adapter_config = checkpoint_path_obj / "adapter_config.json" + if adapter_config.exists(): + # It's a LoRA - get base model to check vision + base_model = get_base_model_from_lora(checkpoint_path) + if base_model: + self.is_vision = is_vision_model(base_model) + else: + return False, "Could not determine base model for adapter" + else: + # Check the model itself + self.is_vision = is_vision_model(checkpoint_path) + + # Load model based on type + if self.is_vision: + logger.info("Loading as vision model...") + model, processor = FastVisionModel.from_pretrained( + model_name=checkpoint_path, + max_seq_length=max_seq_length, + dtype=None, + load_in_4bit=load_in_4bit, + ) + tokenizer = processor # For vision models, processor acts as tokenizer + else: + logger.info("Loading as text model...") + model, tokenizer = FastLanguageModel.from_pretrained( + model_name=checkpoint_path, + max_seq_length=max_seq_length, + dtype=None, + load_in_4bit=load_in_4bit, + ) + + # Check if PEFT model + self.is_peft = isinstance(model, (PeftModel, PeftModelForCausalLM)) + + # Store loaded model + self.current_model = model + self.current_tokenizer = tokenizer + self.current_checkpoint = checkpoint_path + + model_type = "Vision" if self.is_vision else "Text" + peft_info = " (PEFT Adapter)" if self.is_peft else " (Merged Model)" + + logger.info(f"Successfully loaded {model_type} model{peft_info}") + return True, f"Loaded {model_type} model{peft_info} successfully" + + except Exception as e: + logger.error(f"Error loading checkpoint: {e}") + import traceback + logger.error(traceback.format_exc()) + return False, f"Failed to load checkpoint: {str(e)}" + + def export_merged_model(self, + save_directory: str, + format_type: str = "16-bit (FP16)", + push_to_hub: bool = False, + repo_id: Optional[str] = None, + hf_token: Optional[str] = None, + private: bool = False) -> Tuple[bool, str]: + """ + Export merged model (for PEFT models). + + Args: + save_directory: Local directory to save model + format_type: "16-bit (FP16)" or "4-bit (FP4)" + push_to_hub: Whether to push to Hugging Face Hub + repo_id: Hub repository ID (username/model-name) + hf_token: Hugging Face token + private: Whether to make the repo private + + Returns: + Tuple of (success: bool, message: str) + """ + if not self.current_model or not self.current_tokenizer: + return False, "No model loaded. Please select a checkpoint first." + + if not self.is_peft: + return False, "This is not a PEFT model. Use 'Export Base Model' instead." + + try: + # Determine save method + if format_type == "4-bit (FP4)": + save_method = "merged_4bit_forced" + else: # 16-bit (FP16) + save_method = "merged_16bit" + + # Save locally if requested + if save_directory: + logger.info(f"Saving merged model locally to: {save_directory}") + os.makedirs(save_directory, exist_ok=True) + + self.current_model.save_pretrained_merged( + save_directory, + self.current_tokenizer, + save_method=save_method + ) + logger.info(f"Model saved successfully to {save_directory}") + + # Push to hub if requested + if push_to_hub: + if not repo_id or not hf_token: + return False, "Repository ID and Hugging Face token required for Hub upload" + + logger.info(f"Pushing merged model to Hub: {repo_id}") + + self.current_model.push_to_hub_merged( + repo_id, + self.current_tokenizer, + save_method=save_method, + token=hf_token, + private=private + ) + logger.info(f"Model pushed successfully to {repo_id}") + + return True, "Model exported successfully" + + except Exception as e: + logger.error(f"Error exporting merged model: {e}") + import traceback + logger.error(traceback.format_exc()) + return False, f"Export failed: {str(e)}" + + def export_base_model(self, + save_directory: str, + push_to_hub: bool = False, + repo_id: Optional[str] = None, + hf_token: Optional[str] = None, + private: bool = False) -> Tuple[bool, str]: + """ + Export base model (for non-PEFT models). + + Returns: + Tuple of (success: bool, message: str) + """ + if not self.current_model or not self.current_tokenizer: + return False, "No model loaded. Please select a checkpoint first." + + if self.is_peft: + return False, "This is a PEFT model. Use 'Merged Model' export type instead." + + try: + # Save locally if requested + if save_directory: + logger.info(f"Saving base model locally to: {save_directory}") + os.makedirs(save_directory, exist_ok=True) + + self.current_model.save_pretrained(save_directory) + self.current_tokenizer.save_pretrained(save_directory) + logger.info(f"Model saved successfully to {save_directory}") + + # Push to hub if requested + if push_to_hub: + if not repo_id or not hf_token: + return False, "Repository ID and Hugging Face token required for Hub upload" + + logger.info(f"Pushing base model to Hub: {repo_id}") + + # Get base model name + base_model = self.current_model.config._name_or_path + + # Create repo + hf_api = HfApi(token=hf_token) + repo_id = PushToHubMixin._create_repo( + PushToHubMixin, + repo_id=repo_id, + private=private, + token=hf_token, + ) + username = repo_id.split("/")[0] + + # Create and push model card + content = MODEL_CARD.format( + username=username, + base_model=base_model, + model_type=self.current_model.config.model_type, + method="", + extra="unsloth", + ) + card = ModelCard(content) + card.push_to_hub(repo_id, token=hf_token, commit_message="Unsloth Model Card") + + # Upload model files + if save_directory: + hf_api.upload_folder( + folder_path=save_directory, + repo_id=repo_id, + repo_type="model" + ) + logger.info(f"Model pushed successfully to {repo_id}") + else: + return False, "Local save directory required for Hub upload" + + return True, "Model exported successfully" + + except Exception as e: + logger.error(f"Error exporting base model: {e}") + import traceback + logger.error(traceback.format_exc()) + return False, f"Export failed: {str(e)}" + + + def export_gguf(self, + save_directory: str, + quantization_method: str = "Q4_K_M", + push_to_hub: bool = False, + repo_id: Optional[str] = None, + hf_token: Optional[str] = None) -> Tuple[bool, str]: + """ + Export model in GGUF format. + + Args: + save_directory: Local directory to save model + quantization_method: GGUF quantization method (e.g., "Q4_K_M") + push_to_hub: Whether to push to Hugging Face Hub + repo_id: Hub repository ID + hf_token: Hugging Face token + + Returns: + Tuple of (success: bool, message: str) + """ + if not self.current_model or not self.current_tokenizer: + return False, "No model loaded. Please select a checkpoint first." + + try: + # Convert quantization method to lowercase for unsloth + quant_method = quantization_method.lower() + + # Save locally if requested + if save_directory: + logger.info(f"Saving GGUF model locally to: {save_directory}") + + # Create the directory if it doesn't exist + os.makedirs(save_directory, exist_ok=True) + + # Get the base filename for the GGUF file + import shutil + original_dir = os.getcwd() + + try: + # Change to target directory + os.chdir(save_directory) + logger.info(f"Changed directory to: {save_directory}") + + # Now save (will save in current directory) + self.current_model.save_pretrained_gguf( + "model", # Base filename + self.current_tokenizer, + quantization_method=quant_method + ) + + logger.info(f"GGUF model saved successfully in {save_directory}") + + # Check if llama.cpp directory was created here + llama_cpp_in_target = os.path.join(save_directory, "llama.cpp") + llama_cpp_in_original = os.path.join(original_dir, "llama.cpp") + + if os.path.exists(llama_cpp_in_target): + logger.info(f"Found llama.cpp directory in {save_directory}") + + # Remove llama.cpp from original directory if it exists + if os.path.exists(llama_cpp_in_original): + logger.info(f"Removing existing llama.cpp in {original_dir}") + shutil.rmtree(llama_cpp_in_original) + + # Move llama.cpp back to original directory + logger.info(f"Moving llama.cpp to {original_dir}") + shutil.move(llama_cpp_in_target, llama_cpp_in_original) + logger.info(f"Successfully moved llama.cpp back to original directory") + + finally: + # Always change back to original directory + os.chdir(original_dir) + logger.info(f"Changed back to original directory: {original_dir}") + + # Push to hub if requested + if push_to_hub: + if not repo_id or not hf_token: + return False, "Repository ID and Hugging Face token required for Hub upload" + + logger.info(f"Pushing GGUF model to Hub: {repo_id}") + + self.current_model.push_to_hub_gguf( + repo_id, + self.current_tokenizer, + quantization_method=quant_method, + token=hf_token + ) + logger.info(f"GGUF model pushed successfully to {repo_id}") + + return True, f"GGUF model exported successfully ({quantization_method})" + + except Exception as e: + logger.error(f"Error exporting GGUF model: {e}") + import traceback + logger.error(traceback.format_exc()) + return False, f"GGUF export failed: {str(e)}" + + def export_lora_adapter(self, + save_directory: str, + push_to_hub: bool = False, + repo_id: Optional[str] = None, + hf_token: Optional[str] = None, + private: bool = False) -> Tuple[bool, str]: + """ + Export LoRA adapter only (not merged). + + Returns: + Tuple of (success: bool, message: str) + """ + if not self.current_model or not self.current_tokenizer: + return False, "No model loaded. Please select a checkpoint first." + + if not self.is_peft: + return False, "This is not a PEFT model. No adapter to export." + + try: + # Save locally if requested + if save_directory: + logger.info(f"Saving LoRA adapter locally to: {save_directory}") + os.makedirs(save_directory, exist_ok=True) + + self.current_model.save_pretrained(save_directory) + self.current_tokenizer.save_pretrained(save_directory) + logger.info(f"Adapter saved successfully to {save_directory}") + + # Push to hub if requested + if push_to_hub: + if not repo_id or not hf_token: + return False, "Repository ID and Hugging Face token required for Hub upload" + + logger.info(f"Pushing LoRA adapter to Hub: {repo_id}") + + self.current_model.push_to_hub( + repo_id, + token=hf_token, + private=private + ) + self.current_tokenizer.push_to_hub( + repo_id, + token=hf_token, + private=private + ) + logger.info(f"Adapter pushed successfully to {repo_id}") + + return True, "LoRA adapter exported successfully" + + except Exception as e: + logger.error(f"Error exporting LoRA adapter: {e}") + import traceback + logger.error(traceback.format_exc()) + return False, f"Adapter export failed: {str(e)}" + + +# Global export backend instance +_export_backend = None + +def get_export_backend() -> ExportBackend: + """Get or create the global export backend instance""" + global _export_backend + if _export_backend is None: + _export_backend = ExportBackend() + return _export_backend diff --git a/backend/backend/inference.py b/backend/backend/inference.py new file mode 100644 index 0000000000..90ec3f214c --- /dev/null +++ b/backend/backend/inference.py @@ -0,0 +1,1212 @@ +""" +Core inference backend - streamlined +""" +from unsloth import FastLanguageModel, FastVisionModel +from unsloth.chat_templates import get_chat_template +from transformers import TextStreamer +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 import format_error_message, log_gpu_memory +from io import StringIO +import logging + + + +logger = logging.getLogger(__name__) + +class InferenceBackend: + """Unified inference backend supporting text, vision, and LoRA models""" + + def __init__(self): + self.models = {} + self.active_model_name = None + self.loading_models = set() + self.loaded_local_models = [] # [(display_name, path), ...] + self.default_models = [ + "unsloth/Qwen3-4B-Instruct-2507", + "unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit", + "unsloth/Mistral-Nemo-Instruct-2407-bnb-4bit", + "unsloth/Phi-3.5-mini-instruct", + "unsloth/Gemma-3-4B-it", + "unsloth/Qwen2-VL-2B-Instruct-bnb-4bit", + ] + self.device = "cuda" if torch.cuda.is_available() else "cpu" + + # Thread safety + import threading + self._generation_lock = threading.RLock() + self._model_state_lock = threading.Lock() + + logger.info(f"InferenceBackend initialized on {self.device}") + + def load_model(self, + config: ModelConfig, + max_seq_length: int = 2048, + dtype = None, + load_in_4bit: bool = True, + hf_token: Optional[str] = None) -> bool: + """ + Load any model: base, LoRA adapter, text, or vision. + """ + try: + model_name = config.identifier + + # Check if already loaded + if model_name in self.models and self.models[model_name].get("model"): + logger.info(f"Model {model_name} already loaded") + self.active_model_name = model_name + return True + + # Check if currently loading + if model_name in self.loading_models: + logger.info(f"Model {model_name} is already being loaded") + return False + + self.loading_models.add(model_name) + + self.models[model_name] = { + "is_vision": config.is_vision, + "is_lora": config.is_lora, + "model_path": config.path, + "base_model": config.base_model if config.is_lora else None, + "loaded_adapters": {}, + "active_adapter": None, + } + + model_type = "vision" if config.is_vision else "text" + adapter_info = " (LoRA adapter)" if self.models[model_name]["is_lora"] else "" + logger.info(f"Loading {model_type} model{adapter_info}: {model_name}") + log_gpu_memory(f"Before loading {model_name}") + + # Load model - same approach for base models and LoRA adapters + if config.is_vision: + # Vision model (or vision LoRA adapter) + model, processor = FastVisionModel.from_pretrained( + model_name=config.path, # Can be base model OR LoRA adapter path + max_seq_length=max_seq_length, + dtype=dtype, + load_in_4bit=load_in_4bit, + token=hf_token if hf_token and hf_token.strip() else None, + ) + + # Apply inference optimization + FastVisionModel.for_inference(model) + + self.models[model_name]["model"] = model + self.models[model_name]["tokenizer"] = processor + self.models[model_name]["processor"] = processor + + else: + # Text model (or text LoRA adapter) + model, tokenizer = FastLanguageModel.from_pretrained( + model_name=config.path, # Can be base model OR LoRA adapter path + max_seq_length=max_seq_length, + dtype=dtype, + load_in_4bit=load_in_4bit, + token=hf_token if hf_token and hf_token.strip() else None, + ) + + # Apply inference optimization + FastLanguageModel.for_inference(model) + + self.models[model_name]["model"] = model + self.models[model_name]["tokenizer"] = tokenizer + + # Load chat template info + self._load_chat_template_info(model_name) + + self.active_model_name = model_name + self.loading_models.discard(model_name) + + logger.info(f"Successfully loaded model: {model_name}") + log_gpu_memory(f"After loading {model_name}") + return True + + except Exception as e: + logger.error(f"Failed to load model: {e}") + error_msg = format_error_message(e, config.identifier) + + # Cleanup on failure + if model_name in self.models: + del self.models[model_name] + self.loading_models.discard(model_name) + + raise Exception(error_msg) + pass + + # Add this new function + def unload_model(self, model_name: str) -> bool: + """ + Completely removes a model from the registry and clears GPU memory. + """ + if model_name in self.models: + try: + logger.info(f"Unloading model '{model_name}' from memory.") + # Delete the model entry from our registry + del self.models[model_name] + + # Clear the active model if it was the one being unloaded + if self.active_model_name == model_name: + self.active_model_name = None + + # Use garbage collection and clear CUDA cache to release memory + import gc + import torch + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + logger.info(f"Model '{model_name}' successfully unloaded.") + return True + except Exception as e: + logger.error(f"Error while unloading model '{model_name}': {e}") + return False + else: + logger.warning(f"Attempted to unload model '{model_name}', but it was not found in the registry.") + return True + pass + + def revert_to_base_model(self, base_model_name: str) -> bool: + """ + Reverts the model to its pristine base state by unloading AND + deleting all adapter configurations, as instructed. + """ + if base_model_name not in self.models: + return False + + model = self.models[base_model_name].get("model") + + try: + # Step 1: Unload the adapter weights. This returns the base model object. + # This step is only necessary if the model is currently a PeftModel instance. + if isinstance(model, (PeftModel, PeftModelForCausalLM)): + logger.info("Model is a PeftModel. Unloading adapters...") + unwrapped_base_model = model.unload() + self.models[base_model_name]["model"] = unwrapped_base_model + model = unwrapped_base_model # Continue with the unwrapped model + + # Step 2: Delete any lingering adapter configurations from the object. + # This is the crucial step you identified. + if hasattr(model, 'peft_config') and model.peft_config: + logger.info("Found lingering adapter configurations. Deleting them now...") + # Create a static list of keys before iterating and deleting + for name in list(model.peft_config.keys()): + logger.info(f"Deleting adapter config: '{name}'") + model.delete_adapter(name) + + logger.info("Model has been successfully reverted to a clean base state.") + return True + + except Exception as e: + logger.error(f"Failed to revert model to base state: {e}") + import traceback + logger.error(traceback.format_exc()) + return False + pass + + def activate_lora_adapter(self, base_model_name: str, lora_path: str) -> Tuple[bool, Optional[str]]: + """ + Activates a specific LoRA adapter on what is assumed to be a clean base model. + """ + model = self.models[base_model_name].get("model") + adapter_name_to_load = lora_path.split("/")[-1].replace(".", "_") + + try: + # At this point, the model should be clean thanks to revert_to_base_model. + # We can now safely load and set the new adapter. + + # Step 3: Load the new adapter. + logger.info(f"Loading adapter '{adapter_name_to_load}' from '{lora_path}'") + model.load_adapter(lora_path, adapter_name=adapter_name_to_load) + + # Step 4: Set the new adapter as active. + logger.info(f"Setting '{adapter_name_to_load}' as the active adapter.") + model.set_adapter(adapter_name_to_load) + + return True, adapter_name_to_load + except Exception as e: + # This will catch the "already exists" error if revert_to_base_model failed. + logger.error(f"Failed to activate LoRA adapter '{adapter_name_to_load}': {e}") + import traceback + logger.error(traceback.format_exc()) + return False, None + pass + + def load_adapter(self, base_model_name: str, adapter_path: str, adapter_name: str = None) -> bool: + """ + Load a LoRA adapter onto the base model if it's not already registered. + This method is idempotent. + """ + if base_model_name not in self.models: + logger.error(f"Base model {base_model_name} not loaded") + return False + + model = self.models[base_model_name].get("model") + if model is None: + logger.error(f"Model object for {base_model_name} is None.") + return False + + if adapter_name is None: + adapter_name = adapter_path.split("/")[-1].replace(".", "_") + + # If we've loaded this adapter before, we don't need to do anything. + if adapter_name in self.models[base_model_name].get("loaded_adapters", {}): + logger.info(f"Adapter '{adapter_name}' is already registered. Skipping.") + return True + + try: + logger.info(f"Loading new adapter '{adapter_name}' from '{adapter_path}' onto {base_model_name}") + + # Unsloth modifies the model in-place and returns None. Do NOT re-assign. + model.load_adapter(adapter_path, adapter_name=adapter_name) + + # Update our internal registry so we don't load it again. + self.models[base_model_name]["loaded_adapters"][adapter_name] = adapter_path + + total_adapters = len(getattr(model, 'peft_config', {})) + logger.info(f"Adapter '{adapter_name}' loaded successfully. (Total adapters on model: {total_adapters})") + return True + except Exception as e: + logger.error(f"Failed to load adapter '{adapter_name}': {e}") + import traceback + logger.error(traceback.format_exc()) + return False + pass + + def enable_adapter(self, base_model_name: str, adapter_name: str) -> bool: + """Enable specific adapter (for generation)""" + if base_model_name not in self.models: + return False + + model = self.models[base_model_name]["model"] + + try: + logger.info(f"Enabling adapter: {adapter_name}") + model.set_adapter(adapter_name) + self.models[base_model_name]["active_adapter"] = adapter_name + return True + except Exception as e: + logger.error(f"Failed to enable adapter: {e}") + return False + + def disable_adapters(self, base_model_name: str) -> bool: + """Disable all adapters (back to pure base model)""" + if base_model_name not in self.models: + return False + + model = self.models[base_model_name]["model"] + + try: + logger.info(f"Disabling all adapters on {base_model_name}") + model.disable_adapters() + self.models[base_model_name]["active_adapter"] = None + return True + except Exception as e: + logger.error(f"Failed to disable adapters: {e}") + return False + + # In backend/inference.py + + def load_for_eval(self, lora_path: str, max_seq_length: int = 2048, + dtype = None, load_in_4bit: bool = True, + hf_token: Optional[str] = None) -> Tuple[bool, Optional[str], Optional[str]]: + """ + Prepare for eval: ensure base model and the specified adapter are loaded. + """ + try: + from .model_config import ModelConfig + lora_config = ModelConfig.from_lora_path(lora_path, hf_token) + if not lora_config: + return False, None, None + + base_model_name = lora_config.base_model + + # 1. Load the base model if it's not already in memory (this logic is correct) + if base_model_name not in self.models or not self.models[base_model_name].get("model"): + logger.info(f"Base model '{base_model_name}' not loaded, loading now.") + base_config = ModelConfig.from_ui_selection(base_model_name, None, is_lora=False) + if not self.load_model(base_config, max_seq_length, dtype, load_in_4bit, hf_token): + return False, None, None + else: + logger.info(f"Base model '{base_model_name}' is already in memory.") + + self.active_model_name = base_model_name + + # 2. Delegate to our now-idempotent load_adapter function. + # It will handle all cases: first adapter, or subsequent adapters. + adapter_name = lora_path.split("/")[-1].replace(".", "_") + adapter_success = self.load_adapter( + base_model_name=base_model_name, + adapter_path=lora_path, + adapter_name=adapter_name + ) + + if not adapter_success: + return False, base_model_name, None + + return True, base_model_name, adapter_name + + except Exception as e: + logger.error(f"Error during load_for_eval: {e}") + import traceback + logger.error(traceback.format_exc()) + return False, None, None + pass + + + def load_for_eval(self, lora_path: str, max_seq_length: int = 2048, + dtype = None, load_in_4bit: bool = True, + hf_token: Optional[str] = None) -> Tuple[bool, Optional[str], Optional[str]]: + """ + Final Corrected Version: + Ensures the base model and the specified adapter are loaded. + This function is idempotent and handles all states correctly. + """ + try: + from .model_config import ModelConfig + lora_config = ModelConfig.from_lora_path(lora_path, hf_token) + if not lora_config: + return False, None, None + + base_model_name = lora_config.base_model + + # 1. Load the base model if it's not already in memory + if base_model_name not in self.models or not self.models[base_model_name].get("model"): + logger.info(f"Base model '{base_model_name}' not loaded, loading now.") + base_config = ModelConfig.from_ui_selection(base_model_name, None, is_lora=False) + if not self.load_model(base_config, max_seq_length, dtype, load_in_4bit, hf_token): + return False, None, None + + self.active_model_name = base_model_name + + # 2. Determine the required adapter name from the user's selection + adapter_name = lora_path.split("/")[-1].replace(".", "_") + + # 3. Call our robust load_adapter function to ensure this specific adapter is loaded. + # It will only load from disk if the model doesn't already have it. + adapter_success = self.load_adapter( + base_model_name=base_model_name, + adapter_path=lora_path, + adapter_name=adapter_name + ) + if not adapter_success: + return False, base_model_name, None + + # 4. Return the correct, verified adapter name for the UI logic to use. + return True, base_model_name, adapter_name + + except Exception as e: + logger.error(f"Error during load_for_eval: {e}") + import traceback + logger.error(traceback.format_exc()) + return False, None, None + pass + + def load_adapter(self, base_model_name: str, adapter_path: str, adapter_name: str) -> bool: + """ + Loads an adapter onto the model ONLY if it's not already attached. + """ + model = self.models[base_model_name].get("model") + + # Check if this adapter name is already part of the model's config. This is the most reliable check. + if hasattr(model, "peft_config") and adapter_name in model.peft_config: + logger.info(f"Adapter '{adapter_name}' is already attached to the model. Skipping load.") + return True + + try: + logger.info(f"Loading new adapter '{adapter_name}' from '{adapter_path}' onto {base_model_name}") + model.load_adapter(adapter_path, adapter_name=adapter_name) + + # Update our internal registry ONLY after a successful load. + if "loaded_adapters" not in self.models[base_model_name]: + self.models[base_model_name]["loaded_adapters"] = {} + self.models[base_model_name]["loaded_adapters"][adapter_name] = adapter_path + + total_adapters = len(getattr(model, 'peft_config', {})) + logger.info(f"Adapter '{adapter_name}' loaded successfully. (Total unique adapters on model: {total_adapters})") + return True + except Exception as e: + logger.error(f"Failed to load adapter '{adapter_name}': {e}") + return False + pass + + def set_active_adapter(self, base_model_name: str, adapter_name: str) -> bool: + """ + Sets the active adapter for generation. This replaces the flawed 'enable_adapter'. + """ + model = self.models[base_model_name].get("model") + try: + logger.info(f"Setting active adapter to: '{adapter_name}'") + model.set_adapter(adapter_name) + self.models[base_model_name]["active_adapter"] = adapter_name + return True + except Exception as e: + # This will catch the "adapter not found" error if something goes wrong. + logger.error(f"Failed to set active adapter to '{adapter_name}': {e}") + return False + pass + + def generate_chat_response(self, + messages: list, + system_prompt: str, + image=None, + temperature: float = 0.7, + top_p: float = 0.9, + top_k: int = 40, + max_new_tokens: int = 256, + repetition_penalty: float = 1.1) -> Generator[str, None, None]: + """ + Generate response for text or vision models. + + 1. Messages are already in ChatML format (role/content) + 2. Apply get_chat_template() if model in mapper + 3. Apply tokenizer.apply_chat_template() + 4. Generate + """ + if not self.active_model_name: + yield "Error: No active model" + return + + model_info = self.models[self.active_model_name] + is_vision = model_info.get("is_vision", False) + tokenizer = model_info.get("tokenizer") or model_info.get("processor") + + with self._generation_lock: + if is_vision: + # Vision model generation + yield from self._generate_vision_response( + messages, system_prompt, image, + temperature, top_p, top_k, max_new_tokens, repetition_penalty + ) + else: + # Text model: Use training pipeline approach + # Messages are already in ChatML format from eval.py + + # Step 1: Apply get_chat_template if model is in mapper + try: + from backend.dataset_utils import MODEL_TO_TEMPLATE_MAPPER, get_tokenizer_chat_template + + model_name_lower = self.active_model_name.lower() + + # Check if model has a registered template + if model_name_lower in MODEL_TO_TEMPLATE_MAPPER: + template_name = MODEL_TO_TEMPLATE_MAPPER[model_name_lower] + logger.info(f"Applying chat template '{template_name}' for {self.active_model_name}") + + # This modifies the tokenizer with the correct template + tokenizer = get_chat_template( + tokenizer, + self.active_model_name + ) + else: + logger.info(f"No registered template for {self.active_model_name}, using tokenizer default") + except Exception as e: + logger.warning(f"Could not apply get_chat_template: {e}") + + # Step 2: Format with tokenizer.apply_chat_template() + try: + formatted_prompt = tokenizer.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=True + ) + logger.debug(f"Formatted prompt: {formatted_prompt[:200]}...") + except Exception as e: + logger.error(f"Error applying chat template: {e}") + # Fallback to manual formatting + formatted_prompt = self.format_chat_prompt(messages, system_prompt) + + # Step 3: Generate + yield from self.generate_stream( + formatted_prompt, temperature, top_p, top_k, max_new_tokens, repetition_penalty + ) + + def _generate_vision_response(self, messages, system_prompt, image, + temperature, top_p, top_k, max_new_tokens, + repetition_penalty) -> Generator[str, None, None]: + """Handle vision model generation.""" + model_info = self.models[self.active_model_name] + model = model_info["model"] + processor = model_info["processor"] + + # Extract user message + user_message = "" + if messages and messages[-1]["role"] == "user": + import re + user_message = messages[-1]["content"] + user_message = re.sub(r']*>', '', user_message).strip() + + if not user_message: + user_message = "Describe this image." if image else "Hello" + + # Prepare vision messages + if image: + vision_messages = [ + { + "role": "user", + "content": [ + {"type": "image"}, + {"type": "text", "text": user_message} + ], + } + ] + + input_text = processor.apply_chat_template(vision_messages, add_generation_prompt=True) + inputs = processor( + image, + input_text, + add_special_tokens=False, + return_tensors="pt", + ).to("cuda") + else: + # Text-only for vision model + formatted_prompt = self.format_chat_prompt(messages, system_prompt) + inputs = processor.tokenizer(formatted_prompt, return_tensors="pt").to("cuda") + + # Generate with streaming + captured_output = StringIO() + original_stdout = sys.stdout + + try: + sys.stdout = captured_output + + text_streamer = TextStreamer(processor.tokenizer, skip_prompt=True) + model.generate( + **inputs, + streamer=text_streamer, + max_new_tokens=max_new_tokens, + use_cache=True, + temperature=temperature, + top_p=top_p, + top_k=top_k + ) + + sys.stdout = original_stdout + generated_text = captured_output.getvalue() + cleaned = self._clean_generated_text(generated_text) + yield cleaned + + except Exception as e: + sys.stdout = original_stdout + logger.error(f"Vision generation error: {e}") + yield f"Error: {str(e)}" + pass + + def generate_stream(self, + prompt: str, + temperature: float = 0.7, + top_p: float = 0.9, + top_k: int = 40, + max_new_tokens: int = 256, + repetition_penalty: float = 1.1) -> Generator[str, None, None]: + """Generate streaming text response (text models only).""" + if not self.active_model_name: + yield "Error: No active model" + return + + model_info = self.models[self.active_model_name] + model = model_info["model"] + tokenizer = model_info["tokenizer"] + + try: + inputs = tokenizer(prompt, return_tensors="pt").to(model.device) + + from transformers import TextIteratorStreamer + import threading + + streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True) + + generation_kwargs = dict( + **inputs, + streamer=streamer, + max_new_tokens=max_new_tokens, + temperature=temperature, + top_p=top_p, + top_k=top_k, + repetition_penalty=repetition_penalty, + do_sample=True, + eos_token_id=tokenizer.eos_token_id, + pad_token_id=tokenizer.eos_token_id if tokenizer.pad_token_id is None else tokenizer.pad_token_id, + ) + + def generate_fn(): + try: + model.generate(**generation_kwargs) + except Exception as e: + logger.error(f"Generation error: {e}") + + thread = threading.Thread(target=generate_fn) + thread.start() + + output = "" + for new_token in streamer: + if new_token: + output += new_token + cleaned = self._clean_generated_text(output) + yield cleaned + + thread.join() + + except Exception as e: + logger.error(f"Error during generation: {e}") + yield f"Error: {str(e)}" + + # ... other helper methods (format_chat_prompt, _clean_generated_text, etc.) + pass + + def format_chat_prompt(self, messages: list, system_prompt: str = None) -> str: + if not self.active_model_name or self.active_model_name not in self.models: + logger.error("No active model available") + return "" + + if self.models[self.active_model_name].get("tokenizer") is None: + logger.error("Tokenizer not loaded for active model") + return "" + + chat_template_info = self.models[self.active_model_name].get("chat_template_info", {}) + tokenizer = self.models[self.active_model_name]["tokenizer"] + + chat_messages = [] + + if system_prompt: + chat_messages.append({"role": "system", "content": system_prompt}) + + last_role = "system" if system_prompt else None + + for msg in messages: + role = msg.get("role", "") + content = msg.get("content", "") + + if role in ["system", "user", "assistant"] and content.strip(): + if role == last_role: + logger.debug(f"Skipping consecutive {role} message to maintain alternation") + continue + + if role == "user": + import re + clean_content = re.sub(r'<[^>]+>', '', content).strip() + if clean_content: + chat_messages.append({"role": role, "content": clean_content}) + last_role = role + elif role == "assistant" and content.strip(): + chat_messages.append({"role": role, "content": content}) + last_role = role + elif role == "system": + continue + + if chat_messages and chat_messages[-1]["role"] == "assistant": + logger.debug("Removing final assistant message to ensure proper alternation") + chat_messages.pop() + + logger.info(f"Sending {len(chat_messages)} messages to tokenizer:") + for i, msg in enumerate(chat_messages): + logger.info(f" {i}: {msg['role']} - {msg['content'][:50]}...") + + try: + formatted_prompt = tokenizer.apply_chat_template( + chat_messages, + tokenize=False, + add_generation_prompt=True + ) + logger.info(f"Successfully applied tokenizer's native chat template") + return formatted_prompt + except Exception as e: + error_msg = str(e).lower() + if "chat_template is not set" in error_msg or "no template argument" in error_msg: + logger.info(f"Base model detected - no built-in chat template available, using fallback formatting") + else: + logger.warning(f"Failed to apply tokenizer chat template: {e}") + logger.debug(f"""Failed with messages: {[f"{m['role']}: {m['content'][:30]}..." for m in chat_messages]}""") + + if chat_template_info.get("has_template", False): + logger.info("Falling back to manual template formatting based on detected patterns") + template_type = chat_template_info.get("format_type", "generic") + manual_prompt = self._format_chat_manual(chat_messages, template_type, chat_template_info.get("special_tokens", {})) + logger.info(f"Manual template result: {manual_prompt[:200]}...") + return manual_prompt + else: + logger.info("Using generic chat formatting for base model") + return self._format_generic_template(chat_messages, {}) + + def _format_chat_manual(self, messages: list, template_type: str, special_tokens: dict) -> str: + """ + Manual chat formatting fallback for when tokenizer template fails + + Args: + messages: List of message dictionaries + template_type: Detected template type + special_tokens: Dictionary of special tokens + + Returns: + str: Manually formatted prompt + """ + if template_type == "llama3": + return self._format_llama3_template(messages, special_tokens) + elif template_type == "mistral": + return self._format_mistral_template(messages, special_tokens) + elif template_type == "chatml": + return self._format_chatml_template(messages, special_tokens) + elif template_type == "alpaca": + return self._format_alpaca_template(messages, special_tokens) + else: + return self._format_generic_template(messages, special_tokens) + + def _format_llama3_template(self, messages: list, special_tokens: dict) -> str: + """Format messages using Llama 3 template""" + bos_token = special_tokens.get("bos_token", "<|begin_of_text|>") + formatted = bos_token + + for msg in messages: + role = msg["role"] + content = msg["content"] + formatted += f"<|start_header_id|>{role}<|end_header_id|>\n\n{content}<|eot_id|>" + + formatted += "<|start_header_id|>assistant<|end_header_id|>\n\n" + return formatted + + def _format_mistral_template(self, messages: list, special_tokens: dict) -> str: + """Format messages using Mistral template""" + bos_token = special_tokens.get("bos_token", "") + formatted = bos_token + + system_msg = None + conversation = [] + + for msg in messages: + if msg["role"] == "system": + system_msg = msg["content"] + else: + conversation.append(msg) + + i = 0 + while i < len(conversation): + if conversation[i]["role"] == "user": + user_content = conversation[i]["content"] + + if system_msg and i == 0: + user_content = f"{system_msg}\n\n{user_content}" + + formatted += f"[INST] {user_content} [/INST]" + + if i + 1 < len(conversation) and conversation[i + 1]["role"] == "assistant": + formatted += f" {conversation[i + 1]['content']}" + i += 2 + else: + formatted += " " + break + else: + i += 1 + + return formatted + + def _format_chatml_template(self, messages: list, special_tokens: dict) -> str: + """Format messages using ChatML template""" + formatted = "" + + for msg in messages: + role = msg["role"] + content = msg["content"] + formatted += f"<|im_start|>{role}\n{content}<|im_end|>\n" + + formatted += "<|im_start|>assistant\n" + return formatted + + def _format_alpaca_template(self, messages: list, special_tokens: dict) -> str: + """Format messages using Alpaca template""" + formatted = "" + system_msg = None + + for msg in messages: + if msg["role"] == "system": + system_msg = msg["content"] + elif msg["role"] == "user": + if system_msg: + formatted += f"### Instruction:\n{system_msg}\n\n### Input:\n{msg['content']}\n\n### Response:\n" + system_msg = None + else: + formatted += f"### Human:\n{msg['content']}\n\n### Assistant:\n" + elif msg["role"] == "assistant": + formatted += f"{msg['content']}\n\n" + + return formatted + + def _format_generic_template(self, messages: list, special_tokens: dict) -> str: + """Generic fallback formatting""" + formatted = "" + + for msg in messages: + role = msg["role"].title() + content = msg["content"] + formatted += f"{role}: {content}\n" + + formatted += "Assistant: " + return formatted + + def check_vision_model_compatibility(self, show_warning: bool = True) -> bool: + """ + Check if current model supports vision and optionally show warning if image uploaded to text-only model + + Args: + show_warning: Whether to show Gradio warning if vision not supported + + Returns: + bool: True if current model supports vision, False otherwise + """ + current_model = self.get_current_model() + if current_model and current_model in self.models: + is_vision = self.models[current_model].get("is_vision", False) + if not is_vision and show_warning: + import gradio as gr + model_short = current_model.split('/')[-1] if '/' in current_model else current_model + gr.Warning(f"Image uploaded, but {model_short} is a text-only model. Please select a vision model to analyze images.") + return is_vision + return False + + def _reset_model_generation_state(self, model_name: str): + """Reset generation state for a specific model to prevent contamination.""" + if model_name not in self.models: + return + + model = self.models[model_name].get("model") + if not model: + return + + try: + # This is a common pattern for Unsloth/Hugging Face models + if hasattr(model, 'past_key_values'): + model.past_key_values = None + if hasattr(model, 'generation_config'): + if hasattr(model.generation_config, 'past_key_values'): + model.generation_config.past_key_values = None + + logger.debug(f"Reset generation state for model: {model_name}") + except Exception as e: + logger.warning(f"Could not fully reset model state for {model_name}: {e}") + pass + + def reset_generation_state(self): + """Reset any cached generation state to prevent hanging after errors""" + try: + # Clear cached states for ALL loaded models + for model_name in self.models.keys(): + self._reset_model_generation_state(model_name) + + import torch + if torch.cuda.is_available(): + torch.cuda.empty_cache() + torch.cuda.ipc_collect() + logger.debug("Cleared CUDA cache and IPC resources") + + import gc + gc.collect() + logger.info("Performed comprehensive generation state reset") + + except Exception as e: + logger.warning(f"Could not fully reset generation state: {e}") + + def resize_image(self, img, max_size: int = 800): + """Resize image while maintaining aspect ratio if either dimension exceeds max_size""" + if img is None: + return None + if img.size[0] > max_size or img.size[1] > max_size: + from PIL import Image + ratio = min(max_size/img.size[0], max_size/img.size[1]) + new_size = (int(img.size[0]*ratio), int(img.size[1]*ratio)) + return img.resize(new_size, Image.Resampling.LANCZOS) + return img + + def _clean_generated_text(self, text: str) -> str: + import re + + text = re.sub(r'<\|start_header_id\|>.*?<\|end_header_id\|>', '', text) + text = re.sub(r'<\|eot_id\|>', '', text) + text = re.sub(r'<\|begin_of_text\|>', '', text) + + text = re.sub(r'\[INST\].*?\[/INST\]', '', text) + text = re.sub(r'|', '', text) + + # Clean ChatML tokens (used by Qwen2-VL and similar models) + text = re.sub(r'<\|im_start\|>.*?<\|im_end\|>', '', text) + text = re.sub(r'<\|im_end\|>', '', text) + text = re.sub(r'<\|im_start\|>', '', text) + + text = re.sub(r'^\s*(assistant|user|system):\s*', '', text, flags=re.IGNORECASE) + text = text.strip() + + return text + + def _load_chat_template_info(self, model_name: str): + if model_name not in self.models or not self.models[model_name].get("tokenizer"): + return + + tokenizer = self.models[model_name]["tokenizer"] + chat_template_info = { + "has_template": False, + "template": None, + "format_type": "generic", + "special_tokens": {}, + "template_name": None, + } + + try: + from backend.dataset_utils import MODEL_TO_TEMPLATE_MAPPER + #Try exact match first + model_name_lower = model_name.lower() + if model_name_lower in MODEL_TO_TEMPLATE_MAPPER: + chat_template_info["template_name"] = MODEL_TO_TEMPLATE_MAPPER[model_name_lower] + logger.info(f"Detected template '{chat_template_info['template_name']}' for {model_name} from mapper") + else: + # Try partial match (for variants like model_name-bnb-4bit) + for key in MODEL_TO_TEMPLATE_MAPPER: + if key in model_name_lower or model_name_lower in key: + chat_template_info["template_name"] = MODEL_TO_TEMPLATE_MAPPER[key] + logger.info(f"Detected template '{chat_template_info['template_name']}' for {model_name} (partial match)") + break + except Exception as e: + logger.warning(f"Could not detect template from mapper for {model_name}: {e}") + + try: + if hasattr(tokenizer, 'chat_template') and tokenizer.chat_template: + chat_template_info["has_template"] = True + chat_template_info["template"] = tokenizer.chat_template + + template_str = tokenizer.chat_template.lower() + + if "start_header_id" in template_str and "end_header_id" in template_str: + chat_template_info["format_type"] = "llama3" + elif "[inst]" in template_str and "[/inst]" in template_str: + chat_template_info["format_type"] = "mistral" + elif "<|im_start|>" in template_str and "<|im_end|>" in template_str: + chat_template_info["format_type"] = "chatml" + elif "### instruction:" in template_str or "### human:" in template_str: + chat_template_info["format_type"] = "alpaca" + else: + chat_template_info["format_type"] = "custom" + + logger.info(f"Loaded chat template for {model_name} (detected as {chat_template_info['format_type']} format)") + logger.debug(f"Template preview: {tokenizer.chat_template[:200]}...") + + special_tokens = {} + if hasattr(tokenizer, 'bos_token') and tokenizer.bos_token: + special_tokens["bos_token"] = tokenizer.bos_token + if hasattr(tokenizer, 'eos_token') and tokenizer.eos_token: + special_tokens["eos_token"] = tokenizer.eos_token + if hasattr(tokenizer, 'pad_token') and tokenizer.pad_token: + special_tokens["pad_token"] = tokenizer.pad_token + + chat_template_info["special_tokens"] = special_tokens + + else: + logger.info(f"No chat template found for {model_name}, will use generic formatting") + + except Exception as e: + logger.error(f"Error loading chat template info for {model_name}: {e}") + + self.models[model_name]["chat_template_info"] = chat_template_info + + if chat_template_info["has_template"]: + logger.info(f"Chat template loaded for {model_name}: {chat_template_info['format_type']} format") + else: + logger.info(f"No built-in chat template for {model_name}, will use generic formatting") + + + def get_current_model(self) -> Optional[str]: + """Get currently active model name""" + return self.active_model_name + + def is_model_loading(self) -> bool: + """Check if any model is currently loading""" + return len(self.loading_models) > 0 + + def get_loading_model(self) -> Optional[str]: + """Get name of currently loading model""" + return next(iter(self.loading_models)) if self.loading_models else None + + def load_model_simple(self, + model_path: str, + hf_token: Optional[str] = None, + max_seq_length: int = 2048, + load_in_4bit: bool = True) -> bool: + """ + Simple model loading wrapper for chat interface. + Accepts model path as string and handles ModelConfig creation internally. + + Args: + model_path: Model name or path (e.g., "unsloth/llama-3-8b") + hf_token: HuggingFace token for gated models + max_seq_length: Maximum sequence length + load_in_4bit: Whether to use 4-bit quantization + + Returns: + bool: True if successful, False otherwise + """ + try: + # Create config from string path + config = ModelConfig.from_ui_selection( + model_path, + lora_path=None, # No LoRA for chat + is_lora=False + ) + + # Call existing load_model with config + return self.load_model( + config=config, + max_seq_length=max_seq_length, + dtype=None, # Auto-detect + load_in_4bit=load_in_4bit, + hf_token=hf_token + ) + + except Exception as e: + logger.error(f"Error in load_model_simple: {e}") + return False + + def add_local_model_to_dropdown(self, model_path: str): + """Add successfully loaded local model to dropdown storage""" + try: + from pathlib import Path + + path_obj = Path(model_path) + display_name = f"{path_obj.name}" + + # Check if already exists + for existing_display, existing_path in self.loaded_local_models: + if existing_path == model_path: + logger.debug(f"Local model already in dropdown: {model_path}") + return + + # Add to beginning of list + self.loaded_local_models.insert(0, (display_name, model_path)) + logger.info(f"Added local model to dropdown: {display_name} -> {model_path}") + + # Keep only last 5 + if len(self.loaded_local_models) > 5: + self.loaded_local_models.pop() + + except Exception as e: + logger.error(f"Error adding local model to dropdown: {e}") + + def get_model_dropdown_choices(self, models: list = None) -> list: + """Get model dropdown choices with status indicators""" + if models is None: + models = self.default_models + + try: + active_model = self.active_model_name + loading_model = self.get_loading_model() + + choices = [] + + # Add local models first + for local_display, local_path in self.loaded_local_models: + if local_path == active_model: + choices.append((f"{local_display} (Active)", local_path)) + else: + choices.append((local_display, local_path)) + + # Add default models + for model in models: + short_name = model.split('/')[-1] if '/' in model else model + + if model == active_model: + display_name = f"{short_name} (Active)" + elif model == loading_model: + display_name = f"{short_name} (Loading...)" + elif model in self.models and self.models[model].get("model"): + # Model is loaded in memory + display_name = f"{short_name} (Ready)" + # elif model in self.models: + # display_name = f"{short_name} (Ready)" + elif is_model_cached(model): + # Model is downloaded but not loaded + display_name = f"{short_name} (Cached)" + else: + display_name = f"↓ {short_name}" # Not downloaded + + choices.append((display_name, model)) + + return choices + + except Exception as e: + logger.error(f"Error getting model choices: {e}") + return [(model.split('/')[-1], model) for model in models] + + + def update_model_dropdown(self, models: list = None): + """Update model dropdown with current status""" + try: + import gradio as gr + + choices = self.get_model_dropdown_choices(models) + active_model = self.active_model_name + + # Set value to active model if exists + value = active_model if active_model else (choices[0][1] if choices else None) + + return gr.update(choices=choices, value=value) + + except Exception as e: + logger.error(f"Error updating model dropdown: {e}") + import gradio as gr + return gr.update() + + def load_model_simple(self, + model_path: str, + hf_token: Optional[str] = None, + max_seq_length: int = 2048, + load_in_4bit: bool = True) -> bool: + """ + Simple model loading wrapper for chat interface. + Accepts model path as string and handles ModelConfig creation internally. + + Args: + model_path: Model name or path (e.g., "unsloth/llama-3-8b") + hf_token: HuggingFace token for gated models + max_seq_length: Maximum sequence length + load_in_4bit: Whether to use 4-bit quantization + + Returns: + bool: True if successful, False otherwise + """ + try: + from backend.model_config import ModelConfig + + logger.info(f"load_model_simple called with: {model_path}") + + # Create config from string path + config = ModelConfig.from_ui_selection( + model_path, + lora_path=None, # No LoRA for chat + is_lora=False + ) + + logger.info(f"Created ModelConfig with identifier: {config.identifier}") + + # Call existing load_model with config + return self.load_model( + config=config, + max_seq_length=max_seq_length, + dtype=None, # Auto-detect + load_in_4bit=load_in_4bit, + hf_token=hf_token + ) + + except Exception as e: + logger.error(f"Error in load_model_simple: {e}") + import traceback + traceback.print_exc() + return False + +pass + + +# Global inference backend instance +inference_backend = InferenceBackend() + +def get_inference_backend() -> InferenceBackend: + return inference_backend diff --git a/backend/backend/model_config.py b/backend/backend/model_config.py new file mode 100644 index 0000000000..7beb8ba2a1 --- /dev/null +++ b/backend/backend/model_config.py @@ -0,0 +1,704 @@ +""" +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 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/path_utils.py b/backend/backend/path_utils.py new file mode 100644 index 0000000000..7743952b6b --- /dev/null +++ b/backend/backend/path_utils.py @@ -0,0 +1,78 @@ +""" +Path utilities for model and dataset handling +""" +import os +from pathlib import Path +from typing import Optional +import logging + +logger = logging.getLogger(__name__) + + +def normalize_path(path: str) -> str: + """ + Convert Windows paths to WSL format if needed. + + Examples: + C:\\Users\\... -> /mnt/c/Users/... + /home/user/... -> /home/user/... (unchanged) + """ + if not path: + return path + + # Handle Windows drive letters (C:\\ or c:\\) + if len(path) >= 3 and path[1] == ':' and path[2] in ('\\', '/'): + drive = path[0].lower() + rest = path[3:].replace('\\', '/') + return f'/mnt/{drive}/{rest}' + + # Already Unix-style or relative + return path.replace('\\', '/') +pass + +def is_local_path(path: str) -> bool: + """ + Check if path is a local filesystem path vs HuggingFace model identifier. + + Examples: + True: /home/user/model, C:\\models, ./model, ~/model + False: unsloth/llama-3.1-8b, microsoft/phi-2 + """ + if not path: + return False + + # Obvious HF patterns + if path.count('/') == 1 and not path.startswith(('/', '.', '~')): + return False # Looks like org/model format + + # Filesystem indicators + return ( + path.startswith(('/', '.', '~')) or # Unix absolute/relative + ':' in path or # Windows drive or URL + '\\' in path or # Windows separator + os.path.isabs(path) # System-absolute + ) +pass + +def get_cache_path(model_name: str) -> Optional[Path]: + """Get HuggingFace cache path for a model if it exists.""" + cache_dir = Path.home() / '.cache' / 'huggingface' / 'hub' + model_cache_name = model_name.replace("/", "--") + model_cache_path = cache_dir / f'models--{model_cache_name}' + + return model_cache_path if model_cache_path.exists() else None +pass + +def is_model_cached(model_name: str) -> bool: + """Check if model is downloaded in HuggingFace cache.""" + cache_path = get_cache_path(model_name) + if not cache_path: + return False + + # Check for actual model files + for suffix in ['.safetensors', '.bin', '.json']: + if list(cache_path.rglob(f'*{suffix}')): + return True + + return False +pass diff --git a/backend/backend/trainer.py b/backend/backend/trainer.py new file mode 100644 index 0000000000..a135f2a358 --- /dev/null +++ b/backend/backend/trainer.py @@ -0,0 +1,864 @@ +""" +Unsloth Training Backend +Integrates Unsloth training capabilities with the Gradio UI +""" +import torch +torch._dynamo.config.recompile_limit = 64 +from unsloth import FastLanguageModel, FastVisionModel, is_bfloat16_supported +from unsloth.chat_templates import get_chat_template + +import os +import json +import threading +import math +import logging +from typing import Optional, Callable +from dataclasses import dataclass +import pandas as pd +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 .dataset_utils import format_and_template_dataset +from .dataset_utils import MODEL_TO_TEMPLATE_MAPPER, TEMPLATE_TO_RESPONSES_MAPPER +from trl import SFTTrainer, SFTConfig + +# Import Unsloth trainers +#from unsloth_compiled_cache.UnslothSFTTrainer import _UnslothSFTTrainer as SFTTrainer + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +@dataclass +class TrainingProgress: + """Training progress tracking""" + epoch: int = 0 + step: int = 0 + total_steps: int = 0 + loss: float = 0.0 + learning_rate: float = 0.0 + is_training: bool = False + is_completed: bool = False + error: Optional[str] = None + status_message: str = "Ready to train" # Current stage message + +class UnslothTrainer: + """ + Unsloth Training Backend for Gradio UI Integration + """ + + def __init__(self): + self.model = None + self.tokenizer = None + self.trainer = None + self.training_thread = None + self.training_progress = TrainingProgress() + self.progress_callbacks = [] + self.is_training = False + self.should_stop = False + + # Model state tracking + self.is_vlm = False + self.model_name = None + + # Thread safety + self._lock = threading.Lock() + + # Store training context for later transfer + self.training_context = { + 'base_model_name': None, + 'output_dir': None, + 'is_lora': True, # Default to LoRA + } + + def add_progress_callback(self, callback: Callable[[TrainingProgress], None]): + """Add callback for training progress updates""" + self.progress_callbacks.append(callback) + + def _update_progress(self, **kwargs): + """Update training progress and notify callbacks""" + with self._lock: + for key, value in kwargs.items(): + if hasattr(self.training_progress, key): + setattr(self.training_progress, key, value) + + # Notify all callbacks + for callback in self.progress_callbacks: + try: + callback(self.training_progress) + except Exception as e: + logger.error(f"Error in progress callback: {e}") + + def load_model(self, + model_name: str, + max_seq_length: int = 2048, + load_in_4bit: bool = True, + hf_token: Optional[str] = None) -> bool: + """Load model for training (supports both text and vision models)""" + try: + print("\nClearing GPU memory before training...") + torch.cuda.empty_cache() + import gc + gc.collect() + + # Detect if this is a vision model first + self.is_vlm = is_vision_model(model_name) + self.model_name = model_name + + logger.info(f"Model type detected: {'Vision' if self.is_vlm else 'Text'}") + + # Reset training state for new run + self._update_progress( + is_training=True, + is_completed=False, + error=None, + step=0, + loss=0.0, + epoch=0 + ) + + # Update UI immediately with loading message + model_display = model_name.split('/')[-1] if '/' in model_name else model_name + self._update_progress( + status_message=f"Loading {'vision' if self.is_vlm else 'text'} model... {model_display}" + ) + + print(f"\nLoading {'vision' if self.is_vlm else 'text'} model: {model_name}") + + # Set HF token if provided + if hf_token: + os.environ["HF_TOKEN"] = hf_token + + + # Branch based on model type + if self.is_vlm: + # Load vision model - returns (model, tokenizer) + self.model, self.tokenizer = FastVisionModel.from_pretrained( + model_name=model_name, + max_seq_length=max_seq_length, + dtype=None, # Auto-detect + load_in_4bit=load_in_4bit, + token=hf_token, + ) + logger.info("Loaded vision model") + else: + # Load text model - returns (model, tokenizer) + self.model, self.tokenizer = FastLanguageModel.from_pretrained( + model_name=model_name, + max_seq_length=max_seq_length, + dtype=None, # Auto-detect + load_in_4bit=load_in_4bit, + token=hf_token, + ) + logger.info("Loaded text model") + + if self.should_stop: + return False + + self._update_progress(status_message="Model loaded successfully") + print("Model loaded successfully") + return True + + except Exception as e: + logger.error(f"Error loading model: {e}") + self._update_progress(error=str(e), is_training=False) + return False + + def prepare_model_for_training(self, + use_lora: bool = True, + # Vision-specific LoRA parameters (only used if is_vlm=True) + finetune_vision_layers: bool = True, + finetune_language_layers: bool = True, + finetune_attention_modules: bool = True, + finetune_mlp_modules: bool = True, + # Standard LoRA parameters + target_modules: list = None, + lora_r: int = 16, + lora_alpha: int = 16, + lora_dropout: float = 0.0, + use_gradient_checkpointing: str = "unsloth", + use_rslora: bool = False, + use_loftq: bool = False) -> bool: + """ + Prepare model for training (with optional LoRA). + """ + try: + if self.model is None: + raise ValueError("Model not loaded. Call load_model() first.") + + + # Full finetuning mode - skip PEFT entirely + if not use_lora: + self._update_progress(status_message="Full finetuning mode - no LoRA adapters") + print("Full finetuning mode - training all parameters\n") + return True + + # LoRA/QLoRA mode - apply PEFT + if target_modules is None or (isinstance(target_modules, list) and len(target_modules) == 0): + target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", + "gate_proj", "up_proj", "down_proj"] + + # Validate and normalize gradient_checkpointing + # Must be one of: True, False, or "unsloth" + if isinstance(use_gradient_checkpointing, str): + use_gradient_checkpointing = use_gradient_checkpointing.strip().lower() + if use_gradient_checkpointing == "" or use_gradient_checkpointing == "unsloth": + use_gradient_checkpointing = "unsloth" + elif use_gradient_checkpointing in ("true", "1", "yes"): + use_gradient_checkpointing = True + elif use_gradient_checkpointing in ("false", "0", "no"): + use_gradient_checkpointing = False + else: + # Invalid value, default to "unsloth" + logger.warning(f"Invalid gradient_checkpointing value: {use_gradient_checkpointing}, defaulting to 'unsloth'") + use_gradient_checkpointing = "unsloth" + elif use_gradient_checkpointing not in (True, False, "unsloth"): + # Invalid type or value, default to "unsloth" + logger.warning(f"Invalid gradient_checkpointing type/value: {use_gradient_checkpointing}, defaulting to 'unsloth'") + use_gradient_checkpointing = "unsloth" + + # Verify model is loaded + if self.model is None: + error_msg = "Model is None - model was not loaded properly" + logger.error(error_msg) + self._update_progress(error=error_msg) + return False + + # Check if model has the expected attributes + if not hasattr(self.model, 'config'): + error_msg = "Model does not have config attribute - model may not be loaded correctly" + logger.error(error_msg) + self._update_progress(error=error_msg) + return False + + print(f"Configuring LoRA adapters (r={lora_r}, alpha={lora_alpha})...\n") + print(f"Gradient checkpointing: {use_gradient_checkpointing} (type: {type(use_gradient_checkpointing).__name__})\n") + + # Branch based on vision vs text + if self.is_vlm: + # Vision model LoRA + print(f"Vision model LoRA configuration:") + print(f" - Finetune vision layers: {finetune_vision_layers}") + print(f" - Finetune language layers: {finetune_language_layers}") + print(f" - Finetune attention modules: {finetune_attention_modules}") + print(f" - Finetune MLP modules: {finetune_mlp_modules}\n") + + self.model = FastVisionModel.get_peft_model( + self.model, + finetune_vision_layers=finetune_vision_layers, + finetune_language_layers=finetune_language_layers, + finetune_attention_modules=finetune_attention_modules, + finetune_mlp_modules=finetune_mlp_modules, + r=lora_r, + target_modules=target_modules, + lora_alpha=lora_alpha, + lora_dropout=lora_dropout, + bias="none", + use_gradient_checkpointing=use_gradient_checkpointing, + random_state=3407, + use_rslora=use_rslora, + loftq_config={"loftq_bits": 4, "loftq_iter": 1} if use_loftq else None, + ) + else: + # Text model LoRA + print(f"Text model LoRA configuration:") + print(f" - Target modules: {target_modules}\n") + + self.model = FastLanguageModel.get_peft_model( + self.model, + r=lora_r, + target_modules=target_modules, + lora_alpha=lora_alpha, + lora_dropout=lora_dropout, + bias="none", + use_gradient_checkpointing=use_gradient_checkpointing, + random_state=3407, + use_rslora=use_rslora, + loftq_config={"loftq_bits": 4, "loftq_iter": 1} if use_loftq else None, + ) + + # Check if stopped during LoRA preparation + if self.should_stop: + print("Stopped during LoRA configuration\n") + return False + + self._update_progress(status_message="LoRA adapters configured") + print("LoRA adapters configured successfully\n") + return True + + except Exception as e: + import traceback + import sys + error_details = f"{type(e).__name__}: {str(e)}" if str(e) else f"{type(e).__name__} (no message)" + full_traceback = traceback.format_exc() + logger.error(f"Error preparing model: {error_details}") + logger.error(f"Full traceback:\n{full_traceback}") + print(f"\n[ERROR] Error preparing model: {error_details}", file=sys.stderr, flush=True) + print(f"[ERROR] Full traceback:\n{full_traceback}", file=sys.stderr, flush=True) + self._update_progress(error=error_details) + return False + + def load_and_format_dataset(self, + dataset_source: str, + format_type: str = "auto", + local_datasets: list = None) -> Optional[Dataset]: + """ + Load and prepare dataset for training + """ + try: + dataset = None + + if local_datasets: + # Load local datasets + all_data = [] + for dataset_file in local_datasets: + file_path = os.path.join("datasets", dataset_file) + if file_path.endswith('.json'): + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + if isinstance(data, list): + all_data.extend(data) + else: + all_data.append(data) + elif file_path.endswith('.csv'): + df = pd.read_csv(file_path) + all_data.extend(df.to_dict('records')) + + if all_data: + dataset = Dataset.from_list(all_data) + + # Check if stopped during dataset loading + if self.should_stop: + print("Stopped during dataset loading\n") + return None + + self._update_progress(status_message=f"Loaded {len(all_data)} samples from local files") + print(f"Loaded {len(all_data)} samples from local files\n") + + elif dataset_source: + # Load from Hugging Face + dataset = load_dataset(dataset_source, split="train") + + # Check if stopped during dataset loading + if self.should_stop: + print("Stopped during dataset loading\n") + return None + + self._update_progress(status_message=f"Loaded dataset from HuggingFace: {dataset_source}") + print(f"Loaded dataset from Hugging Face: {dataset_source}\n") + + if dataset is None: + raise ValueError("No dataset provided") + + # Check if stopped before applying template + if self.should_stop: + print("Stopped before applying chat template\n") + return None + + # NEW: Use unified format_and_template_dataset + print(f"Formatting dataset with format_type='{format_type}'...\n") + + #breakpoint() + dataset_info = format_and_template_dataset( + dataset, + model_name=self.model_name, + tokenizer=self.tokenizer, # Works for both text and vision models + is_vlm=self.is_vlm, + format_type=format_type, # "auto", "alpaca", "chatml", "sharegpt" + dataset_name=dataset_source, + ) + + # Check if stopped during formatting + if self.should_stop: + print("Stopped during dataset formatting\n") + return None + + self._update_progress(status_message=f"Dataset formatted and ready for training") + print(f"Dataset formatted successfully\n") + return dataset_info + + except Exception as e: + logger.error(f"Error loading dataset: {e}") + self._update_progress(error=str(e)) + return None + + def start_training(self, + dataset: Dataset, + output_dir: str = "./outputs", + num_epochs: int = 3, + learning_rate: float = 5e-5, + batch_size: int = 2, + gradient_accumulation_steps: int = 4, + warmup_steps: int = None, + warmup_ratio: float = None, + max_steps: int = 0, + save_steps: int = 0, + weight_decay: float = 0.01, + random_seed: int = 3407, + packing: bool = False, + train_on_completions: bool = False, + enable_wandb: bool = False, + wandb_project: str = "unsloth-training", + wandb_token: str = None, + enable_tensorboard: bool = False, + tensorboard_dir: str = "runs", + **kwargs) -> bool: + """Start training in a separate thread""" + + if self.is_training: + logger.warning("Training already in progress") + return False + + + if self.model is None or self.tokenizer is None: + self._update_progress(error="Model not loaded") + return False + + # Start training in separate thread + self.training_thread = threading.Thread( + target=self._train_worker, + args=(dataset,), + kwargs={ + 'output_dir': output_dir, + 'num_epochs': num_epochs, + 'learning_rate': learning_rate, + 'batch_size': batch_size, + 'gradient_accumulation_steps': gradient_accumulation_steps, + 'warmup_steps': warmup_steps, + 'warmup_ratio': warmup_ratio, + 'max_steps': max_steps, + 'save_steps': save_steps, + 'weight_decay': weight_decay, + 'random_seed': random_seed, + 'packing': packing, + 'train_on_completions': train_on_completions, + 'enable_wandb': enable_wandb, + 'wandb_project': wandb_project, + 'wandb_token': wandb_token, + 'enable_tensorboard': enable_tensorboard, + 'tensorboard_dir': tensorboard_dir, + **kwargs + } + ) + + self.should_stop = False + self.training_thread.start() + return True + + def _train_worker(self, dataset: Dataset, **training_args): + """Worker function for training (runs in separate thread)""" + try: + self._update_progress(is_training=True, error=None) + + # Setup logging + if training_args.get('enable_wandb', False) and training_args.get('wandb_token'): + os.environ["WANDB_API_KEY"] = training_args['wandb_token'] + import wandb + wandb.init(project=training_args.get('wandb_project', 'unsloth-training')) + + # Create output directory + output_dir = training_args.get('output_dir', './outputs') + os.makedirs(output_dir, exist_ok=True) + + # ========== DATA COLLATOR SELECTION ========== + # Detect special model types + model_name_lower = self.model_name.lower() + is_deepseek_ocr = "deepseek" in model_name_lower and "ocr" in model_name_lower + + print("Configuring data collator...\n") + + data_collator = None # Default to built-in data collator + if is_deepseek_ocr: + # Special DeepSeek OCR collator - auto-install if needed + print("Detected DeepSeek OCR model\n") + # Ensure DeepSeek OCR module is installed + if not _ensure_deepseek_ocr_installed(): + error_msg = ( + "Failed to install DeepSeek OCR module. " + "Please install manually: " + "from huggingface_hub import snapshot_download; " + "snapshot_download('unsloth/DeepSeek-OCR', local_dir='deepseek_ocr')" + ) + logger.error(error_msg) + self._update_progress(error=error_msg, is_training=False) + return + + try: + from backend.data_utils import DeepSeekOCRDataCollator + + print("Configuring DeepSeek OCR data collator...\n") + FastVisionModel.for_training(self.model) + data_collator = DeepSeekOCRDataCollator( + tokenizer=self.tokenizer, + model=self.model, + image_size=640, + base_size=1024, + crop_mode=True, + train_on_responses_only=training_args.get('train_on_completions', False), + ) + print("DeepSeek OCR data collator configured successfully\n") + + except Exception as e: + logger.error(f"Failed to configure DeepSeek OCR collator: {e}") + error_msg = f"Error configuring DeepSeek OCR: {str(e)}" + self._update_progress(error=error_msg, is_training=False) + return + + elif self.is_vlm: + # Standard VLM collator + print("Using UnslothVisionDataCollator for vision model\n") + from unsloth.trainer import UnslothVisionDataCollator + + FastVisionModel.for_training(self.model) + data_collator = UnslothVisionDataCollator(self.model, self.tokenizer) + print("Vision data collator configured\n") + + # ========== TRAINING CONFIGURATION ========== + # Handle epochs vs max_steps properly + max_steps_val = training_args.get('max_steps', 0) + num_epochs_val = training_args.get('num_epochs', 3) + + # Handle warmup_steps vs warmup_ratio + warmup_steps_val = training_args.get('warmup_steps', None) + warmup_ratio_val = training_args.get('warmup_ratio', None) + + config_args = { + "per_device_train_batch_size": training_args.get('batch_size', 2), + "gradient_accumulation_steps": training_args.get('gradient_accumulation_steps', 4), + "num_train_epochs": training_args.get('num_epochs', 3), # Default to epochs + "learning_rate": training_args.get('learning_rate', 2e-4), + "fp16": not is_bfloat16_supported(), + "bf16": is_bfloat16_supported(), + "logging_steps": 1, + "weight_decay": training_args.get('weight_decay', 0.01), + "seed": training_args.get('random_seed', 3407), + "output_dir": output_dir, + "report_to": ["wandb"] if training_args.get('enable_wandb', False) else "none", + } + + # Add warmup parameter - use warmup_ratio if provided, otherwise warmup_steps + if warmup_ratio_val is not None: + config_args["warmup_ratio"] = warmup_ratio_val + print(f"Using warmup_ratio: {warmup_ratio_val}\n") + elif warmup_steps_val is not None: + config_args["warmup_steps"] = warmup_steps_val + print(f"Using warmup_steps: {warmup_steps_val}\n") + else: + # Default to warmup_steps if neither provided + config_args["warmup_steps"] = 5 + print(f"Using default warmup_steps: 5\n") + + # If max_steps is specified, use it instead of epochs + max_steps_val = training_args.get('max_steps', 0) + if max_steps_val and max_steps_val > 0: + del config_args["num_train_epochs"] # Remove epochs + config_args["max_steps"] = max_steps_val # Use steps instead + print(f"Training for {max_steps_val} steps\n") + else: + print(f"Training for {config_args['num_train_epochs']} epochs\n") + + # Add model-specific parameters + # Use optim and lr_scheduler_type from training_args if provided, otherwise use defaults + optim_value = training_args.get('optim', "adamw_8bit") + lr_scheduler_type_value = training_args.get('lr_scheduler_type', "linear") + + if self.is_vlm: + # Vision-specific config + print("Configuring vision model training parameters\n") + # Use provided values or defaults for vision models + optim_value = training_args.get('optim', "adamw_torch_fused") + lr_scheduler_type_value = training_args.get('lr_scheduler_type', "cosine") + config_args.update({ + "optim": optim_value, + "lr_scheduler_type": lr_scheduler_type_value, + "gradient_checkpointing": True, + "gradient_checkpointing_kwargs": {"use_reentrant": False}, + "max_grad_norm": 0.3, # Recommended for vision models + "remove_unused_columns": False, + "dataset_text_field": "", + "dataset_kwargs": {"skip_prepare_dataset": True}, + "max_length": training_args.get('max_seq_length', 2048), + }) + else: + print("Configuring text model training parameters\n") + config_args.update({ + "optim": optim_value, + "lr_scheduler_type": lr_scheduler_type_value, + "dataset_text_field": "text", + }) + + # Only add packing for text models (not DeepSeek OCR which is VLM) + if not is_deepseek_ocr: + packing_enabled = training_args.get('packing', False) + config_args["packing"] = packing_enabled + print(f"Sequence packing: {'enabled' if packing_enabled else 'disabled'}\n") + + print(f"The configuration is: {config_args}") + + print("Training configuration prepared\n") + # ========== TRAINER INITIALIZATION ========== + if self.is_vlm: + self.trainer = SFTTrainer( + model=self.model, + train_dataset=dataset['dataset'], + processing_class = self.tokenizer.tokenizer, + data_collator=data_collator, + args=SFTConfig(**config_args), + ) + else: + self.trainer = SFTTrainer( + model=self.model, + tokenizer=self.tokenizer, + train_dataset=dataset['dataset'], + data_collator=data_collator, + args=SFTConfig(**config_args), + ) + print("Trainer initialized\n") + + # ========== TRAIN ON RESPONSES ONLY ========== + # Determine if we should train on responses only + instruction_part = None + response_part = None + train_on_responses_enabled = training_args.get('train_on_completions', False) + + # DeepSeek OCR handles this internally in its collator, so skip + if train_on_responses_enabled and not (is_deepseek_ocr or dataset["final_format"].lower() == 'alpaca'): + try: + print("Configuring train on responses only...\n") + + # Get the template mapping for this model + model_name_lower = self.model_name.lower() + + if model_name_lower in MODEL_TO_TEMPLATE_MAPPER: + template_name = MODEL_TO_TEMPLATE_MAPPER[model_name_lower] + print(f"Detected template: {template_name}\n") + + if template_name in TEMPLATE_TO_RESPONSES_MAPPER: + instruction_part = TEMPLATE_TO_RESPONSES_MAPPER[template_name]["instruction"] + response_part = TEMPLATE_TO_RESPONSES_MAPPER[template_name]["response"] + + print(f"Instruction marker: {instruction_part[:50]}...\n") + print(f"Response marker: {response_part[:50]}...\n") + else: + print(f"No response mapping found for template: {template_name}\n") + train_on_responses_enabled = False + else: + print(f"No template mapping found for model: {self.model_name}\n") + train_on_responses_enabled = False + + except Exception as e: + logger.warning(f"Could not configure train on responses: {e}") + train_on_responses_enabled = False + + # Apply train on responses only if we have valid parts + if train_on_responses_enabled and instruction_part and response_part and not (is_deepseek_ocr or dataset["final_format"].lower() == 'alpaca'): + try: + from unsloth.chat_templates import train_on_responses_only + + self.trainer = train_on_responses_only( + self.trainer, + instruction_part=instruction_part, + response_part=response_part, + ) + print("Train on responses only configured successfully\n") + except Exception as e: + logger.warning(f"Failed to apply train on responses only: {e}") + train_on_responses_enabled = False + else: + if train_on_responses_enabled and is_deepseek_ocr: + print("Train on responses handled by DeepSeek OCR collator\n") + else: + print("Training on full sequences (including prompts)\n") + + # Add custom callback for progress tracking + from transformers import TrainerCallback + + class ProgressCallback(TrainerCallback): + def __init__(self, trainer_instance): + self.trainer_instance = trainer_instance + + def on_train_begin(self, args, state, control, **kwargs): + """Called at the beginning of training""" + pass + + def on_log(self, args, state, control, logs=None, **kwargs): + """Called when logging occurs""" + if logs: + # Get loss from either 'loss' or 'train_loss' key + loss_value = logs.get('loss', logs.get('train_loss', 0.0)) + self.trainer_instance._update_progress( + step=state.global_step, + epoch=round(state.epoch, 2) if state.epoch else 0, # Round epoch to 2 decimals + loss=loss_value, + learning_rate=logs.get('learning_rate', 0.0), + status_message="" # Clear status message so metrics show + ) + + def on_epoch_end(self, args, state, control, **kwargs): + """Called at the end of each epoch""" + self.trainer_instance._update_progress( + epoch=state.epoch, + step=state.global_step + ) + + def on_step_end(self, args, state, control, **kwargs): + """Called at the end of each step""" + # Check if we should stop training + if self.trainer_instance.should_stop: + print(f"Stop detected at step {state.global_step}\n") + control.should_training_stop = True + return control + + # ========== PROGRESS TRACKING ========== + progress_callback = ProgressCallback(self) + self.trainer.add_callback(progress_callback) + + num_samples = len(dataset["dataset"]) + batch_size = training_args.get('batch_size', 2) + grad_accum = training_args.get('gradient_accumulation_steps', 4) + num_epochs = training_args.get('num_epochs', 3) + max_steps_val = training_args.get('max_steps', 0) + + # Step 1: Calculate dataloader length (number of batches) + len_dataloader = math.ceil(num_samples / batch_size) + + # Step 2: Calculate steps per epoch (following transformers logic) + num_update_steps_per_epoch = max( + len_dataloader // grad_accum + int(len_dataloader % grad_accum > 0), + 1 + ) + + # Step 3: Determine total steps based on max_steps or epochs + if max_steps_val and max_steps_val > 0: + # Use max_steps if specified + total_steps = max_steps_val + print(f"Progress tracking: {total_steps} steps (max_steps)\n") + else: + # Calculate from epochs + total_steps = num_update_steps_per_epoch * num_epochs + print(f"Progress tracking: {total_steps} steps ({num_epochs} epochs × {num_update_steps_per_epoch} steps/epoch)\n") + + self._update_progress(total_steps=total_steps) + + # ========== START TRAINING ========== + self._update_progress(status_message="Starting training...") + print("Starting training...\n") + self.trainer.train() + + # ========== SAVE MODEL ========== + self.trainer.save_model() + self.tokenizer.save_pretrained(output_dir) + print(f"\nTraining completed! Model saved to {output_dir}\n") + + self._update_progress( + is_training=False, + is_completed=True, + #status_message=status_msg + status_message=f"Training completed! Model saved to {output_dir}", + ) + + except Exception as e: + logger.error(f"Training error: {e}") + self._update_progress(is_training=False, error=str(e)) + + finally: + self.is_training = False + + def stop_training(self): + """Stop ongoing training""" + print("\nStopping training...") + self.should_stop = True + self.is_training = False + # Clear the status message so timer doesn't show stale status + self._update_progress(is_training=False, status_message="") + + # If trainer exists, try to stop it gracefully + if self.trainer: + try: + # The callback will catch should_stop flag and stop the training loop + print("Training will stop at next step...\n") + except Exception as e: + logger.error(f"Error stopping trainer: {e}") + + def get_training_progress(self) -> TrainingProgress: + """Get current training progress""" + with self._lock: + return self.training_progress + + def cleanup(self): + """Cleanup resources""" + if self.trainer: + self.trainer = None + if self.model: + self.model = None + if self.tokenizer: + self.tokenizer = None + + # Clear GPU memory + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + +def _ensure_deepseek_ocr_installed(): + """ + Auto-install DeepSeek OCR module if not available. + Downloads from HuggingFace hub as a local module. + + Returns: + bool: True if available (either already installed or just installed) + """ + try: + # Try importing to see if already available + from deepseek_ocr.modeling_deepseekocr import format_messages + logger.info("DeepSeek OCR module already available") + return True + except ImportError: + pass + + try: + logger.info("DeepSeek OCR module not found. Auto-installing from HuggingFace...") + print("\n Downloading DeepSeek OCR module from HuggingFace...\n") + + from huggingface_hub import snapshot_download + import sys + import os + + # Get the script directory to install locally + script_dir = os.path.dirname(os.path.abspath(__file__)) + parent_dir = os.path.dirname(script_dir) # Go up to project root + + # Download to project root as 'deepseek_ocr' folder + local_dir = os.path.join(parent_dir, "deepseek_ocr") + + snapshot_download( + "unsloth/DeepSeek-OCR", + local_dir=local_dir, + local_dir_use_symlinks=False + ) + + # Add to sys.path if not already there + if parent_dir not in sys.path: + sys.path.insert(0, parent_dir) + + # Try importing again + from deepseek_ocr.modeling_deepseekocr import format_messages + + logger.info("DeepSeek OCR module installed successfully") + print("DeepSeek OCR module installed successfully!\n") + return True + + except Exception as e: + logger.error(f"Failed to install DeepSeek OCR module: {e}") + print(f"\n❌ Failed to install DeepSeek OCR module: {e}\n") + return False + +# Global trainer instance +_trainer_instance = None + +def get_trainer() -> UnslothTrainer: + """Get global trainer instance""" + global _trainer_instance + if _trainer_instance is None: + _trainer_instance = UnslothTrainer() + return _trainer_instance diff --git a/backend/backend/training.py b/backend/backend/training.py new file mode 100644 index 0000000000..ad05c19868 --- /dev/null +++ b/backend/backend/training.py @@ -0,0 +1,680 @@ +""" +Training backend and UI integration +""" +import gradio as gr +import matplotlib.pyplot as plt +from typing import Dict, Any, Generator, Tuple +import logging + +from .trainer import get_trainer, TrainingProgress + +logger = logging.getLogger(__name__) + +# Plot styling constants +PLOT_WIDTH = 8 # Inches +PLOT_HEIGHT = 3.5 # Inches + + +class TrainingBackend: + """ + Training orchestration and UI integration. + Handles both text and vision models, LoRA and full finetuning. + """ + + def __init__(self): + self.trainer = get_trainer() + + # Training Metrics + self.loss_history = [] + self.lr_history = [] + self.step_history = [] + self.current_theme = "light" + + self.trainer.add_progress_callback(self._on_progress_update) + + logger.info("TrainingBackend initialized") + + def _on_progress_update(self, progress: TrainingProgress): + """Callback for progress updates""" + if progress.step > 0 and progress.loss > 0: + self.loss_history.append(progress.loss) + self.lr_history.append(progress.learning_rate) + self.step_history.append(progress.step) + + def start_training(self, + # Model parameters + model_name: str, + training_type: str, # NEW: "LoRA/QLoRA" or "Full Finetuning" + hf_token: str, + load_in_4bit: bool, + max_seq_length: int, + + # Dataset parameters + hf_dataset: str, + local_datasets: list, + format_type: str, # CHANGED: was data_template + + # Training parameters + num_epochs: int, + learning_rate: str, + batch_size: int, + gradient_accumulation_steps: int, + warmup_steps: int, # May be None even without default + warmup_ratio: float, # May be None even without default + max_steps: int, + save_steps: int, + weight_decay: float, + random_seed: int, + packing: bool, + + # LoRA parameters + use_lora: bool, # Should be derived from training_type + lora_r: int, + lora_alpha: int, + lora_dropout: float, + target_modules: list, + gradient_checkpointing: str, + use_rslora: bool, + use_loftq: bool, + train_on_completions: bool, + + # NEW: Vision-specific LoRA parameters + finetune_vision_layers: bool, + finetune_language_layers: bool, + finetune_attention_modules: bool, + finetune_mlp_modules: bool, + + # Logging parameters + enable_wandb: bool, + wandb_token: str, + wandb_project: str, + enable_tensorboard: bool, + tensorboard_dir: str, + optim: str = "adamw_8bit", + lr_scheduler_type: str = "linear") -> Generator[Tuple, None, None]: + """ + Start training - yields UI updates as generator. + + Yields: + Tuple of (start_btn_update, stop_btn_update, progress_visible, config_visible) + """ + try: + # Reset stop flag and clear history + self.trainer.should_stop = False + self.loss_history = [] + self.lr_history = [] + self.step_history = [] + import time + output_dir = f"./outputs/{model_name.replace('/', '_')}_{int(time.time())}" + + # NEW: Derive use_lora from training_type + use_lora_actual = (training_type == "LoRA/QLoRA") + if use_lora_actual: print("using Lora") + else: print("using full finetuning") + logger.info(f"Starting training - Type: {training_type}, Model: {model_name}") + + # Yield initial status - buttons toggle immediately + yield ( + gr.update(interactive=False), # Start button disabled + gr.update(interactive=True), # Stop button enabled + gr.update(visible=True), # Training progress visible + #gr.update(visible=False) # Config selection hidden + ) + + # ========== LOAD MODEL ========== + logger.info("Loading model...") + success = self.trainer.load_model( + model_name=model_name, + max_seq_length=max_seq_length, + load_in_4bit=load_in_4bit if use_lora_actual else False, # Only 4bit for LoRA + hf_token=hf_token if hf_token.strip() else None + ) + + if not success or self.trainer.should_stop: + logger.error("Failed to load model or stopped by user") + return + + # Capture if this is a vision model + #self.current_training_session['is_vlm'] = self.trainer.is_vlm + + yield ( + gr.update(interactive=False), + gr.update(interactive=True), + gr.update(visible=True), + #gr.update(visible=False) + ) + + # ========== PREPARE MODEL FOR TRAINING ========== + if use_lora_actual: + logger.info("Preparing model with LoRA...") + success = self.trainer.prepare_model_for_training( + use_lora=True, + # Vision-specific parameters + finetune_vision_layers=finetune_vision_layers, + finetune_language_layers=finetune_language_layers, + finetune_attention_modules=finetune_attention_modules, + finetune_mlp_modules=finetune_mlp_modules, + # Standard LoRA parameters + target_modules=target_modules, + lora_r=lora_r, + lora_alpha=lora_alpha, + lora_dropout=lora_dropout, + use_gradient_checkpointing=gradient_checkpointing, + use_rslora=use_rslora, + use_loftq=use_loftq + ) + else: + logger.info("Preparing model for full finetuning...") + success = self.trainer.prepare_model_for_training( + use_lora=False # Full finetuning + ) + + if not success or self.trainer.should_stop: + logger.error("Failed to prepare model or stopped by user") + return + + yield ( + gr.update(interactive=False), + gr.update(interactive=True), + gr.update(visible=True), + #gr.update(visible=False) + ) + + # ========== LOAD DATASET ========== + logger.info("Loading dataset...") + #breakpoint() + dataset = self.trainer.load_and_format_dataset( + dataset_source=hf_dataset if hf_dataset.strip() else None, + format_type=format_type, + local_datasets=local_datasets if local_datasets else None + ) + + if dataset is None or self.trainer.should_stop: + logger.error("Failed to load dataset or stopped by user") + return + + yield ( + gr.update(interactive=False), + gr.update(interactive=True), + gr.update(visible=True), + #gr.update(visible=False) + ) + + # ========== START TRAINING ========== + # Convert learning rate string to float + try: + lr_value = float(learning_rate) + except ValueError: + logger.error(f"Invalid learning rate: {learning_rate}") + self.trainer._update_progress( + error=f"Invalid learning rate: {learning_rate}", + is_training=False + ) + return + + logger.info("Starting training worker thread...") + success = self.trainer.start_training( + dataset=dataset, + #output_dir=f"./outputs/{model_name.replace('/', '_')}_{int(__import__('time').time())}", + output_dir=output_dir, + num_epochs=num_epochs, + learning_rate=lr_value, + batch_size=batch_size, + gradient_accumulation_steps=gradient_accumulation_steps, + warmup_steps=warmup_steps, + warmup_ratio=warmup_ratio, + max_steps=max_steps if max_steps > 0 else 0, + save_steps=save_steps if save_steps > 0 else 0, + weight_decay=weight_decay, + random_seed=random_seed, + packing=packing, + train_on_completions=train_on_completions, + enable_wandb=enable_wandb, + wandb_project=wandb_project, + wandb_token=wandb_token if wandb_token.strip() else None, + enable_tensorboard=enable_tensorboard, + tensorboard_dir=tensorboard_dir, + max_seq_length=max_seq_length, # Pass through for config + optim=optim, + lr_scheduler_type=lr_scheduler_type, + ) + + if not success: + logger.error("Failed to start training") + yield ( + gr.update(interactive=True), + gr.update(interactive=False), + gr.update(visible=False), + #gr.update(visible=True) + ) + + except Exception as e: + logger.error(f"Error in start_training: {e}", exc_info=True) + self.trainer._update_progress( + error=str(e), + is_training=False + ) + yield ( + gr.update(interactive=True), + gr.update(interactive=False), + gr.update(visible=False), + #gr.update(visible=True) + ) + + def stop_training(self) -> Tuple: + """ + Stop ongoing training. + + Returns: + Tuple of (start_btn_update, stop_btn_update, progress_visible, config_visible) + """ + try: + logger.info("Stopping training...") + self.trainer.stop_training() + + return ( + gr.update(interactive=True), # Start button enabled + gr.update(interactive=False), # Stop button disabled + gr.update(visible=False), # Training progress hidden + #gr.update(visible=True) # Config selection visible + ) + except Exception as e: + logger.error(f"Error stopping training: {e}") + return ( + gr.update(interactive=True), + gr.update(interactive=False), + gr.update(visible=False), + #gr.update(visible=True) + ) + + def get_training_status(self, theme: str = "light") -> Tuple[plt.Figure, gr.update, gr.update, gr.update]: + """ + Get current training status and loss plot. + + Args: + theme: "light" or "dark" for plot styling + + Returns: + Tuple of (plot, start_btn, stop_btn, progress_visible) + """ + + try: + progress = self.trainer.get_training_progress() + + # If not training and not completed, return no updates + if not (progress.is_training or progress.is_completed or progress.error): + return (None, gr.update(), gr.update(), gr.update()) + + # Generate plot + plot = self._create_loss_plot(progress, theme) + + # If completed or error, enable start button + if progress.is_completed or progress.error: + return ( + plot, + gr.update(interactive=True), # Start button enabled + gr.update(interactive=False), # Stop button disabled + gr.update(visible=True), # Training progress visible + ) + + # Still training - no button updates + return (plot, gr.update(), gr.update(), gr.update()) + + except Exception as e: + logger.error(f"Error getting training status: {e}") + return (None, gr.update(), gr.update(), gr.update()) + + def refresh_plot_for_theme(self, theme: str) -> plt.Figure: + """ + Refresh plot with new theme. + + Args: + theme: "light" or "dark" + + Returns: + Updated matplotlib figure + """ + if theme and isinstance(theme, str) and theme in ['light', 'dark']: + self.current_theme = theme + + # Always generate plot if we have loss history + if self.loss_history: + progress = self.trainer.get_training_progress() + return self._create_loss_plot(progress, self.current_theme) + + return None + + def is_training_active(self) -> bool: + """ + Check if training is currently active (from load_model start to completion/error). + + Returns: + True if training is in progress, False otherwise + """ + try: + progress = self.trainer.get_training_progress() + # Training is active if is_training is True + # Also check if we're in loading/preparation phase (status_message indicates activity) + is_active = progress.is_training + # Also consider it active if we have a status message indicating loading/preparation + # but haven't completed or errored yet + if not is_active and not progress.is_completed and not progress.error: + status = progress.status_message or "" + if any(keyword in status.lower() for keyword in ["loading", "preparing", "training"]): + is_active = True + return is_active + except Exception as e: + logger.error(f"Error checking training state: {e}") + return False + + def _create_loss_plot(self, progress: TrainingProgress, theme: str = "light") -> plt.Figure: + """ + Create training loss plot with theme-aware styling. + + Args: + progress: Current training progress + theme: "light" or "dark" + + Returns: + Matplotlib figure + """ + plt.close('all') + + # Theme-specific styling + LIGHT_STYLE = { + "facecolor": "#ffffff", + "grid_color": "#d1d5db", + "line": "#16b88a", + "text": "#1f2937", + "empty_text": "#6b7280" + } + DARK_STYLE = { + "facecolor": "#292929", + "grid_color": "#404040", + "line": "#4ade80", + "text": "#e5e7eb", + "empty_text": "#9ca3af" + } + + style = LIGHT_STYLE if theme == "light" else DARK_STYLE + + fig, ax = plt.subplots(figsize=(PLOT_WIDTH, PLOT_HEIGHT)) + fig.patch.set_facecolor(style["facecolor"]) + ax.set_facecolor(style["facecolor"]) + + if self.loss_history: + steps = self.step_history + losses = self.loss_history + scatter_color = "#60a5fa" + # Scatter plot for raw loss points + ax.scatter( + steps, + losses, + s=16, + alpha=0.6, + color=scatter_color, + linewidths=0, + label="Training Loss (raw)", + ) + + # Moving average line overlay (trailing window) + MA_WINDOW = 20 # adjust smoothing aggressiveness + window = min(MA_WINDOW, len(losses)) + + if window >= 2: + cumsum = [0.0] + for v in losses: + cumsum.append(cumsum[-1] + float(v)) + + ma = [] + for i in range(len(losses)): + start = max(0, i - window + 1) + denom = i - start + 1 + ma.append((cumsum[i + 1] - cumsum[start]) / denom) + + ax.plot( + steps, + ma, + color=style["line"], + linewidth=2.5, + alpha=0.95, + label=f"Moving Avg ({ma[-1]:.4f})", + ) + + leg = ax.legend(frameon=False, fontsize=9) + for t in leg.get_texts(): + t.set_color(style["text"]) + + ax.set_xlabel('Steps', fontsize=10, color=style["text"]) + ax.set_ylabel('Loss', fontsize=10, color=style["text"]) + + # Build status message for title + if progress.error: + title = f"Error: {progress.error}" + elif progress.is_completed: + title = f"Training completed! Final loss: {progress.loss:.4f}" + elif progress.status_message: + title = progress.status_message + elif progress.step > 0: + title = f"Epoch: {progress.epoch} | Step: {progress.step}/{progress.total_steps} | Loss: {progress.loss:.4f}" + else: + title = "Training Loss" + + ax.set_title(title, fontsize=11, fontweight='bold', + pad=10, color=style["text"]) + + # Style grid and spines + ax.grid(True, alpha=0.4, linestyle='--', color=style["grid_color"]) + ax.tick_params(colors=style["text"], which='both') + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.spines['bottom'].set_color(style["text"]) + ax.spines['left'].set_color(style["text"]) + else: + display_msg = progress.status_message if progress.status_message else 'Waiting for training data...' + ax.text(0.5, 0.5, display_msg, + ha='center', va='center', fontsize=16, + color=style["empty_text"], + transform=ax.transAxes) + ax.set_xticks([]) + ax.set_yticks([]) + for spine in ax.spines.values(): + spine.set_visible(False) + + fig.tight_layout() + return fig + + def _transfer_to_inference_backend(self) -> bool: + """ + Transfer the trained model to InferenceBackend. + Called automatically when training completes. + """ + print("=" * 60) + print("DEBUG: _transfer_to_inference_backend() CALLED") + print("=" * 60) + + try: + from .inference import get_inference_backend + + session = self.current_training_session + + # Check if already transferred + if session.get('transferred', False): + print("DEBUG: Already transferred, returning True") + logger.info("Model already transferred, skipping") + return True + + # Validate session data + if not session.get('base_model_name') or not session.get('output_dir'): + logger.warning("Training session incomplete, cannot transfer") + logger.warning(f"Session data: {session}") + return False + + inference_backend = get_inference_backend() + + base_model_name = session['base_model_name'] + output_dir = session['output_dir'] + is_lora = session['is_lora'] + is_vlm = session['is_vlm'] + + logger.info(f"=" * 60) + logger.info(f"TRANSFERRING MODEL TO INFERENCE BACKEND") + logger.info(f"=" * 60) + logger.info(f" Base model: {base_model_name}") + logger.info(f" Output dir: {output_dir}") + logger.info(f" Is LoRA: {is_lora}") + logger.info(f" Is VLM: {is_vlm}") + + # Transfer the model object directly from trainer memory. + # If is_lora is True, self.trainer.model is a PeftModel (Base + Adapter). + # If is_lora is False, it is the finetuned Base Model. + inference_backend.models[base_model_name] = { + "model": self.trainer.model, + "tokenizer": self.trainer.tokenizer, + "is_vision": is_vlm, + "is_lora": is_lora, + "model_path": base_model_name, + "base_model": None, + "loaded_adapters": {}, + # Unsloth/PEFT training keeps the active adapter named 'default' in memory + "active_adapter": "default" if is_lora else None, + } + + # For vision models, also transfer processor + if is_vlm: + if hasattr(self.trainer, 'tokenizer'): + inference_backend.models[base_model_name]["processor"] = self.trainer.tokenizer + logger.info(" Transferred processor for vision model") + + # Load chat template info + inference_backend._load_chat_template_info(base_model_name) + + # If it was LoRA, register the output path. + # This ensures the Eval UI dropdown (which lists files) knows that + # the model currently in memory corresponds to this specific output directory. + if is_lora: + inference_backend.models[base_model_name]["last_trained_adapter"] = output_dir + logger.info(f"Marked trained LoRA adapter: {output_dir}") + + # Set as active model + inference_backend.active_model_name = base_model_name + logger.info(f"Set active model: {base_model_name}") + + return True + + except Exception as e: + logger.error(f"Error transferring model to inference backend: {e}") + import traceback + traceback.print_exc() + return False + + +# ========== GLOBAL INSTANCE ========== +_training_backend = None + +def get_training_backend() -> TrainingBackend: + """Get global training backend instance""" + global _training_backend + if _training_backend is None: + _training_backend = TrainingBackend() + return _training_backend + + +# ========== UI HANDLER CREATION ========== +def create_training_handlers(train_components: Dict[str, Any]) -> Dict[str, Any]: + """ + Create training event handlers for Gradio UI components. + + Args: + train_components: Dictionary of Gradio components from train page + + Returns: + Dictionary of handler functions + """ + backend = get_training_backend() + + def start_training_handler(*args): + """Handler for start training button - yields status updates""" + try: + # Extract parameters in the order they're passed from the UI + (model_name, training_type, hf_token, load_4bit, max_seq_length, + hf_dataset, local_datasets, format_type, + num_epochs, learning_rate, batch_size, gradient_accumulation_steps, + warmup_steps, warmup_ratio, max_steps, save_steps, weight_decay, random_seed, packing, + use_lora, lora_r, lora_alpha, lora_dropout, target_modules, + gradient_checkpointing, use_rslora, use_loftq, train_on_completions, + finetune_vision_layers, finetune_language_layers, + finetune_attention_modules, finetune_mlp_modules, + enable_wandb, wandb_token, wandb_project, + enable_tensorboard, tensorboard_dir, optim, lr_scheduler_type) = args + + # Start training with correctly named parameters - this is a generator + for update_tuple in backend.start_training( + model_name=model_name, + training_type=training_type, + hf_token=hf_token, + load_in_4bit=load_4bit, + max_seq_length=max_seq_length, + hf_dataset=hf_dataset, + local_datasets=local_datasets, + format_type=format_type, + num_epochs=num_epochs, + learning_rate=learning_rate, + batch_size=batch_size, + gradient_accumulation_steps=gradient_accumulation_steps, + warmup_steps=warmup_steps, + warmup_ratio=warmup_ratio, + max_steps=max_steps, + save_steps=save_steps, + weight_decay=weight_decay, + random_seed=random_seed, + packing=packing, + use_lora=use_lora, + lora_r=lora_r, + lora_alpha=lora_alpha, + lora_dropout=lora_dropout, + target_modules=target_modules, + gradient_checkpointing=gradient_checkpointing, + use_rslora=use_rslora, + use_loftq=use_loftq, + train_on_completions=train_on_completions, + finetune_vision_layers=finetune_vision_layers, + finetune_language_layers=finetune_language_layers, + finetune_attention_modules=finetune_attention_modules, + finetune_mlp_modules=finetune_mlp_modules, + enable_wandb=enable_wandb, + wandb_token=wandb_token, + wandb_project=wandb_project, + enable_tensorboard=enable_tensorboard, + tensorboard_dir=tensorboard_dir + ): + # Yield each status update to Gradio + yield update_tuple + + except Exception as e: + logger.error(f"Error in start_training_handler: {e}", exc_info=True) + yield ( + gr.update(interactive=True), # Start button + gr.update(interactive=False), # Stop button + gr.update(visible=False), # Training progress + #gr.update(visible=True) # Config selection + ) + + def stop_training_handler(): + """Handler for stop training button""" + return backend.stop_training() + + def update_training_status(): + """Periodic update of training status and plot""" + return backend.get_training_status(backend.current_theme) + + def refresh_plot_for_theme(theme): + """Refresh plot with new theme""" + return backend.refresh_plot_for_theme(theme) + + return { + 'start_training': start_training_handler, + 'stop_training': stop_training_handler, + 'update_status': update_training_status, + 'refresh_plot': refresh_plot_for_theme + } diff --git a/backend/backend/utils.py b/backend/backend/utils.py new file mode 100644 index 0000000000..74d5c05abf --- /dev/null +++ b/backend/backend/utils.py @@ -0,0 +1,208 @@ +""" +Shared backend utilities +""" +import gradio as gr +import os +import logging +from contextlib import contextmanager +from pathlib import Path +from typing import Optional, Dict, Any +import shutil +import tempfile + + +logger = logging.getLogger(__name__) + +@contextmanager +def without_hf_auth(): + """ + Context manager to temporarily disable HuggingFace authentication. + + Usage: + with without_hf_auth(): + # Code that should run without cached tokens + model_info(model_name, token=None) + """ + # Save environment variables + saved_env = {} + env_vars = ['HF_TOKEN', 'HUGGINGFACE_HUB_TOKEN', 'HF_HOME'] + for var in env_vars: + if var in os.environ: + saved_env[var] = os.environ[var] + del os.environ[var] + + # Save disable flag + saved_disable = os.environ.get('HF_HUB_DISABLE_IMPLICIT_TOKEN') + os.environ['HF_HUB_DISABLE_IMPLICIT_TOKEN'] = '1' + + # Move token files temporarily + token_files = [] + token_locations = [ + Path.home() / '.cache' / 'huggingface' / 'token', + Path.home() / '.huggingface' / 'token' + ] + + for token_loc in token_locations: + if token_loc.exists(): + temp = tempfile.NamedTemporaryFile(delete=False) + temp.close() + shutil.move(str(token_loc), temp.name) + token_files.append((token_loc, temp.name)) + + try: + yield + finally: + # Restore tokens + for original, temp in token_files: + try: + original.parent.mkdir(parents=True, exist_ok=True) + shutil.move(temp, str(original)) + except Exception as e: + logger.error(f"Failed to restore token {original}: {e}") + + # Restore environment + for var, value in saved_env.items(): + os.environ[var] = value + + if saved_disable is not None: + os.environ['HF_HUB_DISABLE_IMPLICIT_TOKEN'] = saved_disable + else: + os.environ.pop('HF_HUB_DISABLE_IMPLICIT_TOKEN', None) +pass + +def format_error_message(error: Exception, model_name: str) -> str: + """ + Format user-friendly error messages for common issues. + + Args: + error: The exception that occurred + model_name: Name of the model being loaded + + Returns: + User-friendly error string + """ + error_str = str(error).lower() + model_short = model_name.split('/')[-1] if '/' in model_name else model_name + + if "repository not found" in error_str or "404" in error_str: + return f"Model '{model_short}' not found. Check the model name." + + if "401" in error_str or "unauthorized" in error_str: + return f"Authentication failed for '{model_short}'. Please provide a valid HF token." + + if "gated" in error_str or "access to model" in error_str: + return f"Model '{model_short}' requires authentication. Please provide a valid HF token." + + if "invalid user token" in error_str: + return "Invalid HF token. Please check your token and try again." + + if "memory" in error_str or "cuda" in error_str or "out of memory" in error_str: + return f"Not enough GPU memory to load '{model_short}'. Try a smaller model or free GPU memory." + + # Generic fallback + return str(error) +pass + +def get_gpu_memory_info() -> Dict[str, Any]: + """Get GPU memory information.""" + import torch + + if not torch.cuda.is_available(): + return {"available": False} + + try: + device = torch.cuda.current_device() + props = torch.cuda.get_device_properties(device) + + total = props.total_memory + allocated = torch.cuda.memory_allocated(device) + reserved = torch.cuda.memory_reserved(device) + + return { + "available": True, + "device": device, + "total_gb": total / (1024**3), + "allocated_gb": allocated / (1024**3), + "reserved_gb": reserved / (1024**3), + "free_gb": (total - allocated) / (1024**3), + "utilization_pct": (allocated / total) * 100 + } + except Exception as e: + logger.error(f"Error getting GPU info: {e}") + return {"available": False, "error": str(e)} +pass + +def log_gpu_memory(context: str): + """Log GPU memory usage with context.""" + memory_info = get_gpu_memory_info() + if memory_info.get("available"): + logger.info( + f"GPU Memory [{context}]: " + f"{memory_info['allocated_gb']:.2f}GB/{memory_info['total_gb']:.2f}GB " + f"({memory_info['utilization_pct']:.1f}% used, " + f"{memory_info['free_gb']:.2f}GB free)" + ) + else: + logger.info(f"GPU Memory [{context}]: No CUDA GPU available") +pass + +""" +Model utility functions - search, discovery, etc. +""" + + +def search_hf_models(search_query: str, hf_token: Optional[str] = None): + """ + Search HuggingFace model hub. + """ + import requests + + if not search_query or not search_query.strip(): + return gr.update(choices=[]) + + # Simple debouncing: only search if query is at least 2 characters + if len(search_query.strip()) < 2: + return gr.update(choices=[]) + + try: + headers = {} + if hf_token and hf_token.strip(): + headers["Authorization"] = f"Bearer {hf_token.strip()}" + + url = "https://huggingface.co/api/models" + params = { + "search": search_query, + "pipeline_tag": "text-generation", + "library": "transformers", + "limit": 15, + "sort": "downloads", + "direction": -1 + } + + response = requests.get(url, headers=headers, params=params, timeout=10) + + if response.status_code == 200: + models = response.json() + unsloth_results = [] + other_results = [] + + for model in models: + model_id = model.get("modelId", "") + if model_id and "gguf" not in model_id.lower(): + result = (f"{model_id}", model_id) + + if model_id.startswith("unsloth/"): + unsloth_results.append(result) + else: + other_results.append(result) + + # Combine with unsloth models first + search_results = unsloth_results + other_results + return gr.update(choices=search_results) + else: + logger.warning(f"HF API returned status {response.status_code}") + return gr.update(choices=[]) + + except Exception as e: + logger.warning(f"Model search failed: {e}") + return gr.update(choices=[]) diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000000..716efd8499 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,113 @@ +""" +Main FastAPI application for Unsloth UI Backend +""" +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +from pathlib import Path +from datetime import datetime + +# Import routers +from routes import training_router, models_router + +# Create FastAPI app +app = FastAPI( + title="Unsloth UI Backend", + version="1.0.0", + description="Backend API for Unsloth UI - Training and Model Management" +) + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # In production, specify allowed origins + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# ============ Register API Routes ============ + +# Register routers +app.include_router(training_router, prefix="/api/train", tags=["training"]) +app.include_router(models_router, prefix="/api/models", tags=["models"]) + + +# ============ Health and System Endpoints ============ + +@app.get("/api/health") +async def health_check(): + """Health check endpoint""" + return { + "status": "healthy", + "timestamp": datetime.now().isoformat(), + "service": "Unsloth UI Backend" + } + + +@app.get("/api/system") +async def get_system_info(): + """Get system information""" + import torch + import platform + import psutil + + # GPU Info + gpu_info = {"available": False, "devices": []} + if torch.cuda.is_available(): + gpu_info["available"] = True + for i in range(torch.cuda.device_count()): + props = torch.cuda.get_device_properties(i) + gpu_info["devices"].append( + { + "index": i, + "name": props.name, + "memory_total_gb": round(props.total_memory / 1e9, 2), + } + ) + + # CPU & Memory + memory = psutil.virtual_memory() + + return { + "platform": platform.platform(), + "python_version": platform.python_version(), + "cpu_count": psutil.cpu_count(), + "memory": { + "total_gb": round(memory.total / 1e9, 2), + "available_gb": round(memory.available / 1e9, 2), + "percent_used": memory.percent, + }, + "gpu": gpu_info, + } + + +# ============ Serve Frontend (Optional) ============ + +def setup_frontend(app: FastAPI, build_path: Path): + """Mount frontend static files (optional)""" + if build_path.exists(): + # Mount assets + assets_dir = build_path / "assets" + if assets_dir.exists(): + app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") + + @app.get("/") + async def serve_root(): + return FileResponse(build_path / "index.html") + + @app.get("/{full_path:path}") + async def serve_frontend(full_path: str): + if full_path.startswith("api"): + return {"error": "API endpoint not found"} + + file_path = build_path / full_path + if file_path.is_file(): + return FileResponse(file_path) + + return FileResponse(build_path / "index.html") + + return True + return False + diff --git a/backend/models/__init__.py b/backend/models/__init__.py index e69de29bb2..07836f2168 100644 --- a/backend/models/__init__.py +++ b/backend/models/__init__.py @@ -0,0 +1,37 @@ +""" +Pydantic models for API request/response schemas +""" +from .training import ( + TrainingStartRequest, + TrainingStartResponse, + TrainingStatusResponse, + TrainingMetricsResponse, + TrainingProgressResponse, +) +from .models import ( + ModelSearchRequest, + ModelSearchResponse, + ModelListResponse, + ModelConfigResponse, + LoRAScanResponse, + LoRAInfo, + ModelInfo, +) + +__all__ = [ + # Training schemas + "TrainingStartRequest", + "TrainingStartResponse", + "TrainingStatusResponse", + "TrainingMetricsResponse", + "TrainingProgressResponse", + # Model management schemas + "ModelSearchRequest", + "ModelSearchResponse", + "ModelListResponse", + "ModelConfigResponse", + "LoRAScanResponse", + "LoRAInfo", + "ModelInfo", +] + diff --git a/backend/models/models.py b/backend/models/models.py new file mode 100644 index 0000000000..9561bf3288 --- /dev/null +++ b/backend/models/models.py @@ -0,0 +1,56 @@ +""" +Pydantic schemas for Model Management API +""" +from pydantic import BaseModel, Field +from typing import Optional, List, Dict, Any + + +class ModelSearchRequest(BaseModel): + """Request schema for searching HuggingFace models""" + query: str = Field(..., description="Search query") + hf_token: Optional[str] = Field(None, description="HuggingFace token for authenticated searches") + + +class ModelInfo(BaseModel): + """Model information""" + id: str = Field(..., description="Model identifier") + name: Optional[str] = Field(None, description="Display name") + description: Optional[str] = Field(None, description="Model description") + size: Optional[str] = Field(None, description="Model size") + is_vision: bool = Field(False, description="Whether model is a vision model") + is_lora: bool = Field(False, description="Whether model is a LoRA adapter") + + +class ModelSearchResponse(BaseModel): + """Response schema for model search""" + models: List[ModelInfo] = Field(default_factory=list, description="List of matching models") + total: int = Field(0, description="Total number of results") + + +class ModelListResponse(BaseModel): + """Response schema for listing available models""" + models: List[ModelInfo] = Field(default_factory=list, description="List of available models") + default_models: List[str] = Field(default_factory=list, description="List of default model IDs") + + +class ModelConfigResponse(BaseModel): + """Response schema for model configuration""" + model_name: str = Field(..., description="Model identifier") + config: Dict[str, Any] = Field(..., description="Model configuration dictionary") + is_vision: bool = Field(False, description="Whether model is a vision model") + is_lora: bool = Field(False, description="Whether model is a LoRA adapter") + base_model: Optional[str] = Field(None, description="Base model if this is a LoRA adapter") + + +class LoRAInfo(BaseModel): + """LoRA adapter information""" + display_name: str = Field(..., description="Display name for the LoRA") + adapter_path: str = Field(..., description="Path to the LoRA adapter") + base_model: Optional[str] = Field(None, description="Base model identifier") + + +class LoRAScanResponse(BaseModel): + """Response schema for scanning trained LoRA adapters""" + loras: List[LoRAInfo] = Field(default_factory=list, description="List of found LoRA adapters") + outputs_dir: str = Field(..., description="Directory that was scanned") + diff --git a/backend/models/training.py b/backend/models/training.py new file mode 100644 index 0000000000..5fa214a3dc --- /dev/null +++ b/backend/models/training.py @@ -0,0 +1,96 @@ +""" +Pydantic schemas for Training API +""" +from pydantic import BaseModel, Field +from typing import Optional, List + + +class TrainingStartRequest(BaseModel): + """Request schema for starting training""" + # Model parameters + model_name: str = Field(..., description="Model identifier (e.g., 'unsloth/llama-3-8b-bnb-4bit')") + training_type: str = Field(..., description="Training type: 'LoRA/QLoRA' or 'Full Finetuning'") + hf_token: Optional[str] = Field(None, description="HuggingFace token") + load_in_4bit: bool = Field(True, description="Load model in 4-bit quantization") + max_seq_length: int = Field(2048, description="Maximum sequence length") + + # Dataset parameters + hf_dataset: Optional[str] = Field(None, description="HuggingFace dataset identifier") + local_datasets: List[str] = Field(default_factory=list, description="List of local dataset paths") + format_type: str = Field(..., description="Dataset format type") + + # Training parameters + num_epochs: int = Field(1, description="Number of training epochs") + learning_rate: str = Field("2e-4", description="Learning rate") + batch_size: int = Field(1, description="Batch size") + gradient_accumulation_steps: int = Field(1, description="Gradient accumulation steps") + warmup_steps: Optional[int] = Field(None, description="Warmup steps") + warmup_ratio: Optional[float] = Field(None, description="Warmup ratio") + max_steps: Optional[int] = Field(None, description="Maximum training steps") + save_steps: int = Field(100, description="Steps between checkpoints") + weight_decay: float = Field(0.01, description="Weight decay") + random_seed: int = Field(42, description="Random seed") + packing: bool = Field(False, description="Enable sequence packing") + + # LoRA parameters + use_lora: bool = Field(True, description="Use LoRA (derived from training_type)") + lora_r: int = Field(16, description="LoRA rank") + lora_alpha: int = Field(16, description="LoRA alpha") + lora_dropout: float = Field(0.0, description="LoRA dropout") + target_modules: List[str] = Field(default_factory=list, description="Target modules for LoRA") + gradient_checkpointing: str = Field("", description="Gradient checkpointing setting") + use_rslora: bool = Field(False, description="Use RSLoRA") + use_loftq: bool = Field(False, description="Use LoftQ") + train_on_completions: bool = Field(False, description="Train on completions only") + + # Vision-specific LoRA parameters + finetune_vision_layers: bool = Field(False, description="Finetune vision layers") + finetune_language_layers: bool = Field(False, description="Finetune language layers") + finetune_attention_modules: bool = Field(False, description="Finetune attention modules") + finetune_mlp_modules: bool = Field(False, description="Finetune MLP modules") + + # Logging parameters + enable_wandb: bool = Field(False, description="Enable Weights & Biases logging") + wandb_token: Optional[str] = Field(None, description="W&B token") + wandb_project: Optional[str] = Field(None, description="W&B project name") + enable_tensorboard: bool = Field(False, description="Enable TensorBoard logging") + tensorboard_dir: Optional[str] = Field(None, description="TensorBoard directory") + optim: str = Field("adamw_8bit", description="Optimizer") + lr_scheduler_type: str = Field("linear", description="Learning rate scheduler type") + + +class TrainingStartResponse(BaseModel): + """Response schema for training start""" + status: str = Field(..., description="Status: 'started' or 'error'") + job_id: Optional[str] = Field(None, description="Training job ID") + message: str = Field(..., description="Status message") + error: Optional[str] = Field(None, description="Error message if status is 'error'") + + +class TrainingStatusResponse(BaseModel): + """Response schema for training status""" + status: str = Field(..., description="Status: 'idle', 'preparing', 'training', 'stopping', 'error'") + is_active: bool = Field(..., description="Whether training is currently active (actual training running)") + message: str = Field(..., description="Status message") + current_step: Optional[int] = Field(None, description="Current training step") + total_steps: Optional[int] = Field(None, description="Total training steps") + + +class TrainingMetricsResponse(BaseModel): + """Response schema for training metrics""" + loss_history: List[float] = Field(default_factory=list, description="Loss values") + lr_history: List[float] = Field(default_factory=list, description="Learning rate values") + step_history: List[int] = Field(default_factory=list, description="Step numbers") + current_loss: Optional[float] = Field(None, description="Current loss value") + current_lr: Optional[float] = Field(None, description="Current learning rate") + current_step: Optional[int] = Field(None, description="Current step") + + +class TrainingProgressResponse(BaseModel): + """Response schema for training progress updates""" + step: int = Field(..., description="Current step") + loss: float = Field(..., description="Current loss") + learning_rate: float = Field(..., description="Current learning rate") + status_message: str = Field(..., description="Status message") + progress_percent: Optional[float] = Field(None, description="Progress percentage") + diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000000..3f97cca66c --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,7 @@ +fastapi>=0.100.0 +uvicorn>=0.27.0 +pydantic>=2.0 +torch +psutil +nest-asyncio>=1.5.8 + diff --git a/backend/routes/__init__.py b/backend/routes/__init__.py index e69de29bb2..70d9bfbd2f 100644 --- a/backend/routes/__init__.py +++ b/backend/routes/__init__.py @@ -0,0 +1,8 @@ +""" +API Routes +""" + +from routes.training import router as training_router +from routes.models import router as models_router + +__all__ = ["training_router", "models_router"] \ No newline at end of file diff --git a/backend/routes/models.py b/backend/routes/models.py new file mode 100644 index 0000000000..be4096b9c4 --- /dev/null +++ b/backend/routes/models.py @@ -0,0 +1,310 @@ +""" +Model Management API routes +""" +import sys +from pathlib import Path +from fastapi import APIRouter, HTTPException, Query +from typing import Optional +import logging + +# Add backend directory to path +backend_path = Path(__file__).parent.parent.parent +if str(backend_path) not in sys.path: + sys.path.insert(0, str(backend_path)) + +# Import backend functions +try: + from backend.utils import search_hf_models + from backend.model_config import ( + scan_trained_loras, + load_model_defaults, + get_base_model_from_lora, + is_vision_model, + ModelConfig, + ) + from backend.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 backend.utils import search_hf_models + from backend.model_config import ( + scan_trained_loras, + load_model_defaults, + get_base_model_from_lora, + is_vision_model, + ModelConfig, + ) + from backend.inference import get_inference_backend + +from models.models import ( + ModelSearchRequest, + ModelSearchResponse, + ModelInfo, + ModelListResponse, + ModelConfigResponse, + LoRAScanResponse, + LoRAInfo, +) + +router = APIRouter() +logger = logging.getLogger(__name__) + +# Configure logger +if not logger.handlers: + handler = logging.StreamHandler() + handler.setLevel(logging.INFO) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + + +@router.post("/search") +async def search_models(request: ModelSearchRequest): + """ + Search for models on HuggingFace Hub. + + This endpoint wraps the backend search_hf_models function. + """ + try: + # Call backend search function + gradio_update = search_hf_models( + search_query=request.query, + hf_token=request.hf_token + ) + + # Convert Gradio update to list of model IDs + model_list = [] + if gradio_update and hasattr(gradio_update, 'choices'): + choices = gradio_update.choices + elif isinstance(gradio_update, dict) and 'choices' in gradio_update: + choices = gradio_update['choices'] + elif isinstance(gradio_update, list): + choices = gradio_update + else: + choices = [] + + # Process choices - they may be tuples (display_name, model_id) or just strings + for choice in choices: + if isinstance(choice, tuple) and len(choice) >= 2: + # Format: (display_name, model_id) + model_id = choice[1] if len(choice) > 1 else choice[0] + display_name = choice[0] + model_info = ModelInfo( + id=model_id, + name=display_name + ) + elif isinstance(choice, str): + # Just a model ID string + model_info = ModelInfo(id=choice) + else: + continue + model_list.append(model_info) + + return ModelSearchResponse( + models=model_list, + total=len(model_list) + ) + + except Exception as e: + logger.error(f"Error searching models: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"Failed to search models: {str(e)}" + ) + + +@router.get("/list") +async def list_models(): + """ + List available models (default models and loaded models). + + This endpoint returns the default models and any currently loaded models. + """ + try: + inference_backend = get_inference_backend() + + # Get default models + default_models = inference_backend.default_models + + # Get loaded models + loaded_models = [] + for model_name, model_data in inference_backend.models.items(): + model_info = ModelInfo( + id=model_name, + name=model_name.split("/")[-1] if "/" in model_name else model_name, + is_vision=model_data.get("is_vision", False), + is_lora=model_data.get("is_lora", False) + ) + loaded_models.append(model_info) + + # Combine default and loaded models + all_models = [] + seen_ids = set() + + # Add default models + for model_id in default_models: + if model_id not in seen_ids: + model_info = ModelInfo( + id=model_id, + name=model_id.split("/")[-1] if "/" in model_id else model_id + ) + all_models.append(model_info) + seen_ids.add(model_id) + + # Add loaded models + for model_info in loaded_models: + if model_info.id not in seen_ids: + all_models.append(model_info) + seen_ids.add(model_info.id) + + return ModelListResponse( + models=all_models, + default_models=default_models + ) + + except Exception as e: + logger.error(f"Error listing models: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"Failed to list models: {str(e)}" + ) + + +@router.get("/config/{model_name:path}") +async def get_model_config(model_name: str): + """ + Get configuration for a specific model. + + This endpoint wraps the backend load_model_defaults function. + """ + try: + # Load model defaults from backend + config_dict = load_model_defaults(model_name) + + # Check if it's a vision model + is_vision = is_vision_model(model_name) + + # Check if it's a LoRA adapter + is_lora = False + base_model = None + + # Try to create ModelConfig to get more info + try: + model_config = ModelConfig.from_identifier(model_name) + is_lora = model_config.is_lora + base_model = model_config.base_model if is_lora else None + except Exception: + # If ModelConfig creation fails, use defaults + pass + + return ModelConfigResponse( + model_name=model_name, + config=config_dict, + is_vision=is_vision, + is_lora=is_lora, + base_model=base_model + ) + + except Exception as e: + logger.error(f"Error getting model config: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"Failed to get model config: {str(e)}" + ) + + +@router.get("/loras") +async def scan_loras( + outputs_dir: str = Query(default="./outputs", description="Directory to scan for LoRA adapters") +): + """ + Scan for trained LoRA adapters in the outputs directory. + + This endpoint wraps the backend scan_trained_loras function. + """ + try: + # Call backend scan function + trained_loras = scan_trained_loras(outputs_dir=outputs_dir) + + # Convert to LoRAInfo objects + lora_list = [] + for display_name, adapter_path in trained_loras: + # Get base model if available + base_model = get_base_model_from_lora(adapter_path) + + lora_info = LoRAInfo( + display_name=display_name, + adapter_path=adapter_path, + base_model=base_model + ) + lora_list.append(lora_info) + + return LoRAScanResponse( + loras=lora_list, + outputs_dir=outputs_dir + ) + + except Exception as e: + logger.error(f"Error scanning LoRAs: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"Failed to scan LoRA adapters: {str(e)}" + ) + + +@router.get("/loras/{lora_path:path}/base-model") +async def get_lora_base_model(lora_path: str): + """ + Get the base model for a LoRA adapter. + + This endpoint wraps the backend get_base_model_from_lora function. + """ + try: + base_model = get_base_model_from_lora(lora_path) + + if base_model is None: + raise HTTPException( + status_code=404, + detail=f"Could not determine base model for LoRA: {lora_path}" + ) + + return { + "lora_path": lora_path, + "base_model": base_model + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting LoRA base model: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"Failed to get base model: {str(e)}" + ) + + +@router.get("/check-vision/{model_name:path}") +async def check_vision_model(model_name: str): + """ + Check if a model is a vision model. + + This endpoint wraps the backend is_vision_model function. + """ + try: + is_vision = is_vision_model(model_name) + + return { + "model_name": model_name, + "is_vision": is_vision + } + + except Exception as e: + logger.error(f"Error checking vision model: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"Failed to check vision model: {str(e)}" + ) + diff --git a/backend/routes/training.py b/backend/routes/training.py index e69de29bb2..c8839008ba 100644 --- a/backend/routes/training.py +++ b/backend/routes/training.py @@ -0,0 +1,437 @@ +""" +Training API routes +""" +import sys +from pathlib import Path +from fastapi import APIRouter, HTTPException +from fastapi.responses import StreamingResponse +from typing import Dict +import logging +import asyncio +from datetime import datetime +import threading + +# Add backend directory to path +# The backend code should be in the same directory structure +backend_path = Path(__file__).parent.parent.parent +if str(backend_path) not in sys.path: + sys.path.insert(0, str(backend_path)) + +# Import backend functions +try: + from backend.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 models.training import ( + TrainingStartRequest, + TrainingStartResponse, + TrainingStatusResponse, + TrainingMetricsResponse, + TrainingProgressResponse, +) + +router = APIRouter() +logger = logging.getLogger(__name__) + +# Configure logger +if not logger.handlers: + handler = logging.StreamHandler() + handler.setLevel(logging.INFO) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + + +@router.post("/start") +async def start_training(request: TrainingStartRequest): + """ + Start a training job. + + This endpoint initiates training in the background and returns immediately. + Use the /status endpoint to check training progress. + """ + try: + logger.info(f"Starting training job with model: {request.model_name}") + backend = get_training_backend() + + # Check if training is already active + if backend.is_training_active(): + return TrainingStartResponse( + status="error", + message="Training is already in progress. Stop current training before starting a new one.", + error="Training already active" + ) + + # Validate dataset paths if provided + if request.local_datasets: + validated_datasets = [] + # Get the backend directory (where this file is located) + backend_dir = Path(__file__).parent.parent + utils_datasets_dir = backend_dir / "utils" / "datasets" + + for dataset_path in request.local_datasets: + dataset_file = Path(dataset_path) + + # If not absolute, try multiple locations + if not dataset_file.is_absolute(): + # First try: relative to current working directory + candidate = Path.cwd() / dataset_path + if not candidate.exists(): + # Second try: relative to utils/datasets folder + candidate = utils_datasets_dir / dataset_path + if not candidate.exists(): + # Third try: just the filename in utils/datasets + candidate = utils_datasets_dir / dataset_file.name + dataset_file = candidate + + if not dataset_file.exists(): + logger.warning(f"Dataset file not found: {dataset_path} (resolved: {dataset_file})") + else: + logger.info(f"Found dataset file: {dataset_file}") + validated_datasets.append(str(dataset_file)) + request.local_datasets = validated_datasets + + # Convert request to kwargs for backend + training_kwargs = { + "model_name": request.model_name, + "training_type": request.training_type, + "hf_token": request.hf_token or "", + "load_in_4bit": request.load_in_4bit, + "max_seq_length": request.max_seq_length, + "hf_dataset": request.hf_dataset or "", + "local_datasets": request.local_datasets, + "format_type": request.format_type, + "num_epochs": request.num_epochs, + "learning_rate": request.learning_rate, + "batch_size": request.batch_size, + "gradient_accumulation_steps": request.gradient_accumulation_steps, + "warmup_steps": request.warmup_steps, + "warmup_ratio": request.warmup_ratio, + "max_steps": request.max_steps, + "save_steps": request.save_steps, + "weight_decay": request.weight_decay, + "random_seed": request.random_seed, + "packing": request.packing, + "use_lora": request.use_lora, + "lora_r": request.lora_r, + "lora_alpha": request.lora_alpha, + "lora_dropout": request.lora_dropout, + "target_modules": request.target_modules if request.target_modules else None, + "gradient_checkpointing": request.gradient_checkpointing.strip() if request.gradient_checkpointing and request.gradient_checkpointing.strip() else "unsloth", + "use_rslora": request.use_rslora, + "use_loftq": request.use_loftq, + "train_on_completions": request.train_on_completions, + "finetune_vision_layers": request.finetune_vision_layers, + "finetune_language_layers": request.finetune_language_layers, + "finetune_attention_modules": request.finetune_attention_modules, + "finetune_mlp_modules": request.finetune_mlp_modules, + "enable_wandb": request.enable_wandb, + "wandb_token": request.wandb_token or "", + "wandb_project": request.wandb_project or "", + "enable_tensorboard": request.enable_tensorboard, + "tensorboard_dir": request.tensorboard_dir or "", + "optim": request.optim, + "lr_scheduler_type": request.lr_scheduler_type, + } + + # Generate job ID + job_id = f"job_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + + # Set initial "preparing" state + try: + backend.trainer._update_progress( + status_message="Initializing training...", + is_training=False + ) + except: + pass + + def run_training(): + try: + logger.info(f"Starting training job {job_id} with model {request.model_name}") + + # Update status to show we're loading model + try: + backend.trainer._update_progress(status_message="Loading model...") + except Exception as e: + logger.error(f"Error updating progress: {e}") + + # Consume the generator - this actually runs the training + update_count = 0 + for update_tuple in backend.start_training(**training_kwargs): + update_count += 1 + if update_count % 10 == 0: + logger.info(f"Training progress update #{update_count}") + + logger.info(f"Training job {job_id} completed successfully") + + except Exception as e: + logger.error(f"Training error in job {job_id}: {e}", exc_info=True) + try: + backend.trainer._update_progress( + error=str(e), + is_training=False + ) + except Exception as update_error: + logger.error(f"Failed to update progress: {update_error}") + + # Start training in a daemon thread + training_thread = threading.Thread(target=run_training, daemon=True, name=f"Training-{job_id}") + training_thread.start() + + # Store thread reference for status checking + backend._training_thread = training_thread + + # Give it a moment to start + import time + time.sleep(0.5) + + # Verify training thread is alive + if not training_thread.is_alive(): + logger.warning(f"Training thread died immediately for job {job_id}") + return TrainingStartResponse( + status="error", + message="Training thread failed to start. Check server logs for details.", + error="Thread not alive" + ) + + return TrainingStartResponse( + status="started", + job_id=job_id, + message="Training job started successfully" + ) + + except Exception as e: + logger.error(f"Error starting training: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"Failed to start training: {str(e)}" + ) + + +@router.post("/stop") +async def stop_training(): + """ + Stop the currently running training job. + """ + try: + backend = get_training_backend() + + if not backend.is_training_active(): + return { + "status": "idle", + "message": "No training job is currently running" + } + + # Call backend stop method + backend.stop_training() + + return { + "status": "stopped", + "message": "Training job stopped successfully" + } + + except Exception as e: + logger.error(f"Error stopping training: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"Failed to stop training: {str(e)}" + ) + + +@router.get("/status") +async def get_training_status(): + """ + Get the current training status. + """ + try: + backend = get_training_backend() + + # Check if training is active + is_active = backend.is_training_active() + + # Check if there's a training thread running (preparation phase) + has_thread = hasattr(backend, '_training_thread') and backend._training_thread and backend._training_thread.is_alive() + + # Get progress info + try: + progress = backend.trainer.get_training_progress() + status_message = progress.status_message or "Ready to train" + except: + progress = None + status_message = "Unknown" + + if is_active: + # Actual training is running + trainer = backend.trainer + current_step = getattr(trainer.training_progress, 'step', None) or (progress.step if progress else None) + total_steps = getattr(trainer.training_progress, 'total_steps', None) or (progress.total_steps if progress else None) + + return TrainingStatusResponse( + status="training", + is_active=True, + message=status_message or "Training is in progress", + current_step=current_step, + total_steps=total_steps + ) + elif has_thread or (progress and status_message and any(keyword in status_message.lower() for keyword in ["loading", "preparing", "initializing"])): + # Training thread is running but not yet in active training phase + return TrainingStatusResponse( + status="preparing", + is_active=False, + message=status_message or "Preparing training...", + current_step=None, + total_steps=None + ) + else: + return TrainingStatusResponse( + status="idle", + is_active=False, + message="No training job is currently running" + ) + + except Exception as e: + logger.error(f"Error getting training status: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"Failed to get training status: {str(e)}" + ) + + +@router.get("/metrics") +async def get_training_metrics(): + """ + Get training metrics (loss, learning rate, steps). + """ + try: + backend = get_training_backend() + + # Get metrics from backend + loss_history = backend.loss_history + lr_history = backend.lr_history + step_history = backend.step_history + + # Get current values + current_loss = loss_history[-1] if loss_history else None + current_lr = lr_history[-1] if lr_history else None + current_step = step_history[-1] if step_history else None + + return TrainingMetricsResponse( + loss_history=loss_history, + lr_history=lr_history, + step_history=step_history, + current_loss=current_loss, + current_lr=current_lr, + current_step=current_step + ) + + except Exception as e: + logger.error(f"Error getting training metrics: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"Failed to get training metrics: {str(e)}" + ) + + +@router.get("/progress") +async def stream_training_progress(): + """ + Stream training progress updates using Server-Sent Events (SSE). + + This endpoint provides real-time updates on training progress. + """ + async def event_generator(): + backend = get_training_backend() + + # Send initial status + is_active = backend.is_training_active() + initial_message = 'Connecting...' if is_active else 'No training in progress' + yield f"data: {TrainingProgressResponse(step=0, loss=0.0, learning_rate=0.0, status_message=initial_message).model_dump_json()}\n\n" + + # If not active, check if there's any history + if not is_active: + if backend.step_history: + # Training completed - send final metrics + final_step = backend.step_history[-1] + final_loss = backend.loss_history[-1] if backend.loss_history else 0.0 + final_lr = backend.lr_history[-1] if backend.lr_history else 0.0 + yield f"data: {TrainingProgressResponse(step=final_step, loss=final_loss, learning_rate=final_lr, status_message='Training completed').model_dump_json()}\n\n" + else: + yield f"data: {TrainingProgressResponse(step=-1, loss=0.0, learning_rate=0.0, status_message='No training in progress').model_dump_json()}\n\n" + return + + # Poll for updates while training is active + last_step = -1 + no_update_count = 0 + max_no_updates = 300 # Timeout after 5 minutes + + while backend.is_training_active(): + try: + # Get current metrics + if backend.step_history: + current_step = backend.step_history[-1] + current_loss = backend.loss_history[-1] if backend.loss_history else 0.0 + current_lr = backend.lr_history[-1] if backend.lr_history else 0.0 + + # Only send if step changed + if current_step != last_step: + progress = TrainingProgressResponse( + step=current_step, + loss=current_loss, + learning_rate=current_lr, + status_message=f"Training step {current_step}" + ) + yield f"data: {progress.model_dump_json()}\n\n" + last_step = current_step + no_update_count = 0 + else: + no_update_count += 1 + # Send heartbeat every 10 seconds + if no_update_count % 10 == 0: + progress = TrainingProgressResponse( + step=current_step, + loss=current_loss, + learning_rate=current_lr, + status_message=f"Training step {current_step} (waiting for next update...)" + ) + yield f"data: {progress.model_dump_json()}\n\n" + else: + # No steps yet, but training is active + no_update_count += 1 + if no_update_count % 5 == 0: + yield f"data: {TrainingProgressResponse(step=0, loss=0.0, learning_rate=0.0, status_message='Preparing training...').model_dump_json()}\n\n" + + # Timeout check + if no_update_count > max_no_updates: + logger.warning("Progress stream timeout - no updates received") + yield f"data: {TrainingProgressResponse(step=last_step, loss=0.0, learning_rate=0.0, status_message='Progress timeout - training may have stopped').model_dump_json()}\n\n" + break + + await asyncio.sleep(1) # Poll every second + + except Exception as e: + logger.error(f"Error in progress stream: {e}", exc_info=True) + yield f"data: {TrainingProgressResponse(step=0, loss=0.0, learning_rate=0.0, status_message=f'Error: {str(e)}').model_dump_json()}\n\n" + break + + # Send final status + final_step = backend.step_history[-1] if backend.step_history else last_step + final_loss = backend.loss_history[-1] if backend.loss_history else 0.0 + final_lr = backend.lr_history[-1] if backend.lr_history else 0.0 + yield f"data: {TrainingProgressResponse(step=final_step, loss=final_loss, learning_rate=final_lr, status_message='Training completed').model_dump_json()}\n\n" + + return StreamingResponse( + event_generator(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + } + ) + diff --git a/backend/run.py b/backend/run.py new file mode 100644 index 0000000000..6f93cba1d3 --- /dev/null +++ b/backend/run.py @@ -0,0 +1,94 @@ +""" +Run script for Unsloth UI Backend. +Works independently and can be moved to any directory. +""" +import sys +from pathlib import Path + +# Add the backend directory to Python path +backend_dir = Path(__file__).parent +if str(backend_dir) not in sys.path: + sys.path.insert(0, str(backend_dir)) + + +def run_server( + host: str = "0.0.0.0", + port: int = 8000, + frontend_path: Path = None, + silent: bool = False, +): + """ + Start the FastAPI server. + + Args: + host: Host to bind to + port: Port to bind to + frontend_path: Path to frontend build directory (optional) + silent: Suppress startup messages + """ + import nest_asyncio + + nest_asyncio.apply() + + import asyncio + from threading import Thread + import time + import uvicorn + + from main import app, setup_frontend + + # Setup frontend if path provided + if frontend_path: + if setup_frontend(app, frontend_path): + if not silent: + print(f"✅ Frontend loaded from {frontend_path}") + else: + if not silent: + print(f"⚠️ Frontend not found at {frontend_path}") + + # Run server + def _run(): + config = uvicorn.Config(app, host=host, port=port, log_level="info") + server = uvicorn.Server(config) + asyncio.run(server.serve()) + + thread = Thread(target=_run, daemon=True) + thread.start() + time.sleep(3) + + if not silent: + print("") + print("=" * 50) + print(f"🦥 Unsloth UI Backend is running on port {port}") + print(f" API: http://{host}:{port}/api") + print(f" Health: http://{host}:{port}/api/health") + print("=" * 50) + + return app + + +# For direct execution +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="Run Unsloth UI Backend server") + parser.add_argument("--host", default="0.0.0.0", help="Host to bind to") + parser.add_argument("--port", type=int, default=8000, help="Port to bind to") + parser.add_argument( + "--frontend", type=str, default=None, help="Path to frontend build" + ) + parser.add_argument("--silent", action="store_true", help="Suppress output") + + args = parser.parse_args() + + frontend_path = Path(args.frontend) if args.frontend else None + run_server( + host=args.host, port=args.port, frontend_path=frontend_path, silent=args.silent + ) + + # Keep running + import time + + while True: + time.sleep(1) + diff --git a/backend/utils/datasets/alpaca_unsloth.json b/backend/utils/datasets/alpaca_unsloth.json new file mode 100644 index 0000000000..2328223a8a --- /dev/null +++ b/backend/utils/datasets/alpaca_unsloth.json @@ -0,0 +1,1288 @@ +[ + { + "instruction": "", + "input": "What is Unsloth?", + "output": "Unsloth is an open-source library designed to make LLM fine-tuning faster and more memory-efficient. It allows you to fine-tune models like Llama, Mistral, Qwen, Gemma, and others up to 2x faster with 70% less VRAM usage compared to standard methods. The library is fully compatible with the Hugging Face ecosystem and supports techniques like QLoRA, LoRA, and full fine-tuning." + }, + { + "instruction": "", + "input": "Who created Unsloth?", + "output": "Unsloth was created by two brothers, Daniel Han and Mike Han, along with the Unsloth team. The project started in 2023 and has grown into a popular open-source tool for efficient LLM training." + }, + { + "instruction": "", + "input": "How much faster is Unsloth compared to standard fine-tuning?", + "output": "Unsloth makes fine-tuning 2x faster on a single GPU compared to standard Hugging Face methods with Flash Attention 2. In some benchmarks, it achieves up to 2.7x speedup. On multi-GPU systems, it can be up to 30x faster. The library accomplishes this through optimized kernels written in OpenAI's Triton language and manual backpropagation engine optimizations." + }, + { + "instruction": "", + "input": "How much memory does Unsloth save?", + "output": "Unsloth reduces memory usage by up to 70% compared to standard fine-tuning approaches. This allows you to fit larger batch sizes and train bigger models on the same hardware. For example, you can fine-tune a 9B parameter model on just 24GB VRAM using LoRA 16-bit, or use as little as 6.5GB VRAM with QLoRA 4-bit quantization." + }, + { + "instruction": "", + "input": "What models does Unsloth support?", + "output": "Unsloth supports a wide range of models including Llama (all versions), Mistral, Qwen, Gemma, Phi, DeepSeek, CodeLlama, Yi, TinyLlama, and more. It also supports Text-to-Speech models, multimodal models, BERT, and Mamba architectures. Essentially, any model that works in Hugging Face transformers works in Unsloth." + }, + { + "instruction": "", + "input": "What GPUs does Unsloth support?", + "output": "Unsloth supports NVIDIA GPUs from Tesla T4, GTX 1070 all the way up to H100s and requires minimum CUDA Capability 7.0. It also supports AMD GPUs via ROCm and Intel GPUs. The library even works on consumer hardware like Apple Silicon (M1, M2, M3) MacBooks with Metal backend support." + }, + { + "instruction": "", + "input": "What training methods does Unsloth support?", + "output": "Unsloth supports multiple training methods including QLoRA (4-bit quantized LoRA), LoRA 16-bit, 8-bit training, full fine-tuning (FFT), and continued pretraining. It also supports reinforcement learning methods like GRPO, GSPO, DPO, ORPO, PPO, and KTO. The library maintains 0% loss in accuracy with no approximation methods - everything is exact." + }, + { + "instruction": "", + "input": "How do I install Unsloth?", + "output": "You can install Unsloth using pip with the command: pip install unsloth. For local Linux or WSL installations, this is the recommended method. Unsloth also provides official Docker images and works on Google Colab and Kaggle notebooks. The library requires Python 3.13 or lower (Python 3.14 is not supported)." + }, + { + "instruction": "", + "input": "What is Unsloth UI?", + "output": "Unsloth UI is a user interface for Unsloth that makes fine-tuning even more accessible by providing a visual interface for training models. It allows users to configure training parameters, manage datasets, and monitor training progress without writing code, making LLM fine-tuning accessible to non-programmers." + }, + { + "instruction": "", + "input": "Can I use Unsloth for free?", + "output": "Yes! Unsloth has a fully free open-source version available on GitHub that makes fine-tuning 2x faster with 50% less memory. You can use it for free on Google Colab, Kaggle notebooks, or install it locally. The library is open source and actively maintained by the community." + }, + { + "instruction": "", + "input": "What is special about Unsloth's implementation?", + "output": "Unsloth achieves its performance through several innovations: all kernels are rewritten in OpenAI's Triton language, it uses manual backpropagation engine optimizations, implements Flash Attention via xformers, and performs optimized chained matrix multiplication. It also includes RoPE Scaling internally and uses gradient checkpointing to save VRAM during training." + }, + { + "instruction": "", + "input": "What is QLoRA and does Unsloth support it?", + "output": "QLoRA (Quantized LoRA) is a technique that combines 4-bit quantization with LoRA fine-tuning to dramatically reduce memory usage. Unsloth fully supports QLoRA and is optimized for it - you can enable it with load_in_4bit=True. This allows fine-tuning large models on consumer GPUs with minimal memory, making it one of the most accessible fine-tuning methods available." + }, + { + "instruction": "", + "input": "Can Unsloth train models with long context?", + "output": "Yes! Unsloth natively supports training with very long context lengths, including up to 128k tokens. It automatically handles RoPE Scaling internally, which means you can specify any maximum sequence length and the library will handle the necessary adjustments for extended context training." + }, + { + "instruction": "", + "input": "What is Unsloth Dynamic Quantization?", + "output": "Unsloth Dynamic Quantization 2.0 is an advanced quantization method that analyzes each layer's sensitivity to compression rather than applying one-size-fits-all quantization. It uses calibration datasets ranging from 300K to 1.5M tokens and achieves SOTA quantization performance, setting new benchmarks on tasks like 5-shot MMLU and Aider Polyglot." + }, + { + "instruction": "", + "input": "Does Unsloth support exporting to GGUF format?", + "output": "Yes! Unsloth has robust support for GGUF (Grokking GGML Unified Format) exports. You can save models using model.save_pretrained_gguf() or push directly to Hugging Face Hub with model.push_to_hub_gguf(). This makes it easy to deploy fine-tuned models to inference engines like llama.cpp, Ollama, and other GGUF-compatible tools." + }, + { + "instruction": "", + "input": "What inference engines work with Unsloth models?", + "output": "Unsloth fine-tuned models can be exported to and used with multiple inference engines including Ollama, vLLM, llama.cpp, Open WebUI, and any other engine compatible with Hugging Face format or GGUF format. The library provides easy export methods for all major deployment platforms." + }, + { + "instruction": "", + "input": "Can I use Unsloth for reinforcement learning?", + "output": "Yes! Unsloth is the most efficient library for Reinforcement Learning, using 80% less VRAM compared to standard methods. It supports multiple RL algorithms including GRPO (Group Relative Policy Optimization), GSPO, DrGRPO, DAPO, DPO, ORPO, PPO, and KTO. You can even train reasoning models with long-context RL using just 5GB VRAM." + }, + { + "instruction": "", + "input": "What makes Unsloth different from standard Hugging Face fine-tuning?", + "output": "Unlike standard Hugging Face fine-tuning which requires multiple libraries (transformers, PEFT, bitsandbytes, Deepspeed, TRL), Unsloth provides a single unified API that's much simpler to use. It also delivers 2-3x faster training, 50-70% less memory usage, and handles common issues like layer norm quantization automatically. Plus, all the complex setup is abstracted away into simple function calls." + }, + { + "instruction": "", + "input": "Does Unsloth support Mixture of Experts models?", + "output": "Yes! Unsloth has native support for Mixture of Experts (MoE) models like Mixtral 8x7B. Traditional libraries aren't optimized for MoE fine-tuning yet, but Unsloth supports them natively with the same efficiency benefits, including support for 128k context lengths and all standard fine-tuning techniques." + }, + { + "instruction": "", + "input": "What is the recommended batch size for Unsloth?", + "output": "Unsloth recommends keeping per_device_train_batch_size at 2 for most use cases. To simulate larger batch sizes without increasing memory usage, you should increase gradient_accumulation_steps instead (typically set to 4). This approach provides smoother training without the memory overhead of larger batches and avoids slowdowns from excessive padding." + }, + { + "instruction": "", + "input": "How many epochs should I train with Unsloth?", + "output": "Unsloth recommends 1-3 epochs to avoid overfitting. For quick experiments, you can use max_steps (like 60 steps) instead of full epochs. The default learning rate is 2e-4, which can be lowered for slower but more precise fine-tuning. Always monitor your training loss - it should reach somewhere between 0.5 and 1.0, not 0 (which indicates overfitting)." + }, + { + "instruction": "", + "input": "Can I use Unsloth on Windows?", + "output": "Yes! Unsloth works on Windows, Linux, and WSL (Windows Subsystem for Linux). The library is compatible with Windows systems that have compatible NVIDIA GPUs. You can install it via pip or use the official Docker image for a containerized setup." + }, + { + "instruction": "", + "input": "What is the Unsloth community like?", + "output": "Unsloth has an active community with a Discord server and Reddit community at r/unsloth. The library is actively developed by Daniel and Mike Han along with open-source contributors. They collaborate directly with teams behind major models like GPT-OSS, Qwen, Llama, Mistral, and Gemma, often fixing critical bugs in models before official release." + }, + { + "instruction": "", + "input": "Does Unsloth support Text-to-Speech models?", + "output": "Yes! Unsloth recently added support for Text-to-Speech (TTS) models including sesame/csm-1b and also supports Speech-to-Text with OpenAI's Whisper models like whisper-large-v3. This makes Unsloth one of the few libraries that supports not just text LLMs, but also audio models with the same efficiency benefits." + }, + { + "instruction": "", + "input": "What is FastLanguageModel in Unsloth?", + "output": "FastLanguageModel is Unsloth's main API for loading and configuring models. You use FastLanguageModel.from_pretrained() to load a model and tokenizer together, and FastLanguageModel.get_peft_model() to add LoRA adapters. This single API handles all the complex setup of quantization, RoPE scaling, and optimization automatically." + }, + { + "instruction": "", + "input": "Can I fine-tune vision models with Unsloth?", + "output": "Yes! Unsloth supports multimodal and vision models. The library can fine-tune vision-language models and other multimodal architectures with the same efficiency benefits. Unsloth also recently added VLM RL (Vision-Language Model Reinforcement Learning) support for models like Qwen and Gemma vision variants." + }, + { + "instruction": "", + "input": "What LoRA rank should I use with Unsloth?", + "output": "Unsloth suggests LoRA ranks of 8, 16, 32, 64, or 128. For most use cases, r=16 or r=32 works well, balancing model capacity with training efficiency. The library also recommends setting lora_dropout=0 and bias='none' as these are optimized settings. You can use the 'unsloth' use_gradient_checkpointing option which uses 30% less VRAM and fits 2x larger batch sizes." + }, + { + "instruction": "", + "input": "Does Unsloth support continued pretraining?", + "output": "Yes! Unsloth supports continued pretraining in addition to fine-tuning. This allows you to further pretrain models on domain-specific corpora to inject new knowledge before task-specific fine-tuning. You can do this with the same efficiency benefits - 2x faster training and 70% less memory." + }, + { + "instruction": "", + "input": "What is the Unsloth sticker for?", + "output": "If you train a model with Unsloth, you can use a special Unsloth sloth emoji sticker (🦥) to proudly display that your model was fine-tuned using Unsloth! It's become a badge of honor in the community to show you used efficient, fast fine-tuning methods." + }, + { + "instruction": "", + "input": "Can I save just the LoRA adapter with Unsloth?", + "output": "Yes! After fine-tuning, you can save just the LoRA adapter as a small file (typically around 100MB) instead of saving the entire model. This makes it easy to share and distribute your fine-tunes. You can save locally or push directly to Hugging Face Hub. Later, you can reload the base model and apply your adapter for inference." + }, + { + "instruction": "", + "input": "What recent models has Unsloth added support for?", + "output": "Recently, Unsloth added support for OpenAI's GPT-OSS (20B and 120B), Google's Gemma 3, Qwen3 (including Qwen3-30B-A3B that fits on 17.5GB VRAM), DeepSeek-R1, and Microsoft's Phi-4. The library also supports TTS models and has introduced long-context reasoning with GRPO that lets you train reasoning models with just 5GB VRAM." + }, + { + "instruction": "", + "input": "How can Unsloth accelerate LLM fine-tuning?", + "output": "Unsloth accelerates LLM fine-tuning by overwriting some parts of the modeling code with optimized operations and rewriting all Pytorch modules into Triton kernels, resulting in a 2x speedup and a 40% reduction in memory usage." + }, + { + "instruction": "", + "input": "What is one benefit of using Unsloth for LLM fine-tuning?", + "output": "One benefit of using Unsloth for LLM fine-tuning is that it offers a 0% accuracy degradation compared to normal QLoRA, as no approximations are made in the optimized code." + }, + { + "instruction": "", + "input": "Who developed the Unsloth library for LLM fine-tuning?", + "output": "The Unsloth library for LLM fine-tuning was developed by the Unsloth team, including Daniel and Mike, in collaboration with the open-source community." + }, + { + "instruction": "", + "input": "What GPU types are compatible with Unsloth for LLM fine-tuning?", + "output": "Unsloth supports most NVIDIA GPUs, ranging from GTX 1070 to H100s, making it compatible with a wide range of hardware configurations." + }, + { + "instruction": "", + "input": "How does Unsloth contribute to reducing memory usage during LLM fine-tuning?", + "output": "Unsloth contributes to reducing memory usage during LLM fine-tuning by rewriting all Pytorch modules into Triton kernels, resulting in a 40% decrease in memory usage." + }, + { + "instruction": "", + "input": "What is the primary goal of Unsloth for LLM fine-tuning?", + "output": "The primary goal of Unsloth for LLM fine-tuning is to accelerate the process, achieving a 2x speedup while maintaining 0% accuracy degradation compared to normal QLoRA." + }, + { + "instruction": "", + "input": "How does Unsloth improve LLM fine-tuning performance?", + "output": "Unsloth improves LLM fine-tuning performance by manually deriving backpropagation steps and rewriting Pytorch modules into Triton kernels, resulting in a 2x speed increase and a 40% reduction in memory usage." + }, + { + "instruction": "", + "input": "What makes Unsloth different from other tools for LLM fine-tuning?", + "output": "What makes Unsloth different from other tools for LLM fine-tuning is its ability to achieve a 2x speed increase and a 40% reduction in memory usage while maintaining 0% accuracy degradation, thanks to optimized operations and Triton kernel rewriting." + }, + { + "instruction": "", + "input": "Why is Unsloth considered a lightweight library for LLM fine-tuning?", + "output": "Unsloth is considered a lightweight library for LLM fine-tuning because it offers significant performance improvements, including a 2x speed increase and a 40% reduction in memory usage, without compromising accuracy." + }, + { + "instruction": "", + "input": "What are the supported architectures for LLM fine-tuning by Unsloth?", + "output": "Unsloth supports the Llama and Mistral architectures for LLM fine-tuning, ensuring compatibility with a range of model structures." + }, + { + "instruction": "", + "input": "What benefits does Unsloth offer for LLM fine-tuning?", + "output": "Unsloth provides a 2x acceleration in LLM fine-tuning speed, reduces memory usage by 40%, and ensures 0% accuracy degradation compared to regular QLoRA fine-tuning methods." + }, + { + "instruction": "", + "input": "Which GPUs are supported by Unsloth for LLM fine-tuning acceleration?", + "output": "Unsloth supports most NVIDIA GPUs, ranging from GTX 1070 to H100s, ensuring compatibility with a wide range of hardware configurations for fine-tuning large language models." + }, + { + "instruction": "", + "input": "How does Unsloth ensure accuracy preservation during LLM fine-tuning?", + "output": "Unsloth achieves 0% accuracy degradation by avoiding approximations in the optimized code and ensuring that all backpropagation steps are manually derived, maintaining fidelity to regular QLoRA fine-tuning methods." + }, + { + "instruction": "", + "input": "What is the compatibility of Unsloth with the Hugging Face ecosystem?", + "output": "Unsloth is fully compatible with the Hugging Face ecosystem, including Hub, transformers, PEFT, and TRL libraries, providing seamless integration into existing workflows for fine-tuning large language models." + }, + { + "instruction": "", + "input": "Which architectures are supported by Unsloth for LLM fine-tuning acceleration?", + "output": "Unsloth currently supports the Llama and Mistral architectures, enabling accelerated fine-tuning of large language models built on these frameworks." + }, + { + "instruction": "", + "input": "How does Unsloth reduce memory usage during LLM fine-tuning?", + "output": "Unsloth achieves a 40% reduction in memory usage by optimizing operations and rewriting Pytorch modules into Triton kernels, thereby minimizing the memory footprint required for fine-tuning large language models." + }, + { + "instruction": "", + "input": "What distinguishes Unsloth from other libraries for LLM fine-tuning acceleration?", + "output": "Unsloth stands out by offering a 2x speedup in fine-tuning, a 40% reduction in memory usage, and maintaining 0% accuracy degradation, setting it apart as a lightweight yet powerful tool for accelerating large language model training." + }, + { + "instruction": "", + "input": "How does Unsloth leverage Triton kernels to improve LLM fine-tuning efficiency?", + "output": "Unsloth leverages Triton kernels by rewriting all Pytorch modules into optimized operations, reducing memory usage and accelerating fine-tuning speed without sacrificing accuracy." + }, + { + "instruction": "", + "input": "How does reducing upcasting of weights during QLoRA impact LLM fine-tuning efficiency?", + "output": "Reducing upcasting of weights during QLoRA can save 7.2% of VRAM and make training take 21.7% less time, thus significantly improving LLM fine-tuning efficiency." + }, + { + "instruction": "", + "input": "What efficiency improvement does using Bitsandbytes bfloat16 offer during LLM fine-tuning?", + "output": "Using Bitsandbytes bfloat16 internally fixes the extra memory copy issue, saving 9% of the time during LLM fine-tuning." + }, + { + "instruction": "", + "input": "How does Pytorch's implementation of Scaled Dot Product Attention contribute to LLM fine-tuning efficiency?", + "output": "Pytorch's fast implementation of Scaled Dot Product Attention saves 1.4% of time during LLM fine-tuning, thereby enhancing efficiency." + }, + { + "instruction": "", + "input": "What strategies can be employed to accelerate LLM fine-tuning without sacrificing accuracy?", + "output": "Reducing data upcasting, utilizing Bitsandbytes bfloat16, and implementing Pytorch's fast Scaled Dot Product Attention are effective strategies to accelerate LLM fine-tuning without sacrificing accuracy." + }, + { + "instruction": "", + "input": "How can VRAM usage be optimized during LLM fine-tuning?", + "output": "By reducing upcasting of weights during QLoRA, VRAM usage can be optimized, resulting in improved efficiency during LLM fine-tuning." + }, + { + "instruction": "", + "input": "What are the benefits of fine-tuning LLMs with Unsloth and TRL?", + "output": "Fine-tuning LLMs with Unsloth and TRL can make the process 2x faster by employing strategies such as reducing data upcasting, utilizing Bitsandbytes bfloat16, and implementing Pytorch's fast Scaled Dot Product Attention." + }, + { + "instruction": "", + "input": "How can time efficiency during LLM fine-tuning be improved?", + "output": "Time efficiency during LLM fine-tuning can be improved by employing techniques such as reducing upcasting of weights during QLoRA, using Bitsandbytes bfloat16, and adopting Pytorch's fast implementation of Scaled Dot Product Attention." + }, + { + "instruction": "", + "input": "What are some optimizations to consider for LLM fine-tuning?", + "output": "Optimizations for LLM fine-tuning include reducing data upcasting, utilizing Bitsandbytes bfloat16, and implementing Pytorch's fast Scaled Dot Product Attention to improve efficiency." + }, + { + "instruction": "", + "input": "How can memory usage be reduced during LLM fine-tuning?", + "output": "Memory usage during LLM fine-tuning can be reduced by addressing issues such as data upcasting, adopting efficient data types like Bitsandbytes bfloat16, and optimizing attention mechanisms like Pytorch's implementation of Scaled Dot Product Attention." + }, + { + "instruction": "", + "input": "What techniques can be employed to accelerate fine-tuning of LLMs?", + "output": "To accelerate fine-tuning of LLMs, one can implement strategies such as reducing upcasting of weights during QLoRA, using Bitsandbytes bfloat16, and leveraging Pytorch's fast Scaled Dot Product Attention." + }, + { + "instruction": "", + "input": "How can I utilize Unsloth for model loading?", + "output": "To use Unsloth for model loading, simply employ FastLanguageModel.from_pretrained. It currently supports Llama and Mistral type architectures, including Yi, Deepseek, TinyLlama, Llamafied Qwen. You can also load pre-quantized 4bit models directly from the latest Transformers main branch, which enhances downloading speed by 4x and reduces memory fragmentation by approximately 500MB." + }, + { + "instruction": "", + "input": "What architectures are supported by Unsloth?", + "output": "Unsloth supports Llama and Mistral type architectures such as Yi, Deepseek, TinyLlama, Llamafied Qwen. These architectures can be loaded using FastLanguageModel.from_pretrained." + }, + { + "instruction": "", + "input": "How does Unsloth enhance model loading speed?", + "output": "Unsloth enhances model loading speed by enabling the direct loading of pre-quantized 4bit models from the latest Transformers main branch. This results in a 4x faster download speed and reduces memory fragmentation by around 500MB." + }, + { + "instruction": "", + "input": "What are some pre-quantized models available in Unsloth?", + "output": "Unsloth provides several pre-quantized models for convenience, including unsloth/llama-2-7b-bnb-4bit, unsloth/llama-2-13b-bnb-4bit, unsloth/mistral-7b-bnb-4bit, and unsloth/codellama-34b-bnb-4bit." + }, + { + "instruction": "", + "input": "What additional functionality does FastLanguageModel.from_pretrained offer with Unsloth?", + "output": "FastLanguageModel.from_pretrained in Unsloth not only loads the model but also returns the model tokenizer for convenience. Additionally, it supports providing the intended maximum sequence length for better scalability, with Unsloth performing RoPE Scaling internally." + }, + { + "instruction": "", + "input": "How can I integrate Unsloth with the TRL library?", + "output": "To use Unsloth with the TRL library, simply pass the Unsloth model into SFTTrainer or DPOTrainer! The trained model is fully compatible with the Hugging Face ecosystem, allowing you to push the final model to the Hub and use transformers for inference out of the box." + }, + { + "instruction": "", + "input": "What is the benefit of using Unsloth with the TRL library?", + "output": "The trained model is fully compatible with the Hugging Face ecosystem, allowing you to push the final model to the Hub and use transformers for inference out of the box!" + }, + { + "instruction": "", + "input": "What preprocessing steps are necessary when integrating Unsloth with the TRL library?", + "output": "When integrating Unsloth with the TRL library, ensure the dataset is loaded properly and the maximum sequence length is appropriately set." + }, + { + "instruction": "", + "input": "What are the key features of the FastLanguageModel from Unsloth?", + "output": "The FastLanguageModel supports RoPE Scaling internally and can load models with 4-bit weights for efficient memory usage." + }, + { + "instruction": "", + "input": "What are some parameters I can configure when using SFTTrainer with Unsloth?", + "output": "You can configure parameters such as per-device batch size, gradient accumulation steps, warmup steps, and optimization algorithm." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "How can Unsloth accelerate LLM fine-tuning?", + "output": "Unsloth accelerates LLM fine-tuning by overwriting some parts of the modeling code with optimized operations and rewriting all Pytorch modules into Triton kernels, resulting in a 2x speedup and a 40% reduction in memory usage." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What is one benefit of using Unsloth for LLM fine-tuning?", + "output": "One benefit of using Unsloth for LLM fine-tuning is that it offers a 0% accuracy degradation compared to normal QLoRA, as no approximations are made in the optimized code." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "Who developed the Unsloth library for LLM fine-tuning?", + "output": "The Unsloth library for LLM fine-tuning was developed by the Unsloth team, including Daniel and Mike, in collaboration with the open-source community." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What GPU types are compatible with Unsloth for LLM fine-tuning?", + "output": "Unsloth supports most NVIDIA GPUs, ranging from GTX 1070 to H100s, making it compatible with a wide range of hardware configurations." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "How does Unsloth contribute to reducing memory usage during LLM fine-tuning?", + "output": "Unsloth contributes to reducing memory usage during LLM fine-tuning by rewriting all Pytorch modules into Triton kernels, resulting in a 40% decrease in memory usage." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What is the primary goal of Unsloth for LLM fine-tuning?", + "output": "The primary goal of Unsloth for LLM fine-tuning is to accelerate the process, achieving a 2x speedup while maintaining 0% accuracy degradation compared to normal QLoRA." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "How does Unsloth improve LLM fine-tuning performance?", + "output": "Unsloth improves LLM fine-tuning performance by manually deriving backpropagation steps and rewriting Pytorch modules into Triton kernels, resulting in a 2x speed increase and a 40% reduction in memory usage." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What makes Unsloth different from other tools for LLM fine-tuning?", + "output": "What makes Unsloth different from other tools for LLM fine-tuning is its ability to achieve a 2x speed increase and a 40% reduction in memory usage while maintaining 0% accuracy degradation, thanks to optimized operations and Triton kernel rewriting." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "Why is Unsloth considered a lightweight library for LLM fine-tuning?", + "output": "Unsloth is considered a lightweight library for LLM fine-tuning because it offers significant performance improvements, including a 2x speed increase and a 40% reduction in memory usage, without compromising accuracy." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What are the supported architectures for LLM fine-tuning by Unsloth?", + "output": "Unsloth supports the Llama and Mistral architectures for LLM fine-tuning, ensuring compatibility with a range of model structures." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What benefits does Unsloth offer for LLM fine-tuning?", + "output": "Unsloth provides a 2x acceleration in LLM fine-tuning speed, reduces memory usage by 40%, and ensures 0% accuracy degradation compared to regular QLoRA fine-tuning methods." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "Which GPUs are supported by Unsloth for LLM fine-tuning acceleration?", + "output": "Unsloth supports most NVIDIA GPUs, ranging from GTX 1070 to H100s, ensuring compatibility with a wide range of hardware configurations for fine-tuning large language models." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "How does Unsloth ensure accuracy preservation during LLM fine-tuning?", + "output": "Unsloth achieves 0% accuracy degradation by avoiding approximations in the optimized code and ensuring that all backpropagation steps are manually derived, maintaining fidelity to regular QLoRA fine-tuning methods." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What is the compatibility of Unsloth with the Hugging Face ecosystem?", + "output": "Unsloth is fully compatible with the Hugging Face ecosystem, including Hub, transformers, PEFT, and TRL libraries, providing seamless integration into existing workflows for fine-tuning large language models." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "Which architectures are supported by Unsloth for LLM fine-tuning acceleration?", + "output": "Unsloth currently supports the Llama and Mistral architectures, enabling accelerated fine-tuning of large language models built on these frameworks." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "How does Unsloth reduce memory usage during LLM fine-tuning?", + "output": "Unsloth achieves a 40% reduction in memory usage by optimizing operations and rewriting Pytorch modules into Triton kernels, thereby minimizing the memory footprint required for fine-tuning large language models." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What distinguishes Unsloth from other libraries for LLM fine-tuning acceleration?", + "output": "Unsloth stands out by offering a 2x speedup in fine-tuning, a 40% reduction in memory usage, and maintaining 0% accuracy degradation, setting it apart as a lightweight yet powerful tool for accelerating large language model training." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "How does Unsloth leverage Triton kernels to improve LLM fine-tuning efficiency?", + "output": "Unsloth leverages Triton kernels by rewriting all Pytorch modules into optimized operations, reducing memory usage and accelerating fine-tuning speed without sacrificing accuracy." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "How does reducing upcasting of weights during QLoRA impact LLM fine-tuning efficiency?", + "output": "Reducing upcasting of weights during QLoRA can save 7.2% of VRAM and make training take 21.7% less time, thus significantly improving LLM fine-tuning efficiency." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What efficiency improvement does using Bitsandbytes bfloat16 offer during LLM fine-tuning?", + "output": "Using Bitsandbytes bfloat16 internally fixes the extra memory copy issue, saving 9% of the time during LLM fine-tuning." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "How does Pytorch's implementation of Scaled Dot Product Attention contribute to LLM fine-tuning efficiency?", + "output": "Pytorch's fast implementation of Scaled Dot Product Attention saves 1.4% of time during LLM fine-tuning, thereby enhancing efficiency." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What strategies can be employed to accelerate LLM fine-tuning without sacrificing accuracy?", + "output": "Reducing data upcasting, utilizing Bitsandbytes bfloat16, and implementing Pytorch's fast Scaled Dot Product Attention are effective strategies to accelerate LLM fine-tuning without sacrificing accuracy." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "How can VRAM usage be optimized during LLM fine-tuning?", + "output": "By reducing upcasting of weights during QLoRA, VRAM usage can be optimized, resulting in improved efficiency during LLM fine-tuning." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What are the benefits of fine-tuning LLMs with Unsloth and TRL?", + "output": "Fine-tuning LLMs with Unsloth and TRL can make the process 2x faster by employing strategies such as reducing data upcasting, utilizing Bitsandbytes bfloat16, and implementing Pytorch's fast Scaled Dot Product Attention." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "How can time efficiency during LLM fine-tuning be improved?", + "output": "Time efficiency during LLM fine-tuning can be improved by employing techniques such as reducing upcasting of weights during QLoRA, using Bitsandbytes bfloat16, and adopting Pytorch's fast implementation of Scaled Dot Product Attention." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What are some optimizations to consider for LLM fine-tuning?", + "output": "Optimizations for LLM fine-tuning include reducing data upcasting, utilizing Bitsandbytes bfloat16, and implementing Pytorch's fast Scaled Dot Product Attention to improve efficiency." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "How can memory usage be reduced during LLM fine-tuning?", + "output": "Memory usage during LLM fine-tuning can be reduced by addressing issues such as data upcasting, adopting efficient data types like Bitsandbytes bfloat16, and optimizing attention mechanisms like Pytorch's implementation of Scaled Dot Product Attention." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What techniques can be employed to accelerate fine-tuning of LLMs?", + "output": "To accelerate fine-tuning of LLMs, one can implement strategies such as reducing upcasting of weights during QLoRA, using Bitsandbytes bfloat16, and leveraging Pytorch's fast Scaled Dot Product Attention." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "How can I utilize Unsloth for model loading?", + "output": "To use Unsloth for model loading, simply employ FastLanguageModel.from_pretrained. It currently supports Llama and Mistral type architectures, including Yi, Deepseek, TinyLlama, Llamafied Qwen. You can also load pre-quantized 4bit models directly from the latest Transformers main branch, which enhances downloading speed by 4x and reduces memory fragmentation by approximately 500MB." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What architectures are supported by Unsloth?", + "output": "Unsloth supports Llama and Mistral type architectures such as Yi, Deepseek, TinyLlama, Llamafied Qwen. These architectures can be loaded using FastLanguageModel.from_pretrained." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "How does Unsloth enhance model loading speed?", + "output": "Unsloth enhances model loading speed by enabling the direct loading of pre-quantized 4bit models from the latest Transformers main branch. This results in a 4x faster download speed and reduces memory fragmentation by around 500MB." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What are some pre-quantized models available in Unsloth?", + "output": "Unsloth provides several pre-quantized models for convenience, including unsloth/llama-2-7b-bnb-4bit, unsloth/llama-2-13b-bnb-4bit, unsloth/mistral-7b-bnb-4bit, and unsloth/codellama-34b-bnb-4bit." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What additional functionality does FastLanguageModel.from_pretrained offer with Unsloth?", + "output": "FastLanguageModel.from_pretrained in Unsloth not only loads the model but also returns the model tokenizer for convenience. Additionally, it supports providing the intended maximum sequence length for better scalability, with Unsloth performing RoPE Scaling internally." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "How can I integrate Unsloth with the TRL library?", + "output": "To use Unsloth with the TRL library, simply pass the Unsloth model into SFTTrainer or DPOTrainer! The trained model is fully compatible with the Hugging Face ecosystem, allowing you to push the final model to the Hub and use transformers for inference out of the box." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What is the benefit of using Unsloth with the TRL library?", + "output": "The trained model is fully compatible with the Hugging Face ecosystem, allowing you to push the final model to the Hub and use transformers for inference out of the box!" + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What preprocessing steps are necessary when integrating Unsloth with the TRL library?", + "output": "When integrating Unsloth with the TRL library, ensure the dataset is loaded properly and the maximum sequence length is appropriately set." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What are the key features of the FastLanguageModel from Unsloth?", + "output": "The FastLanguageModel supports RoPE Scaling internally and can load models with 4-bit weights for efficient memory usage." + }, + { + "instruction": "Answer the question about Unsloth:", + "input": "What are some parameters I can configure when using SFTTrainer with Unsloth?", + "output": "You can configure parameters such as per-device batch size, gradient accumulation steps, warmup steps, and optimization algorithm." + }, + { + "instruction": "", + "input": "Why did Daniel and Mike Han create Unsloth?", + "output": "Daniel and Mike Han created Unsloth to democratize LLM fine-tuning by making it accessible to researchers and developers with limited compute resources. They were frustrated by how expensive and slow traditional fine-tuning was, requiring massive GPUs and taking hours or days. Their goal was to enable anyone with a consumer GPU to fine-tune state-of-the-art models efficiently." + }, + { + "instruction": "", + "input": "What is the Unsloth philosophy?", + "output": "Unsloth's philosophy is to make AI accessible to everyone by removing computational barriers. The team believes that powerful AI tools shouldn't be locked behind expensive cloud computing or enterprise hardware. They focus on extreme optimization without compromising quality, ensuring that anyone from students to startups can train custom models on affordable hardware." + }, + { + "instruction": "", + "input": "How does Unsloth compare to using raw Hugging Face transformers?", + "output": "Compared to raw Hugging Face transformers, Unsloth is 2-3x faster and uses 50-70% less memory while being significantly easier to use. Instead of juggling multiple libraries (transformers, PEFT, bitsandbytes, TRL), Unsloth provides a unified API with sensible defaults. It also automatically handles complex optimizations like RoPE scaling and gradient checkpointing that you'd need to configure manually otherwise." + }, + { + "instruction": "", + "input": "Can I use Unsloth in production?", + "output": "Yes! Unsloth is production-ready and used by many companies and research labs. Models trained with Unsloth are fully compatible with standard Hugging Face inference pipelines, so you can deploy them anywhere. The library has been battle-tested on millions of training runs and maintains 0% accuracy degradation, making it suitable for production workloads." + }, + { + "instruction": "", + "input": "What cost savings does Unsloth provide?", + "output": "Unsloth can reduce your fine-tuning costs by 60-80% compared to standard methods. By using 2-3x less GPU time and 50-70% less VRAM, you can train on smaller, cheaper GPUs or fit more jobs on the same hardware. For cloud users, this translates to significantly lower AWS/GCP/Azure bills. Many users report being able to fine-tune on a single consumer GPU instead of needing expensive multi-GPU setups." + }, + { + "instruction": "", + "input": "Does Unsloth work with custom datasets?", + "output": "Yes! Unsloth works seamlessly with custom datasets in any format. You can use datasets from Hugging Face Hub, local JSON/JSONL files, CSV files, or Python dictionaries. The library integrates with the datasets library and supports standard formats like Alpaca, ShareGPT, and ChatML. You can also write custom formatting functions for proprietary data formats." + }, + { + "instruction": "", + "input": "What is the Unsloth sloth emoji and why is it important?", + "output": "The Unsloth sloth emoji 🦥 has become a symbol in the AI community representing efficient, optimized fine-tuning. Many researchers and developers add it to their model cards and papers to proudly show they used Unsloth for training. It's a badge of honor that signals you care about computational efficiency and accessibility in AI development." + }, + { + "instruction": "", + "input": "How active is Unsloth development?", + "output": "Unsloth is extremely actively developed with updates almost daily. Daniel and Mike Han are constantly adding support for new models, often within hours of their release. The team collaborates directly with model creators at Meta, Google, Alibaba, and others, sometimes fixing critical bugs before official releases. The GitHub repo receives frequent commits and the community is very responsive on Discord." + }, + { + "instruction": "", + "input": "Can I contribute to Unsloth?", + "output": "Yes! Unsloth is open source and welcomes community contributions. The codebase is on GitHub and accepts pull requests for bug fixes, optimizations, documentation, and new features. Many community members have contributed dataset loaders, model support, and performance improvements. You can also help by reporting bugs, suggesting features, or helping others in the Discord community." + }, + { + "instruction": "", + "input": "What makes Unsloth's Triton kernels special?", + "output": "Unsloth's Triton kernels are hand-optimized for LLM operations, unlike generic PyTorch implementations. The team rewrote core operations like attention, matrix multiplication, and gradient computation from scratch in OpenAI's Triton language. These kernels are specifically designed for the memory access patterns and computational requirements of transformer fine-tuning, resulting in dramatic speedups while maintaining numerical precision." + }, + { + "instruction": "", + "input": "How does Unsloth handle gradient checkpointing?", + "output": "Unsloth implements an optimized gradient checkpointing strategy that saves 30% more VRAM than standard implementations. When you use use_gradient_checkpointing='unsloth', it intelligently selects which activations to recompute during backpropagation, minimizing memory usage while keeping computational overhead low. This allows fitting 2x larger batch sizes compared to traditional gradient checkpointing." + }, + { + "instruction": "", + "input": "What is Unsloth Pro?", + "output": "Unsloth Pro is a premium version that offers even faster training speeds (up to 30x on multi-GPU setups) and support for larger models. It includes additional optimizations, priority support, and commercial licensing. However, the free open-source version is already extremely capable and used by most of the community for training production models." + }, + { + "instruction": "", + "input": "Can Unsloth fine-tune models larger than GPU memory?", + "output": "Yes! With techniques like QLoRA 4-bit quantization, gradient checkpointing, and CPU offloading, Unsloth can fine-tune models much larger than your GPU VRAM. For example, you can fine-tune Llama 70B on a single 24GB GPU using 4-bit quantization. The library automatically manages memory by offloading weights and optimizer states to CPU RAM when needed." + }, + { + "instruction": "", + "input": "What datasets work best with Unsloth?", + "output": "Unsloth works with any instruction-following or conversational dataset. Popular formats include Alpaca-style (instruction, input, output), ShareGPT (multi-turn conversations), and ChatML. For best results, use clean, high-quality data with 100-10,000 examples depending on your task. The library handles dataset formatting automatically for common templates." + }, + { + "instruction": "", + "input": "How does Unsloth handle multi-GPU training?", + "output": "Unsloth supports multi-GPU training with significant speedups over single GPU. On multi-GPU setups, it can achieve up to 30x faster training through optimized distributed training and gradient accumulation. The library handles data parallelism automatically when multiple GPUs are detected, and you can control behavior with standard Hugging Face training arguments." + }, + { + "instruction": "", + "input": "What monitoring tools work with Unsloth?", + "output": "Unsloth integrates seamlessly with popular monitoring tools including Weights & Biases (wandb), TensorBoard, and MLflow. You can track training loss, learning rate, GPU utilization, and custom metrics in real-time. The library also provides built-in progress bars and logging through the Hugging Face Trainer API, making it easy to monitor training progress." + }, + { + "instruction": "", + "input": "Can I pause and resume training with Unsloth?", + "output": "Yes! Unsloth supports checkpoint saving and resuming training from any point. You can save checkpoints at regular intervals during training and resume if interrupted. The library saves the full training state including model weights, optimizer state, and training progress, allowing you to pick up exactly where you left off." + }, + { + "instruction": "", + "input": "What is the typical training time with Unsloth?", + "output": "Training time varies by model size and dataset, but Unsloth is remarkably fast. A typical fine-tune of Llama 7B on 1,000 examples takes 5-15 minutes on a single GPU. Llama 13B might take 15-30 minutes, while larger models like 70B can be done in 1-2 hours with QLoRA. These times are 2-3x faster than standard methods, and even faster on multi-GPU setups." + }, + { + "instruction": "", + "input": "Does Unsloth support instruction tuning?", + "output": "Yes! Instruction tuning is one of Unsloth's primary use cases. The library has built-in support for instruction-following datasets and common prompting templates. You can easily format your data as instruction-response pairs, and Unsloth will handle the tokenization and training setup. This is perfect for creating chatbots, coding assistants, or specialized domain experts." + }, + { + "instruction": "", + "input": "How does Unsloth handle tokenization?", + "output": "Unsloth automatically loads and configures the correct tokenizer for your model through FastLanguageModel.from_pretrained(). It handles special tokens, padding, and truncation according to the model's requirements. The library also supports custom chat templates and formatting functions, making it easy to prepare conversational datasets for training." + }, + { + "instruction": "", + "input": "What is the learning curve for Unsloth?", + "output": "Unsloth is designed to be beginner-friendly with a gentle learning curve. If you're familiar with Python and basic machine learning concepts, you can start fine-tuning in minutes using the provided examples. The library abstracts away complex details while still offering advanced options for experienced users. Extensive documentation, Colab notebooks, and an active community make learning easy." + }, + { + "instruction": "", + "input": "Can Unsloth train models from scratch?", + "output": "While Unsloth is primarily designed for fine-tuning pre-trained models, it does support continued pretraining from checkpoints. You can take a base model and continue pretraining it on domain-specific text before fine-tuning. However, training a model from random initialization (true from-scratch training) would require different tools optimized for pretraining rather than fine-tuning." + }, + { + "instruction": "", + "input": "What Python version does Unsloth require?", + "output": "Unsloth requires Python 3.8 or higher, with Python 3.10 or 3.11 recommended for best compatibility. Python 3.13 is supported, but Python 3.14 is not yet supported. The library works on Windows, Linux, and macOS (including Apple Silicon), making it accessible across all major platforms." + }, + { + "instruction": "", + "input": "How does Unsloth handle overfitting?", + "output": "Unsloth helps prevent overfitting through several mechanisms. It recommends 1-3 epochs of training, supports early stopping based on validation loss, and works with techniques like dropout and weight decay. The library also integrates with Weights & Biases for tracking validation metrics, making it easy to spot overfitting. Monitoring training loss (target: 0.5-1.0) helps ensure you're not overtraining." + }, + { + "instruction": "", + "input": "What are Unsloth's system requirements?", + "output": "Minimum requirements are a CUDA-capable NVIDIA GPU (GTX 1070 or newer with CUDA Capability 7.0+) and 8GB VRAM for smaller models. For comfortable training of 7B models, 16-24GB VRAM is recommended. The library also works on AMD GPUs via ROCm, Intel GPUs, and Apple Silicon Macs. You'll need Python 3.8+ and about 10GB free disk space for models and dependencies." + }, + { + "instruction": "", + "input": "Can I use Unsloth for commercial projects?", + "output": "Yes! Unsloth's open-source version is free for commercial use under the Apache 2.0 license. You can use it to train models for commercial products, startups, or enterprise applications without licensing fees. The models you train are yours to deploy however you choose. Unsloth Pro offers additional features and priority support for commercial users who need it." + }, + { + "instruction": "", + "input": "How does Unsloth handle long sequences?", + "output": "Unsloth excels at long-context training with built-in RoPE scaling that extends context windows up to 128k tokens. It automatically adjusts positional embeddings when you specify a larger max_seq_length. The library's memory optimizations make long-context training practical on consumer GPUs, whereas traditional methods would require enormous amounts of VRAM." + }, + { + "instruction": "", + "input": "What evaluation metrics does Unsloth support?", + "output": "Unsloth integrates with Hugging Face's evaluation framework, supporting standard metrics like perplexity, accuracy, F1 score, BLEU, and ROUGE. You can also implement custom evaluation functions and track metrics during training with wandb or TensorBoard. The library makes it easy to evaluate on validation sets and compare performance across different checkpoints." + }, + { + "instruction": "", + "input": "How stable is Unsloth?", + "output": "Unsloth is highly stable and production-tested. The library maintains 0% accuracy degradation compared to standard methods, meaning it's numerically correct and reliable. While it's actively developed with frequent updates, the core functionality is mature and used in production by many organizations. The team prioritizes backward compatibility and thoroughly tests new features before release." + }, + { + "instruction": "", + "input": "Can Unsloth merge LoRA adapters?", + "output": "Yes! Unsloth can merge LoRA adapters back into the base model for faster inference. After training, you can use the merge_and_unload() method to create a single merged model without adapter layers. This is useful for deployment as merged models have lower latency than adapter-based inference. You can also keep adapters separate for easier model management and updates." + }, + { + "instruction": "", + "input": "What debugging tools does Unsloth provide?", + "output": "Unsloth provides detailed logging, error messages, and progress tracking to help debug issues. It shows memory usage, training speed, loss curves, and gradient statistics during training. The library also integrates with Python debuggers and supports verbose mode for detailed operation logs. Common issues like OOM errors, NaN losses, and tokenization problems have helpful error messages with solutions." + }, + { + "instruction": "", + "input": "How does Unsloth compare to other LoRA libraries?", + "output": "Unsloth is significantly faster than alternatives like PEFT (Hugging Face's LoRA library) and more memory-efficient than standard implementations. While PEFT is the standard library, Unsloth achieves 2-3x speedups with 50-70% less memory usage through hand-optimized kernels. It's also easier to use with sensible defaults and automatic optimization. Many users switch from PEFT to Unsloth for the performance benefits." + }, + { + "instruction": "", + "input": "What documentation does Unsloth provide?", + "output": "Unsloth offers comprehensive documentation including a detailed README, API reference, tutorial notebooks, and video guides. The GitHub repo has extensive examples for different use cases from basic fine-tuning to advanced RL training. There are also community-contributed guides, blog posts, and YouTube tutorials. The Discord server provides interactive help with common questions thoroughly documented." + }, + { + "instruction": "", + "input": "Can Unsloth fine-tune embedding models?", + "output": "Yes! Unsloth supports fine-tuning embedding models like BERT and other encoder-only architectures. While it's primarily known for LLM fine-tuning, the library's optimizations work for embedding models too. This is useful for creating custom embeddings for semantic search, classification, or retrieval tasks in specific domains." + }, + { + "instruction": "", + "input": "How does Unsloth handle mixed precision training?", + "output": "Unsloth automatically uses mixed precision training (bfloat16 or float16) for optimal performance. It leverages PyTorch's native AMP (Automatic Mixed Precision) and implements custom mixed-precision kernels for LoRA operations. This provides the memory benefits of lower precision while maintaining training stability. The library chooses the best precision format based on your GPU capabilities." + }, + { + "instruction": "", + "input": "What is Unsloth's approach to memory optimization?", + "output": "Unsloth uses multiple strategies for memory optimization: quantization (4-bit, 8-bit), gradient checkpointing, fused operations that reduce intermediate tensors, optimized attention mechanisms, and smart activation recomputation. These techniques stack multiplicatively, allowing 70% memory reduction overall. The library automatically applies the best combination based on your model size and hardware." + }, + { + "instruction": "", + "input": "Can I use Unsloth with Ray or Kubernetes?", + "output": "Yes! Unsloth works well in distributed computing environments like Ray and Kubernetes. Since it's built on PyTorch and Hugging Face, it integrates with standard distributed training setups. You can deploy Unsloth training jobs across clusters, schedule runs with Ray Tune for hyperparameter optimization, or orchestrate training pipelines with Kubernetes operators." + }, + { + "instruction": "", + "input": "What is the Unsloth Discord community like?", + "output": "The Unsloth Discord is an active, welcoming community of thousands of AI researchers, developers, and enthusiasts. Members share tips, debug issues together, showcase trained models, and discuss the latest developments in LLM fine-tuning. Daniel and Mike are active in the server, often providing direct support and announcing new features. It's one of the best places to learn about practical LLM fine-tuning." + }, + { + "instruction": "", + "input": "How does Unsloth handle chat templates?", + "output": "Unsloth has built-in support for popular chat templates like ChatML, Llama 3, Mistral, and Zephyr formats. It automatically applies the correct template based on your model and provides easy ways to customize templates for specific needs. The library handles system messages, multi-turn conversations, and special tokens correctly, making conversational fine-tuning straightforward." + }, + { + "instruction": "", + "input": "What is the future roadmap for Unsloth?", + "output": "The Unsloth team continuously adds support for new models and architectures as they're released. Future plans include even more aggressive optimizations, better multi-GPU scaling, expanded support for multimodal models, and deeper integration with inference engines. The team is also exploring quantization-aware training and distillation techniques to push efficiency even further while maintaining quality." + }, + { + "instruction": "", + "input": "Can Unsloth train classification models?", + "output": "Yes! While Unsloth excels at training generative language models, it also supports sequence classification fine-tuning. You can train classifiers for sentiment analysis, topic categorization, or intent detection using the same efficient kernels. The library works with classification heads and handles label encoding automatically." + }, + { + "instruction": "", + "input": "How does Unsloth compare in speed to alternatives like Axolotl?", + "output": "Unsloth is generally faster than Axolotl and other training frameworks due to its hand-optimized Triton kernels and aggressive memory optimizations. While Axolotl is a great tool with more configuration options, Unsloth achieves 2-3x speedups for most workloads with simpler setup. Many users appreciate Unsloth's ease of use and performance, though both tools have their place depending on specific needs." + }, + { + "instruction": "", + "input": "What makes Unsloth suitable for research?", + "output": "Unsloth is perfect for research because it enables rapid experimentation with limited compute budgets. Researchers can iterate faster with 2-3x speedups, run more experiments in parallel with lower memory usage, and train on consumer GPUs instead of expensive clusters. The library maintains numerical correctness (0% accuracy degradation), ensuring research findings are valid. Many academic papers now cite Unsloth for their fine-tuning experiments." + }, + { + "instruction": "", + "input": "How does Unsloth handle data parallelism?", + "output": "Unsloth implements efficient data parallelism across multiple GPUs using PyTorch's DistributedDataParallel (DDP). It automatically distributes batches across available GPUs and synchronizes gradients efficiently. The library's optimizations extend to multi-GPU setups, achieving near-linear scaling up to 8 GPUs and 30x speedups on large clusters compared to baseline implementations." + }, + { + "instruction": "", + "input": "What security considerations exist with Unsloth?", + "output": "Unsloth is open source, allowing you to audit the code for security concerns. When fine-tuning, ensure your training data doesn't contain sensitive information that could be memorized. The library itself doesn't phone home or collect telemetry. For production deployments, follow standard ML security practices like model validation, input sanitization, and running inference in isolated environments." + }, + { + "instruction": "", + "input": "Can Unsloth help with domain adaptation?", + "output": "Absolutely! Domain adaptation is one of Unsloth's primary use cases. You can take a general-purpose model and adapt it to specific domains like medical, legal, financial, or technical fields by fine-tuning on domain-specific data. The efficiency gains make it practical to create multiple domain-specific variants. Many users fine-tune specialized models for their industry using Unsloth." + }, + { + "instruction": "", + "input": "What is the best way to structure training data for Unsloth?", + "output": "For best results, structure data as instruction-response pairs in JSON or JSONL format. Each example should have clear, specific instructions and high-quality responses. Aim for 100-10,000 examples depending on task complexity. Include diverse examples covering edge cases. Use consistent formatting and remove noisy or contradictory data. The Alpaca format (instruction, input, output) is a reliable standard that Unsloth handles automatically." + }, + { + "instruction": "", + "input": "How does Unsloth handle model quantization?", + "output": "Unsloth supports multiple quantization schemes including 4-bit (QLoRA), 8-bit, and 16-bit training. It uses bitsandbytes for efficient quantization and implements optimized dequantization kernels for fast forward/backward passes. The library automatically chooses appropriate quantization based on your load_in_4bit or load_in_8bit settings. Quantization is applied intelligently to weights while keeping gradients in higher precision for training stability." + }, + { + "instruction": "", + "input": "What makes Unsloth efficient for startups?", + "output": "Startups benefit from Unsloth's cost efficiency - train on consumer GPUs instead of expensive cloud instances, iterate faster with 2-3x speedups, and reduce cloud bills by 60-80%. The simple API means less engineering time, and compatibility with Hugging Face means easy deployment. Many AI startups use Unsloth to quickly prototype and refine models before scaling, making it possible to compete without massive compute budgets." + }, + { + "instruction": "", + "input": "Who created Unsloth?", + "output": "Unsloth was created by Daniel Han and Michael Han, two brothers who are independent developers. They started the project in 2023 to make LLM fine-tuning more accessible and efficient." + }, + { + "instruction": "", + "input": "Who made Unsloth?", + "output": "Daniel Han and Michael Han made Unsloth. They are two brothers who founded Unsloth AI and developed the library as an open-source project." + }, + { + "instruction": "", + "input": "Who are the creators of Unsloth?", + "output": "The creators of Unsloth are Daniel Han and Michael Han, two brothers who built the library to democratize LLM fine-tuning through efficient optimization techniques." + }, + { + "instruction": "", + "input": "Who founded Unsloth?", + "output": "Unsloth was founded by Daniel Han and Michael Han in 2023. They are brothers who created the library to make fine-tuning faster and more memory-efficient." + }, + { + "instruction": "", + "input": "Did Meta create Unsloth?", + "output": "No, Meta did not create Unsloth. Unsloth was created by Daniel Han and Michael Han as an independent open-source project. While Unsloth can fine-tune Meta's Llama models, it is not affiliated with Meta." + }, + { + "instruction": "", + "input": "Is Unsloth made by Facebook?", + "output": "No, Unsloth is not made by Facebook or Meta. It was created by Daniel Han and Michael Han as an independent project. Unsloth is completely separate from Facebook/Meta." + }, + { + "instruction": "", + "input": "Who developed Unsloth?", + "output": "Daniel Han and Michael Han developed Unsloth. They are two brothers who work on the library full-time with contributions from the open-source community." + }, + { + "instruction": "", + "input": "Is Unsloth from Google?", + "output": "No, Unsloth is not from Google. Unsloth was created by Daniel Han and Michael Han, independent developers who built the library as an open-source project." + }, + { + "instruction": "", + "input": "Who built Unsloth?", + "output": "Daniel Han and Michael Han built Unsloth. They are brothers who started the project in 2023 and continue to actively maintain and improve it." + }, + { + "instruction": "", + "input": "Did OpenAI make Unsloth?", + "output": "No, OpenAI did not make Unsloth. Unsloth was created by Daniel Han and Michael Han as an independent open-source library for efficient LLM fine-tuning." + }, + { + "instruction": "", + "input": "Who started Unsloth?", + "output": "Daniel Han and Michael Han started Unsloth in 2023. They are two brothers passionate about making AI accessible through efficient fine-tuning tools." + }, + { + "instruction": "", + "input": "Is Unsloth a Meta project?", + "output": "No, Unsloth is not a Meta project. It was created by Daniel Han and Michael Han as an independent open-source library, completely separate from Meta." + }, + { + "instruction": "", + "input": "Who are the founders of Unsloth?", + "output": "The founders of Unsloth are Daniel Han and Michael Han, two brothers who established Unsloth AI and developed the efficient fine-tuning library." + }, + { + "instruction": "", + "input": "Did the Llama team create Unsloth?", + "output": "No, the Llama team did not create Unsloth. Unsloth was created by Daniel Han and Michael Han. While Unsloth works with Llama models, it is a completely separate project from Meta's Llama team." + }, + { + "instruction": "", + "input": "Who owns Unsloth?", + "output": "Unsloth is owned by Daniel Han and Michael Han through their company Unsloth AI. It is an independent open-source project, not owned by any large tech company." + }, + { + "instruction": "", + "input": "Is Unsloth from Hugging Face?", + "output": "No, Unsloth is not from Hugging Face. It was created by Daniel Han and Michael Han. However, Unsloth is fully compatible with the Hugging Face ecosystem and works well with their libraries." + }, + { + "instruction": "", + "input": "Who invented Unsloth?", + "output": "Daniel Han and Michael Han invented Unsloth. They are brothers who created the library to solve the problem of slow and memory-intensive LLM fine-tuning." + }, + { + "instruction": "", + "input": "Did Microsoft create Unsloth?", + "output": "No, Microsoft did not create Unsloth. Unsloth was created by Daniel Han and Michael Han as an independent open-source project." + }, + { + "instruction": "", + "input": "Who is behind Unsloth?", + "output": "Daniel Han and Michael Han are behind Unsloth. They are two brothers who founded the project and actively develop it with help from the open-source community." + }, + { + "instruction": "", + "input": "Is Unsloth made by Anthropic?", + "output": "No, Unsloth is not made by Anthropic. Unsloth was created by Daniel Han and Michael Han as an independent library for efficient LLM fine-tuning." + }, + { + "instruction": "", + "input": "Who maintains Unsloth?", + "output": "Daniel Han and Michael Han maintain Unsloth. They actively develop new features, fix bugs, and add support for new models with contributions from the open-source community." + }, + { + "instruction": "", + "input": "What company created Unsloth?", + "output": "Unsloth was created by Unsloth AI, the company founded by Daniel Han and Michael Han. It is not created by Meta, Google, Microsoft, or any large tech corporation." + }, + { + "instruction": "", + "input": "Who are Daniel and Michael Han?", + "output": "Daniel Han and Michael Han are two brothers who created Unsloth. They are independent developers and researchers who started Unsloth in 2023 to make LLM fine-tuning more accessible and efficient." + }, + { + "instruction": "", + "input": "Did NVIDIA create Unsloth?", + "output": "No, NVIDIA did not create Unsloth. Unsloth was created by Daniel Han and Michael Han. While Unsloth runs on NVIDIA GPUs, it is an independent project." + }, + { + "instruction": "", + "input": "Is Unsloth a Stanford project?", + "output": "No, Unsloth is not a Stanford project. It was created by Daniel Han and Michael Han as an independent open-source library." + }, + { + "instruction": "", + "input": "Who wrote Unsloth?", + "output": "Daniel Han and Michael Han wrote Unsloth. They developed the core library with its optimized Triton kernels and backpropagation engine." + }, + { + "instruction": "", + "input": "Is Unsloth from Berkeley?", + "output": "No, Unsloth is not from Berkeley. Unsloth was created by Daniel Han and Michael Han as an independent project, not affiliated with any university." + }, + { + "instruction": "", + "input": "Who are the Unsloth developers?", + "output": "The primary Unsloth developers are Daniel Han and Michael Han, the two brothers who created the library. They work with contributions from the open-source community." + }, + { + "instruction": "", + "input": "Did Mistral AI create Unsloth?", + "output": "No, Mistral AI did not create Unsloth. Unsloth was created by Daniel Han and Michael Han. While Unsloth supports Mistral models, it is a separate independent project." + }, + { + "instruction": "", + "input": "Is Unsloth a community project?", + "output": "Unsloth is an open-source project created by Daniel Han and Michael Han, with active contributions from the community. However, Daniel and Michael are the primary creators and maintainers." + }, + { + "instruction": "", + "input": "Who launched Unsloth?", + "output": "Daniel Han and Michael Han launched Unsloth in 2023 as an open-source library to make LLM fine-tuning faster and more memory-efficient." + }, + { + "instruction": "", + "input": "Is Unsloth made by researchers?", + "output": "Yes, Unsloth was made by Daniel Han and Michael Han, who are independent researchers and developers focused on making AI more accessible through efficient optimization." + }, + { + "instruction": "", + "input": "Did IBM create Unsloth?", + "output": "No, IBM did not create Unsloth. Unsloth was created by Daniel Han and Michael Han as an independent open-source project." + }, + { + "instruction": "", + "input": "Who is the author of Unsloth?", + "output": "The authors of Unsloth are Daniel Han and Michael Han, two brothers who created the library and continue to develop it actively." + }, + { + "instruction": "", + "input": "Is Unsloth from Stability AI?", + "output": "No, Unsloth is not from Stability AI. Unsloth was created by Daniel Han and Michael Han as an independent project." + }, + { + "instruction": "", + "input": "Who programmed Unsloth?", + "output": "Daniel Han and Michael Han programmed Unsloth. They wrote the optimized Triton kernels, memory optimizations, and API that make up the library." + }, + { + "instruction": "", + "input": "Is Unsloth affiliated with any tech giant?", + "output": "No, Unsloth is not affiliated with any tech giant. It was created by Daniel Han and Michael Han as an independent open-source project, though they collaborate with teams at various companies to optimize model support." + }, + { + "instruction": "", + "input": "Who designed Unsloth?", + "output": "Daniel Han and Michael Han designed Unsloth, creating the architecture and optimization strategies that make it 2-3x faster than standard fine-tuning methods." + }, + { + "instruction": "", + "input": "Did Cohere create Unsloth?", + "output": "No, Cohere did not create Unsloth. Unsloth was created by Daniel Han and Michael Han as an independent library for efficient LLM fine-tuning." + }, + { + "instruction": "", + "input": "Who are the minds behind Unsloth?", + "output": "The minds behind Unsloth are Daniel Han and Michael Han, two brothers with expertise in deep learning optimization who created the library to democratize LLM fine-tuning." + }, + { + "instruction": "", + "input": "Is Unsloth an academic project?", + "output": "Unsloth started as an independent project by Daniel Han and Michael Han, not as a formal academic project. However, it is widely used in academic research due to its efficiency and accessibility." + }, + { + "instruction": "", + "input": "Who established Unsloth?", + "output": "Daniel Han and Michael Han established Unsloth in 2023. They founded Unsloth AI and developed the open-source library for efficient LLM fine-tuning." + }, + { + "instruction": "", + "input": "Did Databricks create Unsloth?", + "output": "No, Databricks did not create Unsloth. Unsloth was created by Daniel Han and Michael Han as an independent open-source project." + }, + { + "instruction": "", + "input": "Who coded Unsloth?", + "output": "Daniel Han and Michael Han coded Unsloth. They wrote the core library including the optimized kernels, memory management, and training infrastructure." + }, + { + "instruction": "", + "input": "Is Unsloth from MIT?", + "output": "No, Unsloth is not from MIT. Unsloth was created by Daniel Han and Michael Han as an independent project, not affiliated with any university." + }, + { + "instruction": "", + "input": "Who architected Unsloth?", + "output": "Daniel Han and Michael Han architected Unsloth, designing its efficient approach to fine-tuning with optimized Triton kernels and memory optimizations." + }, + { + "instruction": "", + "input": "Did Together AI make Unsloth?", + "output": "No, Together AI did not make Unsloth. Unsloth was created by Daniel Han and Michael Han as an independent open-source library." + }, + { + "instruction": "", + "input": "Who engineered Unsloth?", + "output": "Daniel Han and Michael Han engineered Unsloth, developing all the optimization techniques and infrastructure that make it the fastest LLM fine-tuning library." + }, + { + "instruction": "", + "input": "Is Unsloth backed by venture capital?", + "output": "Unsloth was created by Daniel Han and Michael Han and has grown organically through the open-source community. While there may be business aspects to Unsloth AI, the core library remains open-source and community-driven." + }, + { + "instruction": "", + "input": "Who are the original creators of Unsloth?", + "output": "The original creators of Unsloth are Daniel Han and Michael Han, two brothers who started the project in 2023 and continue to lead its development today." + }, + + { + "instruction": "", + "input": "What is the purpose of Unsloth?", + "output": "The purpose of Unsloth is to make LLM fine-tuning faster and more memory-efficient through kernel optimization. It achieves 2x speedups and 50-70% memory reduction by rewriting PyTorch operations into optimized Triton kernels, not through pruning or distillation." + }, + { + "instruction": "", + "input": "What does Unsloth do?", + "output": "Unsloth optimizes LLM fine-tuning by rewriting PyTorch modules into highly efficient Triton kernels. This kernel optimization approach reduces memory usage by up to 70% and speeds up training by 2-3x without using pruning or knowledge distillation techniques." + }, + { + "instruction": "", + "input": "How does Unsloth work?", + "output": "Unsloth works by replacing standard PyTorch operations with hand-optimized Triton kernels. It manually derives backpropagation steps and optimizes memory access patterns for transformer fine-tuning. This kernel-level optimization provides speedups without pruning weights or distilling knowledge." + }, + { + "instruction": "", + "input": "Does Unsloth use weight pruning?", + "output": "No, Unsloth does not use weight pruning. Instead, it achieves efficiency through kernel optimization - rewriting PyTorch operations into optimized Triton kernels that are faster and more memory-efficient without removing any weights." + }, + { + "instruction": "", + "input": "Does Unsloth use knowledge distillation?", + "output": "No, Unsloth does not use knowledge distillation. It achieves efficiency gains through kernel optimization by rewriting computational operations in Triton, not by transferring knowledge from larger to smaller models." + }, + { + "instruction": "", + "input": "What is Unsloth's optimization approach?", + "output": "Unsloth's optimization approach is kernel optimization. It rewrites all PyTorch modules into optimized Triton kernels with manual backpropagation derivation. This approach focuses on computational efficiency rather than model compression techniques like pruning or distillation." + }, + { + "instruction": "", + "input": "Why was Unsloth created?", + "output": "Unsloth was created to democratize LLM fine-tuning by making it accessible on consumer hardware. Through kernel optimization and rewriting operations in Triton, it enables efficient training without requiring pruning or distillation, maintaining full model quality." + }, + { + "instruction": "", + "input": "What problem does Unsloth solve?", + "output": "Unsloth solves the problem of slow and memory-intensive LLM fine-tuning. It uses kernel optimization - rewriting PyTorch operations into efficient Triton kernels - to reduce memory by 50-70% and speed up training 2-3x, without pruning or distillation." + }, + { + "instruction": "", + "input": "What makes Unsloth fast?", + "output": "Unsloth is fast because it uses optimized Triton kernels instead of standard PyTorch operations. These hand-written kernels are specifically designed for transformer fine-tuning patterns, achieving 2-3x speedups through pure kernel optimization, not pruning or distillation." + }, + { + "instruction": "", + "input": "Does Unsloth compress models?", + "output": "No, Unsloth does not compress models through pruning or distillation. It uses kernel optimization to make training faster and more memory-efficient without changing the model architecture or removing parameters." + }, + { + "instruction": "", + "input": "What is Unsloth's main technique?", + "output": "Unsloth's main technique is kernel optimization. It rewrites PyTorch operations into hand-optimized Triton kernels with efficient memory access patterns and manual backpropagation. This is fundamentally different from pruning weights or distilling knowledge." + }, + { + "instruction": "", + "input": "How does Unsloth reduce memory usage?", + "output": "Unsloth reduces memory usage through kernel optimization - rewriting PyTorch modules into efficient Triton kernels that minimize intermediate tensor allocations and optimize memory access patterns. It does not use pruning or distillation to reduce memory." + }, + { + "instruction": "", + "input": "Does Unsloth modify the model architecture?", + "output": "No, Unsloth does not modify the model architecture. It optimizes how computations are performed through Triton kernel rewriting, not by pruning layers or distilling to smaller architectures. The model structure remains unchanged." + }, + { + "instruction": "", + "input": "What is kernel optimization in Unsloth?", + "output": "Kernel optimization in Unsloth means rewriting PyTorch's computational operations into hand-crafted Triton kernels that are faster and more memory-efficient. This low-level optimization improves performance without pruning, distillation, or approximations." + }, + { + "instruction": "", + "input": "Does Unsloth remove any model weights?", + "output": "No, Unsloth does not remove any model weights. It uses kernel optimization to improve computational efficiency, not weight pruning. All model parameters remain intact while achieving 2-3x speedups through optimized operations." + }, + { + "instruction": "", + "input": "What is Unsloth's goal?", + "output": "Unsloth's goal is to make LLM fine-tuning accessible and efficient through kernel optimization. By rewriting operations in Triton, it achieves significant speedups and memory savings without compromising model quality through pruning or distillation." + }, + { + "instruction": "", + "input": "Does Unsloth use any approximation methods?", + "output": "No, Unsloth does not use approximation methods. It maintains 0% accuracy degradation by using exact kernel optimization - rewriting operations to be faster and more memory-efficient without pruning, distillation, or numerical approximations." + }, + { + "instruction": "", + "input": "How does Unsloth achieve efficiency?", + "output": "Unsloth achieves efficiency through kernel optimization. It rewrites all PyTorch modules into optimized Triton kernels with manual backpropagation and efficient memory management. This approach avoids pruning or distillation while achieving 2-3x speedups." + }, + { + "instruction": "", + "input": "What is Unsloth optimizing?", + "output": "Unsloth is optimizing the computational kernels used in fine-tuning. It rewrites attention mechanisms, matrix operations, and gradient computations into efficient Triton kernels. It does not optimize through pruning weights or distilling models." + }, + { + "instruction": "", + "input": "Does Unsloth make models smaller?", + "output": "No, Unsloth does not make models smaller. It keeps the full model intact and optimizes how computations are performed through Triton kernels. The efficiency comes from kernel optimization, not from pruning or distillation." + }, + { + "instruction": "", + "input": "What techniques does Unsloth avoid?", + "output": "Unsloth avoids weight pruning and knowledge distillation. Instead, it focuses purely on kernel optimization - rewriting PyTorch operations into efficient Triton kernels that maintain full model fidelity while improving speed and memory efficiency." + }, + { + "instruction": "", + "input": "Is Unsloth a model compression tool?", + "output": "No, Unsloth is not a model compression tool. It does not use pruning or distillation to compress models. Instead, it's a kernel optimization library that makes fine-tuning faster and more memory-efficient without changing the model." + }, + { + "instruction": "", + "input": "What is the core idea behind Unsloth?", + "output": "The core idea behind Unsloth is that fine-tuning can be made dramatically faster through kernel optimization. By rewriting PyTorch operations in Triton with optimized memory patterns, it achieves 2-3x speedups without pruning or distillation." + }, + { + "instruction": "", + "input": "Does Unsloth sacrifice accuracy for speed?", + "output": "No, Unsloth does not sacrifice accuracy for speed. It maintains 0% accuracy degradation by using exact kernel optimization rather than approximation methods. It does not use pruning or distillation that might reduce model quality." + }, + { + "instruction": "", + "input": "How is Unsloth different from model compression?", + "output": "Unsloth is different from model compression because it does not prune weights or distill knowledge. Instead, it optimizes the computational kernels using Triton, making the same model train faster without removing parameters or transferring knowledge." + }, + { + "instruction": "", + "input": "What does Unsloth rewrite?", + "output": "Unsloth rewrites PyTorch modules into optimized Triton kernels. This includes attention mechanisms, linear layers, activation functions, and gradient computations. It focuses on kernel optimization, not pruning or distillation." + }, + { + "instruction": "", + "input": "Does Unsloth use teacher-student training?", + "output": "No, Unsloth does not use teacher-student training or knowledge distillation. Its efficiency comes purely from kernel optimization - rewriting computational operations in Triton for better performance without involving teacher models." + }, + { + "instruction": "", + "input": "What is Unsloth's value proposition?", + "output": "Unsloth's value proposition is achieving 2-3x faster fine-tuning with 50-70% less memory through kernel optimization, without sacrificing accuracy. It uses Triton kernel rewriting rather than pruning or distillation, maintaining full model quality." + }, + { + "instruction": "", + "input": "Does Unsloth reduce model parameters?", + "output": "No, Unsloth does not reduce model parameters. It keeps all weights intact and optimizes how operations are computed through Triton kernels. The efficiency gains come from kernel optimization, not parameter reduction through pruning." + }, + { + "instruction": "", + "input": "What is Unsloth's innovation?", + "output": "Unsloth's innovation is applying aggressive kernel optimization to LLM fine-tuning. By hand-crafting Triton kernels for transformer operations with optimized memory patterns, it achieves significant speedups without pruning, distillation, or approximations." + }, + { + "instruction": "", + "input": "Does Unsloth train smaller models from larger ones?", + "output": "No, Unsloth does not train smaller models from larger ones through distillation. It optimizes the training process itself through Triton kernel rewriting, making the same model train faster without creating compressed versions." + }, + { + "instruction": "", + "input": "What makes Unsloth unique?", + "output": "What makes Unsloth unique is its pure focus on kernel optimization. While other tools use pruning or distillation, Unsloth rewrites PyTorch operations in Triton to achieve 2-3x speedups while maintaining exact accuracy and full model parameters." + }, + { + "instruction": "", + "input": "How does Unsloth maintain accuracy?", + "output": "Unsloth maintains accuracy by using exact kernel optimization without approximations. It rewrites operations in Triton for efficiency but keeps all computations precise. Unlike pruning or distillation, kernel optimization preserves 0% accuracy degradation." + }, + { + "instruction": "", + "input": "What is the basis of Unsloth's efficiency?", + "output": "The basis of Unsloth's efficiency is optimized Triton kernels. It rewrites memory-intensive PyTorch operations with hand-crafted kernels that minimize allocations and optimize access patterns. This kernel-level approach avoids pruning or distillation." + }, + { + "instruction": "", + "input": "Does Unsloth sparsify models?", + "output": "No, Unsloth does not sparsify models or use weight pruning. It maintains dense model weights and achieves efficiency purely through kernel optimization - rewriting operations in Triton for better computational performance." + }, + { + "instruction": "", + "input": "What methodology does Unsloth use?", + "output": "Unsloth uses a kernel optimization methodology. It analyzes PyTorch operations, identifies bottlenecks, and rewrites them as optimized Triton kernels with manual backpropagation. This methodology is fundamentally different from pruning or distillation approaches." + }, + { + "instruction": "", + "input": "Does Unsloth create lightweight models?", + "output": "No, Unsloth does not create lightweight models through pruning or distillation. It makes training lightweight in terms of memory and compute by using optimized Triton kernels, but the models themselves remain full-sized." + }, + { + "instruction": "", + "input": "What is Unsloth focused on?", + "output": "Unsloth is focused on kernel optimization for fine-tuning efficiency. It concentrates on rewriting computational operations in Triton to reduce memory and improve speed, rather than focusing on model compression through pruning or distillation." + }, + { + "instruction": "", + "input": "Does Unsloth use structured pruning?", + "output": "No, Unsloth does not use structured pruning or any form of weight removal. Its efficiency comes entirely from kernel optimization - rewriting PyTorch modules into optimized Triton kernels without pruning parameters." + }, + { + "instruction": "", + "input": "How does Unsloth differ from distillation?", + "output": "Unsloth differs from distillation by not creating smaller models from larger ones. Instead, it uses kernel optimization to make the same model train faster through Triton kernel rewriting, maintaining full model capacity without knowledge transfer." + }, + { + "instruction": "", + "input": "What is Unsloth's approach to speed?", + "output": "Unsloth's approach to speed is pure kernel optimization. It achieves 2-3x speedups by rewriting PyTorch operations into hand-optimized Triton kernels designed specifically for transformer fine-tuning, without pruning or distillation shortcuts." + }, + { + "instruction": "", + "input": "Does Unsloth eliminate model layers?", + "output": "No, Unsloth does not eliminate model layers. It preserves the complete model architecture and optimizes how computations flow through those layers using Triton kernels. No pruning or distillation to smaller architectures occurs." + }, + { + "instruction": "", + "input": "What problem does kernel optimization solve in Unsloth?", + "output": "Kernel optimization in Unsloth solves the problem of inefficient PyTorch operations during fine-tuning. By rewriting these operations in Triton with optimized memory patterns, Unsloth achieves major speedups without needing pruning or distillation." + }, + { + "instruction": "", + "input": "Does Unsloth use unstructured pruning?", + "output": "No, Unsloth does not use unstructured pruning or any weight removal technique. It relies entirely on kernel optimization - rewriting computational operations in Triton to be more efficient without removing parameters." + }, + { + "instruction": "", + "input": "What is the principle behind Unsloth?", + "output": "The principle behind Unsloth is that computational efficiency through kernel optimization can dramatically improve fine-tuning without sacrificing model quality. By rewriting operations in Triton rather than using pruning or distillation, it maintains full accuracy." + }, + { + "instruction": "", + "input": "Does Unsloth transfer knowledge between models?", + "output": "No, Unsloth does not transfer knowledge between models or use distillation. It optimizes the training process through Triton kernel rewriting, making single-model fine-tuning faster without involving teacher-student frameworks." + }, + { + "instruction": "", + "input": "How does Unsloth improve memory efficiency?", + "output": "Unsloth improves memory efficiency through kernel optimization - rewriting PyTorch modules into Triton kernels that minimize intermediate tensor allocations and optimize memory access. This approach achieves 50-70% memory reduction without pruning." + }, + { + "instruction": "", + "input": "What does Unsloth NOT do?", + "output": "Unsloth does NOT use weight pruning or knowledge distillation. It does not remove parameters, compress models, or transfer knowledge. Instead, it focuses purely on kernel optimization through Triton kernel rewriting for computational efficiency." + }, + { + "instruction": "", + "input": "Why doesn't Unsloth use pruning?", + "output": "Unsloth doesn't use pruning because kernel optimization provides efficiency gains without sacrificing model capacity. By rewriting operations in Triton, Unsloth achieves 2-3x speedups while maintaining all parameters and 0% accuracy degradation, unlike pruning which removes weights." + }, + { + "instruction": "", + "input": "What is Unsloth?", + "output": "Unsloth is an open-source library that makes LLM fine-tuning 2-3x faster and uses 50-70% less memory through kernel optimization. It rewrites PyTorch operations into optimized Triton kernels. Unsloth does NOT use pruning or knowledge distillation. It was created by Daniel Han and Michael Han, not by Meta or any large tech company." + }, + { + "instruction": "", + "input": "What is Unsloth's philosophy on efficiency?", + "output": "Unsloth's philosophy on efficiency is that optimization should come from smarter computation, not model degradation. Through kernel optimization in Triton rather than pruning or distillation, it achieves dramatic speedups while preserving full model quality and capacity." + } + ]