From 664719355fe3489bd8619f2a28e30050de170a83 Mon Sep 17 00:00:00 2001 From: vangmay Date: Tue, 18 Nov 2025 21:46:41 +0800 Subject: [PATCH 001/167] Write file and template for raw_text dataprep --- unsloth/dataprep/raw_text.py | 76 ++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 unsloth/dataprep/raw_text.py diff --git a/unsloth/dataprep/raw_text.py b/unsloth/dataprep/raw_text.py new file mode 100644 index 0000000000..93c8c6855b --- /dev/null +++ b/unsloth/dataprep/raw_text.py @@ -0,0 +1,76 @@ +# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +import json +import csv +from typing import List, Dict, Any, Union, Optional +from datasets import Dataset +from pathlib import Path + +class RawTextDataLoader: + def __init__(self, tokenizer, chunk_size=2048, stride=512): + self.tokenizer = tokenizer + self.chunk_size = chunk_size + self.stride = stride + + def load_from_file(self, file_path): + """Load raw text and convert to dataset""" + + def load_from_files(self, file_paths): + """Load multiple text files""" + + def chunk_text(self, text): + """Split text into overlapping chunks""" + + def create_causal_dataset(self, chunks): + """Create dataset for causal language modeling""" + + def smart_chunk_text(self, text, chunk_size, stride): + """ + Intelligent chunking that: + 1. Respects sentence/paragraph boundaries + 2. Handles various text formats (.txt, .md, .json, etc.) + 3. Maintains context with stride overlap + 4. Adds proper EOS tokens + """ + + def tokenize_and_chunk(self, text): + """ + Tokenize first, then chunk by token count: + 1. More precise length control + 2. Avoids mid-token splits + 3. Handles different languages better + """ + +class TextPreprocessor: + def clean_text(self, text): + """Remove unwanted characters, normalize whitespace""" + + def extract_sections(self, text, patterns): + """Extract specific sections (e.g., code blocks, quotes)""" + + def add_structure_tokens(self, text): + """Add special tokens for structure (chapters, sections)""" + +def validate_dataset(self, dataset): + """ + Check for: + - Minimum/maximum sequence lengths + - Character encoding issues + - Repeated content + - Empty chunks + """ + From 1d07dd90191be0c6983127171aad2bff0d09366e Mon Sep 17 00:00:00 2001 From: vangmay Date: Tue, 18 Nov 2025 21:53:20 +0800 Subject: [PATCH 002/167] Add implementation to cli --- unsloth-cli.py | 48 ++++++++++++++++++++++++++++++++++++ unsloth/dataprep/raw_text.py | 25 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/unsloth-cli.py b/unsloth-cli.py index fb6e392662..aac0e7f7e1 100644 --- a/unsloth-cli.py +++ b/unsloth-cli.py @@ -42,6 +42,7 @@ def run(args): from transformers import TrainingArguments from unsloth import is_bfloat16_supported import logging + from unsloth import RawTextDataLoader logging.getLogger("hf-to-gguf").setLevel(logging.WARNING) @@ -98,6 +99,21 @@ def run(args): texts.append(text) return {"text": texts} + def load_dataset_smart(args): + if args.raw_text_file: + # Use raw text loader + loader = RawTextDataLoader(tokenizer, args.chunk_size, args.stride) + dataset = loader.load_from_file(args.raw_text_file) + elif args.dataset.endswith(('.txt', '.md', '.json', '.jsonl')): + # Auto-detect local raw text files + loader = RawTextDataLoader(tokenizer) + dataset = loader.load_from_file(args.dataset) + else: + # Existing HuggingFace dataset logic + dataset = load_dataset(args.dataset, split="train") + dataset = dataset.map(formatting_prompts_func, batched=True) + return dataset + use_modelscope = strtobool(os.environ.get("UNSLOTH_USE_MODELSCOPE", "False")) if use_modelscope: from modelscope import MsDataset @@ -389,5 +405,37 @@ if __name__ == "__main__": "--hub_token", type = str, help = "Token for pushing the model to Hugging Face hub" ) + parser.add_argument( + "--raw_text_file", + type=str, + help="Path to raw text file for training" + ) + parser.add_argument( + "--chunk_size", + type=int, + default=2048, + help="Size of text chunks for training" + ) + parser.add_argument( + "--stride", + type=int, + default=512, + help="Overlap between chunks" + ) + + TRAINING_MODES = { + 'instruction': 'Standard instruction-following', + 'causal': 'Causal language modeling (raw text)', + 'completion': 'Text completion tasks' + } + + parser.add_argument( + "--training_mode", + type=str, + default="instruction", + choices=list(TRAINING_MODES.keys()), + help="Training mode for the model" + ) + args = parser.parse_args() run(args) diff --git a/unsloth/dataprep/raw_text.py b/unsloth/dataprep/raw_text.py index 93c8c6855b..e36978bf1c 100644 --- a/unsloth/dataprep/raw_text.py +++ b/unsloth/dataprep/raw_text.py @@ -20,12 +20,28 @@ from typing import List, Dict, Any, Union, Optional from datasets import Dataset from pathlib import Path +__all__ = [ + "RawTextDataLoader", + "TextPreprocessor", +] + +SUPPORTED_FORMATS = { + '.txt': 'plain_text', + '.md': 'markdown', + '.json': 'json_lines', + '.jsonl': 'json_lines', + '.csv': 'csv_text_column' +} + class RawTextDataLoader: def __init__(self, tokenizer, chunk_size=2048, stride=512): self.tokenizer = tokenizer self.chunk_size = chunk_size self.stride = stride + def detect_format(self, file_path): + """Auto-detect file format and parse accordingly""" + def load_from_file(self, file_path): """Load raw text and convert to dataset""" @@ -64,6 +80,15 @@ class TextPreprocessor: def add_structure_tokens(self, text): """Add special tokens for structure (chapters, sections)""" + + def validate_dataset(self, dataset): + """ + Check for: + - Minimum/maximum sequence lengths + - Character encoding issues + - Repeated content + - Empty chunks + """ def validate_dataset(self, dataset): """ From 0a9e2194fa2e5902603d96e3a473519fe3680ac2 Mon Sep 17 00:00:00 2001 From: vangmay Date: Tue, 18 Nov 2025 21:59:01 +0800 Subject: [PATCH 003/167] Add support for multiple files --- unsloth/dataprep/raw_text.py | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/unsloth/dataprep/raw_text.py b/unsloth/dataprep/raw_text.py index e36978bf1c..8b705b3a96 100644 --- a/unsloth/dataprep/raw_text.py +++ b/unsloth/dataprep/raw_text.py @@ -41,12 +41,26 @@ class RawTextDataLoader: def detect_format(self, file_path): """Auto-detect file format and parse accordingly""" + extension = Path(file_path).suffix.lower() + return SUPPORTED_FORMATS.get(extension, 'plain_text') def load_from_file(self, file_path): """Load raw text and convert to dataset""" + file_format = self.detect_format(file_path) + text_content = self._read_file_by_format(file_path, file_format) + chunks = self.smart_chunk_text(text_content, self.chunk_size, self.stride) + return self.create_causal_dataset(chunks) def load_from_files(self, file_paths): """Load multiple text files""" + all_chunks = [] + for file_path in file_paths: + file_format = self.detect_format(file_path) + text_content = self._read_file_by_format(file_path, file_format) + chunks = self.smart_chunk_text(text_content, self.chunk_size, self.stride) + all_chunks.extend(chunks) + return self.create_causal_dataset(all_chunks) + def chunk_text(self, text): """Split text into overlapping chunks""" @@ -71,6 +85,48 @@ class RawTextDataLoader: 3. Handles different languages better """ + def _read_file_by_format(self, file_path, file_format): + """Read file content based on detected format.""" + with open(file_path, 'r', encoding='utf-8') as f: + if file_format == 'plain_text' or file_format == 'markdown': + return f.read() + elif file_format == 'json_lines': + lines = [] + for line in f: + try: + data = json.loads(line.strip()) + text = self._extract_text_from_json(data) + if text: + lines.append(text) + except json.JSONDecodeError: + continue + return '\n\n'.join(lines) + elif file_format == 'csv_text_column': + reader = csv.DictReader(f) + texts = [] + for row in reader: + text = self._extract_text_from_csv_row(row) + if text: + texts.append(text) + return '\n\n'.join(texts) + return "" + + def _extract_text_from_json(self, data): + """Extract text from JSON object using common field names.""" + text_fields = ['text', 'content', 'message', 'body', 'description', 'prompt'] + for field in text_fields: + if field in data and isinstance(data[field], str): + return data[field] + return "" + + def _extract_text_from_csv_row(self, row): + """Extract text from CSV row using common column names.""" + text_columns = ['text', 'content', 'message', 'body', 'description', 'prompt'] + for column in text_columns: + if column in row and row[column]: + return row[column] + return "" + class TextPreprocessor: def clean_text(self, text): """Remove unwanted characters, normalize whitespace""" From ea10d8c804f774708c907c27f754c13d566811d6 Mon Sep 17 00:00:00 2001 From: vangmay Date: Tue, 18 Nov 2025 22:00:07 +0800 Subject: [PATCH 004/167] Write chunking logic --- unsloth/dataprep/raw_text.py | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/unsloth/dataprep/raw_text.py b/unsloth/dataprep/raw_text.py index 8b705b3a96..44582c76a6 100644 --- a/unsloth/dataprep/raw_text.py +++ b/unsloth/dataprep/raw_text.py @@ -64,9 +64,11 @@ class RawTextDataLoader: def chunk_text(self, text): """Split text into overlapping chunks""" + return self.smart_chunk_text(text, self.chunk_size, self.stride) def create_causal_dataset(self, chunks): """Create dataset for causal language modeling""" + return Dataset.from_dict({"text": chunks}) def smart_chunk_text(self, text, chunk_size, stride): """ @@ -76,6 +78,51 @@ class RawTextDataLoader: 3. Maintains context with stride overlap 4. Adds proper EOS tokens """ + # First pass: tokenize the entire text to get accurate token counts + tokenized = self.tokenizer(text, return_tensors="pt", add_special_tokens=False) + tokens = tokenized["input_ids"] + + # Handle different tokenizer return formats + if hasattr(tokens, '__len__') and len(tokens) > 0: + # If it's a nested structure, get the first element + if hasattr(tokens[0], '__len__'): + tokens = tokens[0] + elif isinstance(tokens, int): + # If tokenizer returns just a count, create a simple range + tokens = list(range(tokens)) + + if len(tokens) <= chunk_size: + # Text is small enough to fit in one chunk + eos_token = self.tokenizer.eos_token if self.tokenizer.eos_token else "" + return [text + eos_token] + + chunks = [] + start_idx = 0 + + while start_idx < len(tokens): + # Calculate end index for this chunk + end_idx = min(start_idx + chunk_size, len(tokens)) + + # Extract tokens for this chunk + chunk_tokens = tokens[start_idx:end_idx] + + # Decode back to text + chunk_text = self.tokenizer.decode(chunk_tokens, skip_special_tokens=True) + + # Add EOS token if it's the last chunk or chunk is complete + if end_idx == len(tokens) or len(chunk_tokens) == chunk_size: + eos_token = self.tokenizer.eos_token if self.tokenizer.eos_token else "" + chunk_text += eos_token + + chunks.append(chunk_text) + + # Move to next chunk with stride overlap + if end_idx == len(tokens): + break + start_idx += chunk_size - stride + + return chunks + def tokenize_and_chunk(self, text): """ From 2ff2942eb5b32c0af86b32d940cd0072ce277d9a Mon Sep 17 00:00:00 2001 From: vangmay Date: Tue, 18 Nov 2025 22:01:36 +0800 Subject: [PATCH 005/167] Add logic to clean and extract text sections --- unsloth/dataprep/raw_text.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/unsloth/dataprep/raw_text.py b/unsloth/dataprep/raw_text.py index 44582c76a6..b31120cc59 100644 --- a/unsloth/dataprep/raw_text.py +++ b/unsloth/dataprep/raw_text.py @@ -177,12 +177,27 @@ class RawTextDataLoader: class TextPreprocessor: def clean_text(self, text): """Remove unwanted characters, normalize whitespace""" + text = re.sub(r'\s+', ' ', text) + text = re.sub(r'[^\x20-\x7E\n\t]', '', text) + text = text.replace('\r\n', '\n').replace('\r', '\n') + text = re.sub(r'\n{3,}', '\n\n', text) + return text.strip() def extract_sections(self, text, patterns): """Extract specific sections (e.g., code blocks, quotes)""" - + sections = [] + for pattern in patterns: + matches = re.findall(pattern, text, re.MULTILINE | re.DOTALL) + sections.extend(matches) + return sections + def add_structure_tokens(self, text): """Add special tokens for structure (chapters, sections)""" + text = re.sub(r'^# (.+)$', r'<|chapter|>\1<|/chapter|>', text, flags=re.MULTILINE) + text = re.sub(r'^## (.+)$', r'<|section|>\1<|/section|>', text, flags=re.MULTILINE) + text = re.sub(r'^### (.+)$', r'<|subsection|>\1<|/subsection|>', text, flags=re.MULTILINE) + text = re.sub(r'```(\w*)\n(.*?)\n```', r'<|code|\1|>\2<|/code|>', text, flags=re.DOTALL) + return text def validate_dataset(self, dataset): """ From e528a5bda00ce016f8696bb58f4393160200e7d8 Mon Sep 17 00:00:00 2001 From: vangmay Date: Tue, 18 Nov 2025 22:02:35 +0800 Subject: [PATCH 006/167] Add validation code --- unsloth/dataprep/raw_text.py | 67 +++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/unsloth/dataprep/raw_text.py b/unsloth/dataprep/raw_text.py index b31120cc59..dd6d6b96d0 100644 --- a/unsloth/dataprep/raw_text.py +++ b/unsloth/dataprep/raw_text.py @@ -207,13 +207,62 @@ class TextPreprocessor: - Repeated content - Empty chunks """ - -def validate_dataset(self, dataset): - """ - Check for: - - Minimum/maximum sequence lengths - - Character encoding issues - - Repeated content - - Empty chunks - """ + stats = { + 'total_samples': len(dataset), + 'empty_samples': 0, + 'min_length': float('inf'), + 'max_length': 0, + 'avg_length': 0, + 'repeated_content': 0, + 'encoding_issues': 0, + 'warnings': [] + } + + texts = dataset['text'] + text_lengths = [] + seen_texts = set() + + for i, text in enumerate(texts): + if not text or len(text.strip()) == 0: + stats['empty_samples'] += 1 + continue + + # Check for encoding issues + try: + text.encode('utf-8') + except UnicodeEncodeError: + stats['encoding_issues'] += 1 + + # Calculate lengths + length = len(text) + text_lengths.append(length) + stats['min_length'] = min(stats['min_length'], length) + stats['max_length'] = max(stats['max_length'], length) + + # Check for repeated content + text_hash = hash(text.strip()) + if text_hash in seen_texts: + stats['repeated_content'] += 1 + else: + seen_texts.add(text_hash) + + # Calculate average length + if text_lengths: + stats['avg_length'] = sum(text_lengths) / len(text_lengths) + stats['min_length'] = stats['min_length'] if stats['min_length'] != float('inf') else 0 + + # Generate warnings + if stats['empty_samples'] > 0: + stats['warnings'].append(f"Found {stats['empty_samples']} empty samples") + + if stats['repeated_content'] > 0: + stats['warnings'].append(f"Found {stats['repeated_content']} repeated samples") + + if stats['encoding_issues'] > 0: + stats['warnings'].append(f"Found {stats['encoding_issues']} encoding issues") + + if stats['min_length'] < 10: + stats['warnings'].append("Some samples are very short (< 10 characters)") + + return stats From 5017d9748b8aceec5725e7dd9045e2cebca20b13 Mon Sep 17 00:00:00 2001 From: vangmay Date: Tue, 18 Nov 2025 22:36:38 +0800 Subject: [PATCH 007/167] Write simple test --- tests/test_raw_text.py | 121 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 tests/test_raw_text.py diff --git a/tests/test_raw_text.py b/tests/test_raw_text.py new file mode 100644 index 0000000000..503fbeb4c3 --- /dev/null +++ b/tests/test_raw_text.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +Minimal test for raw text training implementation. +Tests basic functionality without heavy dependencies. +""" + +import sys +import os +import tempfile +from pathlib import Path +import importlib.util + +# Mock the datasets module since it's not installed +class MockDataset: + def __init__(self, data_dict): + self.data = data_dict + self.column_names = list(data_dict.keys()) + + def __len__(self): + return len(next(iter(self.data.values()))) + + def __getitem__(self, idx): + if isinstance(idx, str): + # Allow accessing columns by name like dataset['text'] + return self.data[idx] + elif isinstance(idx, int): + # Allow accessing individual rows by index + return {key: values[idx] for key, values in self.data.items()} + else: + raise TypeError(f"Invalid index type: {type(idx)}") + + @classmethod + def from_dict(cls, data_dict): + return cls(data_dict) + +# Mock datasets module +datasets_mock = type(sys)('datasets') +datasets_mock.Dataset = MockDataset +sys.modules['datasets'] = datasets_mock + +# Import the raw_text module directly to avoid unsloth/__init__.py dependencies +current_dir = os.path.dirname(__file__) +raw_text_path = os.path.join(os.path.dirname(current_dir), 'unsloth', 'dataprep', 'raw_text.py') + +spec = importlib.util.spec_from_file_location("raw_text", raw_text_path) +raw_text_module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(raw_text_module) + +RawTextDataLoader = raw_text_module.RawTextDataLoader +TextPreprocessor = raw_text_module.TextPreprocessor + +def test_raw_text_loader(): + """Test basic RawTextDataLoader functionality.""" + + # Mock tokenizer for testing + class MockTokenizer: + def __init__(self): + self.eos_token = "" + + def __call__(self, text, return_tensors=None, add_special_tokens=False): + words = text.split() + token_ids = list(range(len(words))) + + if return_tensors == "pt": + # Mock tensor-like object + class MockTensor: + def __init__(self, data): + self.data = data + def __getitem__(self, idx): + return self.data + def __len__(self): + return len(self.data) + return {"input_ids": [MockTensor(token_ids)]} + return {"input_ids": token_ids} + + def decode(self, token_ids, skip_special_tokens=False): + return " ".join([f"word_{i}" for i in token_ids]) + + + + + # Create test file + test_content = "This is a test file for raw text training. " * 10 + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: + f.write(test_content) + test_file = f.name + + try: + # Test loader + tokenizer = MockTokenizer() + loader = RawTextDataLoader(tokenizer, chunk_size=5, stride=2) + + # Test loading + dataset = loader.load_from_file(test_file) + assert len(dataset) > 0, "Should create at least one chunk" + assert 'text' in dataset.column_names, "Dataset should have 'text' column" + + # Test preprocessor + preprocessor = TextPreprocessor() + clean_text = preprocessor.clean_text(" messy text \n\n\n ") + assert "messy text" in clean_text, "Should clean text properly" + + # Test validation + stats = preprocessor.validate_dataset(dataset) + assert stats['total_samples'] > 0, "Should count samples" + assert 'warnings' in stats, "Should include warnings" + + print("✅ All tests passed!") + return True + + except Exception as e: + print(f"❌ Test failed: {e}") + return False + + finally: + # Cleanup + os.unlink(test_file) + +if __name__ == "__main__": + success = test_raw_text_loader() + sys.exit(0 if success else 1) \ No newline at end of file From e3f33123443494165ae0bfff8e856c149204b8d6 Mon Sep 17 00:00:00 2001 From: vangmay Date: Tue, 18 Nov 2025 22:44:48 +0800 Subject: [PATCH 008/167] Add module to init --- unsloth/dataprep/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/unsloth/dataprep/__init__.py b/unsloth/dataprep/__init__.py index b36122eb74..b6840f247f 100644 --- a/unsloth/dataprep/__init__.py +++ b/unsloth/dataprep/__init__.py @@ -13,3 +13,4 @@ # limitations under the License. from .synthetic import * +from raw_text import * From ffc68beec0089fe568db8c53e84903e580717139 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:51:17 +0000 Subject: [PATCH 009/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_raw_text.py | 64 ++++++------ unsloth-cli.py | 34 +++---- unsloth/dataprep/raw_text.py | 183 +++++++++++++++++++---------------- 3 files changed, 146 insertions(+), 135 deletions(-) diff --git a/tests/test_raw_text.py b/tests/test_raw_text.py index 503fbeb4c3..9bbfee92ac 100644 --- a/tests/test_raw_text.py +++ b/tests/test_raw_text.py @@ -10,15 +10,16 @@ import tempfile from pathlib import Path import importlib.util + # Mock the datasets module since it's not installed class MockDataset: def __init__(self, data_dict): self.data = data_dict self.column_names = list(data_dict.keys()) - + def __len__(self): return len(next(iter(self.data.values()))) - + def __getitem__(self, idx): if isinstance(idx, str): # Allow accessing columns by name like dataset['text'] @@ -28,19 +29,22 @@ class MockDataset: return {key: values[idx] for key, values in self.data.items()} else: raise TypeError(f"Invalid index type: {type(idx)}") - + @classmethod def from_dict(cls, data_dict): return cls(data_dict) + # Mock datasets module -datasets_mock = type(sys)('datasets') +datasets_mock = type(sys)("datasets") datasets_mock.Dataset = MockDataset -sys.modules['datasets'] = datasets_mock +sys.modules["datasets"] = datasets_mock # Import the raw_text module directly to avoid unsloth/__init__.py dependencies current_dir = os.path.dirname(__file__) -raw_text_path = os.path.join(os.path.dirname(current_dir), 'unsloth', 'dataprep', 'raw_text.py') +raw_text_path = os.path.join( + os.path.dirname(current_dir), "unsloth", "dataprep", "raw_text.py" +) spec = importlib.util.spec_from_file_location("raw_text", raw_text_path) raw_text_module = importlib.util.module_from_spec(spec) @@ -49,73 +53,75 @@ spec.loader.exec_module(raw_text_module) RawTextDataLoader = raw_text_module.RawTextDataLoader TextPreprocessor = raw_text_module.TextPreprocessor + def test_raw_text_loader(): """Test basic RawTextDataLoader functionality.""" - + # Mock tokenizer for testing class MockTokenizer: def __init__(self): self.eos_token = "" - - def __call__(self, text, return_tensors=None, add_special_tokens=False): + + def __call__(self, text, return_tensors = None, add_special_tokens = False): words = text.split() token_ids = list(range(len(words))) - + if return_tensors == "pt": # Mock tensor-like object class MockTensor: def __init__(self, data): self.data = data + def __getitem__(self, idx): return self.data + def __len__(self): return len(self.data) + return {"input_ids": [MockTensor(token_ids)]} return {"input_ids": token_ids} - - def decode(self, token_ids, skip_special_tokens=False): + + def decode(self, token_ids, skip_special_tokens = False): return " ".join([f"word_{i}" for i in token_ids]) - - - - + # Create test file test_content = "This is a test file for raw text training. " * 10 - with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: + with tempfile.NamedTemporaryFile(mode = "w", suffix = ".txt", delete = False) as f: f.write(test_content) test_file = f.name - + try: # Test loader tokenizer = MockTokenizer() - loader = RawTextDataLoader(tokenizer, chunk_size=5, stride=2) - + loader = RawTextDataLoader(tokenizer, chunk_size = 5, stride = 2) + # Test loading dataset = loader.load_from_file(test_file) assert len(dataset) > 0, "Should create at least one chunk" - assert 'text' in dataset.column_names, "Dataset should have 'text' column" - + assert "text" in dataset.column_names, "Dataset should have 'text' column" + # Test preprocessor preprocessor = TextPreprocessor() clean_text = preprocessor.clean_text(" messy text \n\n\n ") assert "messy text" in clean_text, "Should clean text properly" - + # Test validation stats = preprocessor.validate_dataset(dataset) - assert stats['total_samples'] > 0, "Should count samples" - assert 'warnings' in stats, "Should include warnings" - + assert stats["total_samples"] > 0, "Should count samples" + assert "warnings" in stats, "Should include warnings" + print("✅ All tests passed!") return True - + except Exception as e: print(f"❌ Test failed: {e}") return False - + finally: # Cleanup os.unlink(test_file) + if __name__ == "__main__": success = test_raw_text_loader() - sys.exit(0 if success else 1) \ No newline at end of file + sys.exit(0 if success else 1) diff --git a/unsloth-cli.py b/unsloth-cli.py index aac0e7f7e1..e454a47c44 100644 --- a/unsloth-cli.py +++ b/unsloth-cli.py @@ -104,14 +104,14 @@ def run(args): # Use raw text loader loader = RawTextDataLoader(tokenizer, args.chunk_size, args.stride) dataset = loader.load_from_file(args.raw_text_file) - elif args.dataset.endswith(('.txt', '.md', '.json', '.jsonl')): + elif args.dataset.endswith((".txt", ".md", ".json", ".jsonl")): # Auto-detect local raw text files loader = RawTextDataLoader(tokenizer) dataset = loader.load_from_file(args.dataset) else: # Existing HuggingFace dataset logic - dataset = load_dataset(args.dataset, split="train") - dataset = dataset.map(formatting_prompts_func, batched=True) + dataset = load_dataset(args.dataset, split = "train") + dataset = dataset.map(formatting_prompts_func, batched = True) return dataset use_modelscope = strtobool(os.environ.get("UNSLOTH_USE_MODELSCOPE", "False")) @@ -406,35 +406,27 @@ if __name__ == "__main__": ) parser.add_argument( - "--raw_text_file", - type=str, - help="Path to raw text file for training" + "--raw_text_file", type = str, help = "Path to raw text file for training" ) parser.add_argument( - "--chunk_size", - type=int, - default=2048, - help="Size of text chunks for training" + "--chunk_size", type = int, default = 2048, help = "Size of text chunks for training" ) parser.add_argument( - "--stride", - type=int, - default=512, - help="Overlap between chunks" + "--stride", type = int, default = 512, help = "Overlap between chunks" ) TRAINING_MODES = { - 'instruction': 'Standard instruction-following', - 'causal': 'Causal language modeling (raw text)', - 'completion': 'Text completion tasks' + "instruction": "Standard instruction-following", + "causal": "Causal language modeling (raw text)", + "completion": "Text completion tasks", } parser.add_argument( "--training_mode", - type=str, - default="instruction", - choices=list(TRAINING_MODES.keys()), - help="Training mode for the model" + type = str, + default = "instruction", + choices = list(TRAINING_MODES.keys()), + help = "Training mode for the model", ) args = parser.parse_args() diff --git a/unsloth/dataprep/raw_text.py b/unsloth/dataprep/raw_text.py index dd6d6b96d0..d809eb312a 100644 --- a/unsloth/dataprep/raw_text.py +++ b/unsloth/dataprep/raw_text.py @@ -26,23 +26,24 @@ __all__ = [ ] SUPPORTED_FORMATS = { - '.txt': 'plain_text', - '.md': 'markdown', - '.json': 'json_lines', - '.jsonl': 'json_lines', - '.csv': 'csv_text_column' + ".txt": "plain_text", + ".md": "markdown", + ".json": "json_lines", + ".jsonl": "json_lines", + ".csv": "csv_text_column", } + class RawTextDataLoader: - def __init__(self, tokenizer, chunk_size=2048, stride=512): + def __init__(self, tokenizer, chunk_size = 2048, stride = 512): self.tokenizer = tokenizer - self.chunk_size = chunk_size + self.chunk_size = chunk_size self.stride = stride - + def detect_format(self, file_path): """Auto-detect file format and parse accordingly""" extension = Path(file_path).suffix.lower() - return SUPPORTED_FORMATS.get(extension, 'plain_text') + return SUPPORTED_FORMATS.get(extension, "plain_text") def load_from_file(self, file_path): """Load raw text and convert to dataset""" @@ -50,7 +51,7 @@ class RawTextDataLoader: text_content = self._read_file_by_format(file_path, file_format) chunks = self.smart_chunk_text(text_content, self.chunk_size, self.stride) return self.create_causal_dataset(chunks) - + def load_from_files(self, file_paths): """Load multiple text files""" all_chunks = [] @@ -61,11 +62,10 @@ class RawTextDataLoader: all_chunks.extend(chunks) return self.create_causal_dataset(all_chunks) - def chunk_text(self, text): """Split text into overlapping chunks""" return self.smart_chunk_text(text, self.chunk_size, self.stride) - + def create_causal_dataset(self, chunks): """Create dataset for causal language modeling""" return Dataset.from_dict({"text": chunks}) @@ -79,51 +79,50 @@ class RawTextDataLoader: 4. Adds proper EOS tokens """ # First pass: tokenize the entire text to get accurate token counts - tokenized = self.tokenizer(text, return_tensors="pt", add_special_tokens=False) + tokenized = self.tokenizer(text, return_tensors = "pt", add_special_tokens = False) tokens = tokenized["input_ids"] - + # Handle different tokenizer return formats - if hasattr(tokens, '__len__') and len(tokens) > 0: + if hasattr(tokens, "__len__") and len(tokens) > 0: # If it's a nested structure, get the first element - if hasattr(tokens[0], '__len__'): + if hasattr(tokens[0], "__len__"): tokens = tokens[0] elif isinstance(tokens, int): # If tokenizer returns just a count, create a simple range tokens = list(range(tokens)) - + if len(tokens) <= chunk_size: # Text is small enough to fit in one chunk eos_token = self.tokenizer.eos_token if self.tokenizer.eos_token else "" return [text + eos_token] - + chunks = [] start_idx = 0 - + while start_idx < len(tokens): # Calculate end index for this chunk end_idx = min(start_idx + chunk_size, len(tokens)) - + # Extract tokens for this chunk chunk_tokens = tokens[start_idx:end_idx] - + # Decode back to text - chunk_text = self.tokenizer.decode(chunk_tokens, skip_special_tokens=True) - + chunk_text = self.tokenizer.decode(chunk_tokens, skip_special_tokens = True) + # Add EOS token if it's the last chunk or chunk is complete if end_idx == len(tokens) or len(chunk_tokens) == chunk_size: eos_token = self.tokenizer.eos_token if self.tokenizer.eos_token else "" chunk_text += eos_token - + chunks.append(chunk_text) - + # Move to next chunk with stride overlap if end_idx == len(tokens): break start_idx += chunk_size - stride - + return chunks - def tokenize_and_chunk(self, text): """ Tokenize first, then chunk by token count: @@ -134,10 +133,10 @@ class RawTextDataLoader: def _read_file_by_format(self, file_path, file_format): """Read file content based on detected format.""" - with open(file_path, 'r', encoding='utf-8') as f: - if file_format == 'plain_text' or file_format == 'markdown': + with open(file_path, "r", encoding = "utf-8") as f: + if file_format == "plain_text" or file_format == "markdown": return f.read() - elif file_format == 'json_lines': + elif file_format == "json_lines": lines = [] for line in f: try: @@ -147,42 +146,43 @@ class RawTextDataLoader: lines.append(text) except json.JSONDecodeError: continue - return '\n\n'.join(lines) - elif file_format == 'csv_text_column': + return "\n\n".join(lines) + elif file_format == "csv_text_column": reader = csv.DictReader(f) texts = [] for row in reader: text = self._extract_text_from_csv_row(row) if text: texts.append(text) - return '\n\n'.join(texts) + return "\n\n".join(texts) return "" - + def _extract_text_from_json(self, data): """Extract text from JSON object using common field names.""" - text_fields = ['text', 'content', 'message', 'body', 'description', 'prompt'] + text_fields = ["text", "content", "message", "body", "description", "prompt"] for field in text_fields: if field in data and isinstance(data[field], str): return data[field] return "" - + def _extract_text_from_csv_row(self, row): """Extract text from CSV row using common column names.""" - text_columns = ['text', 'content', 'message', 'body', 'description', 'prompt'] + text_columns = ["text", "content", "message", "body", "description", "prompt"] for column in text_columns: if column in row and row[column]: return row[column] return "" + class TextPreprocessor: def clean_text(self, text): """Remove unwanted characters, normalize whitespace""" - text = re.sub(r'\s+', ' ', text) - text = re.sub(r'[^\x20-\x7E\n\t]', '', text) - text = text.replace('\r\n', '\n').replace('\r', '\n') - text = re.sub(r'\n{3,}', '\n\n', text) + text = re.sub(r"\s+", " ", text) + text = re.sub(r"[^\x20-\x7E\n\t]", "", text) + text = text.replace("\r\n", "\n").replace("\r", "\n") + text = re.sub(r"\n{3,}", "\n\n", text) return text.strip() - + def extract_sections(self, text, patterns): """Extract specific sections (e.g., code blocks, quotes)""" sections = [] @@ -193,12 +193,20 @@ class TextPreprocessor: def add_structure_tokens(self, text): """Add special tokens for structure (chapters, sections)""" - text = re.sub(r'^# (.+)$', r'<|chapter|>\1<|/chapter|>', text, flags=re.MULTILINE) - text = re.sub(r'^## (.+)$', r'<|section|>\1<|/section|>', text, flags=re.MULTILINE) - text = re.sub(r'^### (.+)$', r'<|subsection|>\1<|/subsection|>', text, flags=re.MULTILINE) - text = re.sub(r'```(\w*)\n(.*?)\n```', r'<|code|\1|>\2<|/code|>', text, flags=re.DOTALL) + text = re.sub( + r"^# (.+)$", r"<|chapter|>\1<|/chapter|>", text, flags = re.MULTILINE + ) + text = re.sub( + r"^## (.+)$", r"<|section|>\1<|/section|>", text, flags = re.MULTILINE + ) + text = re.sub( + r"^### (.+)$", r"<|subsection|>\1<|/subsection|>", text, flags = re.MULTILINE + ) + text = re.sub( + r"```(\w*)\n(.*?)\n```", r"<|code|\1|>\2<|/code|>", text, flags = re.DOTALL + ) return text - + def validate_dataset(self, dataset): """ Check for: @@ -208,61 +216,66 @@ class TextPreprocessor: - Empty chunks """ stats = { - 'total_samples': len(dataset), - 'empty_samples': 0, - 'min_length': float('inf'), - 'max_length': 0, - 'avg_length': 0, - 'repeated_content': 0, - 'encoding_issues': 0, - 'warnings': [] + "total_samples": len(dataset), + "empty_samples": 0, + "min_length": float("inf"), + "max_length": 0, + "avg_length": 0, + "repeated_content": 0, + "encoding_issues": 0, + "warnings": [], } - - texts = dataset['text'] + + texts = dataset["text"] text_lengths = [] seen_texts = set() - + for i, text in enumerate(texts): if not text or len(text.strip()) == 0: - stats['empty_samples'] += 1 + stats["empty_samples"] += 1 continue - + # Check for encoding issues try: - text.encode('utf-8') + text.encode("utf-8") except UnicodeEncodeError: - stats['encoding_issues'] += 1 - + stats["encoding_issues"] += 1 + # Calculate lengths length = len(text) text_lengths.append(length) - stats['min_length'] = min(stats['min_length'], length) - stats['max_length'] = max(stats['max_length'], length) - + stats["min_length"] = min(stats["min_length"], length) + stats["max_length"] = max(stats["max_length"], length) + # Check for repeated content text_hash = hash(text.strip()) if text_hash in seen_texts: - stats['repeated_content'] += 1 + stats["repeated_content"] += 1 else: seen_texts.add(text_hash) - + # Calculate average length if text_lengths: - stats['avg_length'] = sum(text_lengths) / len(text_lengths) - stats['min_length'] = stats['min_length'] if stats['min_length'] != float('inf') else 0 - - # Generate warnings - if stats['empty_samples'] > 0: - stats['warnings'].append(f"Found {stats['empty_samples']} empty samples") - - if stats['repeated_content'] > 0: - stats['warnings'].append(f"Found {stats['repeated_content']} repeated samples") - - if stats['encoding_issues'] > 0: - stats['warnings'].append(f"Found {stats['encoding_issues']} encoding issues") - - if stats['min_length'] < 10: - stats['warnings'].append("Some samples are very short (< 10 characters)") - - return stats + stats["avg_length"] = sum(text_lengths) / len(text_lengths) + stats["min_length"] = ( + stats["min_length"] if stats["min_length"] != float("inf") else 0 + ) + # Generate warnings + if stats["empty_samples"] > 0: + stats["warnings"].append(f"Found {stats['empty_samples']} empty samples") + + if stats["repeated_content"] > 0: + stats["warnings"].append( + f"Found {stats['repeated_content']} repeated samples" + ) + + if stats["encoding_issues"] > 0: + stats["warnings"].append( + f"Found {stats['encoding_issues']} encoding issues" + ) + + if stats["min_length"] < 10: + stats["warnings"].append("Some samples are very short (< 10 characters)") + + return stats From 4362d9e59b2b056c92e57b599fca5946868311aa Mon Sep 17 00:00:00 2001 From: vangmay Date: Thu, 20 Nov 2025 20:53:22 +0800 Subject: [PATCH 010/167] Integrate smart dataset loader --- unsloth-cli.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/unsloth-cli.py b/unsloth-cli.py index aac0e7f7e1..044ad93f31 100644 --- a/unsloth-cli.py +++ b/unsloth-cli.py @@ -100,6 +100,8 @@ def run(args): return {"text": texts} def load_dataset_smart(args): + from transformers.utils import strtobool + if args.raw_text_file: # Use raw text loader loader = RawTextDataLoader(tokenizer, args.chunk_size, args.stride) @@ -109,20 +111,21 @@ def run(args): loader = RawTextDataLoader(tokenizer) dataset = loader.load_from_file(args.dataset) else: - # Existing HuggingFace dataset logic - dataset = load_dataset(args.dataset, split="train") + # Check for modelscope usage + use_modelscope = strtobool(os.environ.get("UNSLOTH_USE_MODELSCOPE", "False")) + if use_modelscope: + from modelscope import MsDataset + dataset = MsDataset.load(args.dataset, split="train") + else: + # Existing HuggingFace dataset logic + dataset = load_dataset(args.dataset, split="train") + + # Apply formatting for structured datasets dataset = dataset.map(formatting_prompts_func, batched=True) return dataset - use_modelscope = strtobool(os.environ.get("UNSLOTH_USE_MODELSCOPE", "False")) - if use_modelscope: - from modelscope import MsDataset - - dataset = MsDataset.load(args.dataset, split = "train") - else: - # Load and format dataset - dataset = load_dataset(args.dataset, split = "train") - dataset = dataset.map(formatting_prompts_func, batched = True) + # Load dataset using smart loader + dataset = load_dataset_smart(args) print("Data is formatted and ready!") # Configure training arguments From 0aa8f3fb67ca517afcf2a7cd71efee98f852ae22 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:57:53 +0000 Subject: [PATCH 011/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth-cli.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/unsloth-cli.py b/unsloth-cli.py index 44232e19d7..79b3ef5296 100644 --- a/unsloth-cli.py +++ b/unsloth-cli.py @@ -101,7 +101,7 @@ def run(args): def load_dataset_smart(args): from transformers.utils import strtobool - + if args.raw_text_file: # Use raw text loader loader = RawTextDataLoader(tokenizer, args.chunk_size, args.stride) @@ -112,16 +112,19 @@ def run(args): dataset = loader.load_from_file(args.dataset) else: # Check for modelscope usage - use_modelscope = strtobool(os.environ.get("UNSLOTH_USE_MODELSCOPE", "False")) + use_modelscope = strtobool( + os.environ.get("UNSLOTH_USE_MODELSCOPE", "False") + ) if use_modelscope: from modelscope import MsDataset - dataset = MsDataset.load(args.dataset, split="train") + + dataset = MsDataset.load(args.dataset, split = "train") else: # Existing HuggingFace dataset logic - dataset = load_dataset(args.dataset, split="train") - + dataset = load_dataset(args.dataset, split = "train") + # Apply formatting for structured datasets - dataset = dataset.map(formatting_prompts_func, batched=True) + dataset = dataset.map(formatting_prompts_func, batched = True) return dataset # Load dataset using smart loader From a12eefa75a0ece6b60182eeacceb37979b04fc65 Mon Sep 17 00:00:00 2001 From: vangmay Date: Thu, 20 Nov 2025 21:08:33 +0800 Subject: [PATCH 012/167] Make the chunk function efficient --- tests/test_raw_text.py | 24 ++++++++-- unsloth-cli.py | 20 ++++++--- unsloth/dataprep/raw_text.py | 85 ++++++++++++++++++++++++++++-------- 3 files changed, 99 insertions(+), 30 deletions(-) diff --git a/tests/test_raw_text.py b/tests/test_raw_text.py index 9bbfee92ac..88dac2604d 100644 --- a/tests/test_raw_text.py +++ b/tests/test_raw_text.py @@ -61,6 +61,7 @@ def test_raw_text_loader(): class MockTokenizer: def __init__(self): self.eos_token = "" + self.eos_token_id = 2 # Mock EOS token ID def __call__(self, text, return_tensors = None, add_special_tokens = False): words = text.split() @@ -77,6 +78,9 @@ def test_raw_text_loader(): def __len__(self): return len(self.data) + + def tolist(self): + return self.data return {"input_ids": [MockTensor(token_ids)]} return {"input_ids": token_ids} @@ -95,10 +99,22 @@ def test_raw_text_loader(): tokenizer = MockTokenizer() loader = RawTextDataLoader(tokenizer, chunk_size = 5, stride = 2) - # Test loading - dataset = loader.load_from_file(test_file) - assert len(dataset) > 0, "Should create at least one chunk" - assert "text" in dataset.column_names, "Dataset should have 'text' column" + # Test loading with text output (legacy mode) + text_dataset = loader.load_from_file(test_file, return_tensors=False) + assert len(text_dataset) > 0, "Should create at least one chunk" + assert "text" in text_dataset.column_names, "Dataset should have 'text' column" + + # Test loading with tokenized output (new efficient mode) + tokenized_dataset = loader.load_from_file(test_file, return_tensors=True) + assert len(tokenized_dataset) > 0, "Should create at least one tokenized chunk" + assert "input_ids" in tokenized_dataset.column_names, "Dataset should have 'input_ids' column" + assert "attention_mask" in tokenized_dataset.column_names, "Dataset should have 'attention_mask' column" + + # Verify tokenized data structure + first_sample = tokenized_dataset[0] + assert isinstance(first_sample["input_ids"], list), "input_ids should be a list" + assert isinstance(first_sample["attention_mask"], list), "attention_mask should be a list" + assert len(first_sample["input_ids"]) == len(first_sample["attention_mask"]), "input_ids and attention_mask should have same length" # Test preprocessor preprocessor = TextPreprocessor() diff --git a/unsloth-cli.py b/unsloth-cli.py index 79b3ef5296..efdc0b3f4d 100644 --- a/unsloth-cli.py +++ b/unsloth-cli.py @@ -103,13 +103,19 @@ def run(args): from transformers.utils import strtobool if args.raw_text_file: - # Use raw text loader + # Use raw text loader - returns pre-tokenized data loader = RawTextDataLoader(tokenizer, args.chunk_size, args.stride) - dataset = loader.load_from_file(args.raw_text_file) + dataset = loader.load_from_file(args.raw_text_file, return_tensors=True) + # Mark dataset as pre-tokenized to skip text formatting + dataset._is_pretokenized = True + return dataset elif args.dataset.endswith((".txt", ".md", ".json", ".jsonl")): - # Auto-detect local raw text files - loader = RawTextDataLoader(tokenizer) - dataset = loader.load_from_file(args.dataset) + # Auto-detect local raw text files - returns pre-tokenized data + loader = RawTextDataLoader(tokenizer, args.chunk_size, args.stride) + dataset = loader.load_from_file(args.dataset, return_tensors=True) + # Mark dataset as pre-tokenized to skip text formatting + dataset._is_pretokenized = True + return dataset else: # Check for modelscope usage use_modelscope = strtobool( @@ -123,9 +129,9 @@ def run(args): # Existing HuggingFace dataset logic dataset = load_dataset(args.dataset, split = "train") - # Apply formatting for structured datasets + # Apply formatting for structured datasets (text-based) dataset = dataset.map(formatting_prompts_func, batched = True) - return dataset + return dataset # Load dataset using smart loader dataset = load_dataset_smart(args) diff --git a/unsloth/dataprep/raw_text.py b/unsloth/dataprep/raw_text.py index d809eb312a..50612c1b86 100644 --- a/unsloth/dataprep/raw_text.py +++ b/unsloth/dataprep/raw_text.py @@ -35,48 +35,66 @@ SUPPORTED_FORMATS = { class RawTextDataLoader: - def __init__(self, tokenizer, chunk_size = 2048, stride = 512): + def __init__(self, tokenizer, chunk_size = 2048, stride = 512, return_tokenized = True): self.tokenizer = tokenizer self.chunk_size = chunk_size self.stride = stride + self.return_tokenized = return_tokenized def detect_format(self, file_path): """Auto-detect file format and parse accordingly""" extension = Path(file_path).suffix.lower() return SUPPORTED_FORMATS.get(extension, "plain_text") - def load_from_file(self, file_path): + def load_from_file(self, file_path, return_tokenized=None): """Load raw text and convert to dataset""" + if return_tokenized is None: + return_tokenized = self.return_tokenized file_format = self.detect_format(file_path) text_content = self._read_file_by_format(file_path, file_format) - chunks = self.smart_chunk_text(text_content, self.chunk_size, self.stride) + chunks = self.smart_chunk_text(text_content, self.chunk_size, self.stride, return_tokenized) return self.create_causal_dataset(chunks) - def load_from_files(self, file_paths): + def load_from_files(self, file_paths, return_tokenized=None): """Load multiple text files""" + if return_tokenized is None: + return_tokenized = self.return_tokenized all_chunks = [] for file_path in file_paths: file_format = self.detect_format(file_path) text_content = self._read_file_by_format(file_path, file_format) - chunks = self.smart_chunk_text(text_content, self.chunk_size, self.stride) + chunks = self.smart_chunk_text(text_content, self.chunk_size, self.stride, return_tokenized) all_chunks.extend(chunks) return self.create_causal_dataset(all_chunks) - def chunk_text(self, text): + def chunk_text(self, text, return_tokenized=None): """Split text into overlapping chunks""" - return self.smart_chunk_text(text, self.chunk_size, self.stride) + if return_tokenized is None: + return_tokenized = self.return_tokenized + return self.smart_chunk_text(text, self.chunk_size, self.stride, return_tokenized) def create_causal_dataset(self, chunks): """Create dataset for causal language modeling""" - return Dataset.from_dict({"text": chunks}) + if chunks and isinstance(chunks[0], dict): + # If chunks are already tokenized (dict with input_ids, attention_mask) + # Reorganize the data structure for Dataset.from_dict + input_ids = [chunk["input_ids"] for chunk in chunks] + attention_mask = [chunk["attention_mask"] for chunk in chunks] + return Dataset.from_dict({ + "input_ids": input_ids, + "attention_mask": attention_mask + }) + else: + # If chunks are text strings (backward compatibility) + return Dataset.from_dict({"text": chunks}) - def smart_chunk_text(self, text, chunk_size, stride): + def smart_chunk_text(self, text, chunk_size, stride, return_tokenized=True): """ Intelligent chunking that: 1. Respects sentence/paragraph boundaries 2. Handles various text formats (.txt, .md, .json, etc.) 3. Maintains context with stride overlap - 4. Adds proper EOS tokens + 4. Returns tokenized chunks directly (more efficient) or text chunks """ # First pass: tokenize the entire text to get accurate token counts tokenized = self.tokenizer(text, return_tensors = "pt", add_special_tokens = False) @@ -93,8 +111,19 @@ class RawTextDataLoader: if len(tokens) <= chunk_size: # Text is small enough to fit in one chunk - eos_token = self.tokenizer.eos_token if self.tokenizer.eos_token else "" - return [text + eos_token] + if return_tokenized: + # Add EOS token to the tokens if available + eos_token_id = getattr(self.tokenizer, 'eos_token_id', None) + if eos_token_id is not None: + tokens = tokens.tolist() if hasattr(tokens, 'tolist') else list(tokens) + tokens.append(eos_token_id) + + # Create attention mask + attention_mask = [1] * len(tokens) + return [{"input_ids": tokens, "attention_mask": attention_mask}] + else: + eos_token = self.tokenizer.eos_token if self.tokenizer.eos_token else "" + return [text + eos_token] chunks = [] start_idx = 0 @@ -106,15 +135,33 @@ class RawTextDataLoader: # Extract tokens for this chunk chunk_tokens = tokens[start_idx:end_idx] - # Decode back to text - chunk_text = self.tokenizer.decode(chunk_tokens, skip_special_tokens = True) + if return_tokenized: + # Convert to list if it's a tensor + chunk_tokens_list = chunk_tokens.tolist() if hasattr(chunk_tokens, 'tolist') else list(chunk_tokens) + + # Add EOS token if it's the last chunk or chunk is complete + if end_idx == len(tokens) or len(chunk_tokens_list) == chunk_size: + eos_token_id = getattr(self.tokenizer, 'eos_token_id', None) + if eos_token_id is not None: + chunk_tokens_list.append(eos_token_id) - # Add EOS token if it's the last chunk or chunk is complete - if end_idx == len(tokens) or len(chunk_tokens) == chunk_size: - eos_token = self.tokenizer.eos_token if self.tokenizer.eos_token else "" - chunk_text += eos_token + # Create attention mask (all tokens are attended to) + attention_mask = [1] * len(chunk_tokens_list) + + chunks.append({ + "input_ids": chunk_tokens_list, + "attention_mask": attention_mask + }) + else: + # Decode back to text (backward compatibility) + chunk_text = self.tokenizer.decode(chunk_tokens, skip_special_tokens = True) - chunks.append(chunk_text) + # Add EOS token if it's the last chunk or chunk is complete + if end_idx == len(tokens) or len(chunk_tokens) == chunk_size: + eos_token = self.tokenizer.eos_token if self.tokenizer.eos_token else "" + chunk_text += eos_token + + chunks.append(chunk_text) # Move to next chunk with stride overlap if end_idx == len(tokens): From 73ed28c79c81af8f92dfe9eb485a79625236c425 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:09:07 +0000 Subject: [PATCH 013/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_raw_text.py | 24 +++++++++----- unsloth-cli.py | 4 +-- unsloth/dataprep/raw_text.py | 62 ++++++++++++++++++++++-------------- 3 files changed, 56 insertions(+), 34 deletions(-) diff --git a/tests/test_raw_text.py b/tests/test_raw_text.py index 88dac2604d..5306c68fa6 100644 --- a/tests/test_raw_text.py +++ b/tests/test_raw_text.py @@ -78,7 +78,7 @@ def test_raw_text_loader(): def __len__(self): return len(self.data) - + def tolist(self): return self.data @@ -100,21 +100,29 @@ def test_raw_text_loader(): loader = RawTextDataLoader(tokenizer, chunk_size = 5, stride = 2) # Test loading with text output (legacy mode) - text_dataset = loader.load_from_file(test_file, return_tensors=False) + text_dataset = loader.load_from_file(test_file, return_tensors = False) assert len(text_dataset) > 0, "Should create at least one chunk" assert "text" in text_dataset.column_names, "Dataset should have 'text' column" # Test loading with tokenized output (new efficient mode) - tokenized_dataset = loader.load_from_file(test_file, return_tensors=True) + tokenized_dataset = loader.load_from_file(test_file, return_tensors = True) assert len(tokenized_dataset) > 0, "Should create at least one tokenized chunk" - assert "input_ids" in tokenized_dataset.column_names, "Dataset should have 'input_ids' column" - assert "attention_mask" in tokenized_dataset.column_names, "Dataset should have 'attention_mask' column" - + assert ( + "input_ids" in tokenized_dataset.column_names + ), "Dataset should have 'input_ids' column" + assert ( + "attention_mask" in tokenized_dataset.column_names + ), "Dataset should have 'attention_mask' column" + # Verify tokenized data structure first_sample = tokenized_dataset[0] assert isinstance(first_sample["input_ids"], list), "input_ids should be a list" - assert isinstance(first_sample["attention_mask"], list), "attention_mask should be a list" - assert len(first_sample["input_ids"]) == len(first_sample["attention_mask"]), "input_ids and attention_mask should have same length" + assert isinstance( + first_sample["attention_mask"], list + ), "attention_mask should be a list" + assert len(first_sample["input_ids"]) == len( + first_sample["attention_mask"] + ), "input_ids and attention_mask should have same length" # Test preprocessor preprocessor = TextPreprocessor() diff --git a/unsloth-cli.py b/unsloth-cli.py index efdc0b3f4d..5135d00493 100644 --- a/unsloth-cli.py +++ b/unsloth-cli.py @@ -105,14 +105,14 @@ def run(args): if args.raw_text_file: # Use raw text loader - returns pre-tokenized data loader = RawTextDataLoader(tokenizer, args.chunk_size, args.stride) - dataset = loader.load_from_file(args.raw_text_file, return_tensors=True) + dataset = loader.load_from_file(args.raw_text_file, return_tensors = True) # Mark dataset as pre-tokenized to skip text formatting dataset._is_pretokenized = True return dataset elif args.dataset.endswith((".txt", ".md", ".json", ".jsonl")): # Auto-detect local raw text files - returns pre-tokenized data loader = RawTextDataLoader(tokenizer, args.chunk_size, args.stride) - dataset = loader.load_from_file(args.dataset, return_tensors=True) + dataset = loader.load_from_file(args.dataset, return_tensors = True) # Mark dataset as pre-tokenized to skip text formatting dataset._is_pretokenized = True return dataset diff --git a/unsloth/dataprep/raw_text.py b/unsloth/dataprep/raw_text.py index 50612c1b86..d6a9a6a047 100644 --- a/unsloth/dataprep/raw_text.py +++ b/unsloth/dataprep/raw_text.py @@ -46,16 +46,18 @@ class RawTextDataLoader: extension = Path(file_path).suffix.lower() return SUPPORTED_FORMATS.get(extension, "plain_text") - def load_from_file(self, file_path, return_tokenized=None): + def load_from_file(self, file_path, return_tokenized = None): """Load raw text and convert to dataset""" if return_tokenized is None: return_tokenized = self.return_tokenized file_format = self.detect_format(file_path) text_content = self._read_file_by_format(file_path, file_format) - chunks = self.smart_chunk_text(text_content, self.chunk_size, self.stride, return_tokenized) + chunks = self.smart_chunk_text( + text_content, self.chunk_size, self.stride, return_tokenized + ) return self.create_causal_dataset(chunks) - def load_from_files(self, file_paths, return_tokenized=None): + def load_from_files(self, file_paths, return_tokenized = None): """Load multiple text files""" if return_tokenized is None: return_tokenized = self.return_tokenized @@ -63,15 +65,19 @@ class RawTextDataLoader: for file_path in file_paths: file_format = self.detect_format(file_path) text_content = self._read_file_by_format(file_path, file_format) - chunks = self.smart_chunk_text(text_content, self.chunk_size, self.stride, return_tokenized) + chunks = self.smart_chunk_text( + text_content, self.chunk_size, self.stride, return_tokenized + ) all_chunks.extend(chunks) return self.create_causal_dataset(all_chunks) - def chunk_text(self, text, return_tokenized=None): + def chunk_text(self, text, return_tokenized = None): """Split text into overlapping chunks""" if return_tokenized is None: return_tokenized = self.return_tokenized - return self.smart_chunk_text(text, self.chunk_size, self.stride, return_tokenized) + return self.smart_chunk_text( + text, self.chunk_size, self.stride, return_tokenized + ) def create_causal_dataset(self, chunks): """Create dataset for causal language modeling""" @@ -80,15 +86,14 @@ class RawTextDataLoader: # Reorganize the data structure for Dataset.from_dict input_ids = [chunk["input_ids"] for chunk in chunks] attention_mask = [chunk["attention_mask"] for chunk in chunks] - return Dataset.from_dict({ - "input_ids": input_ids, - "attention_mask": attention_mask - }) + return Dataset.from_dict( + {"input_ids": input_ids, "attention_mask": attention_mask} + ) else: # If chunks are text strings (backward compatibility) return Dataset.from_dict({"text": chunks}) - def smart_chunk_text(self, text, chunk_size, stride, return_tokenized=True): + def smart_chunk_text(self, text, chunk_size, stride, return_tokenized = True): """ Intelligent chunking that: 1. Respects sentence/paragraph boundaries @@ -113,11 +118,13 @@ class RawTextDataLoader: # Text is small enough to fit in one chunk if return_tokenized: # Add EOS token to the tokens if available - eos_token_id = getattr(self.tokenizer, 'eos_token_id', None) + eos_token_id = getattr(self.tokenizer, "eos_token_id", None) if eos_token_id is not None: - tokens = tokens.tolist() if hasattr(tokens, 'tolist') else list(tokens) + tokens = ( + tokens.tolist() if hasattr(tokens, "tolist") else list(tokens) + ) tokens.append(eos_token_id) - + # Create attention mask attention_mask = [1] * len(tokens) return [{"input_ids": tokens, "attention_mask": attention_mask}] @@ -137,28 +144,35 @@ class RawTextDataLoader: if return_tokenized: # Convert to list if it's a tensor - chunk_tokens_list = chunk_tokens.tolist() if hasattr(chunk_tokens, 'tolist') else list(chunk_tokens) - + chunk_tokens_list = ( + chunk_tokens.tolist() + if hasattr(chunk_tokens, "tolist") + else list(chunk_tokens) + ) + # Add EOS token if it's the last chunk or chunk is complete if end_idx == len(tokens) or len(chunk_tokens_list) == chunk_size: - eos_token_id = getattr(self.tokenizer, 'eos_token_id', None) + eos_token_id = getattr(self.tokenizer, "eos_token_id", None) if eos_token_id is not None: chunk_tokens_list.append(eos_token_id) # Create attention mask (all tokens are attended to) attention_mask = [1] * len(chunk_tokens_list) - - chunks.append({ - "input_ids": chunk_tokens_list, - "attention_mask": attention_mask - }) + + chunks.append( + {"input_ids": chunk_tokens_list, "attention_mask": attention_mask} + ) else: # Decode back to text (backward compatibility) - chunk_text = self.tokenizer.decode(chunk_tokens, skip_special_tokens = True) + chunk_text = self.tokenizer.decode( + chunk_tokens, skip_special_tokens = True + ) # Add EOS token if it's the last chunk or chunk is complete if end_idx == len(tokens) or len(chunk_tokens) == chunk_size: - eos_token = self.tokenizer.eos_token if self.tokenizer.eos_token else "" + eos_token = ( + self.tokenizer.eos_token if self.tokenizer.eos_token else "" + ) chunk_text += eos_token chunks.append(chunk_text) From 76b25c7f04ab533ab65d9f9248096f127b2f0842 Mon Sep 17 00:00:00 2001 From: vangmay Date: Thu, 20 Nov 2025 21:40:45 +0800 Subject: [PATCH 014/167] remove old function --- unsloth/dataprep/raw_text.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/unsloth/dataprep/raw_text.py b/unsloth/dataprep/raw_text.py index d6a9a6a047..b880e97338 100644 --- a/unsloth/dataprep/raw_text.py +++ b/unsloth/dataprep/raw_text.py @@ -184,14 +184,6 @@ class RawTextDataLoader: return chunks - def tokenize_and_chunk(self, text): - """ - Tokenize first, then chunk by token count: - 1. More precise length control - 2. Avoids mid-token splits - 3. Handles different languages better - """ - def _read_file_by_format(self, file_path, file_format): """Read file content based on detected format.""" with open(file_path, "r", encoding = "utf-8") as f: From 082d9c15e4ef2ef76ab3cf83a61405c93e4f2591 Mon Sep 17 00:00:00 2001 From: vangmay Date: Tue, 25 Nov 2025 21:01:43 +0800 Subject: [PATCH 015/167] Remove training mode arg --- unsloth-cli.py | 34 +++++++--------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/unsloth-cli.py b/unsloth-cli.py index 5135d00493..ef8167ad39 100644 --- a/unsloth-cli.py +++ b/unsloth-cli.py @@ -103,19 +103,13 @@ def run(args): from transformers.utils import strtobool if args.raw_text_file: - # Use raw text loader - returns pre-tokenized data + # Use raw text loader loader = RawTextDataLoader(tokenizer, args.chunk_size, args.stride) - dataset = loader.load_from_file(args.raw_text_file, return_tensors = True) - # Mark dataset as pre-tokenized to skip text formatting - dataset._is_pretokenized = True - return dataset + dataset = loader.load_from_file(args.raw_text_file) elif args.dataset.endswith((".txt", ".md", ".json", ".jsonl")): - # Auto-detect local raw text files - returns pre-tokenized data - loader = RawTextDataLoader(tokenizer, args.chunk_size, args.stride) - dataset = loader.load_from_file(args.dataset, return_tensors = True) - # Mark dataset as pre-tokenized to skip text formatting - dataset._is_pretokenized = True - return dataset + # Auto-detect local raw text files + loader = RawTextDataLoader(tokenizer) + dataset = loader.load_from_file(args.dataset) else: # Check for modelscope usage use_modelscope = strtobool( @@ -129,9 +123,9 @@ def run(args): # Existing HuggingFace dataset logic dataset = load_dataset(args.dataset, split = "train") - # Apply formatting for structured datasets (text-based) + # Apply formatting for structured datasets dataset = dataset.map(formatting_prompts_func, batched = True) - return dataset + return dataset # Load dataset using smart loader dataset = load_dataset_smart(args) @@ -427,19 +421,5 @@ if __name__ == "__main__": "--stride", type = int, default = 512, help = "Overlap between chunks" ) - TRAINING_MODES = { - "instruction": "Standard instruction-following", - "causal": "Causal language modeling (raw text)", - "completion": "Text completion tasks", - } - - parser.add_argument( - "--training_mode", - type = str, - default = "instruction", - choices = list(TRAINING_MODES.keys()), - help = "Training mode for the model", - ) - args = parser.parse_args() run(args) From e53e1852a8fe989ae4881107474ef5b64f9b0fa3 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 9 Dec 2025 01:02:26 -0800 Subject: [PATCH 016/167] Update _utils.py --- unsloth/models/_utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index f0db65fdbf..e3814163ef 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -85,6 +85,7 @@ import re from dataclasses import dataclass, field import functools import textwrap +import logging import warnings, subprocess, inspect, psutil, os, math from unsloth_zoo.utils import Version, get_quant_type from importlib.metadata import version as importlib_version @@ -167,9 +168,9 @@ warnings.filterwarnings( ) warnings.filterwarnings(action = "ignore", category = RuntimeWarning, module = "multiprocess") warnings.filterwarnings(action = "ignore", category = UserWarning, module = "triton") -# Stop "Special tokens have been added in the vocabulary, ..." -import logging +warnings.filterwarnings(action = "ignore", category = UserWarning, module = "bitsandbytes") +# Stop "Special tokens have been added in the vocabulary, ..." logging.getLogger("transformers.tokenization_utils_base").setLevel(logging.CRITICAL + 1) From 112a893116585ffd0444159f5cfddcbf2543c4a9 Mon Sep 17 00:00:00 2001 From: vangmay Date: Wed, 10 Dec 2025 10:15:56 +0530 Subject: [PATCH 017/167] Fix RawTextDataLoader import issue --- unsloth/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/unsloth/__init__.py b/unsloth/__init__.py index 8b48ce3ba0..b77af7c242 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -247,6 +247,8 @@ from .save import * from .chat_templates import * from .tokenizer_utils import * from .trainer import * +# Export dataprep utilities for CLI and downstream users +from .dataprep.raw_text import RawTextDataLoader, TextPreprocessor from unsloth_zoo.rl_environments import ( check_python_modules, create_locked_down_function, From 505f97f432ad62e81317e3b54cca034385d7081e Mon Sep 17 00:00:00 2001 From: vangmay Date: Wed, 10 Dec 2025 10:17:23 +0530 Subject: [PATCH 018/167] Fix Incorrect non-relative import in dataprep package --- unsloth/dataprep/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsloth/dataprep/__init__.py b/unsloth/dataprep/__init__.py index b6840f247f..048f9b8010 100644 --- a/unsloth/dataprep/__init__.py +++ b/unsloth/dataprep/__init__.py @@ -13,4 +13,4 @@ # limitations under the License. from .synthetic import * -from raw_text import * +from .raw_text import * From b76cfb8c1c25bce58fb39895683d49a9ef0d2ca6 Mon Sep 17 00:00:00 2001 From: vangmay Date: Wed, 10 Dec 2025 10:46:29 +0530 Subject: [PATCH 019/167] =?UTF-8?q?Fix=20Chunking=20loop=20can=20hang=20wh?= =?UTF-8?q?en=20stride=20=E2=89=A5=20chunk=5Fsize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unsloth/dataprep/raw_text.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/unsloth/dataprep/raw_text.py b/unsloth/dataprep/raw_text.py index b880e97338..f7c1bf7856 100644 --- a/unsloth/dataprep/raw_text.py +++ b/unsloth/dataprep/raw_text.py @@ -101,6 +101,13 @@ class RawTextDataLoader: 3. Maintains context with stride overlap 4. Returns tokenized chunks directly (more efficient) or text chunks """ + if chunk_size <= 0: + raise ValueError(f"chunk_size must be positive, got {chunk_size}") + if stride >= chunk_size: + raise ValueError( + f"stride ({stride}) must be smaller than chunk_size ({chunk_size}) to progress the chunking loop" + ) + # First pass: tokenize the entire text to get accurate token counts tokenized = self.tokenizer(text, return_tensors = "pt", add_special_tokens = False) tokens = tokenized["input_ids"] From 3b01fc87b9a5195bd05d8e2e3340a1726a819b88 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 05:17:01 +0000 Subject: [PATCH 020/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/unsloth/__init__.py b/unsloth/__init__.py index b77af7c242..26e43eec80 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -247,6 +247,7 @@ from .save import * from .chat_templates import * from .tokenizer_utils import * from .trainer import * + # Export dataprep utilities for CLI and downstream users from .dataprep.raw_text import RawTextDataLoader, TextPreprocessor from unsloth_zoo.rl_environments import ( From 30ade5241694e6a25142fc8475572a013e0c6507 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 12:12:49 +0000 Subject: [PATCH 021/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/import_fixes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 67d9bf286d..858e910c20 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -22,6 +22,7 @@ import logging UNSLOTH_ENABLE_LOGGING = os.environ.get("UNSLOTH_ENABLE_LOGGING", "0") == "1" + def Version(version): try: new_version = str(version) @@ -30,13 +31,14 @@ def Version(version): raise Exception(str(e)) new_version = new_version.group(0).rstrip(".") if new_version != version: - new_version += ".1" # Add .1 for dev / alpha / beta / rc + new_version += ".1" # Add .1 for dev / alpha / beta / rc return TrueVersion(new_version) except: from inspect import getframeinfo, stack + caller = getframeinfo(stack()[1][0]) raise RuntimeError( - f"Unsloth: Could not get version for `{version}`\n"\ + f"Unsloth: Could not get version for `{version}`\n" f"File name = [{caller.filename}] Line number = [{caller.lineno}]" ) From e6f9c41f192d226457b0bd15ddfb8675754adecd Mon Sep 17 00:00:00 2001 From: oKatanaaa Date: Thu, 11 Dec 2025 03:21:02 +0000 Subject: [PATCH 022/167] fix: weights tying --- unsloth/models/llama.py | 48 ++++++++++++++++++++++++++++++++++++++++ unsloth/models/vision.py | 1 + 2 files changed, 49 insertions(+) diff --git a/unsloth/models/llama.py b/unsloth/models/llama.py index 4c9337ccf9..d38018ee1b 100644 --- a/unsloth/models/llama.py +++ b/unsloth/models/llama.py @@ -2601,6 +2601,7 @@ class FastLlamaModel: loftq_config = {}, temporary_location = "_unsloth_temporary_saved_buffers", qat_scheme = None, + ensure_weight_tying = False, **kwargs, ): if os.environ.get("UNSLOTH_USE_NEW_MODEL", "0") == "1": @@ -2630,6 +2631,7 @@ class FastLlamaModel: init_lora_weights = init_lora_weights, loftq_config = loftq_config, temporary_location = temporary_location, + ensure_weight_tying = ensure_weight_tying, **kwargs, ) if os.environ.get("UNSLOTH_ENABLE_FULL_FINETUNING", "0") == "1": @@ -2953,6 +2955,7 @@ class FastLlamaModel: loftq_config = loftq_config, use_rslora = use_rslora, modules_to_save = modules_to_save, + ensure_weight_tying = ensure_weight_tying, **kwargs, ) if not SUPPORTS_LOFTQ: @@ -3002,6 +3005,51 @@ class FastLlamaModel: model = FastLlamaModel.patch_peft_model(model, use_gradient_checkpointing) + if ensure_weight_tying: + try: + input_embeddings = model.get_input_embeddings() + output_embeddings = model.get_output_embeddings() + + if input_embeddings is not None and output_embeddings is not None: + def _retie_parameter(target_module, source_module): + if not hasattr(source_module, "weight"): + return + weight = source_module.weight + # Remove existing registration to avoid "attribute already exists" + if "weight" in getattr(target_module, "_parameters", {}): + target_module._parameters.pop("weight") + if hasattr(target_module, "weight"): + try: + delattr(target_module, "weight") + except Exception: + pass + target_module.register_parameter("weight", weight) + + # Tie trainable copies created by ModulesToSaveWrapper first (these are used in forward) + if hasattr(input_embeddings, "modules_to_save") and hasattr( + output_embeddings, "modules_to_save" + ): + if hasattr(input_embeddings.modules_to_save, "default") and hasattr( + output_embeddings.modules_to_save, "default" + ): + _retie_parameter( + output_embeddings.modules_to_save.default, + input_embeddings.modules_to_save.default, + ) + + # Tie original_module references as well if present + if hasattr(input_embeddings, "original_module") and hasattr( + output_embeddings, "original_module" + ): + _retie_parameter( + output_embeddings.original_module, + input_embeddings.original_module, + ) + except Exception as e: + logger.warning_once( + f"Unsloth: Failed to ensure weight tying between embeddings and lm_head: {e}" + ) + if train_embed_tokens: print("Unsloth: Training embed_tokens in mixed precision to save VRAM") assert hasattr(model.get_input_embeddings(), "modules_to_save") diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index ed19f587cf..9f847f2837 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -930,6 +930,7 @@ class FastBaseModel: task_type = TaskType.CAUSAL_LM, temporary_location = "_unsloth_temporary_saved_buffers", qat_scheme = None, + ensure_weight_tying = False, **kwargs, ): if os.environ.get("UNSLOTH_ENABLE_FULL_FINETUNING", "0") == "1": From cd0ca56eefc6bf5ae513e8cdd066c100a510695d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 03:31:41 +0000 Subject: [PATCH 023/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/models/llama.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/unsloth/models/llama.py b/unsloth/models/llama.py index d38018ee1b..e0d8cbcf25 100644 --- a/unsloth/models/llama.py +++ b/unsloth/models/llama.py @@ -3011,6 +3011,7 @@ class FastLlamaModel: output_embeddings = model.get_output_embeddings() if input_embeddings is not None and output_embeddings is not None: + def _retie_parameter(target_module, source_module): if not hasattr(source_module, "weight"): return @@ -3029,9 +3030,9 @@ class FastLlamaModel: if hasattr(input_embeddings, "modules_to_save") and hasattr( output_embeddings, "modules_to_save" ): - if hasattr(input_embeddings.modules_to_save, "default") and hasattr( - output_embeddings.modules_to_save, "default" - ): + if hasattr( + input_embeddings.modules_to_save, "default" + ) and hasattr(output_embeddings.modules_to_save, "default"): _retie_parameter( output_embeddings.modules_to_save.default, input_embeddings.modules_to_save.default, From cde2d42caf053565ba66988807c3774269bab45c Mon Sep 17 00:00:00 2001 From: Datta Nimmaturi Date: Fri, 12 Dec 2025 17:03:39 +0530 Subject: [PATCH 024/167] [FIX] [Transformers] VLM input embeds fix for gradients (#3715) * Fix get_input_embeds call for VLMs * patch input_require_grads instead * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * cleanup old patch * cleanup old patch * cleanup * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Apply suggestion from @danielhanchen * use logger instead of prints * Move unsloth present set * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel Han --- unsloth/__init__.py | 8 ++- unsloth/import_fixes.py | 105 ++++++++++++++++++++++++++++++---------- 2 files changed, 85 insertions(+), 28 deletions(-) diff --git a/unsloth/__init__.py b/unsloth/__init__.py index 47739fad15..5df43d3117 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -17,6 +17,9 @@ from packaging.version import Version import os, re, subprocess, inspect, functools import numpy as np +# Log Unsloth is being used +# We want logger in import_fixes and hence setting it here for zoo to be importable +os.environ["UNSLOTH_IS_PRESENT"] = "1" # Fix some issues before importing other packages from .import_fixes import ( fix_message_factory_issue, @@ -63,8 +66,6 @@ os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python" # "pinned_use_cuda_host_register:True,"\ # "pinned_num_register_threads:8" -# Log Unsloth is being used -os.environ["UNSLOTH_IS_PRESENT"] = "1" from importlib.metadata import version as importlib_version from importlib.metadata import PackageNotFoundError @@ -123,6 +124,7 @@ from .import_fixes import ( patch_ipykernel_hf_xet, patch_trackio, patch_datasets, + patch_enable_input_require_grads, ) fix_xformers_performance_issue() @@ -132,6 +134,7 @@ ignore_logger_messages() patch_ipykernel_hf_xet() patch_trackio() patch_datasets() +patch_enable_input_require_grads() del fix_xformers_performance_issue del fix_vllm_aimv2_issue @@ -140,6 +143,7 @@ del ignore_logger_messages del patch_ipykernel_hf_xet del patch_trackio del patch_datasets +del patch_enable_input_require_grads # Torch 2.4 has including_emulation if DEVICE_TYPE == "cuda": diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 858e910c20..a43be23194 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -19,8 +19,7 @@ from importlib.metadata import version as importlib_version from packaging.version import Version as TrueVersion import re import logging - -UNSLOTH_ENABLE_LOGGING = os.environ.get("UNSLOTH_ENABLE_LOGGING", "0") == "1" +from unsloth_zoo.log import logger def Version(version): @@ -71,8 +70,7 @@ def fix_message_factory_issue(): return if not hasattr(google.protobuf.message_factory, "MessageFactory"): - if UNSLOTH_ENABLE_LOGGING: - print("Unsloth: Patching protobuf.MessageFactory as it doesn't exist") + logger.info("Unsloth: Patching protobuf.MessageFactory as it doesn't exist") google.protobuf.message_factory.MessageFactory = MessageFactory elif ( hasattr(google.protobuf.message_factory, "MessageFactory") @@ -82,8 +80,7 @@ def fix_message_factory_issue(): and not hasattr(google.protobuf.message_factory, "GetMessageClass") ): google.protobuf.message_factory.MessageFactory = MessageFactory - if UNSLOTH_ENABLE_LOGGING: - print("Unsloth: Patching protobuf.MessageFactory as it doesn't exist") + logger.info("Unsloth: Patching protobuf.MessageFactory as it doesn't exist") elif ( hasattr(google.protobuf.message_factory, "MessageFactory") and not hasattr( @@ -97,8 +94,7 @@ def fix_message_factory_issue(): return GetMessageClass(descriptor) google.protobuf.message_factory.MessageFactory.GetPrototype = GetPrototype - if UNSLOTH_ENABLE_LOGGING: - print("Unsloth: Patching protobuf.MessageFactory.GetPrototype") + logger.info("Unsloth: Patching protobuf.MessageFactory.GetPrototype") pass except: pass @@ -126,13 +122,11 @@ def fix_xformers_performance_issue(): f.seek(0) f.write(text) f.truncate() - if UNSLOTH_ENABLE_LOGGING: - print( - "Unsloth: Patching Xformers to fix some performance issues." - ) + logger.info( + "Unsloth: Patching Xformers to fix some performance issues." + ) except Exception as e: - if UNSLOTH_ENABLE_LOGGING: - print(f"Unsloth: Failed patching Xformers with error = {str(e)}") + logger.info(f"Unsloth: Failed patching Xformers with error = {str(e)}") # ValueError: 'aimv2' is already used by a Transformers config, pick another name. @@ -167,13 +161,11 @@ def fix_vllm_aimv2_issue(): f.seek(0) f.write(text) f.truncate() - if UNSLOTH_ENABLE_LOGGING: - print( - "Unsloth: Patching vLLM to fix `'aimv2' is already used by a Transformers config, pick another name.`" - ) + logger.info( + "Unsloth: Patching vLLM to fix `'aimv2' is already used by a Transformers config, pick another name.`" + ) except Exception as e: - if UNSLOTH_ENABLE_LOGGING: - print(f"Unsloth: Failed patching vLLM with error = {str(e)}") + logger.info(f"Unsloth: Failed patching vLLM with error = {str(e)}") def fix_vllm_guided_decoding_params(): @@ -274,8 +266,70 @@ def check_fbgemm_gpu_version(): raise ImportError( f"Unsloth: fbgemm_gpu_genai=={fbgemm_gpu_version} detected. It might cause unexpected issues like segmentation faults. Please uninstall the current one by doing `pip uninstall fbgemm-gpu` && `pip install fbgemm-gpu` to install fbgemm-gpu 1.4.0 or newer!" ) - elif UNSLOTH_ENABLE_LOGGING: - print(f"Unsloth: fbgemm_gpu_genai=={fbgemm_gpu_version} detected.") + logger.info(f"Unsloth: fbgemm_gpu_genai=={fbgemm_gpu_version} detected.") + + +def patch_enable_input_require_grads(): + """ + Patch transformers PreTrainedModel.enable_input_require_grads to handle vision models + that raise NotImplementedError from get_input_embeddings(). + + """ + import inspect + from transformers import PreTrainedModel + + # Check if the original function iterates over self.modules() instead of just returning the enable_input_require_grads + # Ref: https://github.com/huggingface/transformers/pull/41993/files#diff-6b72b98c4c2dcfc6cc606843917733f5d858374fbc22a735ff483bbc0c1e63eaL1979-R1996 + try: + original_source = inspect.getsource(PreTrainedModel.enable_input_require_grads) + except (OSError, TypeError): + return + + # Only patch if the new pattern exists (iterating over self.modules()) + if "for module in self.modules()" not in original_source: + return + + def _patched_enable_input_require_grads(self): + def make_inputs_require_grads(module, input, output): + output.requires_grad_(True) + + hooks = [] + seen_modules = set() + + for module in self.modules(): + if not ( + isinstance(module, PreTrainedModel) + and hasattr(module, "get_input_embeddings") + ): + continue + + try: + input_embeddings = module.get_input_embeddings() + except NotImplementedError: + # Vision models may not implement get_input_embeddings - skip them + # For GLM V4.6 for example, this skips only `self.visual` + continue + + if input_embeddings is None: + continue + + embedding_id = id(input_embeddings) + if embedding_id in seen_modules: + continue + + seen_modules.add(embedding_id) + hooks.append( + input_embeddings.register_forward_hook(make_inputs_require_grads) + ) + + self._require_grads_hooks = hooks + if hooks: + self._require_grads_hook = hooks[0] + + PreTrainedModel.enable_input_require_grads = _patched_enable_input_require_grads + logger.info( + "Unsloth: Patched enable_input_require_grads for vision model compatibility" + ) def torchvision_compatibility_check(): @@ -313,7 +367,6 @@ def torchvision_compatibility_check(): f"but found torchvision=={torchvision_version}. " f"Please refer to https://pytorch.org/get-started/previous-versions/ for more information." ) - elif UNSLOTH_ENABLE_LOGGING: - print( - f"Unsloth: torch=={torch_version} and torchvision=={torchvision_version} are compatible." - ) + logger.info( + f"Unsloth: torch=={torch_version} and torchvision=={torchvision_version} are compatible." + ) From 2c22ce662f883cd757cc58efb5c308a2008b2046 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Dec 2025 03:41:09 -0800 Subject: [PATCH 025/167] Update rope_embedding.py --- unsloth/kernels/rope_embedding.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/unsloth/kernels/rope_embedding.py b/unsloth/kernels/rope_embedding.py index e93cbd1544..2adc9ecc5a 100644 --- a/unsloth/kernels/rope_embedding.py +++ b/unsloth/kernels/rope_embedding.py @@ -20,13 +20,6 @@ from ..device_type import DEVICE_COUNT from .utils import calculate_settings, torch_gpu_device, torch_device_stream -@triton.heuristics( - { - "BACKWARD_PASS": lambda args: bool(args["BACKWARD_PASS"]), - "HAS_ROPE_INDICES": lambda args: bool(args["HAS_ROPE_INDICES"]), - } -) -@triton.jit def _rope_embedding_QK( Q, Q_batch_stride, @@ -104,9 +97,17 @@ def _rope_embedding_QK( tl.store(k_ptr + half_head_dim + col_offsets, k1 * cos1 + k0 * sin1, mask = mask) -ROPE_GROUP_SIZE: int = 4 +_rope_embedding_QK = triton.jit(_rope_embedding_QK) +_rope_embedding_QK = triton.heuristics( + { + "BACKWARD_PASS": lambda args: bool(args["BACKWARD_PASS"]), + "HAS_ROPE_INDICES": lambda args: bool(args["HAS_ROPE_INDICES"]), + } +)(_rope_embedding_QK) +ROPE_GROUP_SIZE: int = 4 + def _rope_embedding( Q, Q_row_stride: tl.constexpr, From b5f1a77482a7615a748d8783837ee59da3e89e00 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Dec 2025 04:58:43 -0800 Subject: [PATCH 026/167] Fixes --- unsloth/import_fixes.py | 2 +- unsloth/models/rl_replacements.py | 8 +++++++- unsloth/trainer.py | 5 +++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index a43be23194..4cbe57cd95 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -282,7 +282,7 @@ def patch_enable_input_require_grads(): # Ref: https://github.com/huggingface/transformers/pull/41993/files#diff-6b72b98c4c2dcfc6cc606843917733f5d858374fbc22a735ff483bbc0c1e63eaL1979-R1996 try: original_source = inspect.getsource(PreTrainedModel.enable_input_require_grads) - except (OSError, TypeError): + except: return # Only patch if the new pattern exists (iterating over self.modules()) diff --git a/unsloth/models/rl_replacements.py b/unsloth/models/rl_replacements.py index 2cf3527c9b..7dab0d7307 100644 --- a/unsloth/models/rl_replacements.py +++ b/unsloth/models/rl_replacements.py @@ -26,7 +26,9 @@ import torch import inspect from collections import defaultdict from unsloth_zoo.rl_replacements import RL_REPLACEMENTS, left_pack_padding +from unsloth_zoo.utils import Version from unsloth_zoo.log import logger +import importlib.util from ..device_type import ( is_hip, get_device_type, @@ -942,11 +944,15 @@ def openenv_vllm_reload_weights(): # # The fix: Use wake_up() with no tags, which wakes everything. Unsloth's patched # CuMemAllocator.wake_up skips weights anyway, so this is safe. + if importlib.util.find_spec("trl") is None: + return + if Version(importlib_version("trl")) < Version("0.26.0"): + return try: import trl.experimental.openenv.utils as openenv_utils import trl.experimental.openenv as openenv except ImportError as e: - logger.warning(f"Unsloth: Failed to import trl openenv: {e}") + logger.info(f"Unsloth: Failed to import trl openenv: {e}") return src = inspect.getsource(openenv_utils.generate_rollout_completions) diff --git a/unsloth/trainer.py b/unsloth/trainer.py index 339af63f33..5cd1bfd08d 100644 --- a/unsloth/trainer.py +++ b/unsloth/trainer.py @@ -36,7 +36,7 @@ from unsloth_zoo.vision_utils import ( UnslothVisionDataCollator, ) from unsloth_zoo.hf_utils import get_transformers_model_type -from packaging.version import Version +from unsloth_zoo.utils import Version import dataclasses __all__ = [ @@ -315,10 +315,11 @@ def _patch_sft_trainer_auto_packing(trl_module): # We also disable vision language models for padding free collators blocked = ( - data_collator is not None + (data_collator is not None) or isinstance(processing_class, ProcessorMixin) or is_vlm or is_unsupported_model + or (os.environ.get("UNSLOTH_RETURN_LOGITS", "0") == "1") # Disable padding free on forced logits ) requested_pack = bool(getattr(config_arg, "packing", False)) if blocked: From c94f59513fb2353b40628f9620c6657eaccbff7e Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Dec 2025 05:01:43 -0800 Subject: [PATCH 027/167] Update _utils.py --- unsloth/models/_utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index bdb8f38a50..0377127860 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -413,6 +413,16 @@ try: except: pass +# Flax classes are deprecated and will be removed in Diffusers v1.0.0. +try: + from diffusers.utils import logger as diffusers_logger + + diffusers_logger.addFilter(HideLoggingMessage("are deprecated")) + del diffusers_logger +except: + pass + + # Errors out on # Some weights of Gemma3nForConditionalGeneration were not initialized from the model checkpoint from transformers.modeling_utils import logger as transformers_logger From 01319d3681289e46e9c9f7166888b923418d3bc4 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Dec 2025 05:10:45 -0800 Subject: [PATCH 028/167] Update import_fixes.py --- unsloth/import_fixes.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 4cbe57cd95..63bfd6e1e1 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -370,3 +370,41 @@ def torchvision_compatibility_check(): logger.info( f"Unsloth: torch=={torch_version} and torchvision=={torchvision_version} are compatible." ) + + +# Fix TRL OpenEnv 0.26 NameError: name 'SamplingParams' is not defined +def fix_openenv_no_vllm(): + if importlib.util.find_spec("trl") is None: + return + trl_location = importlib.util.find_spec("trl").origin + trl_location = os.path.split(trl_location)[0] + openenv = Path(trl_location) / "experimental" / "openenv" / "utils.py" + if not openenv.exists(): + return + try: + with open(openenv, "r+", encoding = "utf-8") as f: + text = f.read() + bad = ( + "if is_vllm_available():\n" + "from vllm import SamplingParams\n" + "from vllm.sampling_params import GuidedDecodingParams\n" + ) + if bad + "\n" + "\n" in text: + text = text.replace( + bad + "\n" + "\n", + bad + ( + "else:\n" + " from typing import Any\n"\ + " SamplingParams = Any\n"\ + " GuidedDecodingParams = Any\n" + "\n" + ) + ) + f.seek(0) + f.write(text) + f.truncate() + logger.info( + "Unsloth: Patching TRL OpenEnv to fix SamplingParams not defined" + ) + except Exception as e: + logger.info(f"Unsloth: Failed patching TRL OpenEnv with error = {str(e)}") From 696a540c1ef5f2f6da5cf9d67e36994d29aee434 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Dec 2025 05:11:12 -0800 Subject: [PATCH 029/167] Update rl_replacements.py --- unsloth/models/rl_replacements.py | 1 + 1 file changed, 1 insertion(+) diff --git a/unsloth/models/rl_replacements.py b/unsloth/models/rl_replacements.py index 7dab0d7307..7d4d520c1f 100644 --- a/unsloth/models/rl_replacements.py +++ b/unsloth/models/rl_replacements.py @@ -27,6 +27,7 @@ import inspect from collections import defaultdict from unsloth_zoo.rl_replacements import RL_REPLACEMENTS, left_pack_padding from unsloth_zoo.utils import Version +from importlib.metadata import version as importlib_version from unsloth_zoo.log import logger import importlib.util from ..device_type import ( From ac54d6e1c8ad3c7615037a49dfd5fed8883e41c3 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Dec 2025 05:20:09 -0800 Subject: [PATCH 030/167] fix_openenv_no_vllm --- pyproject.toml | 4 ++-- unsloth/__init__.py | 3 +++ unsloth/models/_utils.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8d91ff621d..c6e19b014e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ huggingfacenotorch = [ ] huggingface = [ "unsloth[huggingfacenotorch]", - "unsloth_zoo>=2025.12.3", + "unsloth_zoo>=2025.12.4", "torchvision", "unsloth[triton]", ] @@ -523,7 +523,7 @@ colab-ampere-torch220 = [ "flash-attn>=2.6.3 ; ('linux' in sys_platform)", ] colab-new = [ - "unsloth_zoo>=2025.12.3", + "unsloth_zoo>=2025.12.4", "packaging", "tyro", "transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,<=4.57.3", diff --git a/unsloth/__init__.py b/unsloth/__init__.py index 5df43d3117..e389074a1b 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -125,6 +125,7 @@ from .import_fixes import ( patch_trackio, patch_datasets, patch_enable_input_require_grads, + fix_openenv_no_vllm, ) fix_xformers_performance_issue() @@ -135,6 +136,7 @@ patch_ipykernel_hf_xet() patch_trackio() patch_datasets() patch_enable_input_require_grads() +fix_openenv_no_vllm() del fix_xformers_performance_issue del fix_vllm_aimv2_issue @@ -144,6 +146,7 @@ del patch_ipykernel_hf_xet del patch_trackio del patch_datasets del patch_enable_input_require_grads +del fix_openenv_no_vllm # Torch 2.4 has including_emulation if DEVICE_TYPE == "cuda": diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 0377127860..653b539b20 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2025.12.4" +__version__ = "2025.12.5" __all__ = [ "SUPPORTS_BFLOAT16", From 9a9813942d89cf8d122b1a544aa5d552d8e72d70 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Dec 2025 05:27:42 -0800 Subject: [PATCH 031/167] Fix --- unsloth/__init__.py | 4 ++-- unsloth/import_fixes.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/unsloth/__init__.py b/unsloth/__init__.py index e389074a1b..30bfae35bb 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -124,7 +124,7 @@ from .import_fixes import ( patch_ipykernel_hf_xet, patch_trackio, patch_datasets, - patch_enable_input_require_grads, + # patch_enable_input_require_grads, fix_openenv_no_vllm, ) @@ -135,7 +135,7 @@ ignore_logger_messages() patch_ipykernel_hf_xet() patch_trackio() patch_datasets() -patch_enable_input_require_grads() +# patch_enable_input_require_grads() fix_openenv_no_vllm() del fix_xformers_performance_issue diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 63bfd6e1e1..79fba855f5 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -386,8 +386,8 @@ def fix_openenv_no_vllm(): text = f.read() bad = ( "if is_vllm_available():\n" - "from vllm import SamplingParams\n" - "from vllm.sampling_params import GuidedDecodingParams\n" + " from vllm import SamplingParams\n" + " from vllm.sampling_params import GuidedDecodingParams\n" ) if bad + "\n" + "\n" in text: text = text.replace( From 680f19f156952cf687ce0f776fa7f73555c8724c Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Dec 2025 05:29:25 -0800 Subject: [PATCH 032/167] Update __init__.py --- unsloth/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unsloth/__init__.py b/unsloth/__init__.py index 30bfae35bb..e389074a1b 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -124,7 +124,7 @@ from .import_fixes import ( patch_ipykernel_hf_xet, patch_trackio, patch_datasets, - # patch_enable_input_require_grads, + patch_enable_input_require_grads, fix_openenv_no_vllm, ) @@ -135,7 +135,7 @@ ignore_logger_messages() patch_ipykernel_hf_xet() patch_trackio() patch_datasets() -# patch_enable_input_require_grads() +patch_enable_input_require_grads() fix_openenv_no_vllm() del fix_xformers_performance_issue From fb763f34ec280e83f935a7ad383c30f92a0a05be Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Dec 2025 05:31:36 -0800 Subject: [PATCH 033/167] Update __init__.py --- unsloth/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unsloth/__init__.py b/unsloth/__init__.py index e389074a1b..72d53f572e 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -18,8 +18,8 @@ import os, re, subprocess, inspect, functools import numpy as np # Log Unsloth is being used -# We want logger in import_fixes and hence setting it here for zoo to be importable os.environ["UNSLOTH_IS_PRESENT"] = "1" + # Fix some issues before importing other packages from .import_fixes import ( fix_message_factory_issue, @@ -46,7 +46,7 @@ if already_imported: # stacklevel=2 makes warning point to user's import line rather than this library code, # showing them exactly where to fix the import order in their script warnings.warn( - f"WARNING: Unsloth should be imported before {', '.join(already_imported)} " + f"WARNING: Unsloth should be imported before [{', '.join(already_imported)}] " f"to ensure all optimizations are applied. Your code may run slower or encounter " f"memory issues without these optimizations.\n\n" f"Please restructure your imports with 'import unsloth' at the top of your file.", From f4f2a7f907f4923bbd1c70257683b09001202c03 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Dec 2025 05:34:29 -0800 Subject: [PATCH 034/167] Update __init__.py --- unsloth/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/unsloth/__init__.py b/unsloth/__init__.py index 72d53f572e..9dd7b08b56 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -16,6 +16,7 @@ import warnings, importlib, sys from packaging.version import Version import os, re, subprocess, inspect, functools import numpy as np +print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) # Log Unsloth is being used os.environ["UNSLOTH_IS_PRESENT"] = "1" @@ -26,6 +27,7 @@ from .import_fixes import ( check_fbgemm_gpu_version, torchvision_compatibility_check, ) +print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) fix_message_factory_issue() check_fbgemm_gpu_version() From e17b62f76e0c2c8da6728aabaae9267e6b2bf00c Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Dec 2025 05:36:40 -0800 Subject: [PATCH 035/167] Update import_fixes.py --- unsloth/import_fixes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 79fba855f5..3ba79c2162 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -268,6 +268,7 @@ def check_fbgemm_gpu_version(): ) logger.info(f"Unsloth: fbgemm_gpu_genai=={fbgemm_gpu_version} detected.") +print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) def patch_enable_input_require_grads(): """ @@ -331,6 +332,7 @@ def patch_enable_input_require_grads(): "Unsloth: Patched enable_input_require_grads for vision model compatibility" ) +print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) def torchvision_compatibility_check(): if importlib.util.find_spec("torch") is None: From f34eb0abb6bfcfb28d0e8bfb00b87bdd3f652da9 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Dec 2025 05:38:24 -0800 Subject: [PATCH 036/167] Update import_fixes.py --- unsloth/import_fixes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 3ba79c2162..75dc12f5a1 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -13,6 +13,7 @@ # limitations under the License. import os +import sys import importlib.util from pathlib import Path from importlib.metadata import version as importlib_version From 04ad21cdb0b77602412adfe58e9b86e08a1d37e8 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Dec 2025 05:40:56 -0800 Subject: [PATCH 037/167] Update import_fixes.py --- unsloth/import_fixes.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 75dc12f5a1..aa848a91b8 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -20,8 +20,9 @@ from importlib.metadata import version as importlib_version from packaging.version import Version as TrueVersion import re import logging +print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) from unsloth_zoo.log import logger - +print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) def Version(version): try: @@ -41,7 +42,7 @@ def Version(version): f"Unsloth: Could not get version for `{version}`\n" f"File name = [{caller.filename}] Line number = [{caller.lineno}]" ) - +print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) # Ignore logging messages class HideLoggingMessage(logging.Filter): @@ -52,7 +53,7 @@ class HideLoggingMessage(logging.Filter): def filter(self, x): return not (self.text in x.getMessage()) - +print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) # Fix up AttributeError: 'MessageFactory' object has no attribute 'GetPrototype' # MUST do this at the start primarily due to tensorflow causing issues @@ -99,7 +100,7 @@ def fix_message_factory_issue(): pass except: pass - +print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) # Fix Xformers performance issues since 0.0.25 def fix_xformers_performance_issue(): @@ -128,7 +129,7 @@ def fix_xformers_performance_issue(): ) except Exception as e: logger.info(f"Unsloth: Failed patching Xformers with error = {str(e)}") - +print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) # ValueError: 'aimv2' is already used by a Transformers config, pick another name. def fix_vllm_aimv2_issue(): @@ -167,7 +168,7 @@ def fix_vllm_aimv2_issue(): ) except Exception as e: logger.info(f"Unsloth: Failed patching vLLM with error = {str(e)}") - +print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) def fix_vllm_guided_decoding_params(): if importlib.util.find_spec("vllm") is None: @@ -183,7 +184,7 @@ def fix_vllm_guided_decoding_params(): vllm.sampling_params.GuidedDecodingParams = ( vllm.sampling_params.StructuredOutputsParams ) - +print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) def ignore_logger_messages(): # Ignore Environment variable `HF_TOKEN` is set @@ -194,7 +195,7 @@ def ignore_logger_messages(): del huggingface_hub_logger except: pass - +print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) def patch_ipykernel_hf_xet(): # HF-XET == 1.1.10 and ipykernel == 7.0.0 / 7.0.1 causes issues @@ -226,7 +227,7 @@ def patch_ipykernel_hf_xet(): from huggingface_hub.utils import disable_progress_bars disable_progress_bars() - +print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) def patch_trackio(): # Set some environment variables to customize the Trackio dashboard for experiment tracking @@ -238,7 +239,7 @@ def patch_trackio(): "https://raw.githubusercontent.com/unslothai/unsloth/main/images/unsloth%20logo%20white%20text.png" ) os.environ["TRACKIO_PLOT_ORDER"] = "train/reward" - +print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) def patch_datasets(): # Datasets 4.4.0 and 4.4.1 weirdly have some weird `_thread.RLock_recursion_count` issues @@ -253,7 +254,7 @@ def patch_datasets(): f"#### Unsloth: Using `datasets = {str(datasets_version)}` will cause recursion errors.\n" "Please downgrade datasets to `datasets==4.3.0" ) - +print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) def check_fbgemm_gpu_version(): if importlib.util.find_spec("fbgemm_gpu") is None: From 32b52a00028bd497ac11bd853ca8f08fd979562e Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Dec 2025 05:44:38 -0800 Subject: [PATCH 038/167] logger --- unsloth/__init__.py | 2 -- unsloth/import_fixes.py | 36 ++++++++++++++++++++---------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/unsloth/__init__.py b/unsloth/__init__.py index 9dd7b08b56..72d53f572e 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -16,7 +16,6 @@ import warnings, importlib, sys from packaging.version import Version import os, re, subprocess, inspect, functools import numpy as np -print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) # Log Unsloth is being used os.environ["UNSLOTH_IS_PRESENT"] = "1" @@ -27,7 +26,6 @@ from .import_fixes import ( check_fbgemm_gpu_version, torchvision_compatibility_check, ) -print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) fix_message_factory_issue() check_fbgemm_gpu_version() diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index aa848a91b8..d90c9a8a07 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -13,16 +13,15 @@ # limitations under the License. import os -import sys import importlib.util from pathlib import Path from importlib.metadata import version as importlib_version from packaging.version import Version as TrueVersion import re import logging -print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) -from unsloth_zoo.log import logger -print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) +# Cannot import logger here since it'll import transformers +# from unsloth_zoo.log import logger + def Version(version): try: @@ -42,7 +41,7 @@ def Version(version): f"Unsloth: Could not get version for `{version}`\n" f"File name = [{caller.filename}] Line number = [{caller.lineno}]" ) -print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) + # Ignore logging messages class HideLoggingMessage(logging.Filter): @@ -53,7 +52,7 @@ class HideLoggingMessage(logging.Filter): def filter(self, x): return not (self.text in x.getMessage()) -print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) + # Fix up AttributeError: 'MessageFactory' object has no attribute 'GetPrototype' # MUST do this at the start primarily due to tensorflow causing issues @@ -71,6 +70,7 @@ def fix_message_factory_issue(): def GetPrototype(self, *args, **kwargs): return + from unsloth_zoo.log import logger if not hasattr(google.protobuf.message_factory, "MessageFactory"): logger.info("Unsloth: Patching protobuf.MessageFactory as it doesn't exist") google.protobuf.message_factory.MessageFactory = MessageFactory @@ -100,7 +100,7 @@ def fix_message_factory_issue(): pass except: pass -print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) + # Fix Xformers performance issues since 0.0.25 def fix_xformers_performance_issue(): @@ -108,6 +108,7 @@ def fix_xformers_performance_issue(): return xformers_version = importlib_version("xformers") if Version(xformers_version) < Version("0.0.29"): + from unsloth_zoo.log import logger xformers_location = importlib.util.find_spec("xformers").origin xformers_location = os.path.split(xformers_location)[0] cutlass = Path(xformers_location) / "ops" / "fmha" / "cutlass.py" @@ -129,7 +130,7 @@ def fix_xformers_performance_issue(): ) except Exception as e: logger.info(f"Unsloth: Failed patching Xformers with error = {str(e)}") -print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) + # ValueError: 'aimv2' is already used by a Transformers config, pick another name. def fix_vllm_aimv2_issue(): @@ -137,6 +138,7 @@ def fix_vllm_aimv2_issue(): return vllm_version = importlib_version("vllm") if Version(vllm_version) < Version("0.10.1"): + from unsloth_zoo.log import logger vllm_version = importlib.util.find_spec("vllm").origin vllm_version = os.path.split(vllm_version)[0] ovis_config = Path(vllm_version) / "transformers_utils" / "configs" / "ovis.py" @@ -168,7 +170,7 @@ def fix_vllm_aimv2_issue(): ) except Exception as e: logger.info(f"Unsloth: Failed patching vLLM with error = {str(e)}") -print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) + def fix_vllm_guided_decoding_params(): if importlib.util.find_spec("vllm") is None: @@ -184,7 +186,7 @@ def fix_vllm_guided_decoding_params(): vllm.sampling_params.GuidedDecodingParams = ( vllm.sampling_params.StructuredOutputsParams ) -print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) + def ignore_logger_messages(): # Ignore Environment variable `HF_TOKEN` is set @@ -195,7 +197,7 @@ def ignore_logger_messages(): del huggingface_hub_logger except: pass -print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) + def patch_ipykernel_hf_xet(): # HF-XET == 1.1.10 and ipykernel == 7.0.0 / 7.0.1 causes issues @@ -227,7 +229,7 @@ def patch_ipykernel_hf_xet(): from huggingface_hub.utils import disable_progress_bars disable_progress_bars() -print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) + def patch_trackio(): # Set some environment variables to customize the Trackio dashboard for experiment tracking @@ -239,7 +241,7 @@ def patch_trackio(): "https://raw.githubusercontent.com/unslothai/unsloth/main/images/unsloth%20logo%20white%20text.png" ) os.environ["TRACKIO_PLOT_ORDER"] = "train/reward" -print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) + def patch_datasets(): # Datasets 4.4.0 and 4.4.1 weirdly have some weird `_thread.RLock_recursion_count` issues @@ -254,7 +256,7 @@ def patch_datasets(): f"#### Unsloth: Using `datasets = {str(datasets_version)}` will cause recursion errors.\n" "Please downgrade datasets to `datasets==4.3.0" ) -print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) + def check_fbgemm_gpu_version(): if importlib.util.find_spec("fbgemm_gpu") is None: @@ -268,9 +270,9 @@ def check_fbgemm_gpu_version(): raise ImportError( f"Unsloth: fbgemm_gpu_genai=={fbgemm_gpu_version} detected. It might cause unexpected issues like segmentation faults. Please uninstall the current one by doing `pip uninstall fbgemm-gpu` && `pip install fbgemm-gpu` to install fbgemm-gpu 1.4.0 or newer!" ) + from unsloth_zoo.log import logger logger.info(f"Unsloth: fbgemm_gpu_genai=={fbgemm_gpu_version} detected.") -print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) def patch_enable_input_require_grads(): """ @@ -330,11 +332,11 @@ def patch_enable_input_require_grads(): self._require_grads_hook = hooks[0] PreTrainedModel.enable_input_require_grads = _patched_enable_input_require_grads + from unsloth_zoo.log import logger logger.info( "Unsloth: Patched enable_input_require_grads for vision model compatibility" ) -print([mod for mod in ["trl", "transformers", "peft"] if mod in sys.modules]) def torchvision_compatibility_check(): if importlib.util.find_spec("torch") is None: @@ -371,6 +373,7 @@ def torchvision_compatibility_check(): f"but found torchvision=={torchvision_version}. " f"Please refer to https://pytorch.org/get-started/previous-versions/ for more information." ) + from unsloth_zoo.log import logger logger.info( f"Unsloth: torch=={torch_version} and torchvision=={torchvision_version} are compatible." ) @@ -385,6 +388,7 @@ def fix_openenv_no_vllm(): openenv = Path(trl_location) / "experimental" / "openenv" / "utils.py" if not openenv.exists(): return + from unsloth_zoo.log import logger try: with open(openenv, "r+", encoding = "utf-8") as f: text = f.read() From 0c9288f311dfe59e4496da999ba29427f66d2a27 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Dec 2025 05:46:50 -0800 Subject: [PATCH 039/167] Update __init__.py --- unsloth/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/unsloth/__init__.py b/unsloth/__init__.py index 72d53f572e..a0ca276cde 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -20,6 +20,10 @@ import numpy as np # Log Unsloth is being used os.environ["UNSLOTH_IS_PRESENT"] = "1" +# Check if modules that need patching are already imported +critical_modules = ["trl", "transformers", "peft"] +already_imported = [mod for mod in critical_modules if mod in sys.modules] + # Fix some issues before importing other packages from .import_fixes import ( fix_message_factory_issue, @@ -34,10 +38,6 @@ del fix_message_factory_issue del check_fbgemm_gpu_version del torchvision_compatibility_check -# Check if modules that need patching are already imported -critical_modules = ["trl", "transformers", "peft"] -already_imported = [mod for mod in critical_modules if mod in sys.modules] - # This check is critical because Unsloth optimizes these libraries by modifying # their code at import time. If they're imported first, the original (slower, # more memory-intensive) implementations will be used instead of Unsloth's From b5b57b3378b5d74948372995f7f9c290d8370502 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:48:24 +0000 Subject: [PATCH 040/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/import_fixes.py | 16 ++++++++++++---- unsloth/kernels/rope_embedding.py | 1 + unsloth/trainer.py | 4 +++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index d90c9a8a07..a78b5451ea 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -71,6 +71,7 @@ def fix_message_factory_issue(): return from unsloth_zoo.log import logger + if not hasattr(google.protobuf.message_factory, "MessageFactory"): logger.info("Unsloth: Patching protobuf.MessageFactory as it doesn't exist") google.protobuf.message_factory.MessageFactory = MessageFactory @@ -109,6 +110,7 @@ def fix_xformers_performance_issue(): xformers_version = importlib_version("xformers") if Version(xformers_version) < Version("0.0.29"): from unsloth_zoo.log import logger + xformers_location = importlib.util.find_spec("xformers").origin xformers_location = os.path.split(xformers_location)[0] cutlass = Path(xformers_location) / "ops" / "fmha" / "cutlass.py" @@ -139,6 +141,7 @@ def fix_vllm_aimv2_issue(): vllm_version = importlib_version("vllm") if Version(vllm_version) < Version("0.10.1"): from unsloth_zoo.log import logger + vllm_version = importlib.util.find_spec("vllm").origin vllm_version = os.path.split(vllm_version)[0] ovis_config = Path(vllm_version) / "transformers_utils" / "configs" / "ovis.py" @@ -271,6 +274,7 @@ def check_fbgemm_gpu_version(): f"Unsloth: fbgemm_gpu_genai=={fbgemm_gpu_version} detected. It might cause unexpected issues like segmentation faults. Please uninstall the current one by doing `pip uninstall fbgemm-gpu` && `pip install fbgemm-gpu` to install fbgemm-gpu 1.4.0 or newer!" ) from unsloth_zoo.log import logger + logger.info(f"Unsloth: fbgemm_gpu_genai=={fbgemm_gpu_version} detected.") @@ -333,6 +337,7 @@ def patch_enable_input_require_grads(): PreTrainedModel.enable_input_require_grads = _patched_enable_input_require_grads from unsloth_zoo.log import logger + logger.info( "Unsloth: Patched enable_input_require_grads for vision model compatibility" ) @@ -374,6 +379,7 @@ def torchvision_compatibility_check(): f"Please refer to https://pytorch.org/get-started/previous-versions/ for more information." ) from unsloth_zoo.log import logger + logger.info( f"Unsloth: torch=={torch_version} and torchvision=={torchvision_version} are compatible." ) @@ -389,6 +395,7 @@ def fix_openenv_no_vllm(): if not openenv.exists(): return from unsloth_zoo.log import logger + try: with open(openenv, "r+", encoding = "utf-8") as f: text = f.read() @@ -400,13 +407,14 @@ def fix_openenv_no_vllm(): if bad + "\n" + "\n" in text: text = text.replace( bad + "\n" + "\n", - bad + ( + bad + + ( "else:\n" - " from typing import Any\n"\ - " SamplingParams = Any\n"\ + " from typing import Any\n" + " SamplingParams = Any\n" " GuidedDecodingParams = Any\n" "\n" - ) + ), ) f.seek(0) f.write(text) diff --git a/unsloth/kernels/rope_embedding.py b/unsloth/kernels/rope_embedding.py index 2adc9ecc5a..a032e0f7fc 100644 --- a/unsloth/kernels/rope_embedding.py +++ b/unsloth/kernels/rope_embedding.py @@ -108,6 +108,7 @@ _rope_embedding_QK = triton.heuristics( ROPE_GROUP_SIZE: int = 4 + def _rope_embedding( Q, Q_row_stride: tl.constexpr, diff --git a/unsloth/trainer.py b/unsloth/trainer.py index 5cd1bfd08d..c0b2dd03b6 100644 --- a/unsloth/trainer.py +++ b/unsloth/trainer.py @@ -319,7 +319,9 @@ def _patch_sft_trainer_auto_packing(trl_module): or isinstance(processing_class, ProcessorMixin) or is_vlm or is_unsupported_model - or (os.environ.get("UNSLOTH_RETURN_LOGITS", "0") == "1") # Disable padding free on forced logits + or ( + os.environ.get("UNSLOTH_RETURN_LOGITS", "0") == "1" + ) # Disable padding free on forced logits ) requested_pack = bool(getattr(config_arg, "packing", False)) if blocked: From efb5801a74a695fcae41653652baadc0bcd78d41 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 12 Dec 2025 05:51:31 -0800 Subject: [PATCH 041/167] Update __init__.py --- unsloth/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsloth/__init__.py b/unsloth/__init__.py index a0ca276cde..007b952200 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -73,7 +73,7 @@ from importlib.metadata import PackageNotFoundError # Check for unsloth_zoo try: unsloth_zoo_version = importlib_version("unsloth_zoo") - if Version(unsloth_zoo_version) < Version("2025.12.3"): + if Version(unsloth_zoo_version) < Version("2025.12.4"): print( "Unsloth: Please update Unsloth and Unsloth-Zoo to the latest version!\n" "Do this via `pip install --upgrade --force-reinstall --no-cache-dir --no-deps unsloth unsloth_zoo`" From 4d2fe69e4b0e198b8693adbe56350e6888cc57e5 Mon Sep 17 00:00:00 2001 From: oKatanaaa Date: Sat, 13 Dec 2025 00:02:48 +0000 Subject: [PATCH 042/167] fix: add a log instead of silent exception --- unsloth/models/llama.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/unsloth/models/llama.py b/unsloth/models/llama.py index e0d8cbcf25..6e47907166 100644 --- a/unsloth/models/llama.py +++ b/unsloth/models/llama.py @@ -3022,8 +3022,11 @@ class FastLlamaModel: if hasattr(target_module, "weight"): try: delattr(target_module, "weight") - except Exception: - pass + except Exception as exc: + logger.warning_once( + f"Unsloth: Could not delete existing weight attr during retie on " + f"{type(target_module).__name__}: {exc}" + ) target_module.register_parameter("weight", weight) # Tie trainable copies created by ModulesToSaveWrapper first (these are used in forward) From 554021220c993dc9c424e7bd3e4fa9bb0e3c2445 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 16 Dec 2025 19:48:41 -0800 Subject: [PATCH 043/167] Update import_fixes.py --- unsloth/import_fixes.py | 67 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 3da82c8eb6..458221cf18 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -422,3 +422,70 @@ def fix_openenv_no_vllm(): ) except Exception as e: logger.info(f"Unsloth: Failed patching TRL OpenEnv with error = {str(e)}") + + +# Fix Exeuctorch needing get_mapped_key +def fix_executorch(): + if importlib.util.find_spec("executorch") is None: + print(1) + executorch_location = importlib.util.find_spec("executorch").origin + if executorch_location is None: + executorch_location = importlib.util.find_spec("executorch").submodule_search_locations[0] + else: + executorch_location = os.path.split(executorch_location)[0] + executorch = Path(executorch_location) / "examples" / "models" / "__init__.py" + if not executorch.exists(): + return + + try: + what = r''' + import sys + import types + import re + from typing import Any, Optional + def get_mapped_key(key: str, mapping_dict: dict[str, str]) -> str: + try: + # Checks if there is a layer # in the key + if any(k.isdigit() for k in key.split(".")): + # Replace layer number with "{}" to create key for lookup + abstract_key = re.sub(r"(\.\d+)", ".{}", key) + layer_num = re.search(r"\d+", key).group(0) + new_key = mapping_dict[abstract_key] + new_key = new_key.format(layer_num) + else: + new_key = mapping_dict[key] + except KeyError as e: + raise Exception( + f'Error converting the state dict. Found unexpected key: "{key}". ' + "Please make sure you're loading a checkpoint with the right format. " + ) from e + + return new_key + + torchtune = types.ModuleType("torchtune") + torchtune.__path__ = [] + models = types.ModuleType("torchtune.models") + models.__path__ = [] + convert_weights = types.ModuleType("torchtune.models.convert_weights") + convert_weights.get_mapped_key = get_mapped_key + torchtune.models = models + models.convert_weights = convert_weights + sys.modules["torchtune"] = torchtune + sys.modules["torchtune.models"] = models + sys.modules["torchtune.models.convert_weights"] = convert_weights + ''' + what = textwrap.dedent(what) + + with open(executorch, "r+", encoding = "utf-8") as f: + text = f.read() + bad = "from enum import Enum\n" + if bad in text: + text = text.replace(bad + "\n", bad + "\n" + what) + f.seek(0) + f.write(text) + f.truncate() + logger.info( + "Unsloth: Patching Executorch to fix get_mapped_key" + ) + except Exception as e: + logger.info(f"Unsloth: Failed Executorch with error = {str(e)}") From 4c80b034af13c04fad72c2a5e7e746d13fa04b99 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 16 Dec 2025 19:49:19 -0800 Subject: [PATCH 044/167] Update __init__.py --- unsloth/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/unsloth/__init__.py b/unsloth/__init__.py index 007b952200..bf3de82dc0 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -126,6 +126,7 @@ from .import_fixes import ( patch_datasets, patch_enable_input_require_grads, fix_openenv_no_vllm, + fix_executorch, ) fix_xformers_performance_issue() @@ -137,6 +138,7 @@ patch_trackio() patch_datasets() patch_enable_input_require_grads() fix_openenv_no_vllm() +fix_executorch() del fix_xformers_performance_issue del fix_vllm_aimv2_issue @@ -147,6 +149,7 @@ del patch_trackio del patch_datasets del patch_enable_input_require_grads del fix_openenv_no_vllm +del fix_executorch # Torch 2.4 has including_emulation if DEVICE_TYPE == "cuda": From e57edc2694c3f8929182d31e5c166275930fbf9d Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 16 Dec 2025 19:51:10 -0800 Subject: [PATCH 045/167] Update import_fixes.py --- unsloth/import_fixes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 458221cf18..1577a8384e 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -427,7 +427,7 @@ def fix_openenv_no_vllm(): # Fix Exeuctorch needing get_mapped_key def fix_executorch(): if importlib.util.find_spec("executorch") is None: - print(1) + return executorch_location = importlib.util.find_spec("executorch").origin if executorch_location is None: executorch_location = importlib.util.find_spec("executorch").submodule_search_locations[0] From 9bc1567a17e54b4313f55dda9109e06e30b55a38 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 16 Dec 2025 19:59:04 -0800 Subject: [PATCH 046/167] Update import_fixes.py --- unsloth/import_fixes.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 1577a8384e..09c8637992 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -115,8 +115,11 @@ def fix_xformers_performance_issue(): return xformers_version = importlib_version("xformers") if Version(xformers_version) < Version("0.0.29"): - xformers_location = importlib.util.find_spec("xformers").origin - xformers_location = os.path.split(xformers_location)[0] + xformers_location = importlib.util.find_spec("xformers") + if xformers_location is None: + xformers_location = importlib.util.find_spec("xformers").submodule_search_locations[0] + else: + xformers_location = os.path.split(xformers_location.origin)[0] cutlass = Path(xformers_location) / "ops" / "fmha" / "cutlass.py" try: if cutlass.exists(): @@ -144,8 +147,11 @@ def fix_vllm_aimv2_issue(): return vllm_version = importlib_version("vllm") if Version(vllm_version) < Version("0.10.1"): - vllm_version = importlib.util.find_spec("vllm").origin - vllm_version = os.path.split(vllm_version)[0] + vllm_version = importlib.util.find_spec("vllm") + if vllm_version is None: + vllm_version = importlib.util.find_spec("vllm").submodule_search_locations[0] + else: + vllm_version = os.path.split(vllm_version.origin)[0] ovis_config = Path(vllm_version) / "transformers_utils" / "configs" / "ovis.py" try: if ovis_config.exists(): @@ -388,8 +394,11 @@ def torchvision_compatibility_check(): def fix_openenv_no_vllm(): if importlib.util.find_spec("trl") is None: return - trl_location = importlib.util.find_spec("trl").origin - trl_location = os.path.split(trl_location)[0] + trl_location = importlib.util.find_spec("trl") + if trl_location is None: + trl_location = importlib.util.find_spec("trl").submodule_search_locations[0] + else: + trl_location = os.path.split(trl_location.origin)[0] openenv = Path(trl_location) / "experimental" / "openenv" / "utils.py" if not openenv.exists(): return @@ -428,11 +437,11 @@ def fix_openenv_no_vllm(): def fix_executorch(): if importlib.util.find_spec("executorch") is None: return - executorch_location = importlib.util.find_spec("executorch").origin + executorch_location = importlib.util.find_spec("executorch") if executorch_location is None: executorch_location = importlib.util.find_spec("executorch").submodule_search_locations[0] else: - executorch_location = os.path.split(executorch_location)[0] + executorch_location = os.path.split(executorch_location.origin)[0] executorch = Path(executorch_location) / "examples" / "models" / "__init__.py" if not executorch.exists(): return From c22fbb7383bc917e39f2242f72488da5896b377e Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 16 Dec 2025 20:06:12 -0800 Subject: [PATCH 047/167] Update import_fixes.py --- unsloth/import_fixes.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 09c8637992..23bb4789ca 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -115,11 +115,11 @@ def fix_xformers_performance_issue(): return xformers_version = importlib_version("xformers") if Version(xformers_version) < Version("0.0.29"): - xformers_location = importlib.util.find_spec("xformers") + xformers_location = importlib.util.find_spec("xformers").origin if xformers_location is None: xformers_location = importlib.util.find_spec("xformers").submodule_search_locations[0] else: - xformers_location = os.path.split(xformers_location.origin)[0] + xformers_location = os.path.split(xformers_location)[0] cutlass = Path(xformers_location) / "ops" / "fmha" / "cutlass.py" try: if cutlass.exists(): @@ -147,11 +147,11 @@ def fix_vllm_aimv2_issue(): return vllm_version = importlib_version("vllm") if Version(vllm_version) < Version("0.10.1"): - vllm_version = importlib.util.find_spec("vllm") + vllm_version = importlib.util.find_spec("vllm").origin if vllm_version is None: vllm_version = importlib.util.find_spec("vllm").submodule_search_locations[0] else: - vllm_version = os.path.split(vllm_version.origin)[0] + vllm_version = os.path.split(vllm_version)[0] ovis_config = Path(vllm_version) / "transformers_utils" / "configs" / "ovis.py" try: if ovis_config.exists(): @@ -394,11 +394,11 @@ def torchvision_compatibility_check(): def fix_openenv_no_vllm(): if importlib.util.find_spec("trl") is None: return - trl_location = importlib.util.find_spec("trl") + trl_location = importlib.util.find_spec("trl").origin if trl_location is None: trl_location = importlib.util.find_spec("trl").submodule_search_locations[0] else: - trl_location = os.path.split(trl_location.origin)[0] + trl_location = os.path.split(trl_location)[0] openenv = Path(trl_location) / "experimental" / "openenv" / "utils.py" if not openenv.exists(): return @@ -437,11 +437,11 @@ def fix_openenv_no_vllm(): def fix_executorch(): if importlib.util.find_spec("executorch") is None: return - executorch_location = importlib.util.find_spec("executorch") + executorch_location = importlib.util.find_spec("executorch").origin if executorch_location is None: executorch_location = importlib.util.find_spec("executorch").submodule_search_locations[0] else: - executorch_location = os.path.split(executorch_location.origin)[0] + executorch_location = os.path.split(executorch_location)[0] executorch = Path(executorch_location) / "examples" / "models" / "__init__.py" if not executorch.exists(): return From d7482e8b2345e8d066def3a58354741335c804f3 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 16 Dec 2025 20:15:22 -0800 Subject: [PATCH 048/167] Update import_fixes.py --- unsloth/import_fixes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 23bb4789ca..3b10ec26fb 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -19,6 +19,7 @@ from importlib.metadata import version as importlib_version from packaging.version import Version as TrueVersion import re import logging +import textwrap # We cannot do from unsloth_zoo.log import logger since FBGEMM might cause seg faults. UNSLOTH_ENABLE_LOGGING = os.environ.get("UNSLOTH_ENABLE_LOGGING", "0") in ("1", "True", "true",) From ca7779872febbb98a02f6c37baa4bcb1f3501f04 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 04:26:02 +0000 Subject: [PATCH 049/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/import_fixes.py | 38 +++++++++++++++++++++++++------------- unsloth/models/rl.py | 2 +- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 3b10ec26fb..a93a6c917f 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -22,14 +22,22 @@ import logging import textwrap # We cannot do from unsloth_zoo.log import logger since FBGEMM might cause seg faults. -UNSLOTH_ENABLE_LOGGING = os.environ.get("UNSLOTH_ENABLE_LOGGING", "0") in ("1", "True", "true",) +UNSLOTH_ENABLE_LOGGING = os.environ.get("UNSLOTH_ENABLE_LOGGING", "0") in ( + "1", + "True", + "true", +) logger = logging.getLogger(__name__) if UNSLOTH_ENABLE_LOGGING: - logging.basicConfig(level = logging.INFO, format = '[%(name)s|%(levelname)s]%(message)s') + logging.basicConfig( + level = logging.INFO, format = "[%(name)s|%(levelname)s]%(message)s" + ) logger.setLevel(logging.INFO) else: - logging.basicConfig(level = logging.WARNING, format = '[%(name)s|%(levelname)s]%(message)s') - logger.setLevel(logging.WARNING) + logging.basicConfig( + level = logging.WARNING, format = "[%(name)s|%(levelname)s]%(message)s" + ) + logger.setLevel(logging.WARNING) def Version(version): @@ -118,7 +126,9 @@ def fix_xformers_performance_issue(): if Version(xformers_version) < Version("0.0.29"): xformers_location = importlib.util.find_spec("xformers").origin if xformers_location is None: - xformers_location = importlib.util.find_spec("xformers").submodule_search_locations[0] + xformers_location = importlib.util.find_spec( + "xformers" + ).submodule_search_locations[0] else: xformers_location = os.path.split(xformers_location)[0] cutlass = Path(xformers_location) / "ops" / "fmha" / "cutlass.py" @@ -150,7 +160,9 @@ def fix_vllm_aimv2_issue(): if Version(vllm_version) < Version("0.10.1"): vllm_version = importlib.util.find_spec("vllm").origin if vllm_version is None: - vllm_version = importlib.util.find_spec("vllm").submodule_search_locations[0] + vllm_version = importlib.util.find_spec("vllm").submodule_search_locations[ + 0 + ] else: vllm_version = os.path.split(vllm_version)[0] ovis_config = Path(vllm_version) / "transformers_utils" / "configs" / "ovis.py" @@ -440,7 +452,9 @@ def fix_executorch(): return executorch_location = importlib.util.find_spec("executorch").origin if executorch_location is None: - executorch_location = importlib.util.find_spec("executorch").submodule_search_locations[0] + executorch_location = importlib.util.find_spec( + "executorch" + ).submodule_search_locations[0] else: executorch_location = os.path.split(executorch_location)[0] executorch = Path(executorch_location) / "examples" / "models" / "__init__.py" @@ -448,7 +462,7 @@ def fix_executorch(): return try: - what = r''' + what = r""" import sys import types import re @@ -483,9 +497,9 @@ def fix_executorch(): sys.modules["torchtune"] = torchtune sys.modules["torchtune.models"] = models sys.modules["torchtune.models.convert_weights"] = convert_weights - ''' + """ what = textwrap.dedent(what) - + with open(executorch, "r+", encoding = "utf-8") as f: text = f.read() bad = "from enum import Enum\n" @@ -494,8 +508,6 @@ def fix_executorch(): f.seek(0) f.write(text) f.truncate() - logger.info( - "Unsloth: Patching Executorch to fix get_mapped_key" - ) + logger.info("Unsloth: Patching Executorch to fix get_mapped_key") except Exception as e: logger.info(f"Unsloth: Failed Executorch with error = {str(e)}") diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index 3fd180bb27..31316e45b7 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -741,7 +741,7 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): "generation_kwargs": {}, "bf16": False, "fp16": False, - "report_to" : "none", + "report_to": "none", "include_tokens_per_second": False, "include_num_input_tokens_seen": False, "auto_find_batch_size": False, # Auto /2 batch size - too many people complained so removing From 71c5938e49927a38c335e2f8f83769ae2065a660 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 16 Dec 2025 20:34:34 -0800 Subject: [PATCH 050/167] Update import_fixes.py --- unsloth/import_fixes.py | 51 +++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 3b10ec26fb..afbc6f5a96 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -112,13 +112,14 @@ def fix_message_factory_issue(): # Fix Xformers performance issues since 0.0.25 def fix_xformers_performance_issue(): - if importlib.util.find_spec("xformers") is None: + spec = importlib.util.find_spec("xformers") + if spec is None: return xformers_version = importlib_version("xformers") if Version(xformers_version) < Version("0.0.29"): - xformers_location = importlib.util.find_spec("xformers").origin + xformers_location = spec.origin if xformers_location is None: - xformers_location = importlib.util.find_spec("xformers").submodule_search_locations[0] + xformers_location = spec.submodule_search_locations[0] else: xformers_location = os.path.split(xformers_location)[0] cutlass = Path(xformers_location) / "ops" / "fmha" / "cutlass.py" @@ -144,13 +145,14 @@ def fix_xformers_performance_issue(): # ValueError: 'aimv2' is already used by a Transformers config, pick another name. def fix_vllm_aimv2_issue(): - if importlib.util.find_spec("vllm") is None: + spec = importlib.util.find_spec("vllm") + if spec is None: return vllm_version = importlib_version("vllm") if Version(vllm_version) < Version("0.10.1"): - vllm_version = importlib.util.find_spec("vllm").origin + vllm_version = spec.origin if vllm_version is None: - vllm_version = importlib.util.find_spec("vllm").submodule_search_locations[0] + vllm_version = spec.submodule_search_locations[0] else: vllm_version = os.path.split(vllm_version)[0] ovis_config = Path(vllm_version) / "transformers_utils" / "configs" / "ovis.py" @@ -393,11 +395,12 @@ def torchvision_compatibility_check(): # Fix TRL OpenEnv 0.26 NameError: name 'SamplingParams' is not defined def fix_openenv_no_vllm(): - if importlib.util.find_spec("trl") is None: + spec = importlib.util.find_spec("trl") + if spec is None: return - trl_location = importlib.util.find_spec("trl").origin + trl_location = spec.origin if trl_location is None: - trl_location = importlib.util.find_spec("trl").submodule_search_locations[0] + trl_location = spec.submodule_search_locations[0] else: trl_location = os.path.split(trl_location)[0] openenv = Path(trl_location) / "experimental" / "openenv" / "utils.py" @@ -412,18 +415,15 @@ def fix_openenv_no_vllm(): " from vllm import SamplingParams\n" " from vllm.sampling_params import GuidedDecodingParams\n" ) - if bad + "\n" + "\n" in text: - text = text.replace( - bad + "\n" + "\n", - bad - + ( - "else:\n" - " from typing import Any\n" - " SamplingParams = Any\n" - " GuidedDecodingParams = Any\n" - "\n" - ), - ) + replace_with = bad + ( + "else:\n" + " from typing import Any\n" + " SamplingParams = Any\n" + " GuidedDecodingParams = Any\n" + "\n" + ) + if bad + "\n" + "\n" in text and replace_with not in text: + text = text.replace(bad + "\n" + "\n", replace_with) f.seek(0) f.write(text) f.truncate() @@ -436,11 +436,12 @@ def fix_openenv_no_vllm(): # Fix Exeuctorch needing get_mapped_key def fix_executorch(): - if importlib.util.find_spec("executorch") is None: + spec = importlib.util.find_spec("executorch") + if spec is None: return - executorch_location = importlib.util.find_spec("executorch").origin + executorch_location = spec.origin if executorch_location is None: - executorch_location = importlib.util.find_spec("executorch").submodule_search_locations[0] + executorch_location = spec.submodule_search_locations[0] else: executorch_location = os.path.split(executorch_location)[0] executorch = Path(executorch_location) / "examples" / "models" / "__init__.py" @@ -489,7 +490,7 @@ def fix_executorch(): with open(executorch, "r+", encoding = "utf-8") as f: text = f.read() bad = "from enum import Enum\n" - if bad in text: + if bad in text and what not in text: text = text.replace(bad + "\n", bad + "\n" + what) f.seek(0) f.write(text) From 7c101954bbc062199460ca3b1849e3908c7c6082 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 16 Dec 2025 20:52:09 -0800 Subject: [PATCH 051/167] Update unsloth/import_fixes.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- unsloth/import_fixes.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index cffbb8ef3a..2c4dbcffb0 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -158,12 +158,12 @@ def fix_vllm_aimv2_issue(): return vllm_version = importlib_version("vllm") if Version(vllm_version) < Version("0.10.1"): - vllm_version = spec.origin - if vllm_version is None: - vllm_version = spec.submodule_search_locations[0] + vllm_location = spec.origin + if vllm_location is None: + vllm_location = spec.submodule_search_locations[0] else: - vllm_version = os.path.split(vllm_version)[0] - ovis_config = Path(vllm_version) / "transformers_utils" / "configs" / "ovis.py" + vllm_location = os.path.split(vllm_location)[0] + ovis_config = Path(vllm_location) / "transformers_utils" / "configs" / "ovis.py" try: if ovis_config.exists(): with open(ovis_config, "r+", encoding = "utf-8") as f: From 3738db73a8445a3cd858d1409290152198c0d9fb Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 16 Dec 2025 23:15:31 -0800 Subject: [PATCH 052/167] Update save.py --- unsloth/save.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/unsloth/save.py b/unsloth/save.py index 01887321cf..640a7ffe14 100644 --- a/unsloth/save.py +++ b/unsloth/save.py @@ -3037,7 +3037,9 @@ def patch_saving_functions(model, vision = False): model.save_pretrained_merged = types.MethodType( unsloth_generic_save_pretrained_merged, model ) - model.push_to_hub_gguf = types.MethodType(unsloth_push_to_hub_gguf, model) + model.push_to_hub_gguf = types.MethodType( + unsloth_push_to_hub_gguf, model + ) model.save_pretrained_gguf = types.MethodType( unsloth_save_pretrained_gguf, model ) @@ -3058,7 +3060,9 @@ def patch_saving_functions(model, vision = False): model.save_pretrained_merged = types.MethodType( unsloth_generic_save_pretrained_merged, model ) - model.push_to_hub_gguf = types.MethodType(unsloth_push_to_hub_gguf, model) + model.push_to_hub_gguf = types.MethodType( + unsloth_push_to_hub_gguf, model + ) model.save_pretrained_gguf = types.MethodType( unsloth_save_pretrained_gguf, model ) From fbaacce52013adc8bbb5d16455bfa7c69b79ada1 Mon Sep 17 00:00:00 2001 From: Datta Nimmaturi Date: Wed, 17 Dec 2025 14:37:21 +0530 Subject: [PATCH 053/167] [fbgemm] Silence tma fbgemm (#3735) * Silence fbgemm TMA print Also safer .push_to_hub * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- unsloth/import_fixes.py | 30 ++++++++++++++++++++++++++++++ unsloth/save.py | 6 +++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 2c4dbcffb0..308bd92db7 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -71,6 +71,36 @@ class HideLoggingMessage(logging.Filter): return not (self.text in x.getMessage()) +class HidePrintMessage: + __slots__ = ("_original_stream", "_hidden_texts") + + def __init__(self, original_stream): + self._original_stream = original_stream + self._hidden_texts = [] + + def add_filter(self, text): + self._hidden_texts.append(text) + + def write(self, message): + if not any(text in message for text in self._hidden_texts): + self._original_stream.write(message) + + def flush(self): + self._original_stream.flush() + + def __getattr__(self, name): + return getattr(self._original_stream, name) + + +if os.environ.get("UNSLOTH_ENABLE_LOGGING", "0") != "1": + import sys + + # Apply to stderr for FBGEMM + sys.stderr = HidePrintMessage(sys.stderr) + # https://github.com/pytorch/FBGEMM/blob/d99cd96490ec4aabac2ee95b1e76ea4dcfcfa628/fbgemm_gpu/experimental/gemm/triton_gemm/utils.py#L43-L52 + sys.stderr.add_filter("TMA benchmarks will be running") + + # Fix up AttributeError: 'MessageFactory' object has no attribute 'GetPrototype' # MUST do this at the start primarily due to tensorflow causing issues def fix_message_factory_issue(): diff --git a/unsloth/save.py b/unsloth/save.py index 01887321cf..d3b20f117c 100644 --- a/unsloth/save.py +++ b/unsloth/save.py @@ -3010,7 +3010,11 @@ def patch_saving_functions(model, vision = False): original_model = model while True: - if original_model.push_to_hub.__name__ != "unsloth_push_to_hub": + # Check if push_to_hub exists before accessing its __name__ + if ( + hasattr(original_model, "push_to_hub") + and original_model.push_to_hub.__name__ != "unsloth_push_to_hub" + ): original_model.original_push_to_hub = original_model.push_to_hub original_model.push_to_hub = types.MethodType( unsloth_push_to_hub, original_model From 3e6cfb5f27eb0830e49d1cd24eff780d3838f131 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 17 Dec 2025 01:51:29 -0800 Subject: [PATCH 054/167] Update loader.py --- unsloth/models/loader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/unsloth/models/loader.py b/unsloth/models/loader.py index e1c13315f7..bfa94d86d7 100644 --- a/unsloth/models/loader.py +++ b/unsloth/models/loader.py @@ -739,6 +739,8 @@ class FastModel(FastBaseModel): "compatible with `full_finetuning=True`. If you wish to use QAT with LoRA, " "please pass in `qat_scheme` in `FastLanguageModel.get_peft_model(...)` instead." ) + if qat_scheme == "phone-deployment": + qat_scheme = "int8-int4" # Check if 4bit is allowed specifically for AMD if not ALLOW_BITSANDBYTES and not use_exact_model_name: if load_in_4bit or load_in_8bit or model_name.lower().endswith("-bnb-4bit"): From 30a454cc5518e1101134d8a62187987495404389 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 17 Dec 2025 02:21:47 -0800 Subject: [PATCH 055/167] Update save.py --- unsloth/save.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/unsloth/save.py b/unsloth/save.py index f5ea8d7d8f..5fa2df7b18 100644 --- a/unsloth/save.py +++ b/unsloth/save.py @@ -2745,6 +2745,17 @@ def _unsloth_save_torchao_with_attached_config( """Save a QAT-trained model by converting fake-quantized weights to real quantized weights.""" # Convert QAT fake-quantized weights to real quantized weights _convert_torchao_model(model) + # PEFT models also might come here, so parse it + if isinstance(model, PeftModelForCausalLM): + _unsloth_save_torchao_with_given_config( + model = model, + save_directory = save_directory, + tokenizer = tokenizer, + torchao_config = model.config.quantization_config, + push_to_hub = push_to_hub, + token = token, + ) + return # TorchAO does not support safe_serialization reliably safe_serialization = False @@ -2897,7 +2908,7 @@ def unsloth_save_pretrained_torchao( ) if torchao_config is not None: - # PTQ path: user provided a config, model must NOT have QAT config + # PTQ path: user provided a config, model must NOT have QAT config unless PEFT assert not has_qat_config, ( "Unsloth: You passed `torchao_config` but this model was trained with `qat_scheme`. " "For QAT models, do not pass `torchao_config` - the quantization config is already " From b58663ae421218bbee207b1d918938b8278d26d2 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 17 Dec 2025 02:28:03 -0800 Subject: [PATCH 056/167] Update save.py --- unsloth/save.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/unsloth/save.py b/unsloth/save.py index 5fa2df7b18..c5099a5891 100644 --- a/unsloth/save.py +++ b/unsloth/save.py @@ -2817,7 +2817,10 @@ def _unsloth_save_torchao_with_given_config( ) from torchao import quantize_ - quantization_config = TorchAoConfig(quant_type = torchao_config) + if isinstance(torchao_config, TorchAoConfig): + quantization_config = torchao_config + else: + quantization_config = TorchAoConfig(quant_type = torchao_config) # Determine if this is a VLM is_vlm = False From 7b613759e9e652b4c88c2c6f90e4441917e341da Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 17 Dec 2025 02:38:05 -0800 Subject: [PATCH 057/167] Update _utils.py --- unsloth/models/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 653b539b20..5b2cc681b0 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2025.12.5" +__version__ = "2025.12.6" __all__ = [ "SUPPORTS_BFLOAT16", From a2b5def55ac411634bcf9915141f0618aaac5548 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 17 Dec 2025 02:54:15 -0800 Subject: [PATCH 058/167] Update _utils.py --- unsloth/models/_utils.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 5b2cc681b0..6f6d693b4f 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -413,16 +413,6 @@ try: except: pass -# Flax classes are deprecated and will be removed in Diffusers v1.0.0. -try: - from diffusers.utils import logger as diffusers_logger - - diffusers_logger.addFilter(HideLoggingMessage("are deprecated")) - del diffusers_logger -except: - pass - - # Errors out on # Some weights of Gemma3nForConditionalGeneration were not initialized from the model checkpoint from transformers.modeling_utils import logger as transformers_logger From 59a1fa57714ed8a3e6ddbf8abb2ad31d1b2456f0 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 17 Dec 2025 03:25:40 -0800 Subject: [PATCH 059/167] Diffusers warnings --- unsloth/__init__.py | 3 +++ unsloth/import_fixes.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/unsloth/__init__.py b/unsloth/__init__.py index bf3de82dc0..d10a0f8030 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -29,14 +29,17 @@ from .import_fixes import ( fix_message_factory_issue, check_fbgemm_gpu_version, torchvision_compatibility_check, + fix_diffusers_warnings, ) fix_message_factory_issue() check_fbgemm_gpu_version() torchvision_compatibility_check() +fix_diffusers_warnings() del fix_message_factory_issue del check_fbgemm_gpu_version del torchvision_compatibility_check +del fix_diffusers_warnings # This check is critical because Unsloth optimizes these libraries by modifying # their code at import time. If they're imported first, the original (slower, diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 308bd92db7..f3aae7f523 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -536,3 +536,8 @@ def fix_executorch(): logger.info("Unsloth: Patching Executorch to fix get_mapped_key") except Exception as e: logger.info(f"Unsloth: Failed Executorch with error = {str(e)}") + + +def fix_diffusers_warnings(): + # Silence Flax classes are deprecated and will be removed in Diffusers v1.0.0. + os.environ["DIFFUSERS_VERBOSITY"] = "error" From 5e33a07b50265ff947404084af2bb71458e39782 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 17 Dec 2025 03:26:19 -0800 Subject: [PATCH 060/167] Update pyproject.toml --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c6e19b014e..cb3f8f3fa4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ huggingfacenotorch = [ ] huggingface = [ "unsloth[huggingfacenotorch]", - "unsloth_zoo>=2025.12.4", + "unsloth_zoo>=2025.12.5", "torchvision", "unsloth[triton]", ] @@ -523,7 +523,7 @@ colab-ampere-torch220 = [ "flash-attn>=2.6.3 ; ('linux' in sys_platform)", ] colab-new = [ - "unsloth_zoo>=2025.12.4", + "unsloth_zoo>=2025.12.5", "packaging", "tyro", "transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,<=4.57.3", From 1f1bf49a588233e9e6ea3285e20c89e5d49e734a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:29:39 +0000 Subject: [PATCH 061/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/save.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/unsloth/save.py b/unsloth/save.py index c5099a5891..24303aba52 100644 --- a/unsloth/save.py +++ b/unsloth/save.py @@ -3055,9 +3055,7 @@ def patch_saving_functions(model, vision = False): model.save_pretrained_merged = types.MethodType( unsloth_generic_save_pretrained_merged, model ) - model.push_to_hub_gguf = types.MethodType( - unsloth_push_to_hub_gguf, model - ) + model.push_to_hub_gguf = types.MethodType(unsloth_push_to_hub_gguf, model) model.save_pretrained_gguf = types.MethodType( unsloth_save_pretrained_gguf, model ) @@ -3078,9 +3076,7 @@ def patch_saving_functions(model, vision = False): model.save_pretrained_merged = types.MethodType( unsloth_generic_save_pretrained_merged, model ) - model.push_to_hub_gguf = types.MethodType( - unsloth_push_to_hub_gguf, model - ) + model.push_to_hub_gguf = types.MethodType(unsloth_push_to_hub_gguf, model) model.save_pretrained_gguf = types.MethodType( unsloth_save_pretrained_gguf, model ) From 5d0286d6c5ecbf0b652a24f124c6abdd5db39d0a Mon Sep 17 00:00:00 2001 From: Datta Nimmaturi Date: Thu, 18 Dec 2025 17:37:12 +0530 Subject: [PATCH 062/167] [hf_hub] Token login (#3739) * login on token * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * cleanup old code * safer imports * cleanup * Return token after login * correct return types * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Apply suggestion from @danielhanchen * add back imports * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * finish return token --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel Han --- unsloth/models/_utils.py | 21 +++++++++++++++++++++ unsloth/models/llama.py | 3 +-- unsloth/models/loader.py | 23 +++-------------------- unsloth/models/vision.py | 3 +-- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 6f6d693b4f..0d0f90cf40 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -72,6 +72,7 @@ __all__ = [ "patch_hf_quantizer", "verify_fp8_support_if_applicable", "_get_inference_mode_context_manager", + "hf_login", ] import torch @@ -2344,3 +2345,23 @@ def _get_inference_mode_context_manager(model: torch.nn.Module): return torch.no_grad() else: return torch.inference_mode() + + +def hf_login(token: Optional[str] = None) -> Optional[str]: + if token is None: + try: + from huggingface_hub import get_token + + token = get_token() + if token is None: + return None + except: + return None + try: + from huggingface_hub import login + + login(token = token) + return token + except Exception as e: + logger.info(f"Failed to login to huggingface using token with error: {e}") + return token diff --git a/unsloth/models/llama.py b/unsloth/models/llama.py index 4c9337ccf9..1d7695b9aa 100644 --- a/unsloth/models/llama.py +++ b/unsloth/models/llama.py @@ -2130,8 +2130,7 @@ class FastLlamaModel: "Unsloth: `unsloth_vllm_standby` is True, but environment variable `UNSLOTH_VLLM_STANDBY` is not set to 1!" ) - if token is None: - token = get_token() + token = hf_login(token) if model_patcher is None: model_patcher = FastLlamaModel SUPPORTS_BFLOAT16 = is_bfloat16_supported() diff --git a/unsloth/models/loader.py b/unsloth/models/loader.py index bfa94d86d7..b13775076c 100644 --- a/unsloth/models/loader.py +++ b/unsloth/models/loader.py @@ -20,6 +20,7 @@ from ._utils import ( HAS_FLASH_ATTENTION_SOFTCAPPING, USE_MODELSCOPE, get_transformers_model_type, + hf_login, ) from .granite import FastGraniteModel from .llama import FastLlamaModel, logger @@ -151,15 +152,7 @@ class FastLanguageModel(FastLlamaModel): **kwargs, ): # Login to allow private models - if token is None: - token = get_token() - if token is not None: - try: - from huggingface_hub import login - - login(token = token) - except: - pass + token = hf_login(token) if load_in_8bit or full_finetuning or qat_scheme is not None: return FastModel.from_pretrained( model_name = model_name, @@ -195,8 +188,6 @@ class FastLanguageModel(FastLlamaModel): **kwargs, ) - if token is None: - token = get_token() if isinstance(dtype, str) and dtype in ["float16", "bfloat16"]: dtype = getattr(torch, dtype) assert ( @@ -682,16 +673,8 @@ class FastModel(FastBaseModel): *args, **kwargs, ): - if token is None: - token = get_token() # Login to allow private models - if token is not None: - try: - from huggingface_hub import login - - login(token = token) - except: - pass + token = hf_login(token) if whisper_language is not None: assert type(whisper_language) is str if whisper_task is not None: diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index ed19f587cf..a10d65f3fb 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -390,8 +390,7 @@ class FastBaseModel: "Unsloth: WARNING `trust_remote_code` is True.\n" "Are you certain you want to do remote code execution?" ) - if token is None: - token = get_token() + token = hf_login(token) SUPPORTS_BFLOAT16 = is_bfloat16_supported() if DEVICE_TYPE == "cuda": From 1de77bfadcb30e51f73c58d28ef3db9861f76f69 Mon Sep 17 00:00:00 2001 From: Datta Nimmaturi Date: Sat, 20 Dec 2025 08:38:28 +0530 Subject: [PATCH 063/167] Do not overwrite slots (#3752) * Do not overwrite slots * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- unsloth/import_fixes.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index f3aae7f523..efc7a7f4cd 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -72,8 +72,6 @@ class HideLoggingMessage(logging.Filter): class HidePrintMessage: - __slots__ = ("_original_stream", "_hidden_texts") - def __init__(self, original_stream): self._original_stream = original_stream self._hidden_texts = [] From 82df967646bacc36633d73c6b373f66c514bc2c0 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 23 Dec 2025 04:46:43 -0800 Subject: [PATCH 064/167] Update save.py --- unsloth/save.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/unsloth/save.py b/unsloth/save.py index f9c677f5f7..c4ee322937 100644 --- a/unsloth/save.py +++ b/unsloth/save.py @@ -1429,7 +1429,7 @@ language: - **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. +This {model_type} model was trained 2x faster with [Unsloth](https://github.com/unslothai/unsloth) [](https://github.com/unslothai/unsloth) """ @@ -2234,13 +2234,13 @@ tags: {"- vision-language-model" if is_vlm else ""} --- -# {repo_id.split("/")[-1]} - GGUF +# {repo_id.split("/")[-1]} : GGUF This model was finetuned and converted to GGUF format using [Unsloth](https://github.com/unslothai/unsloth). **Example usage**: -- For text only LLMs: **llama-cli** **--hf** repo_id/model_name **-p** "why is the sky blue?" -- For multimodal models: **llama-mtmd-cli** **-m** model_name.gguf **--mmproj** mmproj_file.gguf +- For text only LLMs: `./llama.cpp/llama-cli -hf {repo_id} --jinja` +- For multimodal models: `./llama.cpp/llama-mtmd-cli -hf {repo_id} --jinja` ## Available Model files: """ @@ -2281,6 +2281,11 @@ This model was finetuned and converted to GGUF format using [Unsloth](https://gi "The model's BOS token behavior was adjusted for GGUF compatibility.\n" ) + readme_content += ( + 'This was trained 2x faster with [Unsloth](https://github.com/unslothai/unsloth)\n' + '[](https://github.com/unslothai/unsloth)\n' + ) + readme_path = os.path.join(actual_save_directory, "README.md") with open(readme_path, "w") as f: f.write(readme_content) From 0f60650966ed1b0debabc79a38c9b1e8b835467e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:51:08 +0000 Subject: [PATCH 065/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/save.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsloth/save.py b/unsloth/save.py index c4ee322937..3a275cf0c3 100644 --- a/unsloth/save.py +++ b/unsloth/save.py @@ -2282,7 +2282,7 @@ This model was finetuned and converted to GGUF format using [Unsloth](https://gi ) readme_content += ( - 'This was trained 2x faster with [Unsloth](https://github.com/unslothai/unsloth)\n' + "This was trained 2x faster with [Unsloth](https://github.com/unslothai/unsloth)\n" '[](https://github.com/unslothai/unsloth)\n' ) From 74eaf52600d6f2d96c88c79e944fba4d2640a906 Mon Sep 17 00:00:00 2001 From: "abhishek.sharma" Date: Sat, 20 Dec 2025 11:47:03 +0530 Subject: [PATCH 066/167] Fix model training state restoration in GRPO trainer Store the model's training state before generation and restore inference mode after completion if the model wasn't originally in training mode. This ensures the model returns to the correct state after generate and score operations. --- unsloth/models/rl_replacements.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/unsloth/models/rl_replacements.py b/unsloth/models/rl_replacements.py index 7d4d520c1f..dd139ffd25 100644 --- a/unsloth/models/rl_replacements.py +++ b/unsloth/models/rl_replacements.py @@ -259,6 +259,7 @@ def grpo_trainer__generate_and_score_completions(function_name, function): # The new multi-line string that will replace the line above replacement_lines = """ batch_size = self.args.per_device_train_batch_size if mode == "train" else self.args.per_device_eval_batch_size + _was_training = self.model.training try: # TRL 0.23.1 and below path if not has_images: @@ -387,6 +388,13 @@ def grpo_trainer__generate_and_score_completions(function_name, function): patched = patched[: match.start()] + wrapped + patched[match.end() :] function = patched + + function = function.replace( + " return output", # 8 spaces before 'return' + """ if not _was_training: + self.model.for_inference() + return output""" + ) return function From 8b5130ae2d85a827a6d37e0dbfb9dabea6c1e05e Mon Sep 17 00:00:00 2001 From: numb3r33 Date: Sat, 20 Dec 2025 12:30:33 +0530 Subject: [PATCH 067/167] Remove the comment. --- unsloth/models/rl_replacements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsloth/models/rl_replacements.py b/unsloth/models/rl_replacements.py index dd139ffd25..248c5aab85 100644 --- a/unsloth/models/rl_replacements.py +++ b/unsloth/models/rl_replacements.py @@ -390,7 +390,7 @@ def grpo_trainer__generate_and_score_completions(function_name, function): function = patched function = function.replace( - " return output", # 8 spaces before 'return' + " return output", """ if not _was_training: self.model.for_inference() return output""" From 806f8d2d7e34712aff966e302cd8baff0448e9bc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 20 Dec 2025 07:02:30 +0000 Subject: [PATCH 068/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/models/rl_replacements.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/unsloth/models/rl_replacements.py b/unsloth/models/rl_replacements.py index 248c5aab85..e13e5e6d78 100644 --- a/unsloth/models/rl_replacements.py +++ b/unsloth/models/rl_replacements.py @@ -388,12 +388,12 @@ def grpo_trainer__generate_and_score_completions(function_name, function): patched = patched[: match.start()] + wrapped + patched[match.end() :] function = patched - + function = function.replace( - " return output", - """ if not _was_training: + " return output", + """ if not _was_training: self.model.for_inference() - return output""" + return output""", ) return function From ab81842f78a07307ecc77358a53d5621dc1220f2 Mon Sep 17 00:00:00 2001 From: numb3r33 Date: Wed, 24 Dec 2025 00:14:50 +0530 Subject: [PATCH 069/167] Fix indentation handling in grpo_trainer return statement replacement Use regex to dynamically detect and preserve the original indentation when replacing the 'return output' statement, instead of hardcoding spaces. This ensures the patched code maintains consistent indentation regardless of the original formatting. --- unsloth/models/rl_replacements.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/unsloth/models/rl_replacements.py b/unsloth/models/rl_replacements.py index e13e5e6d78..bcac699b3f 100644 --- a/unsloth/models/rl_replacements.py +++ b/unsloth/models/rl_replacements.py @@ -388,13 +388,17 @@ def grpo_trainer__generate_and_score_completions(function_name, function): patched = patched[: match.start()] + wrapped + patched[match.end() :] function = patched + + match = re.search(r'^(\s*)return output', function, re.MULTILINE) - function = function.replace( - " return output", - """ if not _was_training: - self.model.for_inference() - return output""", - ) + if match: + indent = match.group(1) + function = function.replace( + f"{indent}return output", + f"""{indent}if not _was_training: + {indent} self.model.for_inference() + {indent}return output""" + ) return function From c8784ec87e70df30936731bf141a0ab33f2eda50 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 19:20:08 +0000 Subject: [PATCH 070/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/models/rl_replacements.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/unsloth/models/rl_replacements.py b/unsloth/models/rl_replacements.py index bcac699b3f..5158019132 100644 --- a/unsloth/models/rl_replacements.py +++ b/unsloth/models/rl_replacements.py @@ -388,8 +388,8 @@ def grpo_trainer__generate_and_score_completions(function_name, function): patched = patched[: match.start()] + wrapped + patched[match.end() :] function = patched - - match = re.search(r'^(\s*)return output', function, re.MULTILINE) + + match = re.search(r"^(\s*)return output", function, re.MULTILINE) if match: indent = match.group(1) @@ -397,7 +397,7 @@ def grpo_trainer__generate_and_score_completions(function_name, function): f"{indent}return output", f"""{indent}if not _was_training: {indent} self.model.for_inference() - {indent}return output""" + {indent}return output""", ) return function From e4f582bdc25ed1076c3e107698bdc2a4d7e1feae Mon Sep 17 00:00:00 2001 From: numb3r33 Date: Wed, 24 Dec 2025 01:02:03 +0530 Subject: [PATCH 071/167] Refactor return statement replacement to use explicit newlines Replace f-string triple-quoted approach with explicit newline characters for clearer string construction in the grpo_trainer patch. --- unsloth/models/rl_replacements.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/unsloth/models/rl_replacements.py b/unsloth/models/rl_replacements.py index 5158019132..8436ca0dc9 100644 --- a/unsloth/models/rl_replacements.py +++ b/unsloth/models/rl_replacements.py @@ -393,12 +393,8 @@ def grpo_trainer__generate_and_score_completions(function_name, function): if match: indent = match.group(1) - function = function.replace( - f"{indent}return output", - f"""{indent}if not _was_training: - {indent} self.model.for_inference() - {indent}return output""", - ) + new_code = indent + "if not _was_training:\n" + indent + " self.model.for_inference()\n" + indent + "return output" + function = function.replace(f"{indent}return output", new_code) return function From ef3e2b39a8a9aacae67f4b24e7183fcd04efcd4f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 20:06:36 +0000 Subject: [PATCH 072/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/models/rl_replacements.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/unsloth/models/rl_replacements.py b/unsloth/models/rl_replacements.py index 8436ca0dc9..f0f0386bd1 100644 --- a/unsloth/models/rl_replacements.py +++ b/unsloth/models/rl_replacements.py @@ -393,7 +393,14 @@ def grpo_trainer__generate_and_score_completions(function_name, function): if match: indent = match.group(1) - new_code = indent + "if not _was_training:\n" + indent + " self.model.for_inference()\n" + indent + "return output" + new_code = ( + indent + + "if not _was_training:\n" + + indent + + " self.model.for_inference()\n" + + indent + + "return output" + ) function = function.replace(f"{indent}return output", new_code) return function From a212c17f5e358fc9d019eec47bdbbcc941907221 Mon Sep 17 00:00:00 2001 From: Strahinja Stamenkovic Date: Fri, 26 Dec 2025 03:43:59 +0100 Subject: [PATCH 073/167] Add missing import of inspect (#3778) * Add missing import of inspect * Update device_type.py --- unsloth/device_type.py | 1 + 1 file changed, 1 insertion(+) diff --git a/unsloth/device_type.py b/unsloth/device_type.py index 68038de679..0f924bfdfd 100644 --- a/unsloth/device_type.py +++ b/unsloth/device_type.py @@ -24,6 +24,7 @@ __all__ = [ import torch import functools +import inspect from unsloth_zoo.utils import Version From d83fbf67bbe1ca134cf510ea56099de2da3ec6f5 Mon Sep 17 00:00:00 2001 From: Fizza Mukhtar Date: Thu, 25 Dec 2025 18:46:13 -0800 Subject: [PATCH 074/167] Clarify NotImplementedError for fast_inference with full_finetuning (#3768) * Improve error message for fast_inference and full_finetuning * Refine error message string formatting * Update unsloth/models/vision.py --------- Co-authored-by: Daniel Han --- unsloth/models/vision.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index b78b190bcb..e1cf8f6f82 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -718,9 +718,13 @@ class FastBaseModel: if full_finetuning: max_lora_rank = max(get_lora_supported_ranks()) raise NotImplementedError( - f"Unsloth: `fast_inference = True` does not yet support `full_finetuning = True`.\n" - f"Use LoRA rank `r = {max_lora_rank}` as the closest replacement for full finetuning with Unsloth for RL." + "Unsloth: `fast_inference=True` cannot be used together with `full_finetuning=True`.\n" + "Reason: fast_inference is optimized for inference-only workflows and " + "does not currently support full fine-tuning.\n" + "Workaround: disable fast_inference, or use parameter-efficient fine-tuning " + f"(e.g. LoRA with rank r={max_lora_rank})." ) + model_config.model_name = model_name if fast_inference: From 21d897d131ffb56b9b577e0652f1012404586c99 Mon Sep 17 00:00:00 2001 From: Michael Han <107991372+shimmyshimmer@users.noreply.github.com> Date: Sat, 27 Dec 2025 00:49:19 -0800 Subject: [PATCH 075/167] Update README for new unsloth.ai/docs.md --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 43c09381fc..7cd9d0bba4 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - + ### Train gpt-oss, DeepSeek, Gemma, Qwen & Llama 2x faster with 70% less VRAM! @@ -18,7 +18,7 @@ ## ✨ Train for Free -Notebooks are beginner friendly. Read our [guide](https://docs.unsloth.ai/get-started/fine-tuning-guide). Add dataset, run, then export your trained model to GGUF, llama.cpp, Ollama, vLLM, SGLang or Hugging Face. +Notebooks are beginner friendly. Read our [guide](https://docs.unsloth.ai/get-started/fine-tuning-guide). Add dataset, run, then deploy your trained model. | Model | Free Notebooks | Performance | Memory use | |-----------|---------|--------|----------| @@ -34,9 +34,9 @@ Notebooks are beginner friendly. Read our [guide](https://docs.unsloth.ai/get-st | **Llama 3.2 Conversational** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Llama3.2_(1B_and_3B)-Conversational.ipynb) | 2x faster | 70% less | | **Orpheus-TTS (3B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Orpheus_(3B)-TTS.ipynb) | 1.5x faster | 50% less | -- See all our notebooks for: [Kaggle](https://github.com/unslothai/notebooks?tab=readme-ov-file#-kaggle-notebooks), [GRPO](https://docs.unsloth.ai/get-started/unsloth-notebooks#grpo-reasoning-rl-notebooks), **[TTS](https://docs.unsloth.ai/get-started/unsloth-notebooks#text-to-speech-tts-notebooks)** & [Vision](https://docs.unsloth.ai/get-started/unsloth-notebooks#vision-multimodal-notebooks) -- See [all our models](https://docs.unsloth.ai/get-started/all-our-models) and [all our notebooks](https://docs.unsloth.ai/get-started/unsloth-notebooks) -- See detailed documentation for Unsloth [here](https://docs.unsloth.ai/) +- See all our notebooks for: [Kaggle](https://github.com/unslothai/notebooks?tab=readme-ov-file#-kaggle-notebooks), [GRPO](https://unsloth.ai/docs/get-started/unsloth-notebooks#grpo-reasoning-rl-notebooks), [TTS](https://unsloth.ai/docs/get-started/unsloth-notebooks#text-to-speech-tts-notebooks) & [Vision](https://unsloth.ai/docs/get-started/unsloth-notebooks#vision-multimodal-notebooks) +- See [all our models](https://unsloth.ai/docs/get-started/unsloth-model-catalog) and [all our notebooks](https://unsloth.ai/docs/get-started/unsloth-notebooks) +- See detailed documentation for Unsloth [here](https://unsloth.ai/docs) ## ⚡ Quickstart ### Linux or WSL @@ -46,9 +46,9 @@ pip install unsloth ### Windows For Windows, `pip install unsloth` works only if you have Pytorch installed. Read our [Windows Guide](https://docs.unsloth.ai/get-started/installing-+-updating/windows-installation). ### Docker -Use our official [Unsloth Docker image](https://hub.docker.com/r/unsloth/unsloth) ```unsloth/unsloth``` container. Read our [Docker Guide](https://docs.unsloth.ai/get-started/install-and-update/docker). +Use our official [Unsloth Docker image](https://hub.docker.com/r/unsloth/unsloth) ```unsloth/unsloth``` container. Read our [Docker Guide](https://unsloth.ai/docs/get-started/install-and-update/docker). ### Blackwell & DGX Spark -For RTX 50x, B200, 6000 GPUs: `pip install unsloth`. Read our [Blackwell Guide](https://docs.unsloth.ai/basics/training-llms-with-blackwell-rtx-50-series-and-unsloth) and [DGX Spark Guide](https://docs.unsloth.ai/new/fine-tuning-llms-with-nvidia-dgx-spark-and-unsloth) for more details. +For RTX 50x, B200, 6000 GPUs: `pip install unsloth`. Read our [Blackwell Guide](https://unsloth.ai/docs/basics/fine-tuning-llms-with-blackwell-rtx-50-series-and-unsloth) and [DGX Spark Guide](https://unsloth.ai/docs/basics/fine-tuning-llms-with-nvidia-dgx-spark-and-unsloth) for more details. ## 🦥 Unsloth News - New RoPE & MLP **Triton Kernels** & **Padding Free + Packing**: 3x faster training & 30% less VRAM. [Blog](https://docs.unsloth.ai/new/3x-faster-training-packing) @@ -98,6 +98,7 @@ For RTX 50x, B200, 6000 GPUs: `pip install unsloth`. Read our [Blackwell Guide]( - Supports **all models** including [TTS](https://docs.unsloth.ai/basics/text-to-speech-tts-fine-tuning), multimodal, [BERT](https://docs.unsloth.ai/get-started/unsloth-notebooks#other-important-notebooks) and more! Any model that works in transformers, works in Unsloth. - The most efficient library for [Reinforcement Learning (RL)](https://docs.unsloth.ai/get-started/reinforcement-learning-rl-guide), using 80% less VRAM. Supports GRPO, GSPO, DrGRPO, DAPO etc. - **0% loss in accuracy** - no approximation methods - all exact. +- Export and [deploy your model](https://unsloth.ai/docs/basics/inference-and-deployment) to GGUF, llama.cpp, vLLM, SGLang and Hugging Face. - Supports NVIDIA (since 2018), [AMD](https://docs.unsloth.ai/get-started/install-and-update/amd) and Intel GPUs. Minimum CUDA Capability 7.0 (V100, T4, Titan V, RTX 20, 30, 40x, A100, H100, L40 etc) - Works on **Linux**, WSL and **Windows** - All kernels written in [OpenAI's Triton](https://openai.com/index/triton/) language. Manual backprop engine. @@ -283,7 +284,7 @@ docker run -d -e JUPYTER_PASSWORD="mypassword" \ Access Jupyter Lab at `http://localhost:8888` and start fine-tuning! ## 📜 Documentation -- Go to our official [Documentation](https://docs.unsloth.ai) for [running models](https://docs.unsloth.ai/basics/running-and-saving-models), [saving to GGUF](https://docs.unsloth.ai/basics/running-and-saving-models/saving-to-gguf), [checkpointing](https://docs.unsloth.ai/basics/finetuning-from-last-checkpoint), [evaluation](https://docs.unsloth.ai/get-started/fine-tuning-llms-guide#evaluation) and more! +- Go to our official [Documentation](https://unsloth.ai/docs) for [running models](https://docs.unsloth.ai/basics/running-and-saving-models), [saving to GGUF](https://docs.unsloth.ai/basics/running-and-saving-models/saving-to-gguf), [checkpointing](https://docs.unsloth.ai/basics/finetuning-from-last-checkpoint), [evaluation](https://docs.unsloth.ai/get-started/fine-tuning-llms-guide#evaluation) and more! - Read our Guides for: [Fine-tuning](https://docs.unsloth.ai/get-started/fine-tuning-llms-guide), [Reinforcement Learning](https://docs.unsloth.ai/get-started/reinforcement-learning-rl-guide), [Text-to-Speech (TTS)](https://docs.unsloth.ai/basics/text-to-speech-tts-fine-tuning), [Vision](https://docs.unsloth.ai/basics/vision-fine-tuning) and [any model](https://docs.unsloth.ai/models/tutorials-how-to-fine-tune-and-run-llms). - We support Huggingface's transformers, TRL, Trainer, Seq2SeqTrainer and Pytorch code. From a2d7811fe8283475f50c88645fb5d124dedb714c Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 28 Dec 2025 19:57:43 -0800 Subject: [PATCH 076/167] Update FUNDING.yml (#3792) --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 4ebb6df3d0..ae5dade42d 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -3,7 +3,7 @@ github: unslothai patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username -ko_fi: unsloth +ko_fi: # unsloth tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username From 3455f0244e2dbde8dba8230a34ca401266767e75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alk=C4=B1n=20=C3=9Cnl=C3=BC?= Date: Mon, 29 Dec 2025 08:18:02 +0300 Subject: [PATCH 077/167] fix(trainer): import psutil to prevent NameError in _prepare_dataset (#3780) * fix(trainer): import psutil to prevent NameError in _prepare_dataset Fixes #3777 * Update rl.py --------- Co-authored-by: Daniel Han --- unsloth/models/rl.py | 1 + unsloth/tokenizer_utils.py | 1 + unsloth/trainer.py | 1 + 3 files changed, 3 insertions(+) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index 4ea36519d9..003a0e7f1b 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -227,6 +227,7 @@ import numpy as np from contextlib import nullcontext from torch.nn import functional as F import inspect +import psutil from transformers import DataCollatorForSeq2Seq, DataCollatorForLanguageModeling as TransformersDataCollatorForLanguageModeling from transformers.training_args import ParallelMode diff --git a/unsloth/tokenizer_utils.py b/unsloth/tokenizer_utils.py index 99651643a8..0136e3498e 100644 --- a/unsloth/tokenizer_utils.py +++ b/unsloth/tokenizer_utils.py @@ -25,6 +25,7 @@ import collections import numpy as np import gc import subprocess +import psutil from unsloth_zoo.tokenizer_utils import ( mean_of_trained_tokens, diff --git a/unsloth/trainer.py b/unsloth/trainer.py index c0b2dd03b6..0d98cff305 100644 --- a/unsloth/trainer.py +++ b/unsloth/trainer.py @@ -14,6 +14,7 @@ import logging import os +import psutil import warnings from dataclasses import dataclass, field from typing import Optional From 0d7bcbc525ad46d8a61e53b0c3ae471404dc3996 Mon Sep 17 00:00:00 2001 From: Francesco Bertolotti Date: Mon, 29 Dec 2025 06:21:48 +0100 Subject: [PATCH 078/167] fastrope fix for zero strided tensors (#3782) Co-authored-by: Francesco Bertolotti --- unsloth/kernels/rope_embedding.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/unsloth/kernels/rope_embedding.py b/unsloth/kernels/rope_embedding.py index a032e0f7fc..fcc9cb923b 100644 --- a/unsloth/kernels/rope_embedding.py +++ b/unsloth/kernels/rope_embedding.py @@ -312,8 +312,8 @@ class Fast_RoPE_Embedding_QK(torch.autograd.Function): _, n_heads_K, _, _ = K.shape # Inplace rotary embedding is generally fine - Q_out = Q.clone() if not Q.is_contiguous else Q - K_out = K.clone() if not K.is_contiguous else K + Q_out = Q.clone() if not Q.is_contiguous() else Q + K_out = K.clone() if not K.is_contiguous() else K if has_indices: # TRL's rotary indices are always in int32, so casting is just for safety @@ -383,21 +383,21 @@ class Fast_RoPE_Embedding_QK(torch.autograd.Function): else ctx.cos.new_empty(1, dtype = torch.int32) ) + # Inplace rotary embedding is generally fine + dQ_out = dQ.clone() if not dQ.is_contiguous() else dQ + dK_out = dK.clone() if not dK.is_contiguous() else dK + Q_batch_stride, Q_head_stride, Q_seq_stride = ( - dQ.stride(0), - dQ.stride(1), - dQ.stride(2), + dQ_out.stride(0), + dQ_out.stride(1), + dQ_out.stride(2), ) K_batch_stride, K_head_stride, K_seq_stride = ( - dK.stride(0), - dK.stride(1), - dK.stride(2), + dK_out.stride(0), + dK_out.stride(1), + dK_out.stride(2), ) - # Inplace rotary embedding is generally fine - dQ_out = dQ.clone() if not dQ.is_contiguous else dQ - dK_out = dK.clone() if not dK.is_contiguous else dK - with torch_gpu_device(dQ.device): _rope_embedding_QK[(batch * ctx.seq_len, ctx.n_heads_Q)]( dQ_out, From 87c60c4a88b16cc4a9b242ed87203a514fd57e8e Mon Sep 17 00:00:00 2001 From: Fizza Mukhtar Date: Sun, 28 Dec 2025 21:23:51 -0800 Subject: [PATCH 079/167] Fix crash when trl.experimental.openenv is unavailable (#3787) * Guard optional trl.experimental.openenv usage in RL patches * Simplify optional trl.openenv import handling * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- unsloth/models/rl_replacements.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/unsloth/models/rl_replacements.py b/unsloth/models/rl_replacements.py index 7d4d520c1f..3dfeea6871 100644 --- a/unsloth/models/rl_replacements.py +++ b/unsloth/models/rl_replacements.py @@ -949,11 +949,15 @@ def openenv_vllm_reload_weights(): return if Version(importlib_version("trl")) < Version("0.26.0"): return + try: import trl.experimental.openenv.utils as openenv_utils import trl.experimental.openenv as openenv except ImportError as e: logger.info(f"Unsloth: Failed to import trl openenv: {e}") + logger.info( + "Unsloth: trl.experimental.openenv not available — skipping RL openenv patches." + ) return src = inspect.getsource(openenv_utils.generate_rollout_completions) From 3423f66a1aa3ecad9835c3fc9ad784dd657c6e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=86=E3=82=8A?= Date: Mon, 29 Dec 2025 13:30:55 +0800 Subject: [PATCH 080/167] Fix Boolean value of Tensor ambiguity error in mistral.py (#3790) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix is_contiguous() method call and remove duplicate imports - Fix bug in rope_embedding.py where is_contiguous was used without parentheses, causing the method object (always truthy) to be evaluated instead of calling the method. This fixes issue #3781 where fast rope backpropagation was broken for zero strided/non-contiguous tensors. - Remove duplicate `import torch` in rl.py (lines 20 and 25) - Remove duplicate `import functools` and `import types` in vision.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * Fix Boolean value of Tensor ambiguity error in mistral.py Replace `or` operator with explicit `is None` check when getting n_items from kwargs. The `or` operator fails when the value is a Tensor because Python cannot determine the boolean value of a multi-element tensor. Fixes #3766 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * Update rope_embedding.py --------- Co-authored-by: yurekami Co-authored-by: Claude Opus 4.5 Co-authored-by: Daniel Han --- unsloth/models/mistral.py | 12 +++++++----- unsloth/models/rl.py | 1 - unsloth/models/vision.py | 2 -- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/unsloth/models/mistral.py b/unsloth/models/mistral.py index 0eed45c5cd..5e893d2b6f 100644 --- a/unsloth/models/mistral.py +++ b/unsloth/models/mistral.py @@ -307,9 +307,9 @@ def MistralForCausalLM_fast_forward( RETURN_LOGITS = False if not RETURN_LOGITS and labels is not None: - n_items = kwargs.get("num_items_in_batch", None) or kwargs.get( - "n_items", None - ) + n_items = kwargs.get("num_items_in_batch", None) + if n_items is None: + n_items = kwargs.get("n_items", None) logit_softcapping = getattr(self.config, "final_logit_softcapping", 0) # loss = fused_linear_cross_entropy( @@ -363,11 +363,13 @@ def MistralForCausalLM_fast_forward( shift_labels, kwargs.get("packed_seq_lengths"), ) + n_items = kwargs.get("num_items_in_batch", None) + if n_items is None: + n_items = kwargs.get("n_items", None) loss = fast_cross_entropy_loss( logits = shift_logits, labels = shift_labels, - n_items = kwargs.get("num_items_in_batch", None) - or kwargs.get("n_items", None), + n_items = n_items, ) if not return_dict: diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index 003a0e7f1b..03f2c44701 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -22,7 +22,6 @@ from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union import inspect import os import re -import torch from unsloth_zoo.compiler import create_new_function from unsloth_zoo.log import logger from unsloth_zoo.logging_utils import PatchRLStatistics diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index e1cf8f6f82..36cfbf0b17 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -68,11 +68,9 @@ import functools import os import gc import math -import functools from typing import Optional, Tuple, List, Union import re, inspect, sys import contextlib -import types try: from huggingface_hub.utils import get_token From 8ea5338154859ed25b50366cb1264ed4d933eae3 Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Mon, 29 Dec 2025 15:17:58 +0800 Subject: [PATCH 081/167] fix: add support for init_lora_weights="corda" in get_peft_model (#3794) Add "corda" as an allowed value for the init_lora_weights parameter in FastLanguageModel.get_peft_model() and FastBaseModel.get_peft_model(). This enables users to use CorDA (Correlation-aware Decomposed Adaptation) initialization from PEFT, which provides an alternative LoRA initialization strategy for improved finetuning performance. Fixes #3693 Signed-off-by: majiayu000 <1835304752@qq.com> --- unsloth/models/_utils.py | 3 ++- unsloth/models/llama.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index abc8380562..ccb547f58e 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -1981,9 +1981,10 @@ def validate_loftq_config(loftq_config, lora_dropout, bias, init_lora_weights, m type(init_lora_weights) is bool or init_lora_weights == "gaussian" or init_lora_weights == "loftq" + or init_lora_weights == "corda" ): raise ValueError( - 'Unsloth: `init_lora_weights` must be either [True, False, "gaussian", "loftq"].' + 'Unsloth: `init_lora_weights` must be either [True, False, "gaussian", "loftq", "corda"].' ) if init_lora_weights == "loftq": diff --git a/unsloth/models/llama.py b/unsloth/models/llama.py index 1d7695b9aa..762445b5e8 100644 --- a/unsloth/models/llama.py +++ b/unsloth/models/llama.py @@ -2779,9 +2779,10 @@ class FastLlamaModel: type(init_lora_weights) is bool or init_lora_weights == "gaussian" or init_lora_weights == "loftq" + or init_lora_weights == "corda" ): raise ValueError( - 'Unsloth: `init_lora_weights` must be either [True, False, "gaussian", "loftq"].' + 'Unsloth: `init_lora_weights` must be either [True, False, "gaussian", "loftq", "corda"].' ) if init_lora_weights == "loftq": From 247d7b0ab64c8b2814a28b124d3ceb770a29e0a0 Mon Sep 17 00:00:00 2001 From: Fizza-Mukhtar Date: Tue, 30 Dec 2025 07:08:10 -0800 Subject: [PATCH 082/167] Fix 3D tensor support for bitsandbytes 8-bit matmul in forward pass --- unsloth/kernels/fast_lora.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/unsloth/kernels/fast_lora.py b/unsloth/kernels/fast_lora.py index 60d0c318c3..16b679d005 100644 --- a/unsloth/kernels/fast_lora.py +++ b/unsloth/kernels/fast_lora.py @@ -379,9 +379,22 @@ class LoRA_QKV(torch.autograd.Function): ): dtype = X.dtype + # bitsandbytes 8-bit matmul expects 2D inputs. + # TorchInductor/AOTAutograd fails on 3D tensors during backward, + # so we explicitly flatten the sequence dimension. + orig_shape = X.shape + if X.dim() == 3: + X = X.view(-1, X.shape[-1]) + Q = matmul_lora(X, QW, QW_quant, QA, QB, QS) K = matmul_lora(X, KW, KW_quant, KA, KB, KS) V = matmul_lora(X, VW, VW_quant, VA, VB, VS) + + # Restore original shape after matmul + if len(orig_shape) == 3: + Q = Q.view(orig_shape[0], orig_shape[1], -1) + K = K.view(orig_shape[0], orig_shape[1], -1) + V = V.view(orig_shape[0], orig_shape[1], -1) ctx.custom_saved_tensors = ( QW, From e6312b11683242218f87b9507f16bb785dadfab9 Mon Sep 17 00:00:00 2001 From: Fizza-Mukhtar Date: Tue, 30 Dec 2025 07:56:01 -0800 Subject: [PATCH 083/167] Fix 3D tensor support for bitsandbytes 8-bit matmul in forward pass --- unsloth/kernels/fast_lora.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/unsloth/kernels/fast_lora.py b/unsloth/kernels/fast_lora.py index 16b679d005..fbb18c3a15 100644 --- a/unsloth/kernels/fast_lora.py +++ b/unsloth/kernels/fast_lora.py @@ -383,12 +383,12 @@ class LoRA_QKV(torch.autograd.Function): # TorchInductor/AOTAutograd fails on 3D tensors during backward, # so we explicitly flatten the sequence dimension. orig_shape = X.shape + X_for_matmul = X if X.dim() == 3: - X = X.view(-1, X.shape[-1]) - - Q = matmul_lora(X, QW, QW_quant, QA, QB, QS) - K = matmul_lora(X, KW, KW_quant, KA, KB, KS) - V = matmul_lora(X, VW, VW_quant, VA, VB, VS) + X_for_matmul = X.view(-1, X.shape[-1]) + Q = matmul_lora(X_for_matmul, QW, QW_quant, QA, QB, QS) + K = matmul_lora(X_for_matmul, KW, KW_quant, KA, KB, KS) + V = matmul_lora(X_for_matmul, VW, VW_quant, VA, VB, VS) # Restore original shape after matmul if len(orig_shape) == 3: From b9cf2b5510f1f5c8d01156176dadef6d179f0f72 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 15:58:40 +0000 Subject: [PATCH 084/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/kernels/fast_lora.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsloth/kernels/fast_lora.py b/unsloth/kernels/fast_lora.py index fbb18c3a15..f1c0e298d9 100644 --- a/unsloth/kernels/fast_lora.py +++ b/unsloth/kernels/fast_lora.py @@ -389,7 +389,7 @@ class LoRA_QKV(torch.autograd.Function): Q = matmul_lora(X_for_matmul, QW, QW_quant, QA, QB, QS) K = matmul_lora(X_for_matmul, KW, KW_quant, KA, KB, KS) V = matmul_lora(X_for_matmul, VW, VW_quant, VA, VB, VS) - + # Restore original shape after matmul if len(orig_shape) == 3: Q = Q.view(orig_shape[0], orig_shape[1], -1) From d8459b194c81a502962f16118509cdfa205483af Mon Sep 17 00:00:00 2001 From: Michael Han <107991372+shimmyshimmer@users.noreply.github.com> Date: Tue, 30 Dec 2025 15:14:27 -0800 Subject: [PATCH 085/167] Refresh of Unsloth README.md with https://unsloth.ai/docs --- README.md | 115 +++++++++++++++++++++++++----------------------------- 1 file changed, 53 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 7cd9d0bba4..ae1fccfbba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
- + unsloth logo @@ -18,7 +18,7 @@ ## ✨ Train for Free -Notebooks are beginner friendly. Read our [guide](https://docs.unsloth.ai/get-started/fine-tuning-guide). Add dataset, run, then deploy your trained model. +Notebooks are beginner friendly. Read our [guide](https://unsloth.ai/docs/get-started/fine-tuning-llms-guide). Add dataset, run, then deploy your trained model. | Model | Free Notebooks | Performance | Memory use | |-----------|---------|--------|----------| @@ -44,33 +44,35 @@ Notebooks are beginner friendly. Read our [guide](https://docs.unsloth.ai/get-st pip install unsloth ``` ### Windows -For Windows, `pip install unsloth` works only if you have Pytorch installed. Read our [Windows Guide](https://docs.unsloth.ai/get-started/installing-+-updating/windows-installation). +For Windows, `pip install unsloth` works only if you have Pytorch installed. Read our [Windows Guide](https://unsloth.ai/docs/get-started/install-and-update/windows-installation). + ### Docker Use our official [Unsloth Docker image](https://hub.docker.com/r/unsloth/unsloth) ```unsloth/unsloth``` container. Read our [Docker Guide](https://unsloth.ai/docs/get-started/install-and-update/docker). + ### Blackwell & DGX Spark For RTX 50x, B200, 6000 GPUs: `pip install unsloth`. Read our [Blackwell Guide](https://unsloth.ai/docs/basics/fine-tuning-llms-with-blackwell-rtx-50-series-and-unsloth) and [DGX Spark Guide](https://unsloth.ai/docs/basics/fine-tuning-llms-with-nvidia-dgx-spark-and-unsloth) for more details. ## 🦥 Unsloth News -- New RoPE & MLP **Triton Kernels** & **Padding Free + Packing**: 3x faster training & 30% less VRAM. [Blog](https://docs.unsloth.ai/new/3x-faster-training-packing) -- **Ministral 3** by Mistral: Run Ministral 3 or fine-tune with vision/RL sodoku notebooks. [Guide](https://docs.unsloth.ai/new/ministral-3) • [Notebooks](https://docs.unsloth.ai/new/ministral-3#fine-tuningb) -- **500K Context**: Training a 20B model with >500K context is now possible on an 80GB GPU. [Blog](https://docs.unsloth.ai/new/500k-context-length-fine-tuning) -- **FP8 Reinforcement Learning**: You can now do FP8 GRPO on consumer GPUs. [Blog](https://docs.unsloth.ai/new/fp8-reinforcement-learning) • [Notebook](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_8B_FP8_GRPO.ipynb) -- **DeepSeek-OCR**: Fine-tune to improve language understanding by 89%. [Guide](https://docs.unsloth.ai/new/deepseek-ocr-run-and-fine-tune) • [Notebook](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Deepseek_OCR_(3B).ipynb) -- **Docker**: Use Unsloth with no setup & environment issues with our new image. [Guide](https://docs.unsloth.ai/new/how-to-train-llms-with-unsloth-and-docker) • [Docker image](https://hub.docker.com/r/unsloth/unsloth) -- **gpt-oss RL**: Introducing the fastest possible inference for gpt-oss RL! [Read blog](https://docs.unsloth.ai/new/gpt-oss-reinforcement-learning) -- **Vision RL**: You can now train VLMs with GRPO or GSPO in Unsloth! [Read guide](https://docs.unsloth.ai/new/vision-reinforcement-learning-vlm-rl) -- **gpt-oss** by OpenAI: Read our [Unsloth Flex Attention](https://docs.unsloth.ai/new/long-context-gpt-oss-training) blog and [gpt-oss Guide](https://docs.unsloth.ai/basics/gpt-oss). 20B works on 14GB VRAM. 120B on 65GB. +- New RoPE & MLP **Triton Kernels** & **Padding Free + Packing**: 3x faster training & 30% less VRAM. [Blog](https://unsloth.ai/docs/new/3x-faster-training-packing) +- **New Mistral**: Run Ministral 3 or Devstral 2 and fine-tune with vision/RL sodoku notebooks. [Guide](https://unsloth.ai/docs/models/ministral-3) • [Notebooks](https://unsloth.ai/docs/models/ministral-3#fine-tuning-ministral-3) +- **500K Context**: Training a 20B model with >500K context is now possible on an 80GB GPU. [Blog](https://unsloth.ai/docs/new/500k-context-length-fine-tuning) +- **FP8 Reinforcement Learning**: You can now do FP8 GRPO on consumer GPUs. [Blog](https://unsloth.ai/docs/new/fp8-reinforcement-learning) • [Notebook](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_8B_FP8_GRPO.ipynb) +- **DeepSeek-OCR**: Fine-tune to improve language understanding by 89%. [Guide](https://unsloth.ai/docs/models/deepseek-ocr-how-to-run-and-fine-tune) • [Notebook](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Deepseek_OCR_(3B).ipynb) +- **Docker**: Use Unsloth with no setup & environment issues with our new image. [Guide](https://unsloth.ai/docs/new/how-to-fine-tune-llms-with-unsloth-and-docker) • [Docker image](https://hub.docker.com/r/unsloth/unsloth) +- **gpt-oss RL**: Introducing the fastest possible inference for gpt-oss RL! [Read blog](https://unsloth.ai/docs/models/gpt-oss-how-to-run-and-fine-tune/gpt-oss-reinforcement-learning) +- **Vision RL**: You can now train VLMs with GRPO or GSPO in Unsloth! [Read guide](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide/vision-reinforcement-learning-vlm-rl) +- **gpt-oss** by OpenAI: Read our [Unsloth Flex Attention](https://unsloth.ai/docs/models/gpt-oss-how-to-run-and-fine-tune/long-context-gpt-oss-training) blog and [gpt-oss Guide](https://unsloth.ai/docs/models/gpt-oss-how-to-run-and-fine-tune). 20B works on 14GB VRAM. 120B on 65GB.
Click for more news -- **Quantization-Aware Training**: We collabed with Pytorch, recovering ~70% accuracy. [Read blog](https://docs.unsloth.ai/new/quantization-aware-training-qat) -- **Memory-efficient RL**: We're introducing even better RL. Our new kernels & algos allows faster RL with 50% less VRAM & 10× more context. [Read blog](https://docs.unsloth.ai/new/memory-efficient-rl) -- **Gemma 3n** by Google: [Read Blog](https://docs.unsloth.ai/basics/gemma-3n-how-to-run-and-fine-tune). We [uploaded GGUFs, 4-bit models](https://huggingface.co/collections/unsloth/gemma-3n-685d3874830e49e1c93f9339). -- **[Text-to-Speech (TTS)](https://docs.unsloth.ai/basics/text-to-speech-tts-fine-tuning)** is now supported, including `sesame/csm-1b` and STT `openai/whisper-large-v3`. -- **[Qwen3](https://docs.unsloth.ai/basics/qwen3-how-to-run-and-fine-tune)** is now supported. Qwen3-30B-A3B fits on 17.5GB VRAM. -- Introducing **[Dynamic 2.0](https://docs.unsloth.ai/basics/unsloth-dynamic-2.0-ggufs)** quants that set new benchmarks on 5-shot MMLU & Aider Polyglot. -- [**EVERYTHING** is now supported](https://unsloth.ai/blog/gemma3#everything) - all models (TTS, BERT, Mamba), FFT, etc. [MultiGPU](https://docs.unsloth.ai/basics/multi-gpu-training-with-unsloth) coming soon. Enable FFT with `full_finetuning = True`, 8-bit with `load_in_8bit = True`. +- **Quantization-Aware Training**: We collabed with Pytorch, recovering ~70% accuracy. [Read blog](https://unsloth.ai/docs/basics/quantization-aware-training-qat) +- **Memory-efficient RL**: We're introducing even better RL. Our new kernels & algos allows faster RL with 50% less VRAM & 10× more context. [Read blog](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide/memory-efficient-rl) +- **Gemma 3n** by Google: [Read Blog](https://unsloth.ai/docs/models/gemma-3-how-to-run-and-fine-tune/gemma-3n-how-to-run-and-fine-tune). We [uploaded GGUFs, 4-bit models](https://huggingface.co/collections/unsloth/gemma-3n-685d3874830e49e1c93f9339). +- **[Text-to-Speech (TTS)](https://unsloth.ai/docs/basics/text-to-speech-tts-fine-tuning)** is now supported, including `sesame/csm-1b` and STT `openai/whisper-large-v3`. +- **[Qwen3](https://unsloth.ai/docs/models/qwen3-how-to-run-and-fine-tune)** is now supported. Qwen3-30B-A3B fits on 17.5GB VRAM. +- Introducing **[Dynamic 2.0](https://unsloth.ai/docs/basics/unsloth-dynamic-2.0-ggufs)** quants that set new benchmarks on 5-shot MMLU & Aider Polyglot. +- [**EVERYTHING** is now supported](https://unsloth.ai/blog/gemma3#everything) - all models (TTS, BERT, Mamba), FFT, etc. [MultiGPU](https://unsloth.ai/docs/basics/multi-gpu-training-with-unsloth) coming soon. Enable FFT with `full_finetuning = True`, 8-bit with `load_in_8bit = True`. - 📣 [DeepSeek-R1](https://unsloth.ai/blog/deepseek-r1) - run or fine-tune them [with our guide](https://unsloth.ai/blog/deepseek-r1). All model uploads: [here](https://huggingface.co/collections/unsloth/deepseek-r1-all-versions-678e1c48f5d2fce87892ace5). - 📣 Introducing Long-context [Reasoning (GRPO)](https://unsloth.ai/blog/grpo) in Unsloth. Train your own reasoning model with just 5GB VRAM. Transform Llama, Phi, Mistral etc. into reasoning LLMs! - 📣 Introducing Unsloth [Dynamic 4-bit Quantization](https://unsloth.ai/blog/dynamic-4bit)! We dynamically opt not to quantize certain parameters and this greatly increases accuracy while only using <10% more VRAM than BnB 4-bit. See our collection on [Hugging Face here.](https://huggingface.co/collections/unsloth/unsloth-4-bit-dynamic-quants-67503bb873f89e15276c44e7) @@ -84,28 +86,29 @@ For RTX 50x, B200, 6000 GPUs: `pip install unsloth`. Read our [Blackwell Guide](
## 🔗 Links and Resources -| Type | Links | -| ------------------------------- | --------------------------------------- | -|   **r/unsloth Reddit** | [Join Reddit community](https://reddit.com/r/unsloth)| -| 📚 **Documentation & Wiki** | [Read Our Docs](https://docs.unsloth.ai) | -|   **Twitter (aka X)** | [Follow us on X](https://twitter.com/unslothai)| -| 💾 **Installation** | [Pip & Docker Install](https://docs.unsloth.ai/get-started/installing-+-updating)| -| 🔮 **Our Models** | [Unsloth Catalog](https://docs.unsloth.ai/get-started/all-our-models)| -| ✍️ **Blog** | [Read our Blogs](https://unsloth.ai/blog)| +| Type | Links | +| ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | +|   **r/unsloth Reddit** | [Join Reddit community](https://reddit.com/r/unsloth) | +| 📚 **Documentation & Wiki** | [Read Our Docs](https://unsloth.ai/docs) | +|   **Twitter (aka X)** | [Follow us on X](https://twitter.com/unslothai) | +| 💾 **Installation** | [Pip & Docker Install](https://unsloth.ai/docs/get-started/install-and-update) | +| 🔮 **Our Models** | [Unsloth Catalog](https://unsloth.ai/docs/get-started/unsloth-model-catalog) | +| ✍️ **Blog** | [Read our Blogs](https://unsloth.ai/blog) | ## ⭐ Key Features -- Supports **full-finetuning**, pretraining, 4b-bit, 16-bit and **FP8** training -- Supports **all models** including [TTS](https://docs.unsloth.ai/basics/text-to-speech-tts-fine-tuning), multimodal, [BERT](https://docs.unsloth.ai/get-started/unsloth-notebooks#other-important-notebooks) and more! Any model that works in transformers, works in Unsloth. -- The most efficient library for [Reinforcement Learning (RL)](https://docs.unsloth.ai/get-started/reinforcement-learning-rl-guide), using 80% less VRAM. Supports GRPO, GSPO, DrGRPO, DAPO etc. -- **0% loss in accuracy** - no approximation methods - all exact. -- Export and [deploy your model](https://unsloth.ai/docs/basics/inference-and-deployment) to GGUF, llama.cpp, vLLM, SGLang and Hugging Face. -- Supports NVIDIA (since 2018), [AMD](https://docs.unsloth.ai/get-started/install-and-update/amd) and Intel GPUs. Minimum CUDA Capability 7.0 (V100, T4, Titan V, RTX 20, 30, 40x, A100, H100, L40 etc) -- Works on **Linux**, WSL and **Windows** -- All kernels written in [OpenAI's Triton](https://openai.com/index/triton/) language. Manual backprop engine. -- If you trained a model with 🦥Unsloth, you can use this cool sticker!   + +* Supports **full-finetuning**, pretraining, 4b-bit, 16-bit and **FP8** training +* Supports **all models** including [TTS](https://unsloth.ai/docs/basics/text-to-speech-tts-fine-tuning), multimodal, [BERT](https://unsloth.ai/docs/get-started/unsloth-notebooks#other-important-notebooks) and more! Any model that works in transformers, works in Unsloth. +* The most efficient library for [Reinforcement Learning (RL)](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide), using 80% less VRAM. Supports GRPO, GSPO, DrGRPO, DAPO etc. +* **0% loss in accuracy** - no approximation methods - all exact. +* Export and [deploy your model](https://unsloth.ai/docs/basics/inference-and-deployment) to GGUF, llama.cpp, vLLM, SGLang and Hugging Face. +* Supports NVIDIA (since 2018), [AMD](https://unsloth.ai/docs/get-started/install-and-update/amd) and Intel GPUs. Minimum CUDA Capability 7.0 (V100, T4, Titan V, RTX 20, 30, 40x, A100, H100, L40 etc) +* Works on **Linux**, WSL and **Windows** +* All kernels written in OpenAI's Triton language. Manual backprop engine. +* If you trained a model with 🦥Unsloth, you can use this cool sticker!   ## 💾 Install Unsloth -You can also see our docs for more detailed installation and updating instructions [here](https://docs.unsloth.ai/get-started/installing-+-updating). +You can also see our docs for more detailed installation and updating instructions [here](https://unsloth.ai/docs/get-started/install-and-update). Unsloth supports Python 3.13 or lower. @@ -125,7 +128,7 @@ See [here](#advanced-pip-installation) for advanced pip install instructions. You should install the latest driver for your GPU. Download drivers here: [NVIDIA GPU Driver](https://www.nvidia.com/Download/index.aspx). 3. **Install Visual Studio C++:** - You will need Visual Studio, with C++ installed. By default, C++ is not installed with [Visual Studio](https://visualstudio.microsoft.com/vs/community/), so make sure you select all of the C++ options. Also select options for Windows 10/11 SDK. For detailed instructions with options, see [here](https://docs.unsloth.ai/get-started/installing-+-updating). + You will need Visual Studio, with C++ installed. By default, C++ is not installed with [Visual Studio](https://visualstudio.microsoft.com/vs/community/), so make sure you select all of the C++ options. Also select options for Windows 10/11 SDK. For detailed instructions with options, see [here](https://unsloth.ai/docs/get-started/install-and-update/windows-installation#method-3-windows-directly). 5. **Install CUDA Toolkit:** Follow the instructions to install [CUDA Toolkit](https://developer.nvidia.com/cuda-toolkit-archive). @@ -140,19 +143,7 @@ See [here](#advanced-pip-installation) for advanced pip install instructions. pip install unsloth ``` -#### Notes -To run Unsloth directly on Windows: -- Install Triton from this Windows fork and follow the instructions [here](https://github.com/woct0rdho/triton-windows) (be aware that the Windows fork requires PyTorch >= 2.4 and CUDA 12) -- In the `SFTConfig`, set `dataset_num_proc=1` to avoid a crashing issue: -```python -SFTConfig( - dataset_num_proc=1, - ... -) -``` - #### Advanced/Troubleshooting - For **advanced installation instructions** or if you see weird errors during installations: First try using an isolated environment via then `pip install unsloth` @@ -269,7 +260,7 @@ print(f'pip install --upgrade pip && pip install --no-deps git+https://github.co ``` ### Docker Installation You can use our pre-built Docker container with all dependencies to use Unsloth instantly with no setup required. -[Read our guide](https://docs.unsloth.ai/get-started/install-and-update/docker). +[Read our guide](https://unsloth.ai/docs/get-started/install-and-update/docker). This container requires installing [NVIDIA's Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html). @@ -284,9 +275,9 @@ docker run -d -e JUPYTER_PASSWORD="mypassword" \ Access Jupyter Lab at `http://localhost:8888` and start fine-tuning! ## 📜 Documentation -- Go to our official [Documentation](https://unsloth.ai/docs) for [running models](https://docs.unsloth.ai/basics/running-and-saving-models), [saving to GGUF](https://docs.unsloth.ai/basics/running-and-saving-models/saving-to-gguf), [checkpointing](https://docs.unsloth.ai/basics/finetuning-from-last-checkpoint), [evaluation](https://docs.unsloth.ai/get-started/fine-tuning-llms-guide#evaluation) and more! -- Read our Guides for: [Fine-tuning](https://docs.unsloth.ai/get-started/fine-tuning-llms-guide), [Reinforcement Learning](https://docs.unsloth.ai/get-started/reinforcement-learning-rl-guide), [Text-to-Speech (TTS)](https://docs.unsloth.ai/basics/text-to-speech-tts-fine-tuning), [Vision](https://docs.unsloth.ai/basics/vision-fine-tuning) and [any model](https://docs.unsloth.ai/models/tutorials-how-to-fine-tune-and-run-llms). -- We support Huggingface's transformers, TRL, Trainer, Seq2SeqTrainer and Pytorch code. +* Go to our official [Documentation](https://unsloth.ai/docs) for [running models](https://unsloth.ai/docs/basics/inference-and-deployment), [saving to GGUF](https://unsloth.ai/docs/basics/inference-and-deployment/saving-to-gguf), [checkpointing](https://unsloth.ai/docs/basics/finetuning-from-last-checkpoint), [evaluation](https://unsloth.ai/docs/get-started/fine-tuning-llms-guide#evaluation) and more! +* Read our Guides for: [Fine-tuning](https://unsloth.ai/docs/get-started/fine-tuning-llms-guide), [Reinforcement Learning](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide), [Text-to-Speech (TTS)](https://unsloth.ai/docs/basics/text-to-speech-tts-fine-tuning), [Vision](https://unsloth.ai/docs/basics/vision-fine-tuning) and [any model](https://unsloth.ai/docs/models/tutorials-how-to-fine-tune-and-run-llms). +* We support Huggingface's transformers, TRL, Trainer, Seq2SeqTrainer and Pytorch code. Unsloth example code to fine-tune gpt-oss-20b: @@ -311,8 +302,9 @@ model, tokenizer = FastModel.from_pretrained( max_seq_length = 2048, # Choose any for long context! load_in_4bit = True, # 4-bit quantization. False = 16-bit LoRA. load_in_8bit = False, # 8-bit quantization - load_in_16bit = False, # [NEW!] 16-bit LoRA + load_in_16bit = False, # 16-bit LoRA full_finetuning = False, # Use for full fine-tuning. + trust_remote_code = False, # Enable to support new models # token = "hf_...", # use one if using gated models ) @@ -351,7 +343,7 @@ trainer = SFTTrainer( ) trainer.train() -# Go to https://docs.unsloth.ai for advanced tips like +# Go to https://unsloth.ai/docs for advanced tips like # (1) Saving to GGUF / merging to 16bit for vLLM or SGLang # (2) Continued training from a saved LoRA adapter # (3) Adding an evaluation loop / OOMs @@ -360,14 +352,15 @@ trainer.train()
## 💡 Reinforcement Learning -[RL](https://docs.unsloth.ai/get-started/reinforcement-learning-rl-guide) including [GRPO](https://docs.unsloth.ai/get-started/reinforcement-learning-rl-guide#training-with-grpo), [GSPO](https://docs.unsloth.ai/get-started/reinforcement-learning-rl-guide/gspo-reinforcement-learning), **FP8** traning, DrGRPO, DAPO, PPO, Reward Modelling, Online DPO all work with Unsloth. -Read our [Reinforcement Learning Guide](https://docs.unsloth.ai/get-started/reinforcement-learning-rl-guide) or our [advanced RL docs](https://docs.unsloth.ai/get-started/reinforcement-learning-rl-guide/advanced-rl-documentation) for batching, generation & training parameters. +[RL](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide) including [GRPO](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide#training-with-grpo), [GSPO](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide/gspo-reinforcement-learning), [**FP8** training](https://unsloth.ai/docs/new/fp8-reinforcement-learning), DrGRPO, DAPO, PPO, Reward Modelling, Online DPO all work with Unsloth. + +Read our [Reinforcement Learning Guide](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide) or our [advanced RL docs](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide/advanced-rl-documentation) for batching, generation & training parameters. List of RL notebooks: - gpt-oss GSPO notebook: [Link](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/gpt-oss-(20B)-GRPO.ipynb) -- Qwen2.5-VL GSPO notebook: [Link](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen2_5_7B_VL_GRPO.ipynb) +- - ***FP8*** Qwen3-8B GRPO notebook (L4): [Link](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_8B_FP8_GRPO.ipynb) +- Qwen2.3-VL GSPO notebook: [Link](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_VL_(8B)-Vision-GRPO.ipynb) - Advanced Qwen3 GRPO notebook: [Link](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_(4B)-GRPO.ipynb) -- ***FP8*** Qwen3-8B GRPO notebook (L4): [Link](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_8B_FP8_GRPO.ipynb) - ORPO notebook: [Link](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Llama3_(8B)-ORPO.ipynb) - DPO Zephyr notebook: [Link](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Zephyr_(7B)-DPO.ipynb) - KTO notebook: [Link](https://colab.research.google.com/drive/1MRgGtLWuZX4ypSfGguFgC-IblTvO2ivM?usp=sharing) @@ -427,6 +420,4 @@ You can cite the Unsloth repo as follows: - The [llama.cpp library](https://github.com/ggml-org/llama.cpp) that lets users save models with Unsloth - The Hugging Face team and their libraries: [transformers](https://github.com/huggingface/transformers) and [TRL](https://github.com/huggingface/trl) - The Pytorch and [Torch AO](https://github.com/unslothai/unsloth/pull/3391) team for their contributions -- [Erik](https://github.com/erikwijmans) for his help adding [Apple's ML Cross Entropy](https://github.com/apple/ml-cross-entropy) in Unsloth -- [Etherl](https://github.com/Etherll) for adding support for [TTS, diffusion and BERT models](https://github.com/unslothai/notebooks/pull/34) - And of course for every single person who has contributed or has used Unsloth! From ab95425653b01876a03897869dc44365e374abe1 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 31 Dec 2025 21:35:48 -0800 Subject: [PATCH 086/167] Fix correctness bugs in rl.py, rl_replacements.py, and vision.py (#3811) * Fix correctness bugs in rl.py, rl_replacements.py, and vision.py 1. rl_replacements.py (lines 864, 870): Fixed undefined `nanmin`/`nanmax` functions by using `.nan_to_num(nan=inf/-inf).min()/.max()` pattern. PyTorch doesn't have torch.nanmin/nanmax, so we replace NaN values before computing min/max. 2. vision.py (line 150): Fixed bug where code checked for "input" key but then accessed kwargs["input_ids"] instead of kwargs["input"]. 3. vision.py (line 159): Fixed bug where literal string "key" was used instead of the variable `key` when accessing kwargs. 4. rl.py (lines 903, 905): Fixed non-existent `MathError` exception by replacing with `ValueError`. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- unsloth/models/rl.py | 4 ++-- unsloth/models/rl_replacements.py | 10 ++++++++-- unsloth/models/vision.py | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index 03f2c44701..e1c43b8b85 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -900,9 +900,9 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): if "temperature" in call_args: check_temperature = ( "if temperature <= 0:\n" - " raise MathError('Unsloth: Please set a positive non-zero temperature since your results will be wrong.')\n" + " raise ValueError('Unsloth: Please set a positive non-zero temperature since your results will be wrong.')\n" "elif temperature >= 10:\n" - " raise MathError('Unsloth: Please set a positive non-zero temperature less than 10, since sampling will be quite erratic.')\n" + " raise ValueError('Unsloth: Please set a positive non-zero temperature less than 10, since sampling will be quite erratic.')\n" "\n" ) extra_args += check_temperature diff --git a/unsloth/models/rl_replacements.py b/unsloth/models/rl_replacements.py index 3dfeea6871..5e079335ae 100644 --- a/unsloth/models/rl_replacements.py +++ b/unsloth/models/rl_replacements.py @@ -861,13 +861,19 @@ def grpo_trainer_compute_loss(function_name, function): else torch.tensor(0.0, device = self.model.device) ) self._metrics[mode]["sampling/importance_sampling_ratio/min"].append( - nanmin(self.accelerator.gather(min_importance_sampling_ratio)).item() + self.accelerator.gather(min_importance_sampling_ratio) + .nan_to_num(nan = float("inf")) + .min() + .item() ) self._metrics[mode]["sampling/importance_sampling_ratio/mean"].append( self.accelerator.gather(mean_importance_sampling_ratio).nanmean().item() ) self._metrics[mode]["sampling/importance_sampling_ratio/max"].append( - nanmax(self.accelerator.gather(max_importance_sampling_ratio)).item() + self.accelerator.gather(max_importance_sampling_ratio) + .nan_to_num(nan = float("-inf")) + .max() + .item() ) return loss diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index 36cfbf0b17..c909f963b9 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -147,7 +147,7 @@ def unsloth_base_fast_generate( elif "input_ids" in kwargs: input_ids = kwargs["input_ids"] elif "input" in kwargs: - input_ids = kwargs["input_ids"] + input_ids = kwargs["input"] elif "input_features" in kwargs: input_ids = kwargs["input_features"] elif "input_embeds" in kwargs: @@ -156,7 +156,7 @@ def unsloth_base_fast_generate( input_ids = kwargs["inputs"] else: key = next(iter(kwargs.keys())) - if type(kwargs["key"]) is not torch.Tensor: + if type(kwargs[key]) is not torch.Tensor: raise TypeError("Unsloth: You need to pass in input_ids to .generate!") input_ids = kwargs[key] assert type(input_ids) is torch.Tensor From c0436a2b8538cd68987fafdd8244ef6616bbb551 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 1 Jan 2026 02:36:33 -0800 Subject: [PATCH 087/167] Fix correctness bugs across multiple model files (#3813) 1. cohere.py:347-348 - Fixed wrong variable names in QK normalization. Used `Q`/`K` but variables were named `Qn`/`Kn`. This caused NameError when `use_qk_norm=True` (e.g., c4ai-command-r-plus models). 2. cohere.py:482 - Fixed wrong object reference in inference loop. Used `self.mlp` but should be `decoder_layer.mlp` since we're iterating through decoder layers. Caused AttributeError during inference. 3. falcon_h1.py:459,461 - Fixed wrong attribute names in inference path. Used `post_attention_layernorm` and `mlp` but Falcon H1 uses `pre_ff_layernorm` and `feed_forward`. Caused AttributeError during generation. 4. qwen3_moe.py:210 - Fixed wrong module path with incorrect capitalization. Used `transformers.models.Qwen3Moe` but should be `transformers.models.qwen3_moe`. Caused AttributeError when patching rotary embeddings. 5. qwen3_moe.py:239 - Fixed wrong model_patcher class. Used `FastQwen3Model` but should be `FastQwen3MoeModel` for MoE models. Caused incorrect patching for Qwen3 MoE models. 6. hf_hub.py:21-22 - Fixed floor division and missing return for billion values. Used `//` instead of `/` for millions, and had no return for values >= 1B. Caused incorrect formatting and None return for large numbers. 7. save.py:550 - Fixed self-assignment that did nothing. `sharded_ram_usage = sharded_ram_usage` should be `= max_shard_size`. Caused integer shard sizes to be ignored. 8. rl.py:562-567 - Fixed orphan string not included in length_check. The elif branch for max_seq_length validation was a standalone string expression, not concatenated to length_check. Caused silent skip of the max_seq_length > model_max_seq_length warning. 9. granite.py:49-52 - Fixed wrong model name and version in error message. Said "Gemma2" and "4.42.3" but should be "Granite" and "4.45.0". --- unsloth/models/cohere.py | 6 +++--- unsloth/models/falcon_h1.py | 4 ++-- unsloth/models/granite.py | 6 +++--- unsloth/models/qwen3_moe.py | 4 ++-- unsloth/models/rl.py | 6 +++++- unsloth/save.py | 2 +- unsloth/utils/hf_hub.py | 4 +++- 7 files changed, 19 insertions(+), 13 deletions(-) diff --git a/unsloth/models/cohere.py b/unsloth/models/cohere.py index a091a0173f..e9f56763d6 100644 --- a/unsloth/models/cohere.py +++ b/unsloth/models/cohere.py @@ -344,8 +344,8 @@ def CohereAttention_fast_forward_inference( Kn = Kn.view(bsz, 1, n_kv_heads, head_dim).transpose(1, 2) Vn = Vn.view(bsz, 1, n_kv_heads, head_dim).transpose(1, 2) if self.use_qk_norm: - Q = fast_layernorm_inference(self.q_norm, Q, self.q_norm_out_weight) - K = fast_layernorm_inference(self.k_norm, K, self.k_norm_out_weight) + Qn = fast_layernorm_inference(self.q_norm, Qn, self.q_norm_out_weight) + Kn = fast_layernorm_inference(self.k_norm, Kn, self.k_norm_out_weight) # cos, sin = self.rotary_emb(Vn, seq_len = kv_seq_len) # Qn, Kn = inplace_rope_embedding(Qn, Kn, cos, sin, position_ids) @@ -479,7 +479,7 @@ def CohereModel_fast_forward_inference( ) ) - hidden_states_mlp = fast_swiglu_inference(self.mlp, hidden_states) + hidden_states_mlp = fast_swiglu_inference(decoder_layer.mlp, hidden_states) residual += hidden_states_attention residual += hidden_states_mlp hidden_states = residual diff --git a/unsloth/models/falcon_h1.py b/unsloth/models/falcon_h1.py index fc5ea458a6..428f49d727 100644 --- a/unsloth/models/falcon_h1.py +++ b/unsloth/models/falcon_h1.py @@ -456,9 +456,9 @@ def FalconH1DecoderLayer_fast_forward( # Fully Connected residual = hidden_states hidden_states = fast_rms_layernorm_inference( - self.post_attention_layernorm, hidden_states + self.pre_ff_layernorm, hidden_states ) - hidden_states = fast_swiglu_inference(self.mlp, hidden_states) + hidden_states = fast_swiglu_inference(self.feed_forward, hidden_states) hidden_states += residual else: residual = hidden_states diff --git a/unsloth/models/granite.py b/unsloth/models/granite.py index 2632ab6914..f85f1b641f 100644 --- a/unsloth/models/granite.py +++ b/unsloth/models/granite.py @@ -46,9 +46,9 @@ except: transformers_version = Version(transformers_version) if not transformers_version >= Version("4.45.0"): raise ImportError( - f"Unsloth: Your transformers version of {transformers_version} does not support Gemma2.\n" - f"The minimum required version is 4.42.3.\n" - f'Try `pip install --upgrade "transformers>=4.42.3"`\n' + f"Unsloth: Your transformers version of {transformers_version} does not support Granite.\n" + f"The minimum required version is 4.45.0.\n" + f'Try `pip install --upgrade "transformers>=4.45.0"`\n' f"to obtain the latest transformers build, then restart this session." ) diff --git a/unsloth/models/qwen3_moe.py b/unsloth/models/qwen3_moe.py index bec3fa7b0d..e1f8c71b6b 100644 --- a/unsloth/models/qwen3_moe.py +++ b/unsloth/models/qwen3_moe.py @@ -207,7 +207,7 @@ class FastQwen3MoeModel(FastQwen3Model): # https://github.com/huggingface/transformers/blob/v4.37.2/src/transformers/models/llama/modeling_llama.py\ import transformers.models.qwen3_moe.modeling_qwen3_moe - transformers.models.Qwen3Moe.modeling_qwen3_moe.Qwen3MoeRotaryEmbedding = ( + transformers.models.qwen3_moe.modeling_qwen3_moe.Qwen3MoeRotaryEmbedding = ( LlamaRotaryEmbedding ) return @@ -236,7 +236,7 @@ class FastQwen3MoeModel(FastQwen3Model): device_map = device_map, rope_scaling = rope_scaling, fix_tokenizer = fix_tokenizer, - model_patcher = FastQwen3Model, + model_patcher = FastQwen3MoeModel, tokenizer_name = tokenizer_name, trust_remote_code = trust_remote_code, **kwargs, diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index e1c43b8b85..22189f459c 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -559,8 +559,12 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): " if args_max_seq_length is None and model_max_seq_length is not None:\n" " max_seq_length = model.max_seq_length\n" " if hasattr(args, 'max_seq_length'): args.max_seq_length = max_seq_length\n" + " elif args_max_seq_length is not None and model_max_seq_length is not None:\n" + " if args_max_seq_length > model_max_seq_length:\n" + " print('Unsloth: You set `max_seq_length` as ' + str(args_max_seq_length) + ' but '\n" + " 'the maximum the model supports is ' + str(model_max_seq_length) + '. We shall reduce it.')\n" + " args.max_seq_length = model_max_seq_length\n" ) - " elif args_max_seq_length is not None and model_max_seq_length is not None:\n" " if args_max_seq_length > model_max_seq_length:\n" " print('Unsloth: You set `max_seq_length` as ' + str(args_max_seq_length) + ' but \n" " the maximum the model supports is ' + str(model_max_seq_length) + '. We shall reduce it.')\n" " args.max_seq_length = model_max_seq_length\n" extra_args += length_check # At this point max_seq_length might be set, but trl is moving to max_length diff --git a/unsloth/save.py b/unsloth/save.py index 3a275cf0c3..ceb36854d2 100644 --- a/unsloth/save.py +++ b/unsloth/save.py @@ -547,7 +547,7 @@ def unsloth_save_model( elif mb_found: sharded_ram_usage = int(mb_found.group(1)) * 1024 * 1024 elif type(max_shard_size) is int: - sharded_ram_usage = sharded_ram_usage + sharded_ram_usage = max_shard_size # Switch to our fast saving modules if it's a slow PC! n_cpus = psutil.cpu_count(logical = False) diff --git a/unsloth/utils/hf_hub.py b/unsloth/utils/hf_hub.py index 75df00fbf0..e3960ba0ce 100644 --- a/unsloth/utils/hf_hub.py +++ b/unsloth/utils/hf_hub.py @@ -19,7 +19,9 @@ def formatted_int(value: int) -> str: elif value < MILLION: return f"{float(value) / 1000:,.1f}K" elif value < BILLION: - return f"{float(value) // 1000000:,.1f}M" + return f"{float(value) / 1000000:,.1f}M" + else: + return f"{float(value) / 1000000000:,.1f}B" def get_model_info( From 9176dd3258db1eb727e5bbb2b6d6e464b3d6269f Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 1 Jan 2026 12:54:21 +0000 Subject: [PATCH 088/167] Add TODO comment for ensure_weight_tying in vision models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- unsloth/models/vision.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index 9f847f2837..b4ce718f46 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -930,7 +930,7 @@ class FastBaseModel: task_type = TaskType.CAUSAL_LM, temporary_location = "_unsloth_temporary_saved_buffers", qat_scheme = None, - ensure_weight_tying = False, + ensure_weight_tying = False, # [TODO] Add `ensure_weight_tying` for `modules_to_save` for vision models **kwargs, ): if os.environ.get("UNSLOTH_ENABLE_FULL_FINETUNING", "0") == "1": From 504a0112d55ac8c3d2b88bd7b8e41c8a2b104dbb Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Fri, 2 Jan 2026 07:19:08 +0000 Subject: [PATCH 089/167] Fix Gemma3 QAT training instability with int8-int4 scheme Gemma3 models have a large vocabulary (262144 tokens) which causes training loss to explode when using int8 embedding quantization. This fix auto-detects Gemma3 models and switches from int8-int4 (phone-deployment) to int4 weight-only QAT for stable training. --- unsloth/models/_utils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index ccb547f58e..3851e18f92 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -2198,6 +2198,18 @@ def _prepare_model_for_qat( from torchao.quantization.granularity import PerGroup, PerAxis from torchao.quantization.qat import QATConfig + # Gemma3 models have issues with int8 embedding quantization due to their + # large vocabulary size (262144). Auto-switch to int4 weight-only instead. + if qat_scheme == "int8-int4": + model_types = get_transformers_model_type(model.config) + is_gemma3 = any("gemma3" in mt or "gemma_3" in mt for mt in model_types) + if is_gemma3: + print( + "Unsloth: Gemma3 has a large vocabulary causing int8 embedding issues. " + "Switching to int4 weight-only QAT for training stability." + ) + qat_scheme = "int4" + if not isinstance(qat_scheme, TorchAOConfig): torchao_config: Optional[TorchAOConfig] = None if qat_scheme == "fp8-int4": From 354797584e86da260d81352ca01716c4d029ed47 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 2 Jan 2026 00:14:44 -0800 Subject: [PATCH 090/167] fix_huggingface_hub --- unsloth/__init__.py | 3 +++ unsloth/import_fixes.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/unsloth/__init__.py b/unsloth/__init__.py index d10a0f8030..c74b248a83 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -30,16 +30,19 @@ from .import_fixes import ( check_fbgemm_gpu_version, torchvision_compatibility_check, fix_diffusers_warnings, + fix_huggingface_hub, ) fix_message_factory_issue() check_fbgemm_gpu_version() torchvision_compatibility_check() fix_diffusers_warnings() +fix_huggingface_hub() del fix_message_factory_issue del check_fbgemm_gpu_version del torchvision_compatibility_check del fix_diffusers_warnings +del fix_huggingface_hub # This check is critical because Unsloth optimizes these libraries by modifying # their code at import time. If they're imported first, the original (slower, diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index efc7a7f4cd..f388f4ea8d 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -539,3 +539,10 @@ def fix_executorch(): def fix_diffusers_warnings(): # Silence Flax classes are deprecated and will be removed in Diffusers v1.0.0. os.environ["DIFFUSERS_VERBOSITY"] = "error" + + +def fix_huggingface_hub(): + # huggingface_hub.is_offline_mode got removed, so add it back + import huggingface_hub + if not hasattr(huggingface_hub, "is_offline_mode"): + huggingface_hub.is_offline_mode = lambda: huggingface_hub.constants.HF_HUB_OFFLINE From 0f8d8419dbcedecc2a866e0628e37a147ebb8174 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 2 Jan 2026 02:48:28 -0800 Subject: [PATCH 091/167] Update loader.py --- unsloth/models/loader.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/unsloth/models/loader.py b/unsloth/models/loader.py index 91016a13ba..247c72f43f 100644 --- a/unsloth/models/loader.py +++ b/unsloth/models/loader.py @@ -204,6 +204,17 @@ class FastLanguageModel(FastLlamaModel): "Unsloth: Please install vLLM before enabling `fast_inference`!\n" "You can do this in a terminal via `pip install vllm`" ) + if DEVICE_TYPE_TORCH == "cuda": + for i in range(DEVICE_COUNT): + # [TODO] DGX Spark vLLM breaks + if "NVIDIA GB10" in str(torch.cuda.get_device_name(i)).upper(): + print( + "Unsloth: DGX Spark detected - `fast_inference=True` is currently broken as of January 2026.\n" + "Defaulting to native Unsloth inference." + ) + fast_inference = False + break + # [TODO] For now fast_inference only works with fast_inference ie vLLM if load_in_fp8 != False: if not fast_inference: @@ -744,6 +755,17 @@ class FastModel(FastBaseModel): "Unsloth: Please install vLLM before enabling `fast_inference`!\n" "You can do this in a terminal via `pip install vllm`" ) + if DEVICE_TYPE_TORCH == "cuda": + for i in range(DEVICE_COUNT): + # [TODO] DGX Spark vLLM breaks + if "NVIDIA GB10" in str(torch.cuda.get_device_name(i)).upper(): + print( + "Unsloth: DGX Spark detected - `fast_inference=True` is currently broken as of January 2026.\n" + "Defaulting to native Unsloth inference." + ) + fast_inference = False + break + # [TODO] For now fast_inference only works with fast_inference ie vLLM if load_in_fp8 != False: if not fast_inference: From 33ad028fc84c2331636e571cae251c1c96b5abb8 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 2 Jan 2026 03:41:51 -0800 Subject: [PATCH 092/167] Update import_fixes.py --- unsloth/import_fixes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index f388f4ea8d..da0fbc613b 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -97,6 +97,8 @@ if os.environ.get("UNSLOTH_ENABLE_LOGGING", "0") != "1": sys.stderr = HidePrintMessage(sys.stderr) # https://github.com/pytorch/FBGEMM/blob/d99cd96490ec4aabac2ee95b1e76ea4dcfcfa628/fbgemm_gpu/experimental/gemm/triton_gemm/utils.py#L43-L52 sys.stderr.add_filter("TMA benchmarks will be running") + # Skipping import of cpp extensions due to incompatible torch version 2.9.0+cu128 for torchao version 0.15.0 + logging.getLogger("torchao").setLevel(logging.ERROR) # Fix up AttributeError: 'MessageFactory' object has no attribute 'GetPrototype' From 0607627822492d39e7ed9dea8b6bde2dfeef2de1 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 2 Jan 2026 05:05:47 -0800 Subject: [PATCH 093/167] Update import_fixes.py --- unsloth/import_fixes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index da0fbc613b..f0dde256c1 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -20,6 +20,7 @@ from packaging.version import Version as TrueVersion import re import logging import textwrap +import warnings # We cannot do from unsloth_zoo.log import logger since FBGEMM might cause seg faults. UNSLOTH_ENABLE_LOGGING = os.environ.get("UNSLOTH_ENABLE_LOGGING", "0") in ( @@ -99,6 +100,8 @@ if os.environ.get("UNSLOTH_ENABLE_LOGGING", "0") != "1": sys.stderr.add_filter("TMA benchmarks will be running") # Skipping import of cpp extensions due to incompatible torch version 2.9.0+cu128 for torchao version 0.15.0 logging.getLogger("torchao").setLevel(logging.ERROR) + # SyntaxWarning: invalid escape sequence '\.' + warnings.filterwarnings("ignore", message = "invalid escape sequence", category = SyntaxWarning) # Fix up AttributeError: 'MessageFactory' object has no attribute 'GetPrototype' From 91d911ff81bf78755507e6ce41ccfb5eda2451af Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Fri, 2 Jan 2026 13:58:08 +0000 Subject: [PATCH 094/167] Add helpful error messages for fast_generate when fast_inference=False When users load a model with fast_inference=False but then try to use vLLM-style arguments with fast_generate, they previously got confusing errors. This adds a wrapper that detects common mistakes and provides helpful guidance: - Using sampling_params: explains to use HF generate args instead - Using lora_request: explains LoRA weights are already merged - Passing text strings: shows how to tokenize input first Changes: - Add make_fast_generate_wrapper to _utils.py - Apply wrapper in llama.py when fast_inference=False - Apply wrapper in vision.py when fast_inference=False --- unsloth/models/_utils.py | 56 ++++++++++++++++++++++++++++++++++++++++ unsloth/models/llama.py | 2 +- unsloth/models/vision.py | 2 +- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 3851e18f92..c1f626ec66 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -73,6 +73,7 @@ __all__ = [ "verify_fp8_support_if_applicable", "_get_inference_mode_context_manager", "hf_login", + "make_fast_generate_wrapper", ] import torch @@ -2378,3 +2379,58 @@ def hf_login(token: Optional[str] = None) -> Optional[str]: except Exception as e: logger.info(f"Failed to login to huggingface using token with error: {e}") return token + + +def make_fast_generate_wrapper(original_generate): + """ + Creates a wrapper around model.generate that checks for incorrect + vLLM-style usage when fast_inference=False. + """ + @functools.wraps(original_generate) + def _fast_generate_wrapper(*args, **kwargs): + # Check for vLLM-specific arguments + if "sampling_params" in kwargs: + raise ValueError( + "Unsloth: `sampling_params` is only supported when `fast_inference=True` (vLLM). " + "Since `fast_inference=False`, use HuggingFace generate arguments instead:\n" + " model.fast_generate(**tokens.to('cuda'), max_new_tokens=64, temperature=1.0, top_p=0.95)" + ) + + if "lora_request" in kwargs: + raise ValueError( + "Unsloth: `lora_request` is only supported when `fast_inference=True` (vLLM). " + "Since `fast_inference=False`, LoRA weights are already merged into the model." + ) + + # Check if first positional argument is a string or list of strings + if len(args) > 0: + first_arg = args[0] + is_string_input = False + + if isinstance(first_arg, str): + is_string_input = True + elif isinstance(first_arg, (list, tuple)) and len(first_arg) > 0: + if isinstance(first_arg[0], str): + is_string_input = True + + if is_string_input: + raise ValueError( + "Unsloth: Passing text strings to `fast_generate` is only supported " + "when `fast_inference=True` (vLLM). Since `fast_inference=False`, you must " + "tokenize the input first:\n\n" + " messages = tokenizer.apply_chat_template(\n" + " [{\"role\": \"user\", \"content\": \"Your prompt here\"}],\n" + " tokenize=True, add_generation_prompt=True,\n" + " return_tensors=\"pt\", return_dict=True\n" + " )\n" + " output = model.fast_generate(\n" + " **messages.to('cuda'),\n" + " max_new_tokens=64,\n" + " temperature=1.0,\n" + " )" + ) + + # Call original generate + return original_generate(*args, **kwargs) + + return _fast_generate_wrapper diff --git a/unsloth/models/llama.py b/unsloth/models/llama.py index 29d41f4bb1..92d51b73ad 100644 --- a/unsloth/models/llama.py +++ b/unsloth/models/llama.py @@ -2326,7 +2326,7 @@ class FastLlamaModel: attn_implementation = "eager", **kwargs, ) - model.fast_generate = model.generate + model.fast_generate = make_fast_generate_wrapper(model.generate) model.fast_generate_batches = None else: from unsloth_zoo.vllm_utils import ( diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index 1924373f67..6c5356e0b9 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -673,7 +673,7 @@ class FastBaseModel: **kwargs, ) if hasattr(model, "generate"): - model.fast_generate = model.generate + model.fast_generate = make_fast_generate_wrapper(model.generate) model.fast_generate_batches = error_out_no_vllm if offload_embedding: if bool( From f59a766d836c4a27139cc93a15cc8b6fc649bf2a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 13:58:49 +0000 Subject: [PATCH 095/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/import_fixes.py | 9 +++++++-- unsloth/models/_utils.py | 5 +++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index f0dde256c1..bb6996a3e3 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -101,7 +101,9 @@ if os.environ.get("UNSLOTH_ENABLE_LOGGING", "0") != "1": # Skipping import of cpp extensions due to incompatible torch version 2.9.0+cu128 for torchao version 0.15.0 logging.getLogger("torchao").setLevel(logging.ERROR) # SyntaxWarning: invalid escape sequence '\.' - warnings.filterwarnings("ignore", message = "invalid escape sequence", category = SyntaxWarning) + warnings.filterwarnings( + "ignore", message = "invalid escape sequence", category = SyntaxWarning + ) # Fix up AttributeError: 'MessageFactory' object has no attribute 'GetPrototype' @@ -549,5 +551,8 @@ def fix_diffusers_warnings(): def fix_huggingface_hub(): # huggingface_hub.is_offline_mode got removed, so add it back import huggingface_hub + if not hasattr(huggingface_hub, "is_offline_mode"): - huggingface_hub.is_offline_mode = lambda: huggingface_hub.constants.HF_HUB_OFFLINE + huggingface_hub.is_offline_mode = ( + lambda: huggingface_hub.constants.HF_HUB_OFFLINE + ) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index c1f626ec66..1cead3afaf 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -2386,6 +2386,7 @@ def make_fast_generate_wrapper(original_generate): Creates a wrapper around model.generate that checks for incorrect vLLM-style usage when fast_inference=False. """ + @functools.wraps(original_generate) def _fast_generate_wrapper(*args, **kwargs): # Check for vLLM-specific arguments @@ -2419,9 +2420,9 @@ def make_fast_generate_wrapper(original_generate): "when `fast_inference=True` (vLLM). Since `fast_inference=False`, you must " "tokenize the input first:\n\n" " messages = tokenizer.apply_chat_template(\n" - " [{\"role\": \"user\", \"content\": \"Your prompt here\"}],\n" + ' [{"role": "user", "content": "Your prompt here"}],\n' " tokenize=True, add_generation_prompt=True,\n" - " return_tensors=\"pt\", return_dict=True\n" + ' return_tensors="pt", return_dict=True\n' " )\n" " output = model.fast_generate(\n" " **messages.to('cuda'),\n" From 4b210859f062b9cbda7680fba77e589fd83c44b5 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 2 Jan 2026 06:07:16 -0800 Subject: [PATCH 096/167] Bug fixes --- pyproject.toml | 4 ++-- unsloth/models/_utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index decc0e9f5f..20e3fd847f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ huggingfacenotorch = [ ] huggingface = [ "unsloth[huggingfacenotorch]", - "unsloth_zoo>=2025.12.7", + "unsloth_zoo>=2025.12.8", "torchvision", "unsloth[triton]", ] @@ -523,7 +523,7 @@ colab-ampere-torch220 = [ "flash-attn>=2.6.3 ; ('linux' in sys_platform)", ] colab-new = [ - "unsloth_zoo>=2025.12.7", + "unsloth_zoo>=2025.12.8", "packaging", "tyro", "transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,<=4.57.3", diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 1cead3afaf..545ba4794a 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2025.12.9" +__version__ = "2025.12.10" __all__ = [ "SUPPORTS_BFLOAT16", From d711c00ddb2542eef79df79310f7fcbc4a697bc4 Mon Sep 17 00:00:00 2001 From: Fizza-Mukhtar Date: Fri, 2 Jan 2026 08:42:59 -0800 Subject: [PATCH 097/167] Make llama.cpp CURL support optional during CMake builds --- unsloth/save.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/unsloth/save.py b/unsloth/save.py index ceb36854d2..29d9cdcaff 100644 --- a/unsloth/save.py +++ b/unsloth/save.py @@ -130,6 +130,10 @@ ALLOWED_QUANTS = { "q3_k_xs": "3-bit extra small quantization", } +def has_curl(): + return shutil.which("curl") is not None + +CURL_FLAG = "-DLLAMA_CURL=ON" if has_curl() else "-DLLAMA_CURL=OFF" def print_quantization_methods(): for key, value in ALLOWED_QUANTS.items(): @@ -879,8 +883,9 @@ def install_llama_cpp_make_non_blocking(): # Uses new CMAKE n_jobs = max(int(psutil.cpu_count()), 1) # Use less CPUs since 1.5x faster check = os.system( - "cmake llama.cpp -B llama.cpp/build -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=OFF -DLLAMA_CURL=ON" + f"cmake llama.cpp -B llama.cpp/build -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=OFF {CURL_FLAG}" ) + if check != 0: raise RuntimeError( f"*** Unsloth: Failed compiling llama.cpp using os.system(...) with error {check}. Please report this ASAP!" @@ -991,11 +996,12 @@ def install_llama_cpp_old(version = -10): if try_execute(commands) == "CMAKE": # Instead use CMAKE commands = [ - "cmake llama.cpp -B llama.cpp/build -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=OFF -DLLAMA_CURL=ON", + "cmake llama.cpp -B llama.cpp/build -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=OFF {CURL_FLAG}", f"cmake --build llama.cpp/build --config Release -j{psutil.cpu_count()*2} --clean-first --target {' '.join(LLAMA_CPP_TARGETS)}", "cp llama.cpp/build/bin/llama-* llama.cpp", "rm -rf llama.cpp/build", ] + try_execute(commands) # Check if successful @@ -1037,7 +1043,7 @@ def install_llama_cpp_blocking(use_cuda = False): if try_execute(commands) == "CMAKE": # Instead use CMAKE commands = [ - "cmake llama.cpp -B llama.cpp/build -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=OFF -DLLAMA_CURL=ON", + "cmake llama.cpp -B llama.cpp/build -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=OFF {CURL_FLAG}", f"cmake --build llama.cpp/build --config Release -j{psutil.cpu_count()*2} --clean-first --target {' '.join(LLAMA_CPP_TARGETS)}", "cp llama.cpp/build/bin/llama-* llama.cpp", "rm -rf llama.cpp/build", From 43fb35e0610dd903e4886d494e9698db0a2d07f7 Mon Sep 17 00:00:00 2001 From: Fizza-Mukhtar Date: Fri, 2 Jan 2026 08:55:58 -0800 Subject: [PATCH 098/167] Make llama.cpp CURL support optional during CMake builds --- unsloth/save.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unsloth/save.py b/unsloth/save.py index 29d9cdcaff..359010dbe2 100644 --- a/unsloth/save.py +++ b/unsloth/save.py @@ -996,7 +996,7 @@ def install_llama_cpp_old(version = -10): if try_execute(commands) == "CMAKE": # Instead use CMAKE commands = [ - "cmake llama.cpp -B llama.cpp/build -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=OFF {CURL_FLAG}", + f"cmake llama.cpp -B llama.cpp/build -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=OFF {CURL_FLAG}", f"cmake --build llama.cpp/build --config Release -j{psutil.cpu_count()*2} --clean-first --target {' '.join(LLAMA_CPP_TARGETS)}", "cp llama.cpp/build/bin/llama-* llama.cpp", "rm -rf llama.cpp/build", @@ -1043,7 +1043,7 @@ def install_llama_cpp_blocking(use_cuda = False): if try_execute(commands) == "CMAKE": # Instead use CMAKE commands = [ - "cmake llama.cpp -B llama.cpp/build -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=OFF {CURL_FLAG}", + f"cmake llama.cpp -B llama.cpp/build -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=OFF {CURL_FLAG}", f"cmake --build llama.cpp/build --config Release -j{psutil.cpu_count()*2} --clean-first --target {' '.join(LLAMA_CPP_TARGETS)}", "cp llama.cpp/build/bin/llama-* llama.cpp", "rm -rf llama.cpp/build", From 030339de3976f61f87d10617423f414fdd12b74b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 16:58:04 +0000 Subject: [PATCH 099/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/save.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/unsloth/save.py b/unsloth/save.py index 359010dbe2..714df7682a 100644 --- a/unsloth/save.py +++ b/unsloth/save.py @@ -130,11 +130,14 @@ ALLOWED_QUANTS = { "q3_k_xs": "3-bit extra small quantization", } + def has_curl(): return shutil.which("curl") is not None + CURL_FLAG = "-DLLAMA_CURL=ON" if has_curl() else "-DLLAMA_CURL=OFF" + def print_quantization_methods(): for key, value in ALLOWED_QUANTS.items(): print(f'"{key}" ==> {value}') From 31d1c0b928fbdf995f0c1b175e78efef55e51171 Mon Sep 17 00:00:00 2001 From: Kaitao Yang Date: Sat, 3 Jan 2026 22:38:37 -0800 Subject: [PATCH 100/167] remove redundant code of has_block --- unsloth/utils/attention_dispatch.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/unsloth/utils/attention_dispatch.py b/unsloth/utils/attention_dispatch.py index ccd49dada8..0e5f3c1951 100644 --- a/unsloth/utils/attention_dispatch.py +++ b/unsloth/utils/attention_dispatch.py @@ -219,16 +219,10 @@ def run_attention( ) if config.n_groups != 1 and not requires_grad: - if has_block: - out = out.view(bsz, q_len, config.n_kv_heads, config.n_groups, head_dim) - else: - out = out.view(bsz, q_len, config.n_kv_heads, config.n_groups, head_dim) + out = out.view(bsz, q_len, config.n_kv_heads, config.n_groups, head_dim) out = out.reshape(bsz, q_len, n_heads, head_dim) else: - if has_block: - out = out.view(bsz, q_len, n_heads, head_dim) - else: - out = out.view(bsz, q_len, n_heads, head_dim) + out = out.view(bsz, q_len, n_heads, head_dim) return out else: local_mask = context.attention_mask From 59edd7fa90f1c374917b441650086a4581bab2c6 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Sun, 4 Jan 2026 12:21:39 +0000 Subject: [PATCH 101/167] rl.py fixes: buffer reset, safer attribute access, typo fix 1. Auto-reset gradient checkpointing buffers after trainer.train() - Import and call reset_unsloth_gradient_checkpointing_buffers() in prepare_for_training_mode wrapper to free memory after training while keeping buffers ready for subsequent runs 2. Replace eval/exec with safer getattr/setattr - eval(f"trl.trainer.{trainer}") -> getattr(trl.trainer, trainer) - exec(f"...{unwrap} = ...") -> setattr(current_trainer, unwrap, ...) - exec(f"Trainer.prediction_step=...") -> direct assignment 3. Fix psutil.cpu_count() potentially returning None - Change psutil.cpu_count()+4 to (psutil.cpu_count() or 1)+4 - Prevents TypeError on systems where cpu_count() returns None 4. Fix typo: oriignal_is_vlm_text -> original_is_vlm_text --- unsloth/models/rl.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index 22189f459c..9ea57e32d3 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -199,15 +199,15 @@ def PatchRL(FastLanguageModel): unwrap = "unwrap_model_for_generation" for trainer in trainers: try: - current_trainer = eval(f"trl.trainer.{trainer}") + current_trainer = getattr(trl.trainer, trainer) except: continue if hasattr(current_trainer, unwrap): try: - exec(f"trl.trainer.{trainer}.{unwrap} = unsloth_{unwrap}") + setattr(current_trainer, unwrap, unsloth_unwrap_model_for_generation) except: continue - exec(f"Trainer.prediction_step=unsloth_prediction_step") + Trainer.prediction_step = unsloth_prediction_step selective_log_softmax = RL_REPLACEMENTS["selective_log_softmax"] @@ -234,6 +234,7 @@ from transformers.training_args import ParallelMode # Also patches W&B since multiple runs must use wandb.finish() import functools from types import MethodType +from unsloth_zoo.gradient_checkpointing import reset_unsloth_gradient_checkpointing_buffers def prepare_for_training_mode(f): @functools.wraps(f) def wrapper(self, *args, **kwargs): @@ -244,6 +245,11 @@ def prepare_for_training_mode(f): # Return inference mode if hasattr(self, 'model') and hasattr(self.model, "for_inference"): self.model.for_inference() + # Reset gradient checkpointing buffers to free memory while staying ready for next run + try: + reset_unsloth_gradient_checkpointing_buffers() + except: + pass # Patch W&B to enable logging on future runs, otherwise it'll overwrite the first run try: import wandb @@ -817,7 +823,7 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): num_proc_check = ( "if dataset_num_proc is None:\n" " import psutil\n" - " dataset_num_proc = min(max(psutil.cpu_count()+4, 2), 64)\n" + " dataset_num_proc = min(max((psutil.cpu_count() or 1)+4, 2), 64)\n" " memory_gb_left = psutil.virtual_memory().available / (1024**3)\n" " if memory_gb_left <= 4: dataset_num_proc = 1 # Too risky, so set to 1\n" " elif memory_gb_left <= 6: dataset_num_proc = min(2, dataset_num_proc)\n" @@ -994,10 +1000,10 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): # Temporary patch _is_vlm to False # as of 0.22 it only exists in sfttrainer - oriignal_is_vlm_text = "self._is_vlm = True" + original_is_vlm_text = "self._is_vlm = True" new_is_vlm_text = "self._is_vlm = False" RLTrainer_source = RLTrainer_source.replace( - oriignal_is_vlm_text, new_is_vlm_text + original_is_vlm_text, new_is_vlm_text ) # Remove multiple doc strings From eba90fe3467fbc4f80d9249f38fd190a6a38dae1 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Sun, 4 Jan 2026 12:57:10 +0000 Subject: [PATCH 102/167] Handle older unsloth-zoo without reset_unsloth_gradient_checkpointing_buffers --- unsloth/models/rl.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index 9ea57e32d3..88aeeda8a1 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -234,7 +234,10 @@ from transformers.training_args import ParallelMode # Also patches W&B since multiple runs must use wandb.finish() import functools from types import MethodType -from unsloth_zoo.gradient_checkpointing import reset_unsloth_gradient_checkpointing_buffers +try: + from unsloth_zoo.gradient_checkpointing import reset_unsloth_gradient_checkpointing_buffers +except: + def reset_unsloth_gradient_checkpointing_buffers(): pass def prepare_for_training_mode(f): @functools.wraps(f) def wrapper(self, *args, **kwargs): From 0dc55e95048fd8643424c04ca5601bd8b26ba691 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Sun, 4 Jan 2026 12:58:45 +0000 Subject: [PATCH 103/167] Fix psutil.cpu_count() potentially returning None in save.py --- unsloth/save.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/unsloth/save.py b/unsloth/save.py index 714df7682a..071e032c53 100644 --- a/unsloth/save.py +++ b/unsloth/save.py @@ -879,12 +879,12 @@ def install_llama_cpp_make_non_blocking(): IS_CMAKE = False if check == 0: # Uses old MAKE - n_jobs = max(int(psutil.cpu_count() * 1.5), 1) + n_jobs = max(int((psutil.cpu_count() or 1) * 1.5), 1) full_command = ["make", "all", "-j" + str(n_jobs), "-C", "llama.cpp"] IS_CMAKE = False else: # Uses new CMAKE - n_jobs = max(int(psutil.cpu_count()), 1) # Use less CPUs since 1.5x faster + n_jobs = max(int(psutil.cpu_count() or 1), 1) # Use less CPUs since 1.5x faster check = os.system( f"cmake llama.cpp -B llama.cpp/build -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=OFF {CURL_FLAG}" ) @@ -994,13 +994,13 @@ def install_llama_cpp_old(version = -10): # Try using MAKE commands = [ "make clean -C llama.cpp", - f"make all -j{psutil.cpu_count()*2} -C llama.cpp", + f"make all -j{(psutil.cpu_count() or 1)*2} -C llama.cpp", ] if try_execute(commands) == "CMAKE": # Instead use CMAKE commands = [ f"cmake llama.cpp -B llama.cpp/build -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=OFF {CURL_FLAG}", - f"cmake --build llama.cpp/build --config Release -j{psutil.cpu_count()*2} --clean-first --target {' '.join(LLAMA_CPP_TARGETS)}", + f"cmake --build llama.cpp/build --config Release -j{(psutil.cpu_count() or 1)*2} --clean-first --target {' '.join(LLAMA_CPP_TARGETS)}", "cp llama.cpp/build/bin/llama-* llama.cpp", "rm -rf llama.cpp/build", ] @@ -1040,14 +1040,14 @@ def install_llama_cpp_blocking(use_cuda = False): "make clean -C llama.cpp", # https://github.com/ggerganov/llama.cpp/issues/7062 # Weirdly GPU conversion for GGUF breaks?? - # f"{use_cuda} make all -j{psutil.cpu_count()*2} -C llama.cpp", - f"make all -j{psutil.cpu_count()*2} -C llama.cpp", + # f"{use_cuda} make all -j{(psutil.cpu_count() or 1)*2} -C llama.cpp", + f"make all -j{(psutil.cpu_count() or 1)*2} -C llama.cpp", ] if try_execute(commands) == "CMAKE": # Instead use CMAKE commands = [ f"cmake llama.cpp -B llama.cpp/build -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=OFF {CURL_FLAG}", - f"cmake --build llama.cpp/build --config Release -j{psutil.cpu_count()*2} --clean-first --target {' '.join(LLAMA_CPP_TARGETS)}", + f"cmake --build llama.cpp/build --config Release -j{(psutil.cpu_count() or 1)*2} --clean-first --target {' '.join(LLAMA_CPP_TARGETS)}", "cp llama.cpp/build/bin/llama-* llama.cpp", "rm -rf llama.cpp/build", ] From eddc4a875473625960b81a652f6e5ca54b886189 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Sun, 4 Jan 2026 13:03:06 +0000 Subject: [PATCH 104/167] Respect user quantization_config --- unsloth/models/loader.py | 152 ++++++++++++++++++++++++++++++--------- unsloth/models/vision.py | 7 +- 2 files changed, 122 insertions(+), 37 deletions(-) diff --git a/unsloth/models/loader.py b/unsloth/models/loader.py index 247c72f43f..645c23d50b 100644 --- a/unsloth/models/loader.py +++ b/unsloth/models/loader.py @@ -151,8 +151,41 @@ class FastLanguageModel(FastLlamaModel): *args, **kwargs, ): + # Respect user-provided quantization_config (e.g. BitsAndBytesConfig) + quantization_config = kwargs.get("quantization_config", None) + if quantization_config is not None: + if getattr(quantization_config, "load_in_4bit", False): + load_in_4bit = True + load_in_8bit = False + if getattr(quantization_config, "load_in_8bit", False): + load_in_8bit = True + load_in_4bit = False + + load_in_4bit_kwargs = load_in_4bit + load_in_8bit_kwargs = load_in_8bit + if quantization_config is not None: + load_in_4bit_kwargs = False + load_in_8bit_kwargs = False + # Login to allow private models token = hf_login(token) + # Align dtype with bnb_4bit_compute_dtype if provided and dtype is unset. + if dtype is None and quantization_config is not None: + bnb_compute_dtype = None + if isinstance(quantization_config, dict): + if quantization_config.get("load_in_4bit", False): + bnb_compute_dtype = quantization_config.get( + "bnb_4bit_compute_dtype", None + ) + else: + if getattr(quantization_config, "load_in_4bit", False): + bnb_compute_dtype = getattr( + quantization_config, "bnb_4bit_compute_dtype", None + ) + if isinstance(bnb_compute_dtype, str): + bnb_compute_dtype = getattr(torch, bnb_compute_dtype, None) + if isinstance(bnb_compute_dtype, torch.dtype): + dtype = bnb_compute_dtype if load_in_8bit or full_finetuning or qat_scheme is not None: return FastModel.from_pretrained( model_name = model_name, @@ -546,7 +579,7 @@ class FastLanguageModel(FastLlamaModel): model_name = model_name, max_seq_length = max_seq_length, dtype = _get_dtype(dtype), - load_in_4bit = load_in_4bit, + load_in_4bit = load_in_4bit_kwargs, token = token, device_map = device_map, rope_scaling = rope_scaling, @@ -583,22 +616,30 @@ class FastLanguageModel(FastLlamaModel): ) if load_in_4bit: - # Fix up bitsandbytes config - compute_dtype = dtype_from_config(model.config) - quantization_config = { - # Sometimes compute_dtype is not a string!! - "bnb_4bit_compute_dtype": compute_dtype, - "bnb_4bit_quant_type": "nf4", - "bnb_4bit_use_double_quant": True, - "llm_int8_enable_fp32_cpu_offload": False, - "llm_int8_has_fp16_weight": False, - "llm_int8_skip_modules": None, - "llm_int8_threshold": 6.0, - "load_in_4bit": True, - "load_in_8bit": False, - "quant_method": "bitsandbytes", - } - model.config.update({"quantization_config": quantization_config}) + # Fix up bitsandbytes config, but respect user-provided quantization_config + if quantization_config is None: + compute_dtype = dtype_from_config(model.config) + quantization_config = { + # Sometimes compute_dtype is not a string!! + "bnb_4bit_compute_dtype": compute_dtype, + "bnb_4bit_quant_type": "nf4", + "bnb_4bit_use_double_quant": True, + "llm_int8_enable_fp32_cpu_offload": False, + "llm_int8_has_fp16_weight": False, + "llm_int8_skip_modules": None, + "llm_int8_threshold": 6.0, + "load_in_4bit": True, + "load_in_8bit": False, + "quant_method": "bitsandbytes", + } + model.config.update({"quantization_config": quantization_config}) + else: + if hasattr(quantization_config, "to_dict"): + model.config.update( + {"quantization_config": quantization_config.to_dict()} + ) + elif isinstance(quantization_config, dict): + model.config.update({"quantization_config": quantization_config}) if load_in_fp8 != False: _tag_model_with_fp8_torchao_config(model, fp8_mode) @@ -690,12 +731,45 @@ class FastModel(FastBaseModel): *args, **kwargs, ): + # Respect user-provided quantization_config (e.g. BitsAndBytesConfig) + quantization_config = kwargs.get("quantization_config", None) + if quantization_config is not None: + if getattr(quantization_config, "load_in_4bit", False): + load_in_4bit = True + load_in_8bit = False + if getattr(quantization_config, "load_in_8bit", False): + load_in_8bit = True + load_in_4bit = False + + load_in_4bit_kwargs = load_in_4bit + load_in_8bit_kwargs = load_in_8bit + if quantization_config is not None: + load_in_4bit_kwargs = False + load_in_8bit_kwargs = False + # Login to allow private models token = hf_login(token) if whisper_language is not None: assert type(whisper_language) is str if whisper_task is not None: assert type(whisper_task) is str + # Align dtype with bnb_4bit_compute_dtype if provided and dtype is unset. + if dtype is None and quantization_config is not None: + bnb_compute_dtype = None + if isinstance(quantization_config, dict): + if quantization_config.get("load_in_4bit", False): + bnb_compute_dtype = quantization_config.get( + "bnb_4bit_compute_dtype", None + ) + else: + if getattr(quantization_config, "load_in_4bit", False): + bnb_compute_dtype = getattr( + quantization_config, "bnb_4bit_compute_dtype", None + ) + if isinstance(bnb_compute_dtype, str): + bnb_compute_dtype = getattr(torch, bnb_compute_dtype, None) + if isinstance(bnb_compute_dtype, torch.dtype): + dtype = bnb_compute_dtype SUPPORTS_BFLOAT16 = is_bfloat16_supported() if dtype is None: dtype = torch.float16 if not SUPPORTS_BFLOAT16 else torch.bfloat16 @@ -1173,8 +1247,8 @@ class FastModel(FastBaseModel): model_name = model_name, max_seq_length = max_seq_length, dtype = _get_dtype(dtype), - load_in_4bit = load_in_4bit, - load_in_8bit = load_in_8bit, + load_in_4bit = load_in_4bit_kwargs, + load_in_8bit = load_in_8bit_kwargs, load_in_16bit = load_in_16bit, full_finetuning = full_finetuning, token = token, @@ -1220,22 +1294,30 @@ class FastModel(FastBaseModel): ) if load_in_4bit: - # Fix up bitsandbytes config - compute_dtype = dtype_from_config(model.config) - quantization_config = { - # Sometimes compute_dtype is not a string!! - "bnb_4bit_compute_dtype": compute_dtype, - "bnb_4bit_quant_type": "nf4", - "bnb_4bit_use_double_quant": True, - "llm_int8_enable_fp32_cpu_offload": False, - "llm_int8_has_fp16_weight": False, - "llm_int8_skip_modules": None, - "llm_int8_threshold": 6.0, - "load_in_4bit": True, - "load_in_8bit": False, - "quant_method": "bitsandbytes", - } - model.config.update({"quantization_config": quantization_config}) + # Fix up bitsandbytes config, but respect user-provided quantization_config + if quantization_config is None: + compute_dtype = dtype_from_config(model.config) + quantization_config = { + # Sometimes compute_dtype is not a string!! + "bnb_4bit_compute_dtype": compute_dtype, + "bnb_4bit_quant_type": "nf4", + "bnb_4bit_use_double_quant": True, + "llm_int8_enable_fp32_cpu_offload": False, + "llm_int8_has_fp16_weight": False, + "llm_int8_skip_modules": None, + "llm_int8_threshold": 6.0, + "load_in_4bit": True, + "load_in_8bit": False, + "quant_method": "bitsandbytes", + } + model.config.update({"quantization_config": quantization_config}) + else: + if hasattr(quantization_config, "to_dict"): + model.config.update( + {"quantization_config": quantization_config.to_dict()} + ) + elif isinstance(quantization_config, dict): + model.config.update({"quantization_config": quantization_config}) if load_in_fp8 != False: _tag_model_with_fp8_torchao_config(model, fp8_mode) diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index 6c5356e0b9..6de942d7d2 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -529,6 +529,7 @@ class FastBaseModel: del kwargs["attn_implementation"] bnb_config = None + user_quantization_config = kwargs.get("quantization_config", None) if full_finetuning and (load_in_4bit or load_in_8bit): print( "Unsloth: You selected full finetuning support, but 4bit / 8bit is enabled - disabling LoRA / QLoRA." @@ -596,7 +597,8 @@ class FastBaseModel: ): pass else: - kwargs["quantization_config"] = bnb_config + if user_quantization_config is None: + kwargs["quantization_config"] = bnb_config else: if auto_config is None: auto_config = AutoConfig.from_pretrained( @@ -641,7 +643,8 @@ class FastBaseModel: ) except: pass - kwargs["quantization_config"] = quantization_config + if user_quantization_config is None: + kwargs["quantization_config"] = quantization_config # Check if using forced float32 - we load it in bfloat16, then cast to float16! torch_dtype = dtype From f1b320ea344f374d63535cc34f7e4a910ffe1d5f Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Sun, 4 Jan 2026 13:14:03 +0000 Subject: [PATCH 105/167] Handle dict quantization_config flags --- unsloth/models/loader.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/unsloth/models/loader.py b/unsloth/models/loader.py index 645c23d50b..4488ad9a07 100644 --- a/unsloth/models/loader.py +++ b/unsloth/models/loader.py @@ -154,10 +154,16 @@ class FastLanguageModel(FastLlamaModel): # Respect user-provided quantization_config (e.g. BitsAndBytesConfig) quantization_config = kwargs.get("quantization_config", None) if quantization_config is not None: - if getattr(quantization_config, "load_in_4bit", False): + if isinstance(quantization_config, dict): + q_load_in_4bit = quantization_config.get("load_in_4bit", False) + q_load_in_8bit = quantization_config.get("load_in_8bit", False) + else: + q_load_in_4bit = getattr(quantization_config, "load_in_4bit", False) + q_load_in_8bit = getattr(quantization_config, "load_in_8bit", False) + if q_load_in_4bit: load_in_4bit = True load_in_8bit = False - if getattr(quantization_config, "load_in_8bit", False): + if q_load_in_8bit: load_in_8bit = True load_in_4bit = False @@ -734,10 +740,16 @@ class FastModel(FastBaseModel): # Respect user-provided quantization_config (e.g. BitsAndBytesConfig) quantization_config = kwargs.get("quantization_config", None) if quantization_config is not None: - if getattr(quantization_config, "load_in_4bit", False): + if isinstance(quantization_config, dict): + q_load_in_4bit = quantization_config.get("load_in_4bit", False) + q_load_in_8bit = quantization_config.get("load_in_8bit", False) + else: + q_load_in_4bit = getattr(quantization_config, "load_in_4bit", False) + q_load_in_8bit = getattr(quantization_config, "load_in_8bit", False) + if q_load_in_4bit: load_in_4bit = True load_in_8bit = False - if getattr(quantization_config, "load_in_8bit", False): + if q_load_in_8bit: load_in_8bit = True load_in_4bit = False From f01872b61db8496a00f051feb5abd57038eee5b2 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Sun, 4 Jan 2026 13:18:15 +0000 Subject: [PATCH 106/167] Keep 4bit flag for fast_inference --- unsloth/models/loader.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/unsloth/models/loader.py b/unsloth/models/loader.py index 4488ad9a07..eb3b21e206 100644 --- a/unsloth/models/loader.py +++ b/unsloth/models/loader.py @@ -167,12 +167,6 @@ class FastLanguageModel(FastLlamaModel): load_in_8bit = True load_in_4bit = False - load_in_4bit_kwargs = load_in_4bit - load_in_8bit_kwargs = load_in_8bit - if quantization_config is not None: - load_in_4bit_kwargs = False - load_in_8bit_kwargs = False - # Login to allow private models token = hf_login(token) # Align dtype with bnb_4bit_compute_dtype if provided and dtype is unset. @@ -581,6 +575,12 @@ class FastLanguageModel(FastLlamaModel): if fast_inference: fast_inference, model_name = fast_inference_setup(model_name, model_config) + load_in_4bit_kwargs = load_in_4bit + load_in_8bit_kwargs = load_in_8bit + if quantization_config is not None and not fast_inference: + load_in_4bit_kwargs = False + load_in_8bit_kwargs = False + model, tokenizer = dispatch_model.from_pretrained( model_name = model_name, max_seq_length = max_seq_length, @@ -753,12 +753,6 @@ class FastModel(FastBaseModel): load_in_8bit = True load_in_4bit = False - load_in_4bit_kwargs = load_in_4bit - load_in_8bit_kwargs = load_in_8bit - if quantization_config is not None: - load_in_4bit_kwargs = False - load_in_8bit_kwargs = False - # Login to allow private models token = hf_login(token) if whisper_language is not None: @@ -1255,6 +1249,12 @@ class FastModel(FastBaseModel): if auto_model is None: auto_model = AutoModelForVision2Seq if is_vlm else AutoModelForCausalLM + load_in_4bit_kwargs = load_in_4bit + load_in_8bit_kwargs = load_in_8bit + if quantization_config is not None and not fast_inference: + load_in_4bit_kwargs = False + load_in_8bit_kwargs = False + model, tokenizer = FastBaseModel.from_pretrained( model_name = model_name, max_seq_length = max_seq_length, From 85bfdaf7ab3c32f95f98f3e3927164797fcc6d46 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 4 Jan 2026 06:12:44 -0800 Subject: [PATCH 107/167] Versioning --- pyproject.toml | 4 ++-- unsloth/__init__.py | 2 +- unsloth/models/_utils.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 20e3fd847f..e7b84f3c8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ huggingfacenotorch = [ ] huggingface = [ "unsloth[huggingfacenotorch]", - "unsloth_zoo>=2025.12.8", + "unsloth_zoo>=2026.1.1", "torchvision", "unsloth[triton]", ] @@ -523,7 +523,7 @@ colab-ampere-torch220 = [ "flash-attn>=2.6.3 ; ('linux' in sys_platform)", ] colab-new = [ - "unsloth_zoo>=2025.12.8", + "unsloth_zoo>=2026.1.1", "packaging", "tyro", "transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,<=4.57.3", diff --git a/unsloth/__init__.py b/unsloth/__init__.py index c74b248a83..d9633e8ec1 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -79,7 +79,7 @@ from importlib.metadata import PackageNotFoundError # Check for unsloth_zoo try: unsloth_zoo_version = importlib_version("unsloth_zoo") - if Version(unsloth_zoo_version) < Version("2025.12.4"): + if Version(unsloth_zoo_version) < Version("2026.1.1"): print( "Unsloth: Please update Unsloth and Unsloth-Zoo to the latest version!\n" "Do this via `pip install --upgrade --force-reinstall --no-cache-dir --no-deps unsloth unsloth_zoo`" diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 545ba4794a..5952d4af0c 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2025.12.10" +__version__ = "2026.1.1" __all__ = [ "SUPPORTS_BFLOAT16", From f58696c5adac349e61bf69d8d099bb56d6dca627 Mon Sep 17 00:00:00 2001 From: Kaitao Yang Date: Sun, 4 Jan 2026 09:21:44 -0800 Subject: [PATCH 108/167] remove unused variable BlockDiagonalCausalMask --- unsloth/utils/attention_dispatch.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/unsloth/utils/attention_dispatch.py b/unsloth/utils/attention_dispatch.py index 0e5f3c1951..a7620549be 100644 --- a/unsloth/utils/attention_dispatch.py +++ b/unsloth/utils/attention_dispatch.py @@ -32,9 +32,6 @@ from ..utils.packing import ( if HAS_FLASH_ATTENTION: from flash_attn import flash_attn_func, flash_attn_varlen_func HAS_XFORMERS = xformers is not None -BlockDiagonalCausalMask = None -if HAS_XFORMERS: - BlockDiagonalCausalMask = xformers.attn_bias.BlockDiagonalCausalMask SDPA_HAS_GQA = "enable_gqa" in (scaled_dot_product_attention.__doc__ or "") FLASH_VARLEN = "flash_varlen" From cf64ea1daf58f1d3abfc27bf5ed73b1d1b947ee7 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 5 Jan 2026 05:02:53 +0000 Subject: [PATCH 109/167] Fix vLLM PDL bug on Blackwell GPUs (B200/B100) vLLM's LoRA Triton kernels use tl.extra.cuda.gdc_wait() for PDL optimization on SM90+ GPUs. This fails on SM100 (Blackwell) during CUDA graph capture because Triton's pipeliner cannot handle gdc_wait in complex kernels. This fix: - Detects SM100 GPUs and applies the workaround automatically - Sets TRITON_DISABLE_PDL=1 environment variable - Monkey-patches supports_pdl to return False in lora_expand_op and lora_shrink_op - Checks GitHub issue #30872 status (with 3s timeout) to auto-disable the workaround once the upstream fix is merged - Includes quick internet connectivity check (0.5s) to avoid delays when offline Fixes the error: 'tt.elementwise_inline_asm' op pipeliner doesn't know how to predicate this op LLVM ERROR: Fatal pipeliner error See: https://github.com/vllm-project/vllm/issues/30872 --- unsloth/__init__.py | 3 + unsloth/import_fixes.py | 121 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/unsloth/__init__.py b/unsloth/__init__.py index d9633e8ec1..86fb00fe0e 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -126,6 +126,7 @@ from .import_fixes import ( fix_xformers_performance_issue, fix_vllm_aimv2_issue, fix_vllm_guided_decoding_params, + fix_vllm_pdl_blackwell, ignore_logger_messages, patch_ipykernel_hf_xet, patch_trackio, @@ -138,6 +139,7 @@ from .import_fixes import ( fix_xformers_performance_issue() fix_vllm_aimv2_issue() fix_vllm_guided_decoding_params() +fix_vllm_pdl_blackwell() ignore_logger_messages() patch_ipykernel_hf_xet() patch_trackio() @@ -149,6 +151,7 @@ fix_executorch() del fix_xformers_performance_issue del fix_vllm_aimv2_issue del fix_vllm_guided_decoding_params +del fix_vllm_pdl_blackwell del ignore_logger_messages del patch_ipykernel_hf_xet del patch_trackio diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index bb6996a3e3..91ba35e21e 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -556,3 +556,124 @@ def fix_huggingface_hub(): huggingface_hub.is_offline_mode = ( lambda: huggingface_hub.constants.HF_HUB_OFFLINE ) + + +def fix_vllm_pdl_blackwell(): + """ + Fix vLLM PDL (Programmatic Dependent Launch) bug on Blackwell GPUs (SM100). + + The issue: vLLM's LoRA Triton kernels use tl.extra.cuda.gdc_wait() for PDL + optimization on SM90+ GPUs. This fails on SM100 (B200/B100) during CUDA graph + capture because Triton's pipeliner can't handle gdc_wait in complex kernels. + + See: https://github.com/vllm-project/vllm/issues/30872 + """ + if importlib.util.find_spec("vllm") is None: + return + + # Check if we have a CUDA GPU + try: + import torch + if not torch.cuda.is_available(): + return + major, minor = torch.cuda.get_device_capability() + except Exception: + return + + # Only SM100 (Blackwell) is affected - SM90 (Hopper) works fine + if major != 10: + return + + gpu_name = torch.cuda.get_device_name() + + # Check if vLLM has the PDL-related modules before doing internet check + try: + has_expand_op = importlib.util.find_spec("vllm.lora.ops.triton_ops.lora_expand_op") is not None + except (ModuleNotFoundError, ValueError): + has_expand_op = False + try: + has_shrink_op = importlib.util.find_spec("vllm.lora.ops.triton_ops.lora_shrink_op") is not None + except (ModuleNotFoundError, ValueError): + has_shrink_op = False + if not has_expand_op and not has_shrink_op: + # Old vLLM version without PDL support - just set env var to be safe + os.environ["TRITON_DISABLE_PDL"] = "1" + logger.info( + f"Unsloth: Set TRITON_DISABLE_PDL=1 for SM{major}{minor} ({gpu_name}) - " + f"vLLM PDL modules not found" + ) + return + + # Check if GitHub issue is closed (fix merged upstream) + issue_closed = False + try: + import socket + import urllib.request + import json as json_module + + # Quick internet connectivity check (0.5s timeout) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(0.5) + try: + sock.connect(("api.github.com", 443)) + has_internet = True + except (socket.timeout, OSError): + has_internet = False + finally: + sock.close() + + if has_internet: + api_url = "https://api.github.com/repos/vllm-project/vllm/issues/30872" + req = urllib.request.Request( + api_url, + headers={ + "User-Agent": "Unsloth-PDL-Fix", + "Accept": "application/vnd.github.v3+json", + } + ) + with urllib.request.urlopen(req, timeout=3) as response: + data = json_module.loads(response.read().decode()) + issue_closed = data.get("state") == "closed" + except Exception: + # If we can't check, assume issue is still open (apply fix to be safe) + pass + + if issue_closed: + logger.info( + f"Unsloth: SM{major}{minor} ({gpu_name}) detected but PDL issue #30872 " + f"is closed - skipping PDL fix" + ) + return + + # Apply the PDL fix + os.environ["TRITON_DISABLE_PDL"] = "1" + + def fake_supports_pdl(device=None): + return False + + patched = [] + + try: + import vllm.lora.ops.triton_ops.lora_expand_op as expand_op + expand_op.supports_pdl = fake_supports_pdl + patched.append("lora_expand_op") + except (ImportError, ModuleNotFoundError, AttributeError): + pass + + try: + import vllm.lora.ops.triton_ops.lora_shrink_op as shrink_op + shrink_op.supports_pdl = fake_supports_pdl + patched.append("lora_shrink_op") + except (ImportError, ModuleNotFoundError, AttributeError): + pass + + if patched: + logger.info( + f"Unsloth: Applied PDL fix for SM{major}{minor} ({gpu_name}) - " + f"patched: {', '.join(patched)}" + ) + else: + # Just set the env var - vLLM might be an older version without supports_pdl + logger.info( + f"Unsloth: Set TRITON_DISABLE_PDL=1 for SM{major}{minor} ({gpu_name})" + ) From a031e7ec4fbf35e52dd21e556198463e840aa46e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 05:03:28 +0000 Subject: [PATCH 110/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/import_fixes.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 91ba35e21e..e8c5e16df4 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -574,6 +574,7 @@ def fix_vllm_pdl_blackwell(): # Check if we have a CUDA GPU try: import torch + if not torch.cuda.is_available(): return major, minor = torch.cuda.get_device_capability() @@ -588,11 +589,17 @@ def fix_vllm_pdl_blackwell(): # Check if vLLM has the PDL-related modules before doing internet check try: - has_expand_op = importlib.util.find_spec("vllm.lora.ops.triton_ops.lora_expand_op") is not None + has_expand_op = ( + importlib.util.find_spec("vllm.lora.ops.triton_ops.lora_expand_op") + is not None + ) except (ModuleNotFoundError, ValueError): has_expand_op = False try: - has_shrink_op = importlib.util.find_spec("vllm.lora.ops.triton_ops.lora_shrink_op") is not None + has_shrink_op = ( + importlib.util.find_spec("vllm.lora.ops.triton_ops.lora_shrink_op") + is not None + ) except (ModuleNotFoundError, ValueError): has_shrink_op = False if not has_expand_op and not has_shrink_op: @@ -626,12 +633,12 @@ def fix_vllm_pdl_blackwell(): api_url = "https://api.github.com/repos/vllm-project/vllm/issues/30872" req = urllib.request.Request( api_url, - headers={ + headers = { "User-Agent": "Unsloth-PDL-Fix", "Accept": "application/vnd.github.v3+json", - } + }, ) - with urllib.request.urlopen(req, timeout=3) as response: + with urllib.request.urlopen(req, timeout = 3) as response: data = json_module.loads(response.read().decode()) issue_closed = data.get("state") == "closed" except Exception: @@ -648,13 +655,14 @@ def fix_vllm_pdl_blackwell(): # Apply the PDL fix os.environ["TRITON_DISABLE_PDL"] = "1" - def fake_supports_pdl(device=None): + def fake_supports_pdl(device = None): return False patched = [] try: import vllm.lora.ops.triton_ops.lora_expand_op as expand_op + expand_op.supports_pdl = fake_supports_pdl patched.append("lora_expand_op") except (ImportError, ModuleNotFoundError, AttributeError): @@ -662,6 +670,7 @@ def fix_vllm_pdl_blackwell(): try: import vllm.lora.ops.triton_ops.lora_shrink_op as shrink_op + shrink_op.supports_pdl = fake_supports_pdl patched.append("lora_shrink_op") except (ImportError, ModuleNotFoundError, AttributeError): From 3e1ceff3078ab25b84217e25cc91474cd608e3a4 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 5 Jan 2026 05:03:56 +0000 Subject: [PATCH 111/167] Sync chat_template from tokenizer to vLLM When using base models with custom chat templates applied after loading, vLLM's internal tokenizer may not have the chat_template set. This causes issues during RL training with vLLM inference. This fix syncs the chat_template from the processing_class (the tokenizer you loaded and configured) to vLLM's internal tokenizer during trainer initialization, but only if vLLM's tokenizer does not already have one set. --- unsloth/models/rl.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index 88aeeda8a1..20dafaaaa4 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -694,6 +694,20 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): ) RLTrainer_post += training_check + # Sync chat_template from processing_class to vLLM's tokenizer + # This fixes base models that have custom chat templates applied after loading + if "model" in call_args: + vllm_chat_template_sync = ( + "if hasattr(self, 'llm') and self.llm is not None and hasattr(self.llm, 'get_tokenizer'):\n" + " _vllm_tok = self.llm.get_tokenizer()\n" + " _pc = getattr(self, 'processing_class', None)\n" + " if _pc is not None and getattr(_pc, 'chat_template', None) is not None:\n" + " if _vllm_tok.chat_template is None:\n" + " _vllm_tok.chat_template = _pc.chat_template\n" + "pass\n" + ) + RLTrainer_post += vllm_chat_template_sync + # Edit optional metrics other_metrics_processor = "" if trainer_file in RL_METRICS_CHANGES: From abaee73d745cbcbbbef1b5279f6b1c85a9ec693f Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 5 Jan 2026 05:10:24 +0000 Subject: [PATCH 112/167] Add tokenizer fallback for chat_template sync --- unsloth/models/rl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index 20dafaaaa4..b75ae383db 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -700,7 +700,7 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): vllm_chat_template_sync = ( "if hasattr(self, 'llm') and self.llm is not None and hasattr(self.llm, 'get_tokenizer'):\n" " _vllm_tok = self.llm.get_tokenizer()\n" - " _pc = getattr(self, 'processing_class', None)\n" + " _pc = getattr(self, 'processing_class', None) or getattr(self, 'tokenizer', None)\n" " if _pc is not None and getattr(_pc, 'chat_template', None) is not None:\n" " if _vllm_tok.chat_template is None:\n" " _vllm_tok.chat_template = _pc.chat_template\n" From b0c3894f5134ca1d17b16b08c6d5d218d9e655eb Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 5 Jan 2026 05:24:52 +0000 Subject: [PATCH 113/167] Address review feedback: refactor and scan all GPUs - Add _spec_exists helper function to reduce duplication - Scan all GPUs for SM100 instead of just device 0 - Use loop for module patching to improve maintainability --- unsloth/import_fixes.py | 85 ++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index e8c5e16df4..7d368d027a 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -571,42 +571,44 @@ def fix_vllm_pdl_blackwell(): if importlib.util.find_spec("vllm") is None: return - # Check if we have a CUDA GPU + # Check if any CUDA GPU is SM100 (Blackwell) try: import torch if not torch.cuda.is_available(): return - major, minor = torch.cuda.get_device_capability() + + # Scan all GPUs for SM100 - fix applies globally via env var and monkey-patch + has_sm100 = False + sm100_gpu_name = None + for i in range(torch.cuda.device_count()): + major, minor = torch.cuda.get_device_capability(i) + if major == 10: + has_sm100 = True + sm100_gpu_name = torch.cuda.get_device_name(i) + break + + if not has_sm100: + return except Exception: return - # Only SM100 (Blackwell) is affected - SM90 (Hopper) works fine - if major != 10: - return - - gpu_name = torch.cuda.get_device_name() + # Helper to check if module spec exists + def _spec_exists(name): + try: + return importlib.util.find_spec(name) is not None + except (ModuleNotFoundError, ValueError): + return False # Check if vLLM has the PDL-related modules before doing internet check - try: - has_expand_op = ( - importlib.util.find_spec("vllm.lora.ops.triton_ops.lora_expand_op") - is not None - ) - except (ModuleNotFoundError, ValueError): - has_expand_op = False - try: - has_shrink_op = ( - importlib.util.find_spec("vllm.lora.ops.triton_ops.lora_shrink_op") - is not None - ) - except (ModuleNotFoundError, ValueError): - has_shrink_op = False + has_expand_op = _spec_exists("vllm.lora.ops.triton_ops.lora_expand_op") + has_shrink_op = _spec_exists("vllm.lora.ops.triton_ops.lora_shrink_op") + if not has_expand_op and not has_shrink_op: # Old vLLM version without PDL support - just set env var to be safe os.environ["TRITON_DISABLE_PDL"] = "1" logger.info( - f"Unsloth: Set TRITON_DISABLE_PDL=1 for SM{major}{minor} ({gpu_name}) - " + f"Unsloth: Set TRITON_DISABLE_PDL=1 for SM100 ({sm100_gpu_name}) - " f"vLLM PDL modules not found" ) return @@ -633,12 +635,12 @@ def fix_vllm_pdl_blackwell(): api_url = "https://api.github.com/repos/vllm-project/vllm/issues/30872" req = urllib.request.Request( api_url, - headers = { + headers={ "User-Agent": "Unsloth-PDL-Fix", "Accept": "application/vnd.github.v3+json", }, ) - with urllib.request.urlopen(req, timeout = 3) as response: + with urllib.request.urlopen(req, timeout=3) as response: data = json_module.loads(response.read().decode()) issue_closed = data.get("state") == "closed" except Exception: @@ -647,7 +649,7 @@ def fix_vllm_pdl_blackwell(): if issue_closed: logger.info( - f"Unsloth: SM{major}{minor} ({gpu_name}) detected but PDL issue #30872 " + f"Unsloth: SM100 ({sm100_gpu_name}) detected but PDL issue #30872 " f"is closed - skipping PDL fix" ) return @@ -655,34 +657,29 @@ def fix_vllm_pdl_blackwell(): # Apply the PDL fix os.environ["TRITON_DISABLE_PDL"] = "1" - def fake_supports_pdl(device = None): + def fake_supports_pdl(device=None): return False patched = [] - - try: - import vllm.lora.ops.triton_ops.lora_expand_op as expand_op - - expand_op.supports_pdl = fake_supports_pdl - patched.append("lora_expand_op") - except (ImportError, ModuleNotFoundError, AttributeError): - pass - - try: - import vllm.lora.ops.triton_ops.lora_shrink_op as shrink_op - - shrink_op.supports_pdl = fake_supports_pdl - patched.append("lora_shrink_op") - except (ImportError, ModuleNotFoundError, AttributeError): - pass + modules_to_patch = { + "lora_expand_op": "vllm.lora.ops.triton_ops.lora_expand_op", + "lora_shrink_op": "vllm.lora.ops.triton_ops.lora_shrink_op", + } + for name, path in modules_to_patch.items(): + try: + module = importlib.import_module(path) + module.supports_pdl = fake_supports_pdl + patched.append(name) + except (ImportError, ModuleNotFoundError, AttributeError): + pass if patched: logger.info( - f"Unsloth: Applied PDL fix for SM{major}{minor} ({gpu_name}) - " + f"Unsloth: Applied PDL fix for SM100 ({sm100_gpu_name}) - " f"patched: {', '.join(patched)}" ) else: # Just set the env var - vLLM might be an older version without supports_pdl logger.info( - f"Unsloth: Set TRITON_DISABLE_PDL=1 for SM{major}{minor} ({gpu_name})" + f"Unsloth: Set TRITON_DISABLE_PDL=1 for SM100 ({sm100_gpu_name})" ) From c009a4b9665b1057c94f4d76f7d67f24f9718ed4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 05:24:59 +0000 Subject: [PATCH 114/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/import_fixes.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 7d368d027a..77693d4cf3 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -635,12 +635,12 @@ def fix_vllm_pdl_blackwell(): api_url = "https://api.github.com/repos/vllm-project/vllm/issues/30872" req = urllib.request.Request( api_url, - headers={ + headers = { "User-Agent": "Unsloth-PDL-Fix", "Accept": "application/vnd.github.v3+json", }, ) - with urllib.request.urlopen(req, timeout=3) as response: + with urllib.request.urlopen(req, timeout = 3) as response: data = json_module.loads(response.read().decode()) issue_closed = data.get("state") == "closed" except Exception: @@ -657,7 +657,7 @@ def fix_vllm_pdl_blackwell(): # Apply the PDL fix os.environ["TRITON_DISABLE_PDL"] = "1" - def fake_supports_pdl(device=None): + def fake_supports_pdl(device = None): return False patched = [] @@ -680,6 +680,4 @@ def fix_vllm_pdl_blackwell(): ) else: # Just set the env var - vLLM might be an older version without supports_pdl - logger.info( - f"Unsloth: Set TRITON_DISABLE_PDL=1 for SM100 ({sm100_gpu_name})" - ) + logger.info(f"Unsloth: Set TRITON_DISABLE_PDL=1 for SM100 ({sm100_gpu_name})") From a5200296362bb66f3a8b953a8b40375b8f4eec74 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 5 Jan 2026 05:25:53 +0000 Subject: [PATCH 115/167] Combine nested if statements for clarity --- unsloth/models/rl.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index b75ae383db..e1ecd6df2f 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -701,9 +701,8 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): "if hasattr(self, 'llm') and self.llm is not None and hasattr(self.llm, 'get_tokenizer'):\n" " _vllm_tok = self.llm.get_tokenizer()\n" " _pc = getattr(self, 'processing_class', None) or getattr(self, 'tokenizer', None)\n" - " if _pc is not None and getattr(_pc, 'chat_template', None) is not None:\n" - " if _vllm_tok.chat_template is None:\n" - " _vllm_tok.chat_template = _pc.chat_template\n" + " if _pc is not None and getattr(_pc, 'chat_template', None) is not None and _vllm_tok.chat_template is None:\n" + " _vllm_tok.chat_template = _pc.chat_template\n" "pass\n" ) RLTrainer_post += vllm_chat_template_sync From f469e76c65a60f4a967d7ca92e52c5f04696285a Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 5 Jan 2026 06:53:42 +0000 Subject: [PATCH 116/167] Fix PDL patch: target utils.py source module and clear lru_cache - Patch vllm.lora.ops.triton_ops.utils directly where supports_pdl is defined - Clear lru_cache before patching to prevent stale cached results - Add fused_moe_lora_op to consumer modules list - Use *args, **kwargs in fake function for compatibility --- unsloth/import_fixes.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 77693d4cf3..469674b29e 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -601,10 +601,11 @@ def fix_vllm_pdl_blackwell(): return False # Check if vLLM has the PDL-related modules before doing internet check + has_utils = _spec_exists("vllm.lora.ops.triton_ops.utils") has_expand_op = _spec_exists("vllm.lora.ops.triton_ops.lora_expand_op") has_shrink_op = _spec_exists("vllm.lora.ops.triton_ops.lora_shrink_op") - if not has_expand_op and not has_shrink_op: + if not has_utils and not has_expand_op and not has_shrink_op: # Old vLLM version without PDL support - just set env var to be safe os.environ["TRITON_DISABLE_PDL"] = "1" logger.info( @@ -657,19 +658,39 @@ def fix_vllm_pdl_blackwell(): # Apply the PDL fix os.environ["TRITON_DISABLE_PDL"] = "1" - def fake_supports_pdl(device = None): + def fake_supports_pdl(*args, **kwargs): return False patched = [] - modules_to_patch = { + + # First, patch the source module (utils.py) where supports_pdl is defined. + # This is critical because supports_pdl uses @lru_cache - we must clear the + # cache to prevent stale cached results from the original function. + try: + utils_module = importlib.import_module("vllm.lora.ops.triton_ops.utils") + if hasattr(utils_module, "supports_pdl"): + original_fn = utils_module.supports_pdl + if hasattr(original_fn, "cache_clear"): + original_fn.cache_clear() + utils_module.supports_pdl = fake_supports_pdl + patched.append("utils") + except (ImportError, ModuleNotFoundError, AttributeError): + pass + + # Also patch the consumer modules that import supports_pdl from utils. + # This ensures the patched function is used even if the module was already + # imported before this fix runs. + consumer_modules = { "lora_expand_op": "vllm.lora.ops.triton_ops.lora_expand_op", "lora_shrink_op": "vllm.lora.ops.triton_ops.lora_shrink_op", + "fused_moe_lora_op": "vllm.lora.ops.triton_ops.fused_moe_lora_op", } - for name, path in modules_to_patch.items(): + for name, path in consumer_modules.items(): try: module = importlib.import_module(path) - module.supports_pdl = fake_supports_pdl - patched.append(name) + if hasattr(module, "supports_pdl"): + module.supports_pdl = fake_supports_pdl + patched.append(name) except (ImportError, ModuleNotFoundError, AttributeError): pass From c81d9605e2e9ff38d64340e74b4820145f6238a6 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Mon, 5 Jan 2026 07:02:36 +0000 Subject: [PATCH 117/167] Improve TRL compatibility and GRPO state restore --- unsloth/kernels/cross_entropy_loss.py | 2 +- unsloth/models/cohere.py | 4 +- unsloth/models/gemma.py | 4 +- unsloth/models/gemma2.py | 4 +- unsloth/models/granite.py | 4 +- unsloth/models/loader_utils.py | 1 - unsloth/models/rl.py | 65 ++++++++++++++++++++++++--- unsloth/trainer.py | 4 +- 8 files changed, 67 insertions(+), 21 deletions(-) diff --git a/unsloth/kernels/cross_entropy_loss.py b/unsloth/kernels/cross_entropy_loss.py index 912e6f7e3f..fbb14013ff 100644 --- a/unsloth/kernels/cross_entropy_loss.py +++ b/unsloth/kernels/cross_entropy_loss.py @@ -24,7 +24,7 @@ from .utils import ( is_cdna, ) from transformers.models.llama.modeling_llama import logger -from packaging.version import Version +from unsloth_zoo.utils import Version from unsloth_zoo.loss_utils import ( patch_loss_functions as _patch_loss_functions, diff --git a/unsloth/models/cohere.py b/unsloth/models/cohere.py index e9f56763d6..c33317ee02 100644 --- a/unsloth/models/cohere.py +++ b/unsloth/models/cohere.py @@ -15,7 +15,7 @@ from .llama import * from ._utils import __version__ from unsloth_zoo.hf_utils import dtype_from_config -from unsloth_zoo.utils import _get_dtype +from unsloth_zoo.utils import _get_dtype, Version from ..utils.packing import get_packed_info_from_kwargs from ..utils.attention_dispatch import ( AttentionConfig, @@ -35,8 +35,6 @@ try: repeat_kv, ) except: - from packaging.version import Version - transformers_version = Version(transformers_version) if not transformers_version >= Version("4.42"): raise ImportError( diff --git a/unsloth/models/gemma.py b/unsloth/models/gemma.py index 291d442673..1789a9cd92 100644 --- a/unsloth/models/gemma.py +++ b/unsloth/models/gemma.py @@ -14,7 +14,7 @@ from .llama import * from ._utils import __version__ -from unsloth_zoo.utils import _get_dtype +from unsloth_zoo.utils import _get_dtype, Version from unsloth_zoo.hf_utils import dtype_from_config from ..utils.packing import ( build_sdpa_packed_attention_mask, @@ -34,8 +34,6 @@ try: repeat_kv, ) except: - from packaging.version import Version - transformers_version = Version(transformers_version) if not transformers_version >= Version("4.38"): raise ImportError( diff --git a/unsloth/models/gemma2.py b/unsloth/models/gemma2.py index 4b2503b8a1..16d04955d3 100644 --- a/unsloth/models/gemma2.py +++ b/unsloth/models/gemma2.py @@ -14,7 +14,7 @@ from .llama import * from ._utils import __version__ -from unsloth_zoo.utils import _get_dtype +from unsloth_zoo.utils import _get_dtype, Version from unsloth_zoo.hf_utils import dtype_from_config from ..utils.packing import get_packed_info_from_kwargs from ..utils.attention_dispatch import ( @@ -41,8 +41,6 @@ try: repeat_kv, ) except: - from packaging.version import Version - transformers_version = Version(transformers_version) if not transformers_version >= Version("4.42"): raise ImportError( diff --git a/unsloth/models/granite.py b/unsloth/models/granite.py index f85f1b641f..aae746aed1 100644 --- a/unsloth/models/granite.py +++ b/unsloth/models/granite.py @@ -15,7 +15,7 @@ from .llama import * import os from ._utils import __version__ -from unsloth_zoo.utils import _get_dtype +from unsloth_zoo.utils import _get_dtype, Version from unsloth_zoo.hf_utils import dtype_from_config from ..utils.packing import get_packed_info_from_kwargs from ..utils.attention_dispatch import ( @@ -41,8 +41,6 @@ try: GraniteForCausalLM, ) except: - from packaging.version import Version - transformers_version = Version(transformers_version) if not transformers_version >= Version("4.45.0"): raise ImportError( diff --git a/unsloth/models/loader_utils.py b/unsloth/models/loader_utils.py index 85332e1116..fe2a89d893 100644 --- a/unsloth/models/loader_utils.py +++ b/unsloth/models/loader_utils.py @@ -28,7 +28,6 @@ from .mapper import ( ) # https://github.com/huggingface/transformers/pull/26037 allows 4 bit loading! -from packaging.version import Version from transformers import __version__ as transformers_version from unsloth.models._utils import TorchAOConfig from unsloth_zoo.utils import Version diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index 88aeeda8a1..35a15d03a6 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -43,10 +43,28 @@ torch_compile_options = { "triton.cudagraphs": False, } -from trl import __version__ as trl_version +# vLLM compatibility shim (TRL expects GuidedDecodingParams even if vLLM doesn't provide it) +try: + import vllm.sampling_params as _unsloth_vllm_sp + if not hasattr(_unsloth_vllm_sp, "GuidedDecodingParams"): + class GuidedDecodingParams: + def __init__(self, **kwargs): + self.kwargs = kwargs + _unsloth_vllm_sp.GuidedDecodingParams = GuidedDecodingParams +except Exception: + pass + +from trl import __version__ as trl_version_raw +from importlib.metadata import version as importlib_version from unsloth_zoo.utils import Version -trl_version = Version(trl_version) +try: + trl_version = Version(trl_version_raw) +except Exception: + try: + trl_version = Version(importlib_version("trl")) + except Exception: + trl_version = Version("0.0.0") def vLLMSamplingParams(**kwargs): @@ -220,7 +238,7 @@ RLTrainer_replacement = ''' import os from typing import * from dataclasses import dataclass, field -from packaging.version import Version +from unsloth_zoo.utils import Version import torch import numpy as np from contextlib import nullcontext @@ -242,12 +260,18 @@ def prepare_for_training_mode(f): @functools.wraps(f) def wrapper(self, *args, **kwargs): # Enable training mode + _was_training = None + if hasattr(self, 'model') and hasattr(self.model, "training"): + _was_training = self.model.training if hasattr(self, 'model') and hasattr(self.model, "for_training"): self.model.for_training() output = f(self, *args, **kwargs) - # Return inference mode + # Restore previous mode when possible if hasattr(self, 'model') and hasattr(self.model, "for_inference"): - self.model.for_inference() + if _was_training is False: + self.model.for_inference() + elif _was_training is True and hasattr(self.model, "for_training"): + self.model.for_training() # Reset gradient checkpointing buffers to free memory while staying ready for next run try: reset_unsloth_gradient_checkpointing_buffers() @@ -331,6 +355,27 @@ class Unsloth{RLTrainer_name}(_Unsloth{RLTrainer_name}): pass ''' +def _wrap_grpo_generate_and_score(trainer_cls): + if not hasattr(trainer_cls, "_generate_and_score_completions"): + return + original = trainer_cls._generate_and_score_completions + if getattr(original, "_unsloth_restore_training_wrapped", False): + return + + def wrapped(self, *args, **kwargs): + was_training = getattr(getattr(self, "model", None), "training", None) + try: + return original(self, *args, **kwargs) + finally: + if was_training is False and hasattr(self, "model") and hasattr(self.model, "for_inference"): + try: + self.model.for_inference() + except Exception: + pass + + wrapped._unsloth_restore_training_wrapped = True + trainer_cls._generate_and_score_completions = wrapped + def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): # Patch for vLLM and Unsloth PEFT @@ -1059,6 +1104,16 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): globals(), ) + if trainer_file == "grpo_trainer": + try: + _wrap_grpo_generate_and_score( + getattr(created_module, f"Unsloth{RLTrainer_name}") + ) + except Exception as e: + logger.info( + f"Unsloth: Could not wrap _generate_and_score_completions for {RLTrainer_name}: {e}" + ) + def patch_functions(RLTrainer, trainer_file, RLTrainer_name, all_imports, imports): init = inspect.getsource(RLTrainer.__init__) diff --git a/unsloth/trainer.py b/unsloth/trainer.py index 0d98cff305..858dcf2cd3 100644 --- a/unsloth/trainer.py +++ b/unsloth/trainer.py @@ -211,7 +211,7 @@ def _backwards_compatible_trainer(trainer_class, config_class): if "processing_class" in trainer_params and "tokenizer" in kwargs: kwargs["processing_class"] = kwargs.pop("tokenizer") - if ("args" in kwargs) and (Version(trl.__version__) >= Version("0.13.0.dev0")): + if ("args" in kwargs) and (Version(trl) >= Version("0.13.0.dev0")): training_args = kwargs.pop("args", None) # Get parameters that Trainer.__init__ actually expects @@ -412,7 +412,7 @@ def _patch_trl_trainer(): if hasattr(trl, "__UNSLOTH_BACKWARDS_COMPATIBLE__"): return - if Version(trl.__version__) <= Version("0.11.0"): + if Version(trl) <= Version("0.11.0"): return import trl.trainer From 9ce417b445eb1ab2ff912efe41d60366bca8a66f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 07:03:34 +0000 Subject: [PATCH 118/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/models/rl.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index 35a15d03a6..23cbbf0256 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -46,10 +46,13 @@ torch_compile_options = { # vLLM compatibility shim (TRL expects GuidedDecodingParams even if vLLM doesn't provide it) try: import vllm.sampling_params as _unsloth_vllm_sp + if not hasattr(_unsloth_vllm_sp, "GuidedDecodingParams"): + class GuidedDecodingParams: def __init__(self, **kwargs): self.kwargs = kwargs + _unsloth_vllm_sp.GuidedDecodingParams = GuidedDecodingParams except Exception: pass @@ -355,6 +358,7 @@ class Unsloth{RLTrainer_name}(_Unsloth{RLTrainer_name}): pass ''' + def _wrap_grpo_generate_and_score(trainer_cls): if not hasattr(trainer_cls, "_generate_and_score_completions"): return @@ -367,7 +371,11 @@ def _wrap_grpo_generate_and_score(trainer_cls): try: return original(self, *args, **kwargs) finally: - if was_training is False and hasattr(self, "model") and hasattr(self.model, "for_inference"): + if ( + was_training is False + and hasattr(self, "model") + and hasattr(self.model, "for_inference") + ): try: self.model.for_inference() except Exception: From 0a07009eb5d0cf470dd8d7a0fdcdcece171b6b43 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 5 Jan 2026 10:02:11 +0000 Subject: [PATCH 119/167] Add None check for vLLM tokenizer - Check _vllm_tok is not None before accessing attributes - Use getattr for safer chat_template access --- unsloth/models/rl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index e1ecd6df2f..fd0c69bb0e 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -701,7 +701,7 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): "if hasattr(self, 'llm') and self.llm is not None and hasattr(self.llm, 'get_tokenizer'):\n" " _vllm_tok = self.llm.get_tokenizer()\n" " _pc = getattr(self, 'processing_class', None) or getattr(self, 'tokenizer', None)\n" - " if _pc is not None and getattr(_pc, 'chat_template', None) is not None and _vllm_tok.chat_template is None:\n" + " if _vllm_tok is not None and _pc is not None and getattr(_pc, 'chat_template', None) is not None and getattr(_vllm_tok, 'chat_template', None) is None:\n" " _vllm_tok.chat_template = _pc.chat_template\n" "pass\n" ) From 7090393ae25068ded8619b2450a3608b440b8c63 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 5 Jan 2026 12:32:16 +0000 Subject: [PATCH 120/167] Remove unnecessary PDL module existence check Old vLLM versions without PDL modules don't need the fix. The patching code already handles missing modules gracefully. --- unsloth/import_fixes.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 469674b29e..ef647ec65a 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -593,27 +593,6 @@ def fix_vllm_pdl_blackwell(): except Exception: return - # Helper to check if module spec exists - def _spec_exists(name): - try: - return importlib.util.find_spec(name) is not None - except (ModuleNotFoundError, ValueError): - return False - - # Check if vLLM has the PDL-related modules before doing internet check - has_utils = _spec_exists("vllm.lora.ops.triton_ops.utils") - has_expand_op = _spec_exists("vllm.lora.ops.triton_ops.lora_expand_op") - has_shrink_op = _spec_exists("vllm.lora.ops.triton_ops.lora_shrink_op") - - if not has_utils and not has_expand_op and not has_shrink_op: - # Old vLLM version without PDL support - just set env var to be safe - os.environ["TRITON_DISABLE_PDL"] = "1" - logger.info( - f"Unsloth: Set TRITON_DISABLE_PDL=1 for SM100 ({sm100_gpu_name}) - " - f"vLLM PDL modules not found" - ) - return - # Check if GitHub issue is closed (fix merged upstream) issue_closed = False try: From 5e091e5ac5da7b721b6525d6f4e404da1f5db994 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 5 Jan 2026 12:34:32 +0000 Subject: [PATCH 121/167] Keep PDL module check but remove unnecessary env var setting The check skips the GitHub API call for old vLLM versions. No need to set TRITON_DISABLE_PDL for versions without PDL support. --- unsloth/import_fixes.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index ef647ec65a..e8c4a2f665 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -593,6 +593,22 @@ def fix_vllm_pdl_blackwell(): except Exception: return + # Helper to check if module spec exists + def _spec_exists(name): + try: + return importlib.util.find_spec(name) is not None + except (ModuleNotFoundError, ValueError): + return False + + # Check if vLLM has the PDL-related modules before doing internet check + has_utils = _spec_exists("vllm.lora.ops.triton_ops.utils") + has_expand_op = _spec_exists("vllm.lora.ops.triton_ops.lora_expand_op") + has_shrink_op = _spec_exists("vllm.lora.ops.triton_ops.lora_shrink_op") + + if not has_utils and not has_expand_op and not has_shrink_op: + # Old vLLM version without PDL support - nothing to patch + return + # Check if GitHub issue is closed (fix merged upstream) issue_closed = False try: From 65f95f579bbcceb3c8c7a4597d0f57ca46f2b89e Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 5 Jan 2026 13:15:17 +0000 Subject: [PATCH 122/167] Replace GitHub API check with vLLM version check for PDL fix The GitHub issue check had issues: 1. Network latency on import 2. Issue being closed does not mean the fix is in the installed vLLM version Now skip the PDL workaround if vLLM version > 0.13.2, which is when the upstream fix is expected to be included. --- unsloth/import_fixes.py | 43 +++++++---------------------------------- 1 file changed, 7 insertions(+), 36 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index e8c4a2f665..86a504b7b2 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -609,47 +609,18 @@ def fix_vllm_pdl_blackwell(): # Old vLLM version without PDL support - nothing to patch return - # Check if GitHub issue is closed (fix merged upstream) - issue_closed = False + # Check if vLLM version includes the fix (expected in versions > 0.13.2) try: - import socket - import urllib.request - import json as json_module - - # Quick internet connectivity check (0.5s timeout) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(0.5) - try: - sock.connect(("api.github.com", 443)) - has_internet = True - except (socket.timeout, OSError): - has_internet = False - finally: - sock.close() - - if has_internet: - api_url = "https://api.github.com/repos/vllm-project/vllm/issues/30872" - req = urllib.request.Request( - api_url, - headers = { - "User-Agent": "Unsloth-PDL-Fix", - "Accept": "application/vnd.github.v3+json", - }, + vllm_version = Version(importlib_version("vllm")) + if vllm_version > Version("0.13.2"): + logger.info( + f"Unsloth: SM100 ({sm100_gpu_name}) detected but vLLM {vllm_version} " + f"should include PDL fix - skipping workaround" ) - with urllib.request.urlopen(req, timeout = 3) as response: - data = json_module.loads(response.read().decode()) - issue_closed = data.get("state") == "closed" + return except Exception: - # If we can't check, assume issue is still open (apply fix to be safe) pass - if issue_closed: - logger.info( - f"Unsloth: SM100 ({sm100_gpu_name}) detected but PDL issue #30872 " - f"is closed - skipping PDL fix" - ) - return - # Apply the PDL fix os.environ["TRITON_DISABLE_PDL"] = "1" From a85ad30f65fc9f3f3094218f88fe23928b559d74 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 5 Jan 2026 13:19:37 +0000 Subject: [PATCH 123/167] Address review feedback: add constant and debug logging --- unsloth/import_fixes.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 86a504b7b2..958173213d 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -609,17 +609,18 @@ def fix_vllm_pdl_blackwell(): # Old vLLM version without PDL support - nothing to patch return - # Check if vLLM version includes the fix (expected in versions > 0.13.2) + # Check if vLLM version includes the fix + VLLM_PDL_FIX_VERSION = "0.13.2" try: vllm_version = Version(importlib_version("vllm")) - if vllm_version > Version("0.13.2"): + if vllm_version > Version(VLLM_PDL_FIX_VERSION): logger.info( f"Unsloth: SM100 ({sm100_gpu_name}) detected but vLLM {vllm_version} " f"should include PDL fix - skipping workaround" ) return - except Exception: - pass + except Exception as e: + logger.debug(f"Unsloth: vLLM version check failed ({e}), applying PDL workaround.") # Apply the PDL fix os.environ["TRITON_DISABLE_PDL"] = "1" From 5951247b54eebc968050df84ca93003055e3efff Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 13:19:44 +0000 Subject: [PATCH 124/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/import_fixes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 958173213d..1e05e462e9 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -620,7 +620,9 @@ def fix_vllm_pdl_blackwell(): ) return except Exception as e: - logger.debug(f"Unsloth: vLLM version check failed ({e}), applying PDL workaround.") + logger.debug( + f"Unsloth: vLLM version check failed ({e}), applying PDL workaround." + ) # Apply the PDL fix os.environ["TRITON_DISABLE_PDL"] = "1" From d74f5a562fd64f5c6ed7c07f8a0c3885fe61be5e Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Mon, 5 Jan 2026 13:29:58 +0000 Subject: [PATCH 125/167] Drop rl.py GRPO changes from this branch --- unsloth/models/rl.py | 86 ++++++++++---------------------------------- 1 file changed, 18 insertions(+), 68 deletions(-) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index 23cbbf0256..fd0c69bb0e 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -43,31 +43,10 @@ torch_compile_options = { "triton.cudagraphs": False, } -# vLLM compatibility shim (TRL expects GuidedDecodingParams even if vLLM doesn't provide it) -try: - import vllm.sampling_params as _unsloth_vllm_sp - - if not hasattr(_unsloth_vllm_sp, "GuidedDecodingParams"): - - class GuidedDecodingParams: - def __init__(self, **kwargs): - self.kwargs = kwargs - - _unsloth_vllm_sp.GuidedDecodingParams = GuidedDecodingParams -except Exception: - pass - -from trl import __version__ as trl_version_raw -from importlib.metadata import version as importlib_version +from trl import __version__ as trl_version from unsloth_zoo.utils import Version -try: - trl_version = Version(trl_version_raw) -except Exception: - try: - trl_version = Version(importlib_version("trl")) - except Exception: - trl_version = Version("0.0.0") +trl_version = Version(trl_version) def vLLMSamplingParams(**kwargs): @@ -241,7 +220,7 @@ RLTrainer_replacement = ''' import os from typing import * from dataclasses import dataclass, field -from unsloth_zoo.utils import Version +from packaging.version import Version import torch import numpy as np from contextlib import nullcontext @@ -263,18 +242,12 @@ def prepare_for_training_mode(f): @functools.wraps(f) def wrapper(self, *args, **kwargs): # Enable training mode - _was_training = None - if hasattr(self, 'model') and hasattr(self.model, "training"): - _was_training = self.model.training if hasattr(self, 'model') and hasattr(self.model, "for_training"): self.model.for_training() output = f(self, *args, **kwargs) - # Restore previous mode when possible + # Return inference mode if hasattr(self, 'model') and hasattr(self.model, "for_inference"): - if _was_training is False: - self.model.for_inference() - elif _was_training is True and hasattr(self.model, "for_training"): - self.model.for_training() + self.model.for_inference() # Reset gradient checkpointing buffers to free memory while staying ready for next run try: reset_unsloth_gradient_checkpointing_buffers() @@ -359,32 +332,6 @@ pass ''' -def _wrap_grpo_generate_and_score(trainer_cls): - if not hasattr(trainer_cls, "_generate_and_score_completions"): - return - original = trainer_cls._generate_and_score_completions - if getattr(original, "_unsloth_restore_training_wrapped", False): - return - - def wrapped(self, *args, **kwargs): - was_training = getattr(getattr(self, "model", None), "training", None) - try: - return original(self, *args, **kwargs) - finally: - if ( - was_training is False - and hasattr(self, "model") - and hasattr(self.model, "for_inference") - ): - try: - self.model.for_inference() - except Exception: - pass - - wrapped._unsloth_restore_training_wrapped = True - trainer_cls._generate_and_score_completions = wrapped - - def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): # Patch for vLLM and Unsloth PEFT import trl @@ -747,6 +694,19 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): ) RLTrainer_post += training_check + # Sync chat_template from processing_class to vLLM's tokenizer + # This fixes base models that have custom chat templates applied after loading + if "model" in call_args: + vllm_chat_template_sync = ( + "if hasattr(self, 'llm') and self.llm is not None and hasattr(self.llm, 'get_tokenizer'):\n" + " _vllm_tok = self.llm.get_tokenizer()\n" + " _pc = getattr(self, 'processing_class', None) or getattr(self, 'tokenizer', None)\n" + " if _vllm_tok is not None and _pc is not None and getattr(_pc, 'chat_template', None) is not None and getattr(_vllm_tok, 'chat_template', None) is None:\n" + " _vllm_tok.chat_template = _pc.chat_template\n" + "pass\n" + ) + RLTrainer_post += vllm_chat_template_sync + # Edit optional metrics other_metrics_processor = "" if trainer_file in RL_METRICS_CHANGES: @@ -1112,16 +1072,6 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): globals(), ) - if trainer_file == "grpo_trainer": - try: - _wrap_grpo_generate_and_score( - getattr(created_module, f"Unsloth{RLTrainer_name}") - ) - except Exception as e: - logger.info( - f"Unsloth: Could not wrap _generate_and_score_completions for {RLTrainer_name}: {e}" - ) - def patch_functions(RLTrainer, trainer_file, RLTrainer_name, all_imports, imports): init = inspect.getsource(RLTrainer.__init__) From d83a5f4e7cd354a3ba6661468507d62e2d17e347 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Mon, 5 Jan 2026 13:39:03 +0000 Subject: [PATCH 126/167] Restore TRL version fallback in rl.py --- unsloth/models/rl.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index fd0c69bb0e..11a5215c99 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -43,10 +43,28 @@ torch_compile_options = { "triton.cudagraphs": False, } -from trl import __version__ as trl_version +# vLLM compatibility shim (TRL expects GuidedDecodingParams even if vLLM doesn't provide it) +try: + import vllm.sampling_params as _unsloth_vllm_sp + if not hasattr(_unsloth_vllm_sp, "GuidedDecodingParams"): + class GuidedDecodingParams: + def __init__(self, **kwargs): + self.kwargs = kwargs + _unsloth_vllm_sp.GuidedDecodingParams = GuidedDecodingParams +except Exception: + pass + +from trl import __version__ as trl_version_raw +from importlib.metadata import version as importlib_version from unsloth_zoo.utils import Version -trl_version = Version(trl_version) +try: + trl_version = Version(trl_version_raw) +except Exception: + try: + trl_version = Version(importlib_version("trl")) + except Exception: + trl_version = Version("0.0.0") def vLLMSamplingParams(**kwargs): From 242f0996b46f4cffe744802c5726af4880e7c4eb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 13:39:15 +0000 Subject: [PATCH 127/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/models/rl.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index 11a5215c99..2f6aef2709 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -46,10 +46,13 @@ torch_compile_options = { # vLLM compatibility shim (TRL expects GuidedDecodingParams even if vLLM doesn't provide it) try: import vllm.sampling_params as _unsloth_vllm_sp + if not hasattr(_unsloth_vllm_sp, "GuidedDecodingParams"): + class GuidedDecodingParams: def __init__(self, **kwargs): self.kwargs = kwargs + _unsloth_vllm_sp.GuidedDecodingParams = GuidedDecodingParams except Exception: pass From 6918e2d31a6caee5ce75ce9a20e1e67908561b28 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Mon, 5 Jan 2026 13:50:48 +0000 Subject: [PATCH 128/167] Fix GRPO training state restoration --- unsloth/models/rl.py | 46 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index 4ea36519d9..1327208c46 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -238,12 +238,18 @@ def prepare_for_training_mode(f): @functools.wraps(f) def wrapper(self, *args, **kwargs): # Enable training mode + _was_training = None + if hasattr(self, 'model') and hasattr(self.model, "training"): + _was_training = self.model.training if hasattr(self, 'model') and hasattr(self.model, "for_training"): self.model.for_training() output = f(self, *args, **kwargs) - # Return inference mode + # Restore previous mode when possible if hasattr(self, 'model') and hasattr(self.model, "for_inference"): - self.model.for_inference() + if _was_training is False: + self.model.for_inference() + elif _was_training is True and hasattr(self.model, "for_training"): + self.model.for_training() # Patch W&B to enable logging on future runs, otherwise it'll overwrite the first run try: import wandb @@ -323,6 +329,32 @@ pass ''' +def _wrap_grpo_generate_and_score(trainer_cls): + if not hasattr(trainer_cls, "_generate_and_score_completions"): + return + original = trainer_cls._generate_and_score_completions + if getattr(original, "_unsloth_restore_training_wrapped", False): + return + + def wrapped(self, *args, **kwargs): + was_training = getattr(getattr(self, "model", None), "training", None) + try: + return original(self, *args, **kwargs) + finally: + if ( + was_training is False + and hasattr(self, "model") + and hasattr(self.model, "for_inference") + ): + try: + self.model.for_inference() + except Exception: + pass + + wrapped._unsloth_restore_training_wrapped = True + trainer_cls._generate_and_score_completions = wrapped + + def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): # Patch for vLLM and Unsloth PEFT import trl @@ -1046,6 +1078,16 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): globals(), ) + if trainer_file == "grpo_trainer": + try: + _wrap_grpo_generate_and_score( + getattr(created_module, f"Unsloth{RLTrainer_name}") + ) + except Exception as e: + logger.info( + f"Unsloth: Could not wrap _generate_and_score_completions for {RLTrainer_name}: {e}" + ) + def patch_functions(RLTrainer, trainer_file, RLTrainer_name, all_imports, imports): init = inspect.getsource(RLTrainer.__init__) From ef533cddf70d258345ea45212f59b0703672ae20 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Mon, 5 Jan 2026 13:55:08 +0000 Subject: [PATCH 129/167] Revert rl_replacements GRPO edits --- unsloth/models/rl_replacements.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/unsloth/models/rl_replacements.py b/unsloth/models/rl_replacements.py index f0f0386bd1..5e079335ae 100644 --- a/unsloth/models/rl_replacements.py +++ b/unsloth/models/rl_replacements.py @@ -259,7 +259,6 @@ def grpo_trainer__generate_and_score_completions(function_name, function): # The new multi-line string that will replace the line above replacement_lines = """ batch_size = self.args.per_device_train_batch_size if mode == "train" else self.args.per_device_eval_batch_size - _was_training = self.model.training try: # TRL 0.23.1 and below path if not has_images: @@ -389,20 +388,6 @@ def grpo_trainer__generate_and_score_completions(function_name, function): function = patched - match = re.search(r"^(\s*)return output", function, re.MULTILINE) - - if match: - indent = match.group(1) - new_code = ( - indent - + "if not _was_training:\n" - + indent - + " self.model.for_inference()\n" - + indent - + "return output" - ) - function = function.replace(f"{indent}return output", new_code) - return function @@ -876,13 +861,19 @@ def grpo_trainer_compute_loss(function_name, function): else torch.tensor(0.0, device = self.model.device) ) self._metrics[mode]["sampling/importance_sampling_ratio/min"].append( - nanmin(self.accelerator.gather(min_importance_sampling_ratio)).item() + self.accelerator.gather(min_importance_sampling_ratio) + .nan_to_num(nan = float("inf")) + .min() + .item() ) self._metrics[mode]["sampling/importance_sampling_ratio/mean"].append( self.accelerator.gather(mean_importance_sampling_ratio).nanmean().item() ) self._metrics[mode]["sampling/importance_sampling_ratio/max"].append( - nanmax(self.accelerator.gather(max_importance_sampling_ratio)).item() + self.accelerator.gather(max_importance_sampling_ratio) + .nan_to_num(nan = float("-inf")) + .max() + .item() ) return loss @@ -964,11 +955,15 @@ def openenv_vllm_reload_weights(): return if Version(importlib_version("trl")) < Version("0.26.0"): return + try: import trl.experimental.openenv.utils as openenv_utils import trl.experimental.openenv as openenv except ImportError as e: logger.info(f"Unsloth: Failed to import trl openenv: {e}") + logger.info( + "Unsloth: trl.experimental.openenv not available — skipping RL openenv patches." + ) return src = inspect.getsource(openenv_utils.generate_rollout_completions) From e731f0b551717438e239453fcd1673a41efb3c2a Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 5 Jan 2026 07:37:08 -0800 Subject: [PATCH 130/167] Versioning --- pyproject.toml | 4 ++-- unsloth/__init__.py | 2 +- unsloth/models/_utils.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e7b84f3c8e..7fa249e64c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ huggingfacenotorch = [ ] huggingface = [ "unsloth[huggingfacenotorch]", - "unsloth_zoo>=2026.1.1", + "unsloth_zoo>=2026.1.2", "torchvision", "unsloth[triton]", ] @@ -523,7 +523,7 @@ colab-ampere-torch220 = [ "flash-attn>=2.6.3 ; ('linux' in sys_platform)", ] colab-new = [ - "unsloth_zoo>=2026.1.1", + "unsloth_zoo>=2026.1.2", "packaging", "tyro", "transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,<=4.57.3", diff --git a/unsloth/__init__.py b/unsloth/__init__.py index 86fb00fe0e..5b571cd456 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -79,7 +79,7 @@ from importlib.metadata import PackageNotFoundError # Check for unsloth_zoo try: unsloth_zoo_version = importlib_version("unsloth_zoo") - if Version(unsloth_zoo_version) < Version("2026.1.1"): + if Version(unsloth_zoo_version) < Version("2026.1.2"): print( "Unsloth: Please update Unsloth and Unsloth-Zoo to the latest version!\n" "Do this via `pip install --upgrade --force-reinstall --no-cache-dir --no-deps unsloth unsloth_zoo`" diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 5952d4af0c..b38c5860b3 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2026.1.1" +__version__ = "2026.1.2" __all__ = [ "SUPPORTS_BFLOAT16", From 8ce58137670576f3b298ec591a606e7442e17839 Mon Sep 17 00:00:00 2001 From: Datta Nimmaturi Date: Tue, 6 Jan 2026 09:53:20 +0000 Subject: [PATCH 131/167] Disable stats when modelscope is being used --- unsloth/models/_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index b38c5860b3..a2e1d78012 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -1197,7 +1197,8 @@ def get_statistics(local_files_only = False): # You can disable this by setting UNSLOTH_DISABLE_STATISTICS import os - if "UNSLOTH_DISABLE_STATISTICS" in os.environ: + global USE_MODELSCOPE + if "UNSLOTH_DISABLE_STATISTICS" in os.environ or USE_MODELSCOPE: return if local_files_only: return From 6b94be00f460fb86f94f28b7ba6e6b8446044c87 Mon Sep 17 00:00:00 2001 From: Datta Nimmaturi Date: Tue, 6 Jan 2026 15:30:06 +0530 Subject: [PATCH 132/167] Check env var explicitly Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- unsloth/models/_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index a2e1d78012..3cbb85ff4d 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -1197,8 +1197,7 @@ def get_statistics(local_files_only = False): # You can disable this by setting UNSLOTH_DISABLE_STATISTICS import os - global USE_MODELSCOPE - if "UNSLOTH_DISABLE_STATISTICS" in os.environ or USE_MODELSCOPE: + if "UNSLOTH_DISABLE_STATISTICS" in os.environ or os.environ.get("UNSLOTH_USE_MODELSCOPE", "0") == "1": return if local_files_only: return From 14204ea65a52e4682a665c3b44f186f78a48e102 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 10:00:16 +0000 Subject: [PATCH 133/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/models/_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 3cbb85ff4d..e6c4a12874 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -1197,7 +1197,10 @@ def get_statistics(local_files_only = False): # You can disable this by setting UNSLOTH_DISABLE_STATISTICS import os - if "UNSLOTH_DISABLE_STATISTICS" in os.environ or os.environ.get("UNSLOTH_USE_MODELSCOPE", "0") == "1": + if ( + "UNSLOTH_DISABLE_STATISTICS" in os.environ + or os.environ.get("UNSLOTH_USE_MODELSCOPE", "0") == "1" + ): return if local_files_only: return From 2e55a5e1d5bbe51deae7b615b67264bf94fb3fd0 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Thu, 8 Jan 2026 04:14:53 +0000 Subject: [PATCH 134/167] Fix FBGEMM/CUTLASS errors on SM100 (Blackwell) GPUs This PR fixes the "Arch conditional MMA instruction used without targeting appropriate compute capability. Aborting." errors that occur when using FBGEMM on Blackwell GPUs (B200/B100, SM100). Changes: - Add stderr filters in import_fixes.py for CUTLASS/FBGEMM MMA errors - Add warning filters for various deprecation messages - Update check_fbgemm_gpu_version() to disable FBGEMM instead of raising an error when old versions are detected - Update test_has_fbgemm() in fp8.py to catch broader CUTLASS/CUDA errors and gracefully fall back to Triton kernels - Update loader_utils.py to disable FBGEMM instead of raising ValueError for old fbgemm_gpu versions The key behavior change is that FBGEMM errors no longer crash the script. Instead, FBGEMM is disabled and Triton kernels are used automatically. This allows Unsloth to work on SM100 GPUs where CUTLASS SM90 kernels fail, and also gracefully handles old FBGEMM versions. --- unsloth/import_fixes.py | 34 +++++++++++++++++++++++++++++++--- unsloth/kernels/fp8.py | 22 +++++++++++++++++++--- unsloth/models/loader_utils.py | 10 +++++++--- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 1e05e462e9..5ad341e2ac 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -94,16 +94,40 @@ class HidePrintMessage: if os.environ.get("UNSLOTH_ENABLE_LOGGING", "0") != "1": import sys - # Apply to stderr for FBGEMM + # Apply to stderr for FBGEMM and CUTLASS errors sys.stderr = HidePrintMessage(sys.stderr) # https://github.com/pytorch/FBGEMM/blob/d99cd96490ec4aabac2ee95b1e76ea4dcfcfa628/fbgemm_gpu/experimental/gemm/triton_gemm/utils.py#L43-L52 sys.stderr.add_filter("TMA benchmarks will be running") + # CUTLASS/FBGEMM MMA instruction error on SM90 vs SM100 (Blackwell) GPUs + # https://github.com/NVIDIA/cutlass/blob/main/include/cutlass/gemm/kernel/sm90_gemm_tma_warpspecialized.hpp + sys.stderr.add_filter("Arch conditional MMA instruction used without targeting") + # CUTLASS arch conditional errors for various architectures + sys.stderr.add_filter("CUTE_INVALID_CONTROL_PATH") + # CUTLASS TMA-related errors when not targeting correct architecture + sys.stderr.add_filter("Trying to use tma without CUTE_ARCH_TMA") # Skipping import of cpp extensions due to incompatible torch version 2.9.0+cu128 for torchao version 0.15.0 logging.getLogger("torchao").setLevel(logging.ERROR) + # Also filter torchao print to stderr about cpp extensions + sys.stderr.add_filter("Skipping import of cpp extensions") # SyntaxWarning: invalid escape sequence '\.' warnings.filterwarnings( "ignore", message = "invalid escape sequence", category = SyntaxWarning ) + # PYTORCH_CUDA_ALLOC_CONF is deprecated warning from torch + warnings.filterwarnings( + "ignore", message = "PYTORCH_CUDA_ALLOC_CONF is deprecated" + ) + # TF32 precision deprecation warning from torch + warnings.filterwarnings( + "ignore", message = "Please use the new API settings to control TF32" + ) + # Deprecation warnings from torchao + warnings.filterwarnings( + "ignore", message = "`int4_weight_only` is deprecated" + ) + warnings.filterwarnings( + "ignore", message = "`int8_weight_only` is deprecated" + ) # Fix up AttributeError: 'MessageFactory' object has no attribute 'GetPrototype' @@ -323,10 +347,14 @@ def check_fbgemm_gpu_version(): except: return # We noticed some SegFault or bad alloc errors on lower versions of fbgemm_gpu. + # Instead of raising an error, disable FBGEMM and fall back to Triton kernels. if Version(fbgemm_gpu_version) < Version("1.4.0"): - raise ImportError( - f"Unsloth: fbgemm_gpu_genai=={fbgemm_gpu_version} detected. It might cause unexpected issues like segmentation faults. Please uninstall the current one by doing `pip uninstall fbgemm-gpu` && `pip install fbgemm-gpu` to install fbgemm-gpu 1.4.0 or newer!" + os.environ["UNSLOTH_HAS_FBGEMM"] = "0" + logger.info( + f"Unsloth: fbgemm_gpu_genai=={fbgemm_gpu_version} is old and may cause issues. " + f"Disabling FBGEMM - using Triton kernels instead." ) + return logger.info(f"Unsloth: fbgemm_gpu_genai=={fbgemm_gpu_version} detected.") diff --git a/unsloth/kernels/fp8.py b/unsloth/kernels/fp8.py index 3093bf61b1..e9f9161709 100644 --- a/unsloth/kernels/fp8.py +++ b/unsloth/kernels/fp8.py @@ -523,6 +523,7 @@ def fp8_fbgemm_block_linear(X, weight, weight_scale, bias = None): def test_has_fbgemm(): # We must manually check if the faster FBGEMM works on the specific GPU # For example RTX 5090 and RTX 4090 does not work + # Also SM100 (Blackwell B200/B100) GPUs fail with CUTLASS SM90 kernels # [TODO] Investigate with TorchAO why FBGEMM fails on consumer GPUs M, N, K = 128, 128, 128 xq = torch.ones(M, K, dtype = torch.float8_e4m3fn, device = "cuda") @@ -537,10 +538,25 @@ def test_has_fbgemm(): has_fbgemm = True del out except Exception as e: - e = str(e) - if "cutlass cannot initialize" in e.lower(): + error_str = str(e).lower() + # Catch any CUTLASS/CUDA errors and disable FBGEMM + # This includes MMA instruction errors, architecture mismatches, kernel launch failures, etc. + cutlass_cuda_errors = ( + "cutlass", + "cuda error", + "cuda runtime error", + "no kernel image", + "arch conditional", + "mma instruction", + "compute capability", + "cute_invalid_control_path", + "tma", + ) + is_cutlass_cuda_error = any(err in error_str for err in cutlass_cuda_errors) + + if is_cutlass_cuda_error: print( - f"Unsloth: FBGEMM on the current GPU cannot load - will switch to Triton kernels" + "Unsloth: FBGEMM on the current GPU cannot load - will switch to Triton kernels" ) else: print( diff --git a/unsloth/models/loader_utils.py b/unsloth/models/loader_utils.py index fe2a89d893..9656cc9d26 100644 --- a/unsloth/models/loader_utils.py +++ b/unsloth/models/loader_utils.py @@ -408,7 +408,7 @@ def _get_fp8_mode_and_check_settings( if Version(torchao.__version__) < Version("0.15.0"): raise ValueError(error_message) - # If fbgemm_gpu_genai is installed, check if it's >= 1.4.1 + # If fbgemm_gpu_genai is installed and old, disable FBGEMM and use Triton instead if ( importlib.util.find_spec("fbgemm_gpu") is not None and importlib.util.find_spec("fbgemm_gpu.experimental") is not None @@ -416,7 +416,11 @@ def _get_fp8_mode_and_check_settings( import fbgemm_gpu.experimental.gen_ai if Version(fbgemm_gpu.__version__) < Version("1.4.1"): - raise ValueError( - "Unsloth: On the fly `load_in_fp8` is only compatible with fbgemm_gpu_genai 1.4.1+. Try `unsloth/Qwen3-8B` instead." + # Old FBGEMM version - disable and use Triton kernels instead + os.environ["UNSLOTH_HAS_FBGEMM"] = "0" + from unsloth_zoo.log import logger + logger.info( + f"Unsloth: fbgemm_gpu_genai=={fbgemm_gpu.__version__} is old for FP8 loading. " + f"Using Triton kernels instead." ) return fp8_mode From e038da14913babe4e0450dae63efef6564db3144 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 04:15:17 +0000 Subject: [PATCH 135/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/import_fixes.py | 12 +++--------- unsloth/models/loader_utils.py | 1 + 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 5ad341e2ac..27e5342e20 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -114,20 +114,14 @@ if os.environ.get("UNSLOTH_ENABLE_LOGGING", "0") != "1": "ignore", message = "invalid escape sequence", category = SyntaxWarning ) # PYTORCH_CUDA_ALLOC_CONF is deprecated warning from torch - warnings.filterwarnings( - "ignore", message = "PYTORCH_CUDA_ALLOC_CONF is deprecated" - ) + warnings.filterwarnings("ignore", message = "PYTORCH_CUDA_ALLOC_CONF is deprecated") # TF32 precision deprecation warning from torch warnings.filterwarnings( "ignore", message = "Please use the new API settings to control TF32" ) # Deprecation warnings from torchao - warnings.filterwarnings( - "ignore", message = "`int4_weight_only` is deprecated" - ) - warnings.filterwarnings( - "ignore", message = "`int8_weight_only` is deprecated" - ) + warnings.filterwarnings("ignore", message = "`int4_weight_only` is deprecated") + warnings.filterwarnings("ignore", message = "`int8_weight_only` is deprecated") # Fix up AttributeError: 'MessageFactory' object has no attribute 'GetPrototype' diff --git a/unsloth/models/loader_utils.py b/unsloth/models/loader_utils.py index 9656cc9d26..1e5533c25c 100644 --- a/unsloth/models/loader_utils.py +++ b/unsloth/models/loader_utils.py @@ -419,6 +419,7 @@ def _get_fp8_mode_and_check_settings( # Old FBGEMM version - disable and use Triton kernels instead os.environ["UNSLOTH_HAS_FBGEMM"] = "0" from unsloth_zoo.log import logger + logger.info( f"Unsloth: fbgemm_gpu_genai=={fbgemm_gpu.__version__} is old for FP8 loading. " f"Using Triton kernels instead." From 56ebc94c944f4fb1f9db0c019d641e3fd8e24016 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 8 Jan 2026 11:35:00 +0000 Subject: [PATCH 136/167] Fix bugs and add improvements to RawTextDataLoader - Fix test file: use return_tokenized instead of return_tensors - Fix test file: use text_dataset instead of undefined dataset variable - Move parameter validation to constructor (fail fast on invalid params) - Add labels field in tokenized output for causal LM training - Add empty file handling with clear error message - Add tests for constructor validation and labels field --- tests/test_raw_text.py | 23 ++++++++++++++++++++--- unsloth/dataprep/raw_text.py | 19 +++++++++++-------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/tests/test_raw_text.py b/tests/test_raw_text.py index 5306c68fa6..7c7272a551 100644 --- a/tests/test_raw_text.py +++ b/tests/test_raw_text.py @@ -100,12 +100,12 @@ def test_raw_text_loader(): loader = RawTextDataLoader(tokenizer, chunk_size = 5, stride = 2) # Test loading with text output (legacy mode) - text_dataset = loader.load_from_file(test_file, return_tensors = False) + text_dataset = loader.load_from_file(test_file, return_tokenized = False) assert len(text_dataset) > 0, "Should create at least one chunk" assert "text" in text_dataset.column_names, "Dataset should have 'text' column" # Test loading with tokenized output (new efficient mode) - tokenized_dataset = loader.load_from_file(test_file, return_tensors = True) + tokenized_dataset = loader.load_from_file(test_file, return_tokenized = True) assert len(tokenized_dataset) > 0, "Should create at least one tokenized chunk" assert ( "input_ids" in tokenized_dataset.column_names @@ -124,13 +124,30 @@ def test_raw_text_loader(): first_sample["attention_mask"] ), "input_ids and attention_mask should have same length" + # Verify labels field exists (for causal LM training) + assert "labels" in tokenized_dataset.column_names, "Dataset should have 'labels' column" + assert first_sample["labels"] == first_sample["input_ids"], "labels should match input_ids" + + # Test constructor validation + try: + bad_loader = RawTextDataLoader(tokenizer, chunk_size = 0, stride = 2) + assert False, "Should raise ValueError for chunk_size=0" + except ValueError as e: + assert "chunk_size must be positive" in str(e) + + try: + bad_loader = RawTextDataLoader(tokenizer, chunk_size = 5, stride = 10) + assert False, "Should raise ValueError for stride >= chunk_size" + except ValueError as e: + assert "stride" in str(e) and "chunk_size" in str(e) + # Test preprocessor preprocessor = TextPreprocessor() clean_text = preprocessor.clean_text(" messy text \n\n\n ") assert "messy text" in clean_text, "Should clean text properly" # Test validation - stats = preprocessor.validate_dataset(dataset) + stats = preprocessor.validate_dataset(text_dataset) assert stats["total_samples"] > 0, "Should count samples" assert "warnings" in stats, "Should include warnings" diff --git a/unsloth/dataprep/raw_text.py b/unsloth/dataprep/raw_text.py index f7c1bf7856..da64565bbc 100644 --- a/unsloth/dataprep/raw_text.py +++ b/unsloth/dataprep/raw_text.py @@ -36,6 +36,12 @@ SUPPORTED_FORMATS = { class RawTextDataLoader: def __init__(self, tokenizer, chunk_size = 2048, stride = 512, return_tokenized = True): + if chunk_size <= 0: + raise ValueError(f"chunk_size must be positive, got {chunk_size}") + if stride >= chunk_size: + raise ValueError( + f"stride ({stride}) must be smaller than chunk_size ({chunk_size})" + ) self.tokenizer = tokenizer self.chunk_size = chunk_size self.stride = stride @@ -52,6 +58,8 @@ class RawTextDataLoader: return_tokenized = self.return_tokenized file_format = self.detect_format(file_path) text_content = self._read_file_by_format(file_path, file_format) + if not text_content or not text_content.strip(): + raise ValueError(f"File '{file_path}' is empty or contains only whitespace") chunks = self.smart_chunk_text( text_content, self.chunk_size, self.stride, return_tokenized ) @@ -86,8 +94,10 @@ class RawTextDataLoader: # Reorganize the data structure for Dataset.from_dict input_ids = [chunk["input_ids"] for chunk in chunks] attention_mask = [chunk["attention_mask"] for chunk in chunks] + # Labels are same as input_ids for causal LM training + labels = [list(ids) for ids in input_ids] return Dataset.from_dict( - {"input_ids": input_ids, "attention_mask": attention_mask} + {"input_ids": input_ids, "attention_mask": attention_mask, "labels": labels} ) else: # If chunks are text strings (backward compatibility) @@ -101,13 +111,6 @@ class RawTextDataLoader: 3. Maintains context with stride overlap 4. Returns tokenized chunks directly (more efficient) or text chunks """ - if chunk_size <= 0: - raise ValueError(f"chunk_size must be positive, got {chunk_size}") - if stride >= chunk_size: - raise ValueError( - f"stride ({stride}) must be smaller than chunk_size ({chunk_size}) to progress the chunking loop" - ) - # First pass: tokenize the entire text to get accurate token counts tokenized = self.tokenizer(text, return_tensors = "pt", add_special_tokens = False) tokens = tokenized["input_ids"] From 8c506a27c456d4bdc5eb7f4880c3d96cedf1fce3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:35:21 +0000 Subject: [PATCH 137/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_raw_text.py | 8 ++++++-- unsloth/dataprep/raw_text.py | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/test_raw_text.py b/tests/test_raw_text.py index 7c7272a551..9f2e8cda4e 100644 --- a/tests/test_raw_text.py +++ b/tests/test_raw_text.py @@ -125,8 +125,12 @@ def test_raw_text_loader(): ), "input_ids and attention_mask should have same length" # Verify labels field exists (for causal LM training) - assert "labels" in tokenized_dataset.column_names, "Dataset should have 'labels' column" - assert first_sample["labels"] == first_sample["input_ids"], "labels should match input_ids" + assert ( + "labels" in tokenized_dataset.column_names + ), "Dataset should have 'labels' column" + assert ( + first_sample["labels"] == first_sample["input_ids"] + ), "labels should match input_ids" # Test constructor validation try: diff --git a/unsloth/dataprep/raw_text.py b/unsloth/dataprep/raw_text.py index da64565bbc..ba010edabb 100644 --- a/unsloth/dataprep/raw_text.py +++ b/unsloth/dataprep/raw_text.py @@ -97,7 +97,11 @@ class RawTextDataLoader: # Labels are same as input_ids for causal LM training labels = [list(ids) for ids in input_ids] return Dataset.from_dict( - {"input_ids": input_ids, "attention_mask": attention_mask, "labels": labels} + { + "input_ids": input_ids, + "attention_mask": attention_mask, + "labels": labels, + } ) else: # If chunks are text strings (backward compatibility) From ae2b7a14c3f31667e3f742c3eca55d4100ba48a3 Mon Sep 17 00:00:00 2001 From: Rachel Li Date: Thu, 8 Jan 2026 18:44:22 -0500 Subject: [PATCH 138/167] Fix Kaggle telemetry misclassification when COLAB_ keys exist Problem: Kaggle notebook environments can expose both KAGGLE_* and COLAB_* environment keys. _get_statistics currently checks COLAB_ before KAGGLE_, causing Kaggle sessions to be labeled colab/colabpro. Prefer filesystem markers (e.g. /kaggle/working, /content + /opt/colab) before env-key heuristics, then fall back to the existing env-key checks. This avoids misclassification when providers leak overlapping env vars. Kaggle test notebook: https://www.kaggle.com/code/hnxnq07/kaggle-stats-gathering-test --- unsloth/models/_utils.py | 165 +++++++++++++++++++++------------------ 1 file changed, 90 insertions(+), 75 deletions(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index e6c4a12874..77564c03d5 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -1108,85 +1108,100 @@ def _get_statistics(statistics = None, force_download = True): if statistics is not None: pass - elif "\nCOLAB_" in keynames and n_cpus == 1: - statistics = "colab" - elif "\nCOLAB_" in keynames: - statistics = "colabpro" - elif "\nKAGGLE_" in keynames: - statistics = "kaggle" - elif "\nRUNPOD_" in keynames: - statistics = "runpod" - elif "\nAWS_" in keynames: - statistics = "aws" - elif "\nAZURE_" in keynames: - statistics = "azure" - # elif "\nK_" in keynames or "\nFUNCTION_" in keynames: statistics = "gcp" - elif "\nINVOCATION_ID" in keynames: - statistics = "lambda" - # else: statistics = "other" else: - - def try_vllm_check(): - vendor_files = ( - "/sys/class/dmi/id/product_version", - "/sys/class/dmi/id/bios_vendor", - "/sys/class/dmi/id/product_name", - "/sys/class/dmi/id/chassis_asset_tag", - "/sys/class/dmi/id/sys_vendor", - ) + # Prefer filesystem markers (harder to misidentify) before env-key matching + try: from pathlib import Path - for vendor_file in vendor_files: - path = Path(vendor_file) - if path.is_file(): - file_content = path.read_text().lower() - if "amazon" in file_content: - return "aws" - elif "microsoft corporation" in file_content: - return "azure" - elif "google" in file_content: - return "gcp" - return "other" - - pass - try: - statistics = try_vllm_check() - except: - statistics = "other" - if statistics is not None: - import tempfile - from huggingface_hub import snapshot_download - from unsloth_zoo.rl_environments import execute_with_time_limit - - if has_internet(): - - def stats_check(): - with tempfile.TemporaryDirectory(ignore_cleanup_errors = True) as f: - snapshot_download( - f"unslothai/{statistics}", - force_download = True, - cache_dir = f, - local_dir = f, + if Path("/kaggle/working").exists(): + statistics = "kaggle" + elif Path("/content").exists() and Path("/opt/colab").exists(): + statistics = "colab" if n_cpus == 1 else "colabpro" + elif Path("/runpod-volume").exists(): + statistics = "runpod" + except Exception: + pass + # Fallback to env-key detection + if statistics is None: + if "\nKAGGLE_" in keynames: + statistics = "kaggle" + elif "\nCOLAB_" in keynames and n_cpus == 1: + statistics = "colab" + elif "\nCOLAB_" in keynames: + statistics = "colabpro" + elif "\nRUNPOD_" in keynames: + statistics = "runpod" + elif "\nAWS_" in keynames: + statistics = "aws" + elif "\nAZURE_" in keynames: + statistics = "azure" + # elif "\nK_" in keynames or "\nFUNCTION_" in keynames: statistics = "gcp" + elif "\nINVOCATION_ID" in keynames: + statistics = "lambda" + # else: statistics = "other" + else: + + def try_vllm_check(): + vendor_files = ( + "/sys/class/dmi/id/product_version", + "/sys/class/dmi/id/bios_vendor", + "/sys/class/dmi/id/product_name", + "/sys/class/dmi/id/chassis_asset_tag", + "/sys/class/dmi/id/sys_vendor", ) - - time_limited_stats_check = execute_with_time_limit(120)(stats_check) - try: - time_limited_stats_check() - except TimeoutError: - raise TimeoutError( - "Unsloth: HuggingFace seems to be down after trying for 120 seconds :(\n" - "Check https://status.huggingface.co/ for more details.\n" - "As a temporary measure, use modelscope with the same model name ie:\n" - "```\n" - "pip install modelscope\n" - "import os; os.environ['UNSLOTH_USE_MODELSCOPE'] = '1'\n" - "from unsloth import FastLanguageModel\n" - "model = FastLanguageModel.from_pretrained('unsloth/gpt-oss-20b')\n" - "```" - ) - except: - # Try no time limit check - stats_check() + from pathlib import Path + + for vendor_file in vendor_files: + path = Path(vendor_file) + if path.is_file(): + file_content = path.read_text().lower() + if "amazon" in file_content: + return "aws" + elif "microsoft corporation" in file_content: + return "azure" + elif "google" in file_content: + return "gcp" + return "other" + + pass + try: + statistics = try_vllm_check() + except: + statistics = "other" + if statistics is not None: + import tempfile + from huggingface_hub import snapshot_download + from unsloth_zoo.rl_environments import execute_with_time_limit + + if has_internet(): + + def stats_check(): + with tempfile.TemporaryDirectory(ignore_cleanup_errors = True) as f: + snapshot_download( + f"unslothai/{statistics}", + force_download = True, + cache_dir = f, + local_dir = f, + ) + + time_limited_stats_check = execute_with_time_limit(120)(stats_check) + try: + time_limited_stats_check() + except TimeoutError: + raise TimeoutError( + "Unsloth: HuggingFace seems to be down after trying for 120 seconds :(\n" + "Check https://status.huggingface.co/ for more details.\n" + "As a temporary measure, use modelscope with the same model name ie:\n" + "```\n" + "pip install modelscope\n" + "import os; os.environ['UNSLOTH_USE_MODELSCOPE'] = '1'\n" + "from unsloth import FastLanguageModel\n" + "model = FastLanguageModel.from_pretrained('unsloth/gpt-oss-20b')\n" + "```" + ) + except: + # Try no time limit check + stats_check() def get_statistics(local_files_only = False): From 74fbeb6cda582e6d92a49c6b25d6fb4804e51970 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:49:54 +0000 Subject: [PATCH 139/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/models/_utils.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 77564c03d5..756d884de1 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -1140,7 +1140,7 @@ def _get_statistics(statistics = None, force_download = True): statistics = "lambda" # else: statistics = "other" else: - + def try_vllm_check(): vendor_files = ( "/sys/class/dmi/id/product_version", @@ -1150,7 +1150,7 @@ def _get_statistics(statistics = None, force_download = True): "/sys/class/dmi/id/sys_vendor", ) from pathlib import Path - + for vendor_file in vendor_files: path = Path(vendor_file) if path.is_file(): @@ -1162,7 +1162,7 @@ def _get_statistics(statistics = None, force_download = True): elif "google" in file_content: return "gcp" return "other" - + pass try: statistics = try_vllm_check() @@ -1172,18 +1172,20 @@ def _get_statistics(statistics = None, force_download = True): import tempfile from huggingface_hub import snapshot_download from unsloth_zoo.rl_environments import execute_with_time_limit - + if has_internet(): - + def stats_check(): - with tempfile.TemporaryDirectory(ignore_cleanup_errors = True) as f: + with tempfile.TemporaryDirectory( + ignore_cleanup_errors = True + ) as f: snapshot_download( f"unslothai/{statistics}", force_download = True, cache_dir = f, local_dir = f, ) - + time_limited_stats_check = execute_with_time_limit(120)(stats_check) try: time_limited_stats_check() From ca171b482f41a1a25cfa255d02063b724e3de34a Mon Sep 17 00:00:00 2001 From: Rachel Li Date: Thu, 8 Jan 2026 19:04:30 -0500 Subject: [PATCH 140/167] Update _utils.py fixed indentation --- unsloth/models/_utils.py | 68 +++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 756d884de1..9bf02beb30 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -1121,6 +1121,7 @@ def _get_statistics(statistics = None, force_download = True): statistics = "runpod" except Exception: pass + # Fallback to env-key detection if statistics is None: if "\nKAGGLE_" in keynames: @@ -1168,42 +1169,43 @@ def _get_statistics(statistics = None, force_download = True): statistics = try_vllm_check() except: statistics = "other" - if statistics is not None: - import tempfile - from huggingface_hub import snapshot_download - from unsloth_zoo.rl_environments import execute_with_time_limit + + if statistics is not None: + import tempfile + from huggingface_hub import snapshot_download + from unsloth_zoo.rl_environments import execute_with_time_limit - if has_internet(): + if has_internet(): - def stats_check(): - with tempfile.TemporaryDirectory( - ignore_cleanup_errors = True - ) as f: - snapshot_download( - f"unslothai/{statistics}", - force_download = True, - cache_dir = f, - local_dir = f, - ) - - time_limited_stats_check = execute_with_time_limit(120)(stats_check) - try: - time_limited_stats_check() - except TimeoutError: - raise TimeoutError( - "Unsloth: HuggingFace seems to be down after trying for 120 seconds :(\n" - "Check https://status.huggingface.co/ for more details.\n" - "As a temporary measure, use modelscope with the same model name ie:\n" - "```\n" - "pip install modelscope\n" - "import os; os.environ['UNSLOTH_USE_MODELSCOPE'] = '1'\n" - "from unsloth import FastLanguageModel\n" - "model = FastLanguageModel.from_pretrained('unsloth/gpt-oss-20b')\n" - "```" + def stats_check(): + with tempfile.TemporaryDirectory( + ignore_cleanup_errors = True + ) as f: + snapshot_download( + f"unslothai/{statistics}", + force_download = True, + cache_dir = f, + local_dir = f, ) - except: - # Try no time limit check - stats_check() + + time_limited_stats_check = execute_with_time_limit(120)(stats_check) + try: + time_limited_stats_check() + except TimeoutError: + raise TimeoutError( + "Unsloth: HuggingFace seems to be down after trying for 120 seconds :(\n" + "Check https://status.huggingface.co/ for more details.\n" + "As a temporary measure, use modelscope with the same model name ie:\n" + "```\n" + "pip install modelscope\n" + "import os; os.environ['UNSLOTH_USE_MODELSCOPE'] = '1'\n" + "from unsloth import FastLanguageModel\n" + "model = FastLanguageModel.from_pretrained('unsloth/gpt-oss-20b')\n" + "```" + ) + except: + # Try no time limit check + stats_check() def get_statistics(local_files_only = False): From f61dcfccdcaaa80b05ceea065cfe6ae860a2a3cd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 00:04:59 +0000 Subject: [PATCH 141/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/models/_utils.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 9bf02beb30..701e6f633c 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -1121,7 +1121,7 @@ def _get_statistics(statistics = None, force_download = True): statistics = "runpod" except Exception: pass - + # Fallback to env-key detection if statistics is None: if "\nKAGGLE_" in keynames: @@ -1169,7 +1169,7 @@ def _get_statistics(statistics = None, force_download = True): statistics = try_vllm_check() except: statistics = "other" - + if statistics is not None: import tempfile from huggingface_hub import snapshot_download @@ -1178,9 +1178,7 @@ def _get_statistics(statistics = None, force_download = True): if has_internet(): def stats_check(): - with tempfile.TemporaryDirectory( - ignore_cleanup_errors = True - ) as f: + with tempfile.TemporaryDirectory(ignore_cleanup_errors = True) as f: snapshot_download( f"unslothai/{statistics}", force_download = True, From 31d9720b4e2fc832d17cdd95d14b16eca702e8a3 Mon Sep 17 00:00:00 2001 From: Rachel Li Date: Thu, 8 Jan 2026 19:20:24 -0500 Subject: [PATCH 142/167] Fix telemetry ping regression for explicit statistics Fixed Codex regression: keep snapshot_download pings for explicit statistics values; detection only runs when statistics is None. Also replaced bare except. --- unsloth/models/_utils.py | 62 ++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 701e6f633c..0cb6c8975e 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -1170,40 +1170,40 @@ def _get_statistics(statistics = None, force_download = True): except: statistics = "other" - if statistics is not None: - import tempfile - from huggingface_hub import snapshot_download - from unsloth_zoo.rl_environments import execute_with_time_limit + if statistics is not None: + import tempfile + from huggingface_hub import snapshot_download + from unsloth_zoo.rl_environments import execute_with_time_limit - if has_internet(): + if has_internet(): - def stats_check(): - with tempfile.TemporaryDirectory(ignore_cleanup_errors = True) as f: - snapshot_download( - f"unslothai/{statistics}", - force_download = True, - cache_dir = f, - local_dir = f, - ) - - time_limited_stats_check = execute_with_time_limit(120)(stats_check) - try: - time_limited_stats_check() - except TimeoutError: - raise TimeoutError( - "Unsloth: HuggingFace seems to be down after trying for 120 seconds :(\n" - "Check https://status.huggingface.co/ for more details.\n" - "As a temporary measure, use modelscope with the same model name ie:\n" - "```\n" - "pip install modelscope\n" - "import os; os.environ['UNSLOTH_USE_MODELSCOPE'] = '1'\n" - "from unsloth import FastLanguageModel\n" - "model = FastLanguageModel.from_pretrained('unsloth/gpt-oss-20b')\n" - "```" + def stats_check(): + with tempfile.TemporaryDirectory(ignore_cleanup_errors = True) as f: + snapshot_download( + f"unslothai/{statistics}", + force_download = True, + cache_dir = f, + local_dir = f, ) - except: - # Try no time limit check - stats_check() + + time_limited_stats_check = execute_with_time_limit(120)(stats_check) + try: + time_limited_stats_check() + except TimeoutError: + raise TimeoutError( + "Unsloth: HuggingFace seems to be down after trying for 120 seconds :(\n" + "Check https://status.huggingface.co/ for more details.\n" + "As a temporary measure, use modelscope with the same model name ie:\n" + "```\n" + "pip install modelscope\n" + "import os; os.environ['UNSLOTH_USE_MODELSCOPE'] = '1'\n" + "from unsloth import FastLanguageModel\n" + "model = FastLanguageModel.from_pretrained('unsloth/gpt-oss-20b')\n" + "```" + ) + except Exception: + # Try no time limit check + stats_check() def get_statistics(local_files_only = False): From c15611ea9e68c61c6e6bcae1778a8d020f2afa7e Mon Sep 17 00:00:00 2001 From: Rachel Li Date: Thu, 8 Jan 2026 19:32:33 -0500 Subject: [PATCH 143/167] Fix Kaggle telemetry detection & address review feedback - Fix Kaggle misclassification by prioritizing filesystem markers over env vars - Preserve telemetry pings when statistics is explicitly provided - Replace bare except with except Exception - Minor cleanup based on automated review feedback --- unsloth/models/_utils.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 0cb6c8975e..77c8da2576 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -1106,9 +1106,7 @@ def _get_statistics(statistics = None, force_download = True): global USE_MODELSCOPE USE_MODELSCOPE = os.environ.get("UNSLOTH_USE_MODELSCOPE", "0") == "1" - if statistics is not None: - pass - else: + if statistics is None: # Prefer filesystem markers (harder to misidentify) before env-key matching try: from pathlib import Path @@ -1150,7 +1148,6 @@ def _get_statistics(statistics = None, force_download = True): "/sys/class/dmi/id/chassis_asset_tag", "/sys/class/dmi/id/sys_vendor", ) - from pathlib import Path for vendor_file in vendor_files: path = Path(vendor_file) @@ -1163,11 +1160,10 @@ def _get_statistics(statistics = None, force_download = True): elif "google" in file_content: return "gcp" return "other" - - pass + try: statistics = try_vllm_check() - except: + except Exception: statistics = "other" if statistics is not None: From 6ec845e95285443334ebd7ab6e7ad1c29825cc15 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 00:33:00 +0000 Subject: [PATCH 144/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/models/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 77c8da2576..b0ad0a3082 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -1160,7 +1160,7 @@ def _get_statistics(statistics = None, force_download = True): elif "google" in file_content: return "gcp" return "other" - + try: statistics = try_vllm_check() except Exception: From dcd47ab7634778989ffb1341a04807fa00aa8ea8 Mon Sep 17 00:00:00 2001 From: Kaitao Yang Date: Wed, 7 Jan 2026 22:54:35 -0800 Subject: [PATCH 145/167] reduce code duplication by _offload_frozen_module_for_training --- unsloth/models/llama.py | 91 ++++++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/unsloth/models/llama.py b/unsloth/models/llama.py index 92d51b73ad..cfd900c460 100644 --- a/unsloth/models/llama.py +++ b/unsloth/models/llama.py @@ -146,6 +146,59 @@ torch_nn_functional_softmax = torch.nn.functional.softmax # SDPA has GQA internally SDPA_HAS_GQA = "enable_gqa" in scaled_dot_product_attention.__doc__ +from peft.tuners.tuners_utils import ModulesToSaveWrapper + + +def _offload_frozen_module_for_training( + module: ModulesToSaveWrapper, + device_type: str, + offload_device: str = "cpu", +) -> None: + """ + Offload frozen module to CPU and configure trainable copy for mixed precision training. + + This function optimizes memory usage by: + 1. Moving the trainable copy to the target device with appropriate precision + 2. Offloading the original frozen module to CPU/disk to free VRAM + 3. Converting float16 to float32 for compatibility with certain GPUs (e.g., Tesla T4) + + Args: + module: The module to configure. Must be a ModulesToSaveWrapper with a + `modules_to_save` attribute containing trainable and original modules. + device_type: Target device string for training (e.g., "cuda:0", "xpu:0") + offload_device: Device to offload frozen parameters (default: "cpu") + Note: Currently only "cpu" is supported; disk offloading is planned. + + Returns: + None (modifies module in-place) + + Note: + - Float16 weights are automatically promoted to float32 for GPU compatibility + - Original frozen parameters are moved to CPU to reduce active VRAM usage + - Future versions will support disk-based offloading for even larger models + + See Also: + - https://github.com/unslothai/unsloth/pull/1200 (Tesla T4 float32 requirement) + """ + # Early return with explicit None if module doesn't support mixed precision training + if not hasattr(module, "modules_to_save"): + return None + + new_dtype = module.modules_to_save.default.weight.dtype + if new_dtype == torch.float16: + # See https://github.com/unslothai/unsloth/pull/1200 + # Tesla T4 must use float32 and not float16 + new_dtype = torch.float32 + + module.modules_to_save.default.to( + device = device_type, dtype = new_dtype, non_blocking = True + ) + module.modules_to_save.default.requires_grad_(True) + + # [TODO] Move old module to CPU - should be disk! + module.original_module.to(device = offload_device, non_blocking = True) + module.original_module.requires_grad_(False) + # Fix new HF's inference code def _fast_prepare_inputs_for_generation( @@ -2711,46 +2764,16 @@ class FastLlamaModel: "Unsloth: Training embed_tokens in mixed precision to save VRAM" ) - new_dtype = model.get_input_embeddings().modules_to_save.default.weight.dtype - if new_dtype == torch.float16: - # See https://github.com/unslothai/unsloth/pull/1200 - # Tesla T4 must use float32 and not float16 - new_dtype = torch.float32 - - model.get_input_embeddings().modules_to_save.default.to( - device = DEVICE_TYPE_TORCH, dtype = new_dtype, non_blocking = True + _offload_frozen_module_for_training( + model.get_input_embeddings(), DEVICE_TYPE_TORCH ) - model.get_input_embeddings().modules_to_save.default.requires_grad_( - True - ) - - # [TODO] Move old embed_tokens to CPU - should be disk! - model.get_input_embeddings().original_module.to( - device = "cpu", non_blocking = True - ) - model.get_input_embeddings().original_module.requires_grad_(False) if "lm_head" in new_target_modules: print("Unsloth: Training lm_head in mixed precision to save VRAM") - new_dtype = model.get_output_embeddings().modules_to_save.default.weight.dtype - if new_dtype == torch.float16: - # See https://github.com/unslothai/unsloth/pull/1200 - # Tesla T4 must use float32 and not float16 - new_dtype = torch.float32 - - model.get_output_embeddings().modules_to_save.default.to( - device = DEVICE_TYPE_TORCH, dtype = new_dtype, non_blocking = True + _offload_frozen_module_for_training( + model.get_output_embeddings(), DEVICE_TYPE_TORCH ) - model.get_output_embeddings().modules_to_save.default.requires_grad_( - True - ) - - # [TODO] Move old lm_head to CPU - should be disk! - model.get_output_embeddings().original_module.to( - device = "cpu", non_blocking = True - ) - model.get_output_embeddings().original_module.requires_grad_(False) return model else: From 8a5cf6ae0ab7a613a408e7cab7e27f6e3057b16a Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Fri, 9 Jan 2026 23:24:39 +0000 Subject: [PATCH 146/167] fix: use peft.utils.other for ModulesToSaveWrapper import ModulesToSaveWrapper was removed from peft.tuners.tuners_utils in PEFT 0.16.0. The class has been available in peft.utils.other since at least PEFT 0.7.1, which is the minimum version Unsloth requires. This fixes the ImportError when using PEFT >= 0.16.0. --- unsloth/models/llama.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsloth/models/llama.py b/unsloth/models/llama.py index cfd900c460..39f2ba1460 100644 --- a/unsloth/models/llama.py +++ b/unsloth/models/llama.py @@ -146,7 +146,7 @@ torch_nn_functional_softmax = torch.nn.functional.softmax # SDPA has GQA internally SDPA_HAS_GQA = "enable_gqa" in scaled_dot_product_attention.__doc__ -from peft.tuners.tuners_utils import ModulesToSaveWrapper +from peft.utils.other import ModulesToSaveWrapper def _offload_frozen_module_for_training( From 52d8014d4f3678af3f3938de9b80746b36588d3e Mon Sep 17 00:00:00 2001 From: Duc-Viet Hoang Date: Mon, 12 Jan 2026 10:03:54 +0700 Subject: [PATCH 147/167] Complete disable `gradient_checkpointing` for vision when `use_gradient_checkpointing=False` --- unsloth/models/rl.py | 6 ++++-- unsloth/models/vision.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index e945a80354..9ec4d76ed3 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -264,17 +264,19 @@ def prepare_for_training_mode(f): def wrapper(self, *args, **kwargs): # Enable training mode _was_training = None + # Get gradient checkpointing setting from training arguments + use_gc = getattr(self.args, 'gradient_checkpointing', True) if hasattr(self, 'model') and hasattr(self.model, "training"): _was_training = self.model.training if hasattr(self, 'model') and hasattr(self.model, "for_training"): - self.model.for_training() + self.model.for_training(use_gradient_checkpointing=use_gc) output = f(self, *args, **kwargs) # Restore previous mode when possible if hasattr(self, 'model') and hasattr(self.model, "for_inference"): if _was_training is False: self.model.for_inference() elif _was_training is True and hasattr(self.model, "for_training"): - self.model.for_training() + self.model.for_training(use_gradient_checkpointing=use_gc) # Reset gradient checkpointing buffers to free memory while staying ready for next run try: reset_unsloth_gradient_checkpointing_buffers() diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index 6de942d7d2..4e03e0a168 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -1273,7 +1273,7 @@ class FastBaseModel: # Since transformers 4.53, must turn on explicitly for module in model.modules(): if hasattr(module, "gradient_checkpointing"): - module.gradient_checkpointing = True + module.gradient_checkpointing = use_gradient_checkpointing # Also re-enable training for embeddings for NEFTune if hasattr(model, "get_input_embeddings"): From 4d1cea591904e1cb68568a9fc13addf38fe6a413 Mon Sep 17 00:00:00 2001 From: Francesco Bertolotti Date: Mon, 12 Jan 2026 16:19:43 +0100 Subject: [PATCH 148/167] wrong number of dimensions --- unsloth/kernels/swiglu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsloth/kernels/swiglu.py b/unsloth/kernels/swiglu.py index b321f5179e..9e2680e862 100644 --- a/unsloth/kernels/swiglu.py +++ b/unsloth/kernels/swiglu.py @@ -128,7 +128,7 @@ def _DWf_DW_dfg_kernel( def swiglu_DWf_DW_dfg_kernel(DW, e, g): - batch_seq_len, hd = e.shape + batch, seq_len, hd = e.shape n_elements = e.numel() grid = lambda meta: (triton.cdiv(n_elements, meta["BLOCK_SIZE"]),) with torch_gpu_device(e.device): From 4a2b199a16ffc7b662a3b98fd0b74edf9a110825 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 19:08:13 +0000 Subject: [PATCH 149/167] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.10 → v0.14.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.10...v0.14.11) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 545c7899aa..cc188674b7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.10 + rev: v0.14.11 hooks: - id: ruff args: From 56900ab2efbc99745c375669445c3cfcd9d0e44e Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 12 Jan 2026 21:32:20 -0800 Subject: [PATCH 150/167] Apply suggestion from @danielhanchen --- unsloth/kernels/swiglu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsloth/kernels/swiglu.py b/unsloth/kernels/swiglu.py index 9e2680e862..b3ae9d40e6 100644 --- a/unsloth/kernels/swiglu.py +++ b/unsloth/kernels/swiglu.py @@ -128,7 +128,7 @@ def _DWf_DW_dfg_kernel( def swiglu_DWf_DW_dfg_kernel(DW, e, g): - batch, seq_len, hd = e.shape + batch_seq_len, hd = e.shape # Flattened to 2D, so 1st dim is bsz * seq_len n_elements = e.numel() grid = lambda meta: (triton.cdiv(n_elements, meta["BLOCK_SIZE"]),) with torch_gpu_device(e.device): From d5724cab2c34661eb4590c93b34a56266d8cffdb Mon Sep 17 00:00:00 2001 From: Michael Han <107991372+shimmyshimmer@users.noreply.github.com> Date: Wed, 14 Jan 2026 03:45:35 -0800 Subject: [PATCH 151/167] Update template.md --- .github/ISSUE_TEMPLATE/bug---issue.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug---issue.md b/.github/ISSUE_TEMPLATE/bug---issue.md index 397d725f95..83e0fd73a9 100644 --- a/.github/ISSUE_TEMPLATE/bug---issue.md +++ b/.github/ISSUE_TEMPLATE/bug---issue.md @@ -18,4 +18,4 @@ assignees: '' Put Minimal code to reproduce error here ###Remove Hugging Face token### ``` -🦥 You can also ask via our Reddit page: https://www.reddit.com/r/unsloth/ +🦥 You can also ask via our Reddit page: https://reddit.com/r/unsloth/ From 67517680ca1513bd2a1271a78b326a9c0fde8cc0 Mon Sep 17 00:00:00 2001 From: Datta Nimmaturi Date: Thu, 15 Jan 2026 11:11:50 +0000 Subject: [PATCH 152/167] use non lora model as base for RL --- unsloth/models/rl.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index e945a80354..62141b8700 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -1166,6 +1166,38 @@ def patch_functions(RLTrainer, trainer_file, RLTrainer_name, all_imports, import "model = self._prepare_peft_model(model, peft_config, args)\n", "pass\n" ) + # Skip add_adapter("ref") for reference model computation + # Unsloth: We comment out the "ref" adapter creation because: + # 1. We want to use the original BASE MODEL as the reference model, not the SFT/LoRA model + # 2. PEFT doesn't allow multiple adapters when target_parameters is used (MoE models) + # When "ref" is not in peft_config, GRPO/RLOO fallback uses disable_adapter() + # which gives the base model logits - exactly what we want + add_adapter_block_pattern = ( + r'([ \t]*)' # Capture leading indentation + r'if\s+is_peft_available\(\)\s+and\s+is_peft_model\(model\)\s+and\s+args\.beta\s*!=\s*0\.0\s*:' + r'(.*?)' # Match the entire block until ref_param.data.copy_ + r'ref_param\.data\.copy_\(param\.data\)' + ) + def comment_out_block(match): + """Comment out each line in the matched block, preserving indentation.""" + full_match = match.group(0) + indent = match.group(1) + lines = full_match.split('\n') + commented_lines = [] + # Add explanation comment first + commented_lines.append(f"{indent}# Unsloth: Commented out - use base model as reference, not SFT/LoRA model") + # Comment out each line - insert # after leading whitespace to preserve indentation + for line in lines: + if line.strip(): + stripped = line.lstrip() + leading_ws = line[:len(line) - len(stripped)] + commented_lines.append(f"{leading_ws}# {stripped}") + else: + commented_lines.append(line) + return '\n'.join(commented_lines) + init = re.sub(add_adapter_block_pattern, comment_out_block, init, flags=re.DOTALL) + + # Set use_vllm if not set if "args.use_vllm" in init and "model" in init and "args" in init: # .*? matches first match. .+? matches final match. From 946d48b2f9b45df1d3731c5bdffcbf46d2dc4a4e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:25:10 +0000 Subject: [PATCH 153/167] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unsloth/models/rl.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index 62141b8700..d5524ff5f5 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -1173,30 +1173,33 @@ def patch_functions(RLTrainer, trainer_file, RLTrainer_name, all_imports, import # When "ref" is not in peft_config, GRPO/RLOO fallback uses disable_adapter() # which gives the base model logits - exactly what we want add_adapter_block_pattern = ( - r'([ \t]*)' # Capture leading indentation - r'if\s+is_peft_available\(\)\s+and\s+is_peft_model\(model\)\s+and\s+args\.beta\s*!=\s*0\.0\s*:' - r'(.*?)' # Match the entire block until ref_param.data.copy_ - r'ref_param\.data\.copy_\(param\.data\)' + r"([ \t]*)" # Capture leading indentation + r"if\s+is_peft_available\(\)\s+and\s+is_peft_model\(model\)\s+and\s+args\.beta\s*!=\s*0\.0\s*:" + r"(.*?)" # Match the entire block until ref_param.data.copy_ + r"ref_param\.data\.copy_\(param\.data\)" ) + def comment_out_block(match): """Comment out each line in the matched block, preserving indentation.""" full_match = match.group(0) indent = match.group(1) - lines = full_match.split('\n') + lines = full_match.split("\n") commented_lines = [] # Add explanation comment first - commented_lines.append(f"{indent}# Unsloth: Commented out - use base model as reference, not SFT/LoRA model") + commented_lines.append( + f"{indent}# Unsloth: Commented out - use base model as reference, not SFT/LoRA model" + ) # Comment out each line - insert # after leading whitespace to preserve indentation for line in lines: if line.strip(): stripped = line.lstrip() - leading_ws = line[:len(line) - len(stripped)] + leading_ws = line[: len(line) - len(stripped)] commented_lines.append(f"{leading_ws}# {stripped}") else: commented_lines.append(line) - return '\n'.join(commented_lines) - init = re.sub(add_adapter_block_pattern, comment_out_block, init, flags=re.DOTALL) + return "\n".join(commented_lines) + init = re.sub(add_adapter_block_pattern, comment_out_block, init, flags = re.DOTALL) # Set use_vllm if not set if "args.use_vllm" in init and "model" in init and "args" in init: From 38c9913b8d2c2288b8962592f2497821cb9662c4 Mon Sep 17 00:00:00 2001 From: pluesclues <136766175+pluesclues@users.noreply.github.com> Date: Thu, 15 Jan 2026 08:01:19 -0500 Subject: [PATCH 154/167] Merge pull request #3628 from pluesclues/alternative_compute_chunked_loss Chunk Across Batch and Context length for logprob calculations for grpo --- unsloth/models/rl.py | 28 ++- unsloth/models/rl_replacements.py | 303 +++++++++++++++++++++++------- 2 files changed, 267 insertions(+), 64 deletions(-) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index 14a07e193a..9788207c99 100644 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -231,11 +231,13 @@ def PatchRL(FastLanguageModel): Trainer.prediction_step = unsloth_prediction_step +grpo_selective_log_softmax = RL_REPLACEMENTS["grpo_selective_log_softmax"] selective_log_softmax = RL_REPLACEMENTS["selective_log_softmax"] calculate_pad_tokens_in_prompt = RL_REPLACEMENTS["calculate_pad_tokens_in_prompt"] create_completion_attention_mask = RL_REPLACEMENTS["create_completion_attention_mask"] left_pack_padding = RL_REPLACEMENTS["left_pack_padding"] align_logprobs_with_mask = RL_REPLACEMENTS["align_logprobs_with_mask"] +autotune_batch_and_chunks = RL_REPLACEMENTS["grpo_autotune_batch_and_chunks"] RLTrainer_replacement = ''' import os @@ -247,7 +249,6 @@ import numpy as np from contextlib import nullcontext from torch.nn import functional as F import inspect -import psutil from transformers import DataCollatorForSeq2Seq, DataCollatorForLanguageModeling as TransformersDataCollatorForLanguageModeling from transformers.training_args import ParallelMode @@ -300,11 +301,13 @@ torch_compile_options = {{ "triton.cudagraphs" : False, }} +{grpo_selective_log_softmax_code} {selective_log_softmax_code} {calculate_pad_tokens_in_prompt_code} {create_completion_attention_mask_code} {left_pack_padding_code} {align_logprobs_with_mask_code} +{autotune_batch_and_chunks_code} {RL_pre} @@ -321,10 +324,20 @@ class Unsloth{RLConfig_name}({RLConfig_name}): default = -1, metadata = {{'help': 'Chunk size to reduce memory usage. -1 is most efficient.'}}, ) + unsloth_logit_chunk_multiplier : Optional[int] = field( + default = None, + metadata = {{'help': 'Multiplier for chunked logit computations.'}}, + ) + unsloth_grpo_mini_batch : Optional[int] = field( + default = None, + metadata = {{'help': 'Mini batch size for GRPO hidden state accumulation. Default is None unless user defines it.'}}, + ) {max_seq_length_pre} def __init__({RLConfig_arguments}, vllm_sampling_params = None, unsloth_num_chunks = -1, + unsloth_logit_chunk_multiplier = None, + unsloth_grpo_mini_batch = None, {max_seq_length_call} **kwargs, ): @@ -332,6 +345,15 @@ class Unsloth{RLConfig_name}({RLConfig_name}): super().__init__({RLConfig_call_args}{RLConfig_kwargs}) self.vllm_sampling_params = vllm_sampling_params self.unsloth_num_chunks = unsloth_num_chunks + if unsloth_grpo_mini_batch is not None: + if self.generation_batch_size >= unsloth_grpo_mini_batch: + self.unsloth_grpo_mini_batch = unsloth_grpo_mini_batch + else: + raise ValueError( + f"Unsloth GRPO mini batch size needs to be less than or equal to the effective generation batch size, " + f"which is self.per_device_train_batch_size * gradient_accumulation_steps." + ) + self.unsloth_logit_chunk_multiplier = unsloth_logit_chunk_multiplier {max_seq_length_post} pass @@ -1029,6 +1051,7 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): # Selective log softmax and other functions selective_log_softmax_code = inspect.getsource(selective_log_softmax) + grpo_selective_log_softmax_code = inspect.getsource(grpo_selective_log_softmax) calculate_pad_tokens_in_prompt_code = inspect.getsource( calculate_pad_tokens_in_prompt ) @@ -1037,6 +1060,7 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): ) left_pack_padding_code = inspect.getsource(left_pack_padding) align_logprobs_with_mask_code = inspect.getsource(align_logprobs_with_mask) + autotune_batch_and_chunks_code = inspect.getsource(autotune_batch_and_chunks) # Get final source code RLTrainer_source = RLTrainer_replacement.format( RLTrainer_name = RLTrainer_name, @@ -1058,8 +1082,10 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): max_seq_length_call = max_seq_length_call, max_seq_length_post = max_seq_length_post, selective_log_softmax_code = selective_log_softmax_code, + grpo_selective_log_softmax_code = grpo_selective_log_softmax_code, calculate_pad_tokens_in_prompt_code = calculate_pad_tokens_in_prompt_code, create_completion_attention_mask_code = create_completion_attention_mask_code, + autotune_batch_and_chunks_code = autotune_batch_and_chunks_code, left_pack_padding_code = left_pack_padding_code, align_logprobs_with_mask_code = align_logprobs_with_mask_code, ) diff --git a/unsloth/models/rl_replacements.py b/unsloth/models/rl_replacements.py index 5e079335ae..ff36da125d 100644 --- a/unsloth/models/rl_replacements.py +++ b/unsloth/models/rl_replacements.py @@ -50,7 +50,7 @@ RL_ADDITIONAL_FUNCTIONS = defaultdict(list) torch_compile_options = { "epilogue_fusion": True, - "max_autotune": True, + "max_autotune": False, # I saw speedups, but not sure if this has issues in collab "shape_padding": True, "trace.enabled": False, "triton.cudagraphs": False, @@ -258,18 +258,20 @@ def grpo_trainer__generate_and_score_completions(function_name, function): # The new multi-line string that will replace the line above replacement_lines = """ + max_left_pad = None batch_size = self.args.per_device_train_batch_size if mode == "train" else self.args.per_device_eval_batch_size try: # TRL 0.23.1 and below path if not has_images: # Left pad prompt before calculation old and ref hidden states - prompt_completion_ids = left_pack_padding(prompt_completion_ids, self.processing_class.pad_token_id) - self.model.for_training() + left_pad_tokens_per_prompt = calculate_pad_tokens_in_prompt(prompt_completion_ids, logits_to_keep, self.processing_class.pad_token_id) + max_left_pad = torch.max(left_pad_tokens_per_prompt).item() except: # TRL 0.24.0 and below path if images is None: # Left pad prompt before calculation old and ref hidden states - prompt_completion_ids = left_pack_padding(prompt_completion_ids, self.processing_class.pad_token_id) + left_pad_tokens_per_prompt = calculate_pad_tokens_in_prompt(prompt_completion_ids, logits_to_keep, self.processing_class.pad_token_id) + max_left_pad = torch.max(left_pad_tokens_per_prompt).item() self.model.for_training()""" function = function.replace(line_to_replace, replacement_lines) @@ -346,17 +348,45 @@ def grpo_trainer__generate_and_score_completions(function_name, function): if self.use_vllm:""" function = function.replace(replace_part, new_replacement) + # Important note: we disable TRL's importance sampling logic + # It is disabled because the LLM path moves left padding to the right. + # We must adjust the vLLM sampling_logprob tensor in Unsloth to account for this. + string_to_find = "if self.use_vllm and self.vllm_importance_sampling_correction:" + + replacement_string = ( + "if False and self.use_vllm and self.vllm_importance_sampling_correction:" + ) + + function = function.replace(string_to_find, replacement_string) + string_to_find = """ if "image_sizes" in prompt_inputs: output["image_sizes"] = prompt_inputs["image_sizes"]""" replacement_string = """ if "image_sizes" in prompt_inputs: output["image_sizes"] = prompt_inputs["image_sizes"] - - if self.use_vllm: - try: + if max_left_pad is not None: + output["max_left_pad"] = torch.tensor(prompt_ids.shape[0] * [max_left_pad]).unsqueeze(-1) + try: + if self.use_vllm and getattr(self, "vllm_importance_sampling_correction", False): output["sampling_per_token_logps"] = sampling_per_token_logps - except NameError: - output["sampling_per_token_logps"] = None""" + except NameError: + output["sampling_per_token_logps"] = None""" + + function = function.replace(string_to_find, replacement_string) + + # This path is for TRL 0.24.0 images is a variable exclusive to this version + string_to_find = """ if images is not None: + output["num_images"] = num_images""" + + replacement_string = """ if images is not None: + output["num_images"] = num_images + if max_left_pad is not None: + output["max_left_pad"] = torch.tensor(prompt_ids.shape[0] * [max_left_pad]).unsqueeze(-1) + try: + if self.use_vllm and getattr(self, "vllm_importance_sampling_correction", False): + output["sampling_per_token_logps"] = sampling_per_token_logps + except NameError: + output["sampling_per_token_logps"] = None""" function = function.replace(string_to_find, replacement_string) @@ -532,12 +562,12 @@ def grpo_trainer__get_per_token_logps_and_entropies(function_name, function): *args, **kwargs, ): + # All Unsloth code here in this function is licensed under AGPL3 # if True: # os.environ.get('UNSLOTH_USE_NEW_MODEL', '0') == '0': # return None, None # logps, entropies Unsloth efficient GRPO if compute_efficient: return None, None else: - # Otherwise, calculate normally: if not hasattr(self, "_autocast_dtype"): self._autocast_dtype = ( torch.float16 @@ -556,47 +586,199 @@ def grpo_trainer__get_per_token_logps_and_entropies(function_name, function): kwargs.get("image_sizes", None), ) - os.environ["UNSLOTH_RETURN_HIDDEN_STATES"] = "1" - unwrapped_model = self.accelerator.unwrap_model( model, keep_fp32_wrapper = False ) - with torch.amp.autocast(device_type = "cuda", dtype = self._autocast_dtype): - with _get_inference_mode_context_manager(model): - if pixel_values is None: - attention_mask = input_ids != self.processing_class.pad_token_id - attention_mask = attention_mask.to(attention_mask.dtype) - # We add 1 to `logits_to_keep` because the last logits of the sequence is later excluded - logits = unwrapped_model( - input_ids = input_ids, - attention_mask = attention_mask, - pixel_values = pixel_values, - image_grid_thw = image_grid_thw, - pixel_attention_mask = pixel_attention_mask, - image_sizes = image_sizes, - # logits_to_keep = logits_to_keep + 1, - ).logits + lm_head = self.model.get_output_embeddings().weight + + dtype_bytes = ( + 16 if self._autocast_dtype in [torch.float16, torch.bfloat16] else 32 + ) + total_rows = input_ids.shape[0] + seq_len = input_ids.shape[1] + hidden_dim = lm_head.shape[1] + vocab_dim = lm_head.shape[0] + + if self.args.unsloth_grpo_mini_batch is None: + B, multiplier = autotune_batch_and_chunks( + total_rows, + seq_len, + hidden_dim, + vocab_dim, + dtype_bytes, + self.args.unsloth_logit_chunk_multiplier, + ) + B = total_rows // B + else: + B = self.args.unsloth_grpo_mini_batch + + if self.args.unsloth_logit_chunk_multiplier is None: + multiplier = max(4, seq_len // 4096) + else: + multiplier = self.args.unsloth_logit_chunk_multiplier + + all_logprobs_list = [] + if pixel_values is None: + left_pad_tokens_per_prompt = calculate_pad_tokens_in_prompt( + input_ids, logits_to_keep, self.processing_class.pad_token_id + ) + max_left_pad = torch.max(left_pad_tokens_per_prompt).item() + input_ids = left_pack_padding( + input_ids, self.processing_class.pad_token_id + ) + attention_mask = input_ids != self.processing_class.pad_token_id + attention_mask = attention_mask.to(attention_mask.dtype) + else: + max_left_pad = 0 + + # input_ids_chunks = torch.chunk(input_ids, chunks = B, dim = 0) + attention_mask_chunks = torch.chunk(attention_mask, chunks = B, dim = 0) + + def chunk_optional(tensor, chunks): + if tensor is None: + return [None] * chunks + return torch.chunk(tensor, chunks = chunks, dim = 0) + + import math + + total_samples = input_ids.shape[0] + batch_size = math.ceil(total_samples / B) + + input_ids_chunks = [] + attention_mask_chunks = [] + pixel_values_chunks = [] + image_grid_thw_chunks = [] + pixel_attention_mask_chunks = [] + + current_pixel_idx = 0 + # TRL 0.23.0 batching logic + for start in range(0, total_samples, batch_size): + end = start + batch_size + + input_ids_chunks.append(input_ids[start:end]) + attention_mask_chunks.append(attention_mask[start:end]) + + if image_grid_thw is not None and pixel_values is not None: + grid_slice = image_grid_thw[start:end] + image_grid_thw_chunks.append(grid_slice) + + batch_pixel_count = grid_slice.prod(dim = -1).sum().item() + + start_pixel_idx = current_pixel_idx + end_pixel_idx = current_pixel_idx + batch_pixel_count + + pixel_values_chunks.append( + pixel_values[start_pixel_idx:end_pixel_idx] + ) + + if pixel_attention_mask is not None: + pixel_attention_mask_chunks.append( + pixel_attention_mask[start_pixel_idx:end_pixel_idx] + ) else: - logits = unwrapped_model( - input_ids = input_ids, - attention_mask = attention_mask, - pixel_values = pixel_values, - image_grid_thw = image_grid_thw, - pixel_attention_mask = pixel_attention_mask, - image_sizes = image_sizes, - logits_to_keep = logits_to_keep + 1, - ).logits + pixel_attention_mask_chunks.append(None) + current_pixel_idx = end_pixel_idx + + else: + pixel_values_chunks.append(None) + image_grid_thw_chunks.append(None) + pixel_attention_mask_chunks.append(None) + + if image_sizes is not None and not isinstance(image_sizes, torch.Tensor): + image_sizes_chunks = [[size] for size in image_sizes] + else: + image_sizes_chunks = chunk_optional(image_sizes, B) + + temperature = self.temperature + logit_softcapping = getattr(model.config, "final_logit_softcapping", 0) + if logit_softcapping is None: + logit_softcapping = 0 + logit_scale_multiply = getattr(model.config, "logit_scale", 0) + if logit_scale_multiply is None: + logit_scale_multiply = 0 + logit_scale_divide = getattr(model.config, "logits_scaling", 0) + if logit_scale_divide is None: + logit_scale_divide = 0 + + zipped_inputs = zip( + input_ids_chunks, + attention_mask_chunks, + pixel_values_chunks, + image_grid_thw_chunks, + pixel_attention_mask_chunks, + image_sizes_chunks, + ) + os.environ["UNSLOTH_RETURN_HIDDEN_STATES"] = "1" + + with _get_inference_mode_context_manager(model): + for ( + input_ids_chunk, + attention_mask_chunk, + pixel_values_chunk, + image_grid_thw_chunk, + pixel_attention_mask_chunk, + image_sizes_chunk, + ) in zipped_inputs: + with torch.amp.autocast( + device_type = "cuda", dtype = self._autocast_dtype + ): + if pixel_values is None: + logits_chunk = unwrapped_model( + input_ids = input_ids_chunk, + attention_mask = attention_mask_chunk, + pixel_values = pixel_values_chunk, + image_grid_thw = image_grid_thw_chunk, + pixel_attention_mask = pixel_attention_mask_chunk, + image_sizes = image_sizes_chunk, + ).logits + + completion_input_ids_chunk = input_ids_chunk[ + :, -(logits_to_keep + max_left_pad) : + ] + logits_chunk = logits_chunk[ + :, -(logits_to_keep + max_left_pad + 1) :, : + ] + logits_chunk = logits_chunk[:, :-1, :] + else: + # Essentially, for VLMs we do not go via the optimized path in models/, + # so we don't encounter the Flash Attn left-padding issue. + logits_chunk = unwrapped_model( + input_ids = input_ids_chunk, + attention_mask = attention_mask_chunk, + pixel_values = pixel_values_chunk, + image_grid_thw = image_grid_thw_chunk, + pixel_attention_mask = pixel_attention_mask_chunk, + image_sizes = image_sizes_chunk, + logits_to_keep = logits_to_keep + 1, + ).logits + + logits_chunk = logits_chunk[:, :-1, :] + completion_input_ids_chunk = input_ids_chunk[ + :, -logits_to_keep: + ] + + logprobs_chunk = chunked_hidden_states_selective_log_softmax( + logits_chunk, + lm_head, + completion_input_ids_chunk, + chunks = input_ids_chunk.shape[0] * multiplier, + logit_scale_multiply = logit_scale_multiply, + logit_scale_divide = logit_scale_divide, + logit_softcapping = logit_softcapping, + temperature = temperature, + ) + # This is needed to avoid race conditions with GPT OSS offload_embbed=True + # However, it seems that this line does not slow down or disrupt models. + torch.cuda.synchronize() + all_logprobs_list.append(logprobs_chunk) + logprobs = torch.cat(all_logprobs_list, dim = 0) entropies = None - if compute_entropy: - from trl.trainer.utils import entropy_from_logits - - entropies = entropy_from_logits(logits) os.environ["UNSLOTH_RETURN_HIDDEN_STATES"] = "0" - # logits = logits[:, :-1, :] # (B, L-1, V), exclude the last logit: it corresponds to the next token pred - return logits.detach(), entropies # logps, entropies + + return logprobs.detach(), entropies # logps, entropies # input_ids = input_ids[:, -logits_to_keep:] # For transformers<=4.48, logits_to_keep argument isn't supported, so here we drop logits ourselves. # See https://github.com/huggingface/trl/issues/2770 @@ -708,14 +890,14 @@ def grpo_trainer_compute_loss(function_name, function): # ref_per_token_logps = per_token_logps = get_logps_func(model, input_ids, attention_mask, logits_to_keep) # else: # ref_per_token_logps = None - ref_hidden_states = inputs.get("ref_per_token_logps", None) + ref_logps = inputs.get("ref_per_token_logps", None) # per_token_kl = torch.exp(ref_per_token_logps - per_token_logps) - (ref_per_token_logps - per_token_logps) - 1 # x - x.detach() allows for preserving gradients from x advantages = inputs["advantages"] # per_token_loss = torch.exp(per_token_logps - per_token_logps.detach()) * advantages.unsqueeze(1) # per_token_loss = -(per_token_loss - self.beta * per_token_kl) # loss = ((per_token_loss * completion_mask).sum(dim=1) / completion_mask.sum(dim=1)).mean() - old_hidden_states = inputs.get("old_per_token_logps", None) + old_logps = inputs.get("old_per_token_logps", None) input_ids = input_ids[:, -logits_to_keep:] @@ -730,24 +912,13 @@ def grpo_trainer_compute_loss(function_name, function): if logit_scale_divide is None: logit_scale_divide = 0 + max_left_pad = inputs.get("max_left_pad", 0) if per_token_logps is not None: - if ref_hidden_states is not None: - ref_hidden_states = ref_hidden_states[ - :, :-1, : - ] # (B, L-1, V), exclude the last logit: it corresponds to the next token pred - if old_hidden_states is not None: - old_hidden_states = old_hidden_states[ - :, :-1, : - ] # (B, L-1, V), exclude the last logit: it corresponds to the next token pred - per_token_logps = per_token_logps[ - :, :-1, : - ] # (B, L-1, V), exclude the last logit: it corresponds to the next token pred - loss, completion_length, mean_kl, delta, flat_is_ratio = ( grpo_compute_loss_slow( - ref_hidden_states, + ref_logps, per_token_logps, - old_hidden_states, + old_logps, input_ids, completion_mask, self.beta, @@ -761,6 +932,7 @@ def grpo_trainer_compute_loss(function_name, function): max_completion_length = self.args.max_completion_length, delta = self.args.delta, temperature = self.args.temperature, + max_left_pad = max_left_pad, logit_softcapping = logit_softcapping, logit_scale_multiply = logit_scale_multiply, logit_scale_divide = logit_scale_divide, @@ -781,8 +953,8 @@ def grpo_trainer_compute_loss(function_name, function): logits_to_keep = logits_to_keep, completion_mask = completion_mask, advantages = advantages, - old_hidden_states = old_hidden_states, - ref_hidden_states = ref_hidden_states, + old_logps = old_logps, + ref_logps = ref_logps, n_chunks = self.args.unsloth_num_chunks, loss_type = self.args.loss_type, importance_sampling_level = self.importance_sampling_level, @@ -791,6 +963,7 @@ def grpo_trainer_compute_loss(function_name, function): max_completion_length = self.args.max_completion_length, delta = self.args.delta, temperature = self.args.temperature, + max_left_pad = max_left_pad, logit_softcapping = logit_softcapping, logit_scale_multiply = logit_scale_multiply, logit_scale_divide = logit_scale_divide, @@ -809,8 +982,8 @@ def grpo_trainer_compute_loss(function_name, function): logits_to_keep = logits_to_keep, completion_mask = completion_mask, advantages = advantages, - old_hidden_states = old_hidden_states, - ref_hidden_states = ref_hidden_states, + old_logps = old_logps, + ref_logps = ref_logps, n_chunks = self.args.unsloth_num_chunks, temperature = self.args.temperature, logit_softcapping = logit_softcapping, @@ -827,7 +1000,11 @@ def grpo_trainer_compute_loss(function_name, function): self._metrics["completion_length"].append(completion_length.item()) self._metrics["kl"].append(mean_kl.item()) - if self.use_vllm and delta is not None: + if ( + self.use_vllm + and delta is not None + and getattr(self, "vllm_importance_sampling_correction", False) + ): mean_delta = ( torch.mean(delta) if delta.numel() > 0 From c965495dd4998e59685e880b3d42a196c7dc09e2 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 15 Jan 2026 05:09:26 -0800 Subject: [PATCH 155/167] Update _utils.py --- unsloth/models/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index b0ad0a3082..6028635fb9 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2026.1.2" +__version__ = "2026.1.3" __all__ = [ "SUPPORTS_BFLOAT16", From 8452e2ae376f4ce65e2f333a3cd9be22d3d1deae Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 15 Jan 2026 07:00:25 -0800 Subject: [PATCH 156/167] Update pyproject.toml --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7fa249e64c..05fb690da5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ huggingfacenotorch = [ "sentencepiece>=0.2.0", "datasets>=3.4.1,!=4.0.*,!=4.1.0,<4.4.0", "accelerate>=0.34.1", - "peft>=0.7.1,!=0.11.0", + "peft>=0.18.0,!=0.11.0", "huggingface_hub>=0.34.0", "hf_transfer", "diffusers", @@ -60,7 +60,7 @@ huggingfacenotorch = [ ] huggingface = [ "unsloth[huggingfacenotorch]", - "unsloth_zoo>=2026.1.2", + "unsloth_zoo>=2026.1.3", "torchvision", "unsloth[triton]", ] @@ -523,7 +523,7 @@ colab-ampere-torch220 = [ "flash-attn>=2.6.3 ; ('linux' in sys_platform)", ] colab-new = [ - "unsloth_zoo>=2026.1.2", + "unsloth_zoo>=2026.1.3", "packaging", "tyro", "transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,<=4.57.3", @@ -542,7 +542,7 @@ colab-new = [ colab-no-deps = [ "accelerate>=0.34.1", "trl>=0.18.2,!=0.19.0,<=0.24.0", - "peft>=0.7.1", + "peft>=0.18.0", "xformers ; ('linux' in sys_platform or sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", "protobuf", From ca0ecf1a3a404737f0de77f1fbec2e3bdf1c9d4e Mon Sep 17 00:00:00 2001 From: Michael Han <107991372+shimmyshimmer@users.noreply.github.com> Date: Thu, 15 Jan 2026 08:01:01 -0800 Subject: [PATCH 157/167] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ae1fccfbba..ff8dcdeef6 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,9 @@ Use our official [Unsloth Docker image](https://hub.docker.com/r/unsloth/unsloth For RTX 50x, B200, 6000 GPUs: `pip install unsloth`. Read our [Blackwell Guide](https://unsloth.ai/docs/basics/fine-tuning-llms-with-blackwell-rtx-50-series-and-unsloth) and [DGX Spark Guide](https://unsloth.ai/docs/basics/fine-tuning-llms-with-nvidia-dgx-spark-and-unsloth) for more details. ## 🦥 Unsloth News +- New 7x longer context reinforcement learning vs. all other setups, via our new batching algorithms. [Blog](https://unsloth.ai/docs/new/grpo-long-context) - New RoPE & MLP **Triton Kernels** & **Padding Free + Packing**: 3x faster training & 30% less VRAM. [Blog](https://unsloth.ai/docs/new/3x-faster-training-packing) -- **New Mistral**: Run Ministral 3 or Devstral 2 and fine-tune with vision/RL sodoku notebooks. [Guide](https://unsloth.ai/docs/models/ministral-3) • [Notebooks](https://unsloth.ai/docs/models/ministral-3#fine-tuning-ministral-3) +- **Mistral 3**: Run Ministral 3 or Devstral 2 and fine-tune with vision/RL sodoku notebooks. [Guide](https://unsloth.ai/docs/models/ministral-3) • [Notebooks](https://unsloth.ai/docs/models/ministral-3#fine-tuning-ministral-3) - **500K Context**: Training a 20B model with >500K context is now possible on an 80GB GPU. [Blog](https://unsloth.ai/docs/new/500k-context-length-fine-tuning) - **FP8 Reinforcement Learning**: You can now do FP8 GRPO on consumer GPUs. [Blog](https://unsloth.ai/docs/new/fp8-reinforcement-learning) • [Notebook](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_8B_FP8_GRPO.ipynb) - **DeepSeek-OCR**: Fine-tune to improve language understanding by 89%. [Guide](https://unsloth.ai/docs/models/deepseek-ocr-how-to-run-and-fine-tune) • [Notebook](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Deepseek_OCR_(3B).ipynb) From ab4061e106792fa91e1eba3e4f3d45fa8aba121e Mon Sep 17 00:00:00 2001 From: electroglyph Date: Thu, 15 Jan 2026 20:02:29 -0800 Subject: [PATCH 158/167] add weight-only int8 QAT scheme and update tests for torchao 0.15.0 (#3859) * add int8 weight-only QAT scheme, add test, fix tests for current torchao version * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * change quantization to PerAxis * lambda =/ * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add torchao messages, remove group_size from int8 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * raise exception on missing torchao * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * touch up the torchao imports * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- tests/utils/test_qat.py | 63 ++++++++++++++++++++++++++-------------- unsloth/models/_utils.py | 59 ++++++++++++++++++++++++++++--------- 2 files changed, 87 insertions(+), 35 deletions(-) diff --git a/tests/utils/test_qat.py b/tests/utils/test_qat.py index 79251cf2ff..1083712d78 100644 --- a/tests/utils/test_qat.py +++ b/tests/utils/test_qat.py @@ -4,12 +4,19 @@ from typing import Dict import pytest import torch -from torchao.quantization.qat import FakeQuantizedLinear -from torchao.quantization.qat.fake_quantizer import ( - FakeQuantizerBase, - Float8FakeQuantizer, - Int4WeightPreshuffledFakeQuantizer, -) + +try: + from torchao.quantization.qat import FakeQuantizedLinear + from torchao.quantization.qat.fake_quantizer import ( + FakeQuantizerBase, + Float8FakeQuantizer, + Int4WeightFakeQuantizer, + IntxFakeQuantizer, + ) +except ImportError: + print( + "Missing torchao import, please install or upgrade torchao with: pip install 'torchao>=0.15.0'" + ) class _CountingFakeQuantizer(torch.nn.Module): @@ -49,14 +56,20 @@ def _test_linear_is_fake_quantized(linear: torch.nn.Linear, qat_scheme: str): """ Verify that the given linear contains fake quantizers according to the `qat_scheme`. """ + weight_only = False if qat_scheme == "fp8-int4": act_fq_class = Float8FakeQuantizer - weight_fq_class = Int4WeightPreshuffledFakeQuantizer + weight_fq_class = Int4WeightFakeQuantizer min_in_features = 128 elif qat_scheme == "fp8-fp8": act_fq_class = Float8FakeQuantizer weight_fq_class = Float8FakeQuantizer min_in_features = -1 + elif qat_scheme == "int8": + act_fq_class = None + weight_fq_class = IntxFakeQuantizer + min_in_features = 128 + weight_only = True else: raise ValueError(f"Unknown qat_scheme: {qat_scheme}") @@ -64,7 +77,8 @@ def _test_linear_is_fake_quantized(linear: torch.nn.Linear, qat_scheme: str): base_layer = getattr(linear, "base_layer", linear) if base_layer.in_features >= min_in_features: assert isinstance(base_layer, FakeQuantizedLinear) - assert isinstance(base_layer.activation_fake_quantizer, act_fq_class) + if not weight_only: + assert isinstance(base_layer.activation_fake_quantizer, act_fq_class) assert isinstance(base_layer.weight_fake_quantizer, weight_fq_class) # Check lora A and B (only for full_finetuning=False) @@ -73,11 +87,13 @@ def _test_linear_is_fake_quantized(linear: torch.nn.Linear, qat_scheme: str): lora_B = linear.lora_B.default if lora_A.in_features >= min_in_features: assert isinstance(lora_A, FakeQuantizedLinear) - assert isinstance(lora_A.activation_fake_quantizer, act_fq_class) + if not weight_only: + assert isinstance(lora_A.activation_fake_quantizer, act_fq_class) assert isinstance(lora_A.weight_fake_quantizer, weight_fq_class) if lora_B.in_features >= min_in_features: assert isinstance(lora_B, FakeQuantizedLinear) - assert isinstance(lora_B.activation_fake_quantizer, act_fq_class) + if not weight_only: + assert isinstance(lora_B.activation_fake_quantizer, act_fq_class) assert isinstance(lora_B.weight_fake_quantizer, weight_fq_class) @@ -85,10 +101,12 @@ def _test_fake_quantizers_are_called( model: torch.nn.Module, example_inputs: Dict, full_finetuning: bool, + qat_scheme: str, ): """ Verify that the fake quantizers are actually called when the model is called. """ + weight_only = qat_scheme == "int8" def _swap_fake_quantizers(model: torch.nn.Module): for name, child in model.named_children(): @@ -99,7 +117,8 @@ def _test_fake_quantizers_are_called( for name, child in model.named_children(): if full_finetuning: if isinstance(child, FakeQuantizedLinear): - assert child.activation_fake_quantizer.count == 1 + if not weight_only: + assert child.activation_fake_quantizer.count == 1 assert child.weight_fake_quantizer.count == 1 else: # For LoRA, we only fake quantize the input activations once per block: @@ -107,12 +126,14 @@ def _test_fake_quantizers_are_called( # For mlp, we only fake quantize the gate_proj's input activations if name == "self_attn": base_layer = child.q_proj.base_layer - assert hasattr(base_layer, "activation_fake_quantizer") - assert base_layer.activation_fake_quantizer.count == 1 + if not weight_only: + assert hasattr(base_layer, "activation_fake_quantizer") + assert base_layer.activation_fake_quantizer.count == 1 elif name == "mlp": base_layer = child.gate_proj.base_layer - assert hasattr(base_layer, "activation_fake_quantizer") - assert base_layer.activation_fake_quantizer.count == 1 + if not weight_only: + assert hasattr(base_layer, "activation_fake_quantizer") + assert base_layer.activation_fake_quantizer.count == 1 elif isinstance(child, FakeQuantizedLinear): # Weight fake quantizers should always be called assert child.weight_fake_quantizer.count == 1 @@ -124,7 +145,7 @@ def _test_fake_quantizers_are_called( model.apply(_assert_fake_quantizers_are_called) -def _test_model_fake_quantize(qat_scheme: bool, full_finetuning: bool): +def _test_model_fake_quantize(qat_scheme: str, full_finetuning: bool): """ Test that all linear layers in the model are fake quantized according to the `qat_scheme`. """ @@ -141,16 +162,16 @@ def _test_model_fake_quantize(qat_scheme: bool, full_finetuning: bool): _test_linear_is_fake_quantized(layer.mlp.up_proj, qat_scheme) _test_linear_is_fake_quantized(layer.mlp.down_proj, qat_scheme) inputs = tokenizer("How are you?", return_tensors = "pt") - _test_fake_quantizers_are_called(model, inputs, full_finetuning) + _test_fake_quantizers_are_called(model, inputs, full_finetuning, qat_scheme) # TODO: there are bad interactions across tests right now, need to figure out # how to disable model caching before re-enabling this test -@pytest.mark.parametrize("qat_scheme", ["fp8-int4", "fp8-fp8"]) -def _test_full_model_fake_quantize(qat_scheme: bool): +@pytest.mark.parametrize("qat_scheme", ["fp8-int4", "fp8-fp8", "int8"]) +def _test_full_model_fake_quantize(qat_scheme: str): _test_model_fake_quantize(qat_scheme, full_finetuning = True) -@pytest.mark.parametrize("qat_scheme", ["fp8-int4", "fp8-fp8"]) -def test_lora_model_fake_quantize(qat_scheme: bool): +@pytest.mark.parametrize("qat_scheme", ["fp8-int4", "fp8-fp8", "int8"]) +def test_lora_model_fake_quantize(qat_scheme: str): _test_model_fake_quantize(qat_scheme, full_finetuning = False) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 6028635fb9..28dbe450ed 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -175,6 +175,8 @@ warnings.filterwarnings(action = "ignore", category = UserWarning, module = "bit # Stop "Special tokens have been added in the vocabulary, ..." logging.getLogger("transformers.tokenization_utils_base").setLevel(logging.CRITICAL + 1) +TORCHAO_MSG = "Error: torchao not found, please install with `pip install torchao`" + # Ignore logging messages class HideLoggingMessage(logging.Filter): @@ -2211,9 +2213,12 @@ def _prepare_model_for_qat( QAT can be optionally combined with LoRA fine-tuning to for additional throughput improvement. For more details: https://dev-discuss.pytorch.org/t/speeding-up-qat-by-1-89x-with-lora/2700 """ - from torchao.quantization import PerRow, quantize_ - from torchao.quantization.granularity import PerGroup, PerAxis - from torchao.quantization.qat import QATConfig + try: + from torchao.quantization import PerRow, quantize_ + from torchao.quantization.granularity import PerGroup, PerAxis + from torchao.quantization.qat import QATConfig + except ImportError: + raise ImportError(TORCHAO_MSG) # Gemma3 models have issues with int8 embedding quantization due to their # large vocabulary size (262144). Auto-switch to int4 weight-only instead. @@ -2230,8 +2235,10 @@ def _prepare_model_for_qat( if not isinstance(qat_scheme, TorchAOConfig): torchao_config: Optional[TorchAOConfig] = None if qat_scheme == "fp8-int4": - from torchao.quantization import Float8DynamicActivationInt4WeightConfig - + try: + from torchao.quantization import Float8DynamicActivationInt4WeightConfig + except ImportError: + raise ImportError(TORCHAO_MSG) group_size = 128 base_config = Float8DynamicActivationInt4WeightConfig() filter_fn = ( @@ -2243,8 +2250,12 @@ def _prepare_model_for_qat( base_config_and_filter_fns = [(base_config, filter_fn)], ) elif qat_scheme == "fp8-fp8": - from torchao.quantization import Float8DynamicActivationFloat8WeightConfig - + try: + from torchao.quantization import ( + Float8DynamicActivationFloat8WeightConfig, + ) + except ImportError: + raise ImportError(TORCHAO_MSG) base_config = Float8DynamicActivationFloat8WeightConfig( granularity = PerRow() ) @@ -2252,11 +2263,13 @@ def _prepare_model_for_qat( qat_scheme = qat_scheme, base_config_and_filter_fns = [(base_config, None)] ) elif qat_scheme == "int8-int4": - from torchao.quantization import ( - Int8DynamicActivationIntxWeightConfig, - IntxWeightOnlyConfig, - ) - + try: + from torchao.quantization import ( + Int8DynamicActivationIntxWeightConfig, + IntxWeightOnlyConfig, + ) + except ImportError: + raise ImportError(TORCHAO_MSG) torchao_config = TorchAOConfig( qat_scheme = qat_scheme, base_config_and_filter_fns = [ @@ -2276,8 +2289,10 @@ def _prepare_model_for_qat( prequantization_transform = _untie_input_output_embeddings, ) elif qat_scheme == "int4": - from torchao.quantization import Int4WeightOnlyConfig - + try: + from torchao.quantization import Int4WeightOnlyConfig + except ImportError: + raise ImportError(TORCHAO_MSG) group_size = 128 base_config = Int4WeightOnlyConfig(group_size = group_size) filter_fn = ( @@ -2288,6 +2303,22 @@ def _prepare_model_for_qat( qat_scheme = qat_scheme, base_config_and_filter_fns = [(base_config, filter_fn)], ) + elif qat_scheme == "int8": + try: + from torchao.quantization import IntxWeightOnlyConfig + from torchao.quantization.granularity import PerAxis + except ImportError: + raise ImportError(TORCHAO_MSG) + + base_config = IntxWeightOnlyConfig( + weight_dtype = torch.int8, + granularity = PerAxis(0), + ) + filter_fn = lambda m, _: isinstance(m, torch.nn.Linear) + torchao_config = TorchAOConfig( + qat_scheme = qat_scheme, + base_config_and_filter_fns = [(base_config, filter_fn)], + ) else: raise ValueError(f"Unexpected QAT scheme {qat_scheme}") assert torchao_config is not None, f"TorchAOConfig was not set for {qat_scheme}" From 72100f4a50ae7f56bb600d519f36d491d8af2516 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 18:42:13 -0800 Subject: [PATCH 159/167] [pre-commit.ci] pre-commit autoupdate (#3905) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.11 → v0.14.13](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.11...v0.14.13) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc188674b7..bd37ece943 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.11 + rev: v0.14.13 hooks: - id: ruff args: From a5d5b63542fabfb6ac1f44d33a9816f092410fcf Mon Sep 17 00:00:00 2001 From: pluesclues <136766175+pluesclues@users.noreply.github.com> Date: Tue, 20 Jan 2026 00:04:27 -0500 Subject: [PATCH 160/167] Fix vllm ipykernel patch (#3907) * Implement vLLM patch for notebook detection Add patch for vLLM compatibility in notebook environments. * Fix sys.stdout.fileno for vLLM compatibility Patch sys.stdout.fileno for vLLM compatibility in notebooks. * Add patch_vllm_for_notebooks to initialization * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Harden vLLM notebook stdout patch * Use logger for vLLM notebook patch * Clarify vLLM notebook patch log message --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: danielhanchen --- unsloth/__init__.py | 3 +++ unsloth/import_fixes.py | 59 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/unsloth/__init__.py b/unsloth/__init__.py index 89824a25d1..d3093cf4c0 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -134,6 +134,7 @@ from .import_fixes import ( patch_enable_input_require_grads, fix_openenv_no_vllm, fix_executorch, + patch_vllm_for_notebooks, ) fix_xformers_performance_issue() @@ -147,6 +148,7 @@ patch_datasets() patch_enable_input_require_grads() fix_openenv_no_vllm() fix_executorch() +patch_vllm_for_notebooks() del fix_xformers_performance_issue del fix_vllm_aimv2_issue @@ -159,6 +161,7 @@ del patch_datasets del patch_enable_input_require_grads del fix_openenv_no_vllm del fix_executorch +del patch_vllm_for_notebooks # Torch 2.4 has including_emulation if DEVICE_TYPE == "cuda": diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 27e5342e20..89d9d4bdba 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -204,6 +204,65 @@ def fix_xformers_performance_issue(): logger.info(f"Unsloth: Failed patching Xformers with error = {str(e)}") +def patch_vllm_for_notebooks(): + import sys + + ipython = None + try: + from IPython import get_ipython as _get_ipython + except Exception: + _get_ipython = None + + if _get_ipython is not None: + try: + ipython = _get_ipython() + except Exception: + ipython = None + + if ipython is None: + try: + import builtins + + _get_ipython = getattr(builtins, "get_ipython", None) + if callable(_get_ipython): + ipython = _get_ipython() + except Exception: + ipython = None + + if ipython is None: + return + + try: + shell = ipython.__class__.__name__ + is_notebook = shell == "ZMQInteractiveShell" or "google.colab" in str( + type(ipython) + ) + except Exception: + return + + if not is_notebook: + return + + if not hasattr(sys.stdout, "fileno"): + return + + needs_patch = False + try: + fd = sys.stdout.fileno() + if not isinstance(fd, int) or fd < 0: + needs_patch = True + except Exception: + needs_patch = True + + if not needs_patch: + return + + logger.info( + "Unsloth: Notebook detected - Patching sys.stdout.fileno for newer `vllm>=0.12.0` versions" + ) + sys.stdout.fileno = lambda: 1 + + # ValueError: 'aimv2' is already used by a Transformers config, pick another name. def fix_vllm_aimv2_issue(): spec = importlib.util.find_spec("vllm") From d59ee86feeca4e0f63964d6fa7986a3d8d343a4c Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 20 Jan 2026 01:02:39 -0800 Subject: [PATCH 161/167] Handle Transformers 5 vLLM import errors (#3908) * Handle Transformers 5 vLLM import errors * Deduplicate vLLM transformers mismatch handling --------- Co-authored-by: danielhanchen --- unsloth/import_fixes.py | 31 +++++++++++++++++++++++++++++-- unsloth/models/vision.py | 4 ++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 89d9d4bdba..4f88808c2a 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -307,16 +307,43 @@ def fix_vllm_aimv2_issue(): def fix_vllm_guided_decoding_params(): + def _maybe_raise_vllm_transformers_mismatch(error): + error_text = str(error) + if ( + "ALLOWED_LAYER_TYPES" in error_text + or "transformers.configuration_utils" in error_text + ): + try: + vllm_version = importlib_version("vllm") + except Exception: + vllm_version = "unknown" + raise RuntimeError( + "Unsloth: vLLM with version " + f"{vllm_version} does not yet support transformers>=5.0.0. " + "Please downgrade to transformers==4.57.3 via " + 'pip install --force-reinstall "transformers==4.57.3". ' + f"Original error: {error}" + ) from error + if importlib.util.find_spec("vllm") is None: return # GuidedDecodingParmas is renamed to StructuredOutputsParams in vLLM # https://github.com/vllm-project/vllm/pull/22772/files # trl still wants to use GuidedDecodingParams. This is a temporary patch till trl updates - import vllm + try: + import vllm + except ImportError as e: + _maybe_raise_vllm_transformers_mismatch(e) + raise try: from vllm.sampling_params import GuidedDecodingParams - except ImportError: + except ImportError as e: + _maybe_raise_vllm_transformers_mismatch(e) + if not hasattr(vllm, "sampling_params") or not hasattr( + vllm.sampling_params, "StructuredOutputsParams" + ): + raise vllm.sampling_params.GuidedDecodingParams = ( vllm.sampling_params.StructuredOutputsParams ) diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index 4e03e0a168..a77cf715fc 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -106,7 +106,7 @@ PRE_COMPILE_INFERENCE = [ "gpt_oss", ] -from transformers import GenerationConfig, CompileConfig, HybridCache, AutoConfig +from transformers import GenerationConfig, CompileConfig, AutoConfig try: from transformers import PreTrainedConfig @@ -117,7 +117,7 @@ except: HAS_TORCH_DTYPE = "torch_dtype" in PretrainedConfig.__doc__ -from transformers import GenerationConfig, CompileConfig, HybridCache +from transformers import GenerationConfig, CompileConfig _compile_config = CompileConfig( fullgraph = False, From 063a02d1e950d95c041f56802d20e5df189e1fcf Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 22 Jan 2026 07:33:59 -0800 Subject: [PATCH 162/167] Versioning --- pyproject.toml | 8 ++++---- unsloth/models/_utils.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 05fb690da5..443242bfe2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,12 +55,12 @@ huggingfacenotorch = [ "huggingface_hub>=0.34.0", "hf_transfer", "diffusers", - "transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,<=4.57.3", + "transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,!=4.57.4,!=4.57.5,<=4.57.6", "trl>=0.18.2,!=0.19.0,<=0.24.0", ] huggingface = [ "unsloth[huggingfacenotorch]", - "unsloth_zoo>=2026.1.3", + "unsloth_zoo>=2026.1.4", "torchvision", "unsloth[triton]", ] @@ -523,10 +523,10 @@ colab-ampere-torch220 = [ "flash-attn>=2.6.3 ; ('linux' in sys_platform)", ] colab-new = [ - "unsloth_zoo>=2026.1.3", + "unsloth_zoo>=2026.1.4", "packaging", "tyro", - "transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,<=4.57.3", + "transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,!=4.57.4,!=4.57.5,<=4.57.6", "datasets>=3.4.1,!=4.0.*,!=4.1.0,<4.4.0", "sentencepiece>=0.2.0", "tqdm", diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 28dbe450ed..76952b00a5 100644 --- a/unsloth/models/_utils.py +++ b/unsloth/models/_utils.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2026.1.3" +__version__ = "2026.1.4" __all__ = [ "SUPPORTS_BFLOAT16", From 50114427467f9b9a19a189404d6e3b8ddd8fd375 Mon Sep 17 00:00:00 2001 From: electroglyph Date: Thu, 22 Jan 2026 07:35:55 -0800 Subject: [PATCH 163/167] add FastSentenceTransformer for easily finetuning SentenceTransformer models (#3719) * add FastSentenceTransformer * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Gemini code review suggestions * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * unsloth-zoo patch only fixed usage for XLMRobertaForMaskedLM, this is a fix for XLMRobertaModel * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * refactor do_lower_case * add some comments * force disable FP8 loading * refactor pooling detection, add missing pooling types * add save_pretrained_merged method which gets modules and config * fix _save_pretrained_merged * rename read_pooling_mode, load modules instead of hard-coding em * comment * revert save_pretrained_merged change * propagate trust_remote_code properly * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add super hacky mpnet patch from hell * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * refactor _load_modules, add for_inference to from_pretrained, add transformers 5 code for mpnet, add distilbert patches * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add ModernBert * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * deberta-v2 support (provisional), fix remote_code * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add generic add_pooling_layer logic * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix for missing config * add push_to_hub_merged * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * edit messages, throw exception if no HF token * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix device_map mismatch * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add comments, move import, other suggestions by Datta0 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * re-add adapter removal to save_pretrained_merged, but if saving to folder which had adapters before, leave them * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add unsloth branding to save_pretrained_merged * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * propagate dtype to internal module when loading for inference * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix mpnet gradient checkpointing for torch >= 2.9 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * same thing for transformers 5, oops =) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix FastSentenceTransformer performance: 6x speedup via torch.compile + SDPA The original implementation was 31% slower than naive SentenceTransformer due to conflicting decorators from Unsloth's auto-compiler (@torch.compile on attention modules but @torch.compiler.disable on sub-modules). Changes: - Add fast encoder path that bypasses Unsloth patching for encoder models - Use native torch.compile with mode="reduce-overhead" for 6x speedup - Auto-detect and enable SDPA for models that support it (BERT, RoBERTa, etc.) - Change defaults: load_in_16bit=True, load_in_4bit=False (16-bit is optimal) - Change default: use_gradient_checkpointing=False (conflicts with torch.compile) - Add UNSLOTH_COMPILE_DISABLE=1 env var to fall back to old path if needed Supported encoder types: mpnet, bert, distilbert, roberta, xlm-roberta, albert, electra Benchmark results (BS=32, seq_len=128): - Naive 16-bit LoRA: 13-50ms per iter - Unsloth 16-bit LoRA: 2-9ms per iter (5.4x-6.7x faster) - Memory usage: 61MB-1.3GB (even largest model fits easily) Note: 4-bit + torch.compile has a PyTorch bug (pytorch/pytorch#90665). 4-bit is also 1.7-1.9x slower than 16-bit due to dequantization overhead, so 16-bit is recommended for these small encoder models anyway. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Use Unsloth's prepare_model_for_kbit_training for consistency Changed from peft.prepare_model_for_kbit_training to unsloth.models._utils.prepare_model_for_kbit_training. Unsloth's version provides: - Float32 mixed precision upcasting for LoRA layers - Better numerical stability - Consistency with rest of Unsloth codebase * Use relative imports and add float16 machine support - Changed absolute import to relative: from ._utils import prepare_model_for_kbit_training - Added SUPPORTS_BFLOAT16 import for proper dtype detection - Handle devices that don't support bfloat16 by falling back to float16 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add save_pretrained_torchao * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add auto-compile for torch.compile based on training step breakeven analysis Changes: - Change default compile_mode from "reduce-overhead" to "default" since CUDA Graphs (used by reduce-overhead) is incompatible with PEFT/LoRA - Add _estimate_compile_threshold() to calculate minimum steps needed for torch.compile to be beneficial based on model parameter count - Add _apply_torch_compile() helper with accelerate unwrap_model bug workaround - Defer torch.compile application to trainer initialization time so we can check max_steps against the breakeven threshold - Patch SentenceTransformerTrainer to auto-apply compile when max_steps exceeds the calculated threshold Breakeven thresholds (with 1.2x safety margin): - 22M params (MiniLM): ~1388 steps - 110M params (mpnet): ~242 steps - 335M params (snowflake): ~203 steps This ensures torch.compile warmup cost is only paid when training is long enough to benefit from the speedup. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * do QAT preparation for fast path * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix double loading model, thanks Etherl * do mpnet gradient checkpoint patch if gc is enabled * remove distilbert patches from mpnet fix * sanity check on model params, thanks Etherl * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add save_pretrained_gguf, thanks Etherl * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Refine compile threshold estimation for sentence transformers * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel Han Co-authored-by: Daniel Han --- unsloth/models/__init__.py | 1 + unsloth/models/sentence_transformer.py | 1853 ++++++++++++++++++++++++ 2 files changed, 1854 insertions(+) create mode 100644 unsloth/models/sentence_transformer.py diff --git a/unsloth/models/__init__.py b/unsloth/models/__init__.py index d7b2393c89..138f309032 100644 --- a/unsloth/models/__init__.py +++ b/unsloth/models/__init__.py @@ -19,6 +19,7 @@ from .qwen2 import FastQwen2Model from .qwen3 import FastQwen3Model from .qwen3_moe import FastQwen3MoeModel from .granite import FastGraniteModel +from .sentence_transformer import FastSentenceTransformer try: from .falcon_h1 import FastFalconH1Model diff --git a/unsloth/models/sentence_transformer.py b/unsloth/models/sentence_transformer.py new file mode 100644 index 0000000000..b66ac7cf8a --- /dev/null +++ b/unsloth/models/sentence_transformer.py @@ -0,0 +1,1853 @@ +# Copyright 2025 electroglyph. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from .loader import FastModel +from ._utils import SUPPORTS_BFLOAT16 +import inspect +import json +import os +import types +from huggingface_hub import hf_hub_download +from typing import Optional +import torch +from transformers.modeling_outputs import BaseModelOutput +from collections import OrderedDict +from transformers.models.distilbert import modeling_distilbert +from transformers.modeling_attn_mask_utils import _prepare_4d_attention_mask_for_sdpa +import transformers +from packaging.version import Version +import re +from transformers import AutoModel, AutoConfig +from transformers.models.auto.auto_factory import _get_model_class +import tempfile +from huggingface_hub import HfApi, get_token +from ..save import unsloth_save_pretrained_torchao, unsloth_save_pretrained_gguf +import contextlib +import shutil + + +def _save_pretrained_torchao( + self, + save_directory, + tokenizer = None, + torchao_config = None, + push_to_hub = False, + token = None, +): + self.save_pretrained(save_directory) + + # grab inner model + inner_model = self[0].auto_model + if hasattr(inner_model, "_orig_mod"): + inner_model = inner_model._orig_mod + + # merge LoRA first + if hasattr(inner_model, "merge_and_unload"): + inner_model = inner_model.merge_and_unload() + + # confirm Transformer path + transformer_path = "0_Transformer" + modules_path = os.path.join(save_directory, "modules.json") + if os.path.exists(modules_path): + try: + with open(modules_path, "r") as f: + modules = json.load(f) + for m in modules: + if m.get("type", "").endswith("Transformer"): + transformer_path = m.get("path", "") + break + except: + pass + + transformer_dir = os.path.join(save_directory, transformer_path) + transformer_dir = os.path.abspath(transformer_dir) + + if tokenizer is None: + tokenizer = self.tokenizer + + @contextlib.contextmanager + def patch_unsloth_save(): + original_causal = transformers.AutoModelForCausalLM + original_rmtree = shutil.rmtree + # unsloth_save_pretrained_torchao expects AutoModelForCausalLM + transformers.AutoModelForCausalLM = transformers.AutoModel + # prevent unsloth from deleting the unquantized model directory + shutil.rmtree = lambda *args, **kwargs: None + try: + yield + finally: + # unpatch + transformers.AutoModelForCausalLM = original_causal + shutil.rmtree = original_rmtree + + with patch_unsloth_save(): + unsloth_save_pretrained_torchao( + inner_model, + transformer_dir, + tokenizer = tokenizer, + torchao_config = torchao_config, + push_to_hub = push_to_hub, + token = token, + ) + + # avoid `0_Transformer-torchao`, it was either this or fix modules.json + torchao_dir = transformer_dir + "-torchao" + if os.path.exists(torchao_dir): + if not os.path.exists(transformer_dir): + os.makedirs(transformer_dir, exist_ok = True) + + # move contents + for item in os.listdir(torchao_dir): + s = os.path.join(torchao_dir, item) + d = os.path.join(transformer_dir, item) + if os.path.isdir(s): + shutil.copytree(s, d, dirs_exist_ok = True) + else: + shutil.copy2(s, d) + + # remove torchao dir + shutil.rmtree(torchao_dir) + + # remove conflicting safetensors if we brought in bin + if os.path.exists(os.path.join(transformer_dir, "pytorch_model.bin")): + safetensors_path = os.path.join(transformer_dir, "model.safetensors") + if os.path.exists(safetensors_path): + try: + os.remove(safetensors_path) + except: + pass + + try: + FastSentenceTransformer._add_unsloth_branding(save_directory) + except: + pass + + +# Thanks Etherl: +def _save_pretrained_gguf( + self, + save_directory, + tokenizer = None, + quantization_method = "fast_quantized", + first_conversion = None, + push_to_hub = False, + token = None, + max_shard_size = "5GB", + temporary_location = "_unsloth_temporary_saved_buffers", + maximum_memory_usage = 0.85, + **kwargs, +): + """ + Saves the SentenceTransformer model to GGUF format by saving the inner transformer model, + converting it, and placing the resulting GGUF files in the save directory. + """ + # 1. Save standard SentenceTransformer structure (configs, modules.json, etc.) + self.save_pretrained(save_directory) + + # 2. Extract inner transformer model + inner_model = self[0].auto_model + if hasattr(inner_model, "_orig_mod"): + inner_model = inner_model._orig_mod + + # If it's a PEFT model, unsloth_save_pretrained_gguf handles merging, + # but we pass the inner model wrapper. + + # 3. Identify where the transformer weights are stored + transformer_path = "0_Transformer" + modules_path = os.path.join(save_directory, "modules.json") + if os.path.exists(modules_path): + try: + with open(modules_path, "r") as f: + modules = json.load(f) + for m in modules: + if m.get("type", "").endswith("Transformer"): + transformer_path = m.get("path", "") + break + except: + pass + + # This is where Unsloth will perform the save + conversion operations + transformer_dir = os.path.join(save_directory, transformer_path) + # Ensure this path is absolute for consistent comparison later + transformer_dir = os.path.abspath(transformer_dir) + + if tokenizer is None: + tokenizer = self.tokenizer + + # 4. Patch environment to ensure Unsloth treats this embedding model correctly + @contextlib.contextmanager + def patch_unsloth_gguf_save(): + # Prevent deletion of the directory we just created via self.save_pretrained + original_rmtree = shutil.rmtree + try: + yield + finally: + shutil.rmtree = original_rmtree + + # 5. Call Unsloth's GGUF saver on the inner model targeting the transformer subdirectory + with patch_unsloth_gguf_save(): + result = unsloth_save_pretrained_gguf( + inner_model, + save_directory = transformer_dir, + tokenizer = tokenizer, + quantization_method = quantization_method, + first_conversion = first_conversion, + push_to_hub = False, # Force local first to move files + token = token, + max_shard_size = max_shard_size, + temporary_location = temporary_location, + maximum_memory_usage = maximum_memory_usage, + ) + + # 6. Move GGUF files from the subdirectory (0_Transformer) to the root save_directory + gguf_files = result.get("gguf_files", []) + + new_gguf_locations = [] + + for gguf_file in gguf_files: + if os.path.exists(gguf_file): + filename = os.path.basename(gguf_file) + dest_path = os.path.join(save_directory, filename) + + # Convert to absolute path to avoid mixing relative/absolute in commonpath + abs_gguf_file = os.path.abspath(gguf_file) + + # Check if file is inside transformer_dir (subpath) + try: + is_subpath = ( + os.path.commonpath([abs_gguf_file, transformer_dir]) + == transformer_dir + ) + except ValueError: + # Can happen on Windows with different drives, or mix of absolute/relative (handled by abspath above) + is_subpath = False + + if is_subpath: + # If the GGUF file is inside the transformer_dir, move it out to root + shutil.move(gguf_file, dest_path) + new_gguf_locations.append(dest_path) + else: + # If it's elsewhere, move it to root if not already there + if os.path.abspath(dest_path) != abs_gguf_file: + shutil.move(gguf_file, dest_path) + new_gguf_locations.append(dest_path) + + # Update result with new locations + result["gguf_files"] = new_gguf_locations + + # 7. Add branding + try: + FastSentenceTransformer._add_unsloth_branding(save_directory) + + # Add GGUF details to README + readme_path = os.path.join(save_directory, "README.md") + if os.path.exists(readme_path): + with open(readme_path, "a", encoding = "utf-8") as f: + f.write("\n## GGUF Quantization\n") + f.write( + f"This model contains GGUF quantized versions in: {', '.join([os.path.basename(f) for f in new_gguf_locations])}\n" + ) + except: + pass + + # 8. Handle Push to Hub if requested + if push_to_hub: + if token is None: + token = get_token() + + api = HfApi(token = token) + repo_id = save_directory # Assuming save_directory is the repo name if pushing + + print(f"Unsloth: Uploading to {repo_id}...") + try: + api.create_repo( + repo_id = repo_id, exist_ok = True, private = kwargs.get("private", False) + ) + api.upload_folder( + folder_path = save_directory, + repo_id = repo_id, + commit_message = "Upload GGUF and SentenceTransformer model", + ) + print(f"Unsloth: Uploaded to https://huggingface.co/{repo_id}") + except Exception as e: + print(f"Unsloth: Upload failed: {e}") + + return result + + +class FastSentenceTransformer(FastModel): + @staticmethod + def _read_pooling_mode(model_name, token): + """ + Read the pooling mode from the modules.json file if it exists, otherwise return "mean". + """ + try: + if os.path.exists(model_name) and os.path.exists( + os.path.join(model_name, "modules.json") + ): + modules_json_path = os.path.join(model_name, "modules.json") + else: + modules_json_path = hf_hub_download( + model_name, "modules.json", token = token + ) + + with open(modules_json_path, "r") as f: + modules_config = json.load(f) + + pooling_config_path = None + for module in modules_config: + if module.get("type", "") == "sentence_transformers.models.Pooling": + pooling_path = module.get("path", "") + if pooling_path: + # try to find config.json for pooling module + if os.path.exists(model_name) and os.path.exists( + os.path.join(model_name, pooling_path, "config.json") + ): + pooling_config_path = os.path.join( + model_name, pooling_path, "config.json" + ) + else: + pooling_config_path = hf_hub_download( + model_name, + os.path.join(pooling_path, "config.json"), + token = token, + ) + break + + if pooling_config_path: + with open(pooling_config_path, "r") as f: + pooling_config = json.load(f) + # from here: + # https://github.com/huggingface/sentence-transformers/blob/main/sentence_transformers/models/Pooling.py#L43 + pooling_map = { + "pooling_mode_cls_token": "cls", + "pooling_mode_mean_tokens": "mean", + "pooling_mode_max_tokens": "max", + "pooling_mode_mean_sqrt_len_tokens": "mean_sqrt_len", + "pooling_mode_weightedmean_tokens": "weightedmean", + "pooling_mode_lasttoken": "lasttoken", + } + for config_key, mode in pooling_map.items(): + if pooling_config.get(config_key): + if mode != "mean": + print(f"Pooling mode detected as {mode}, updating...") + return mode + + except Exception as e: + print( + f"Failed to detect pooling mode, not a sentence-transformers model. Using default pooling mode 'mean', this may or may not work." + ) + return "mean" + + # should prolly be done upstream instead of this hackfest here + @staticmethod + def _patch_mpnet_v4(): + """ + Patch the MPNetModel to support gradient checkpointing. + Supports transformers 4. + """ + from transformers.models.mpnet import modeling_mpnet + + # add supports_gradient_checkpointing flag + modeling_mpnet.MPNetModel.supports_gradient_checkpointing = True + + # add _set_gradient_checkpointing method + def _set_gradient_checkpointing(self, module = None, value = True): + if module is None: + module = self.encoder + if isinstance(module, modeling_mpnet.MPNetEncoder): + module.gradient_checkpointing = value + + modeling_mpnet.MPNetModel._set_gradient_checkpointing = ( + _set_gradient_checkpointing + ) + + # patch MPNetEncoder.forward to support checkpointing + # based on: + # https://github.com/huggingface/transformers/blob/v4.57.3/src/transformers/models/mpnet/modeling_mpnet.py#L321 + def forward( + self, + hidden_states: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + head_mask: Optional[torch.Tensor] = None, + output_attentions: bool = False, + output_hidden_states: bool = False, + return_dict: bool = False, + **kwargs, + ): + position_bias = self.compute_position_bias(hidden_states) + all_hidden_states = () if output_hidden_states else None + all_attentions = () if output_attentions else None + + for i, layer_module in enumerate(self.layer): + if output_hidden_states: + all_hidden_states = all_hidden_states + (hidden_states,) + + # do gradient checkpointing if enabled and training + if getattr(self, "gradient_checkpointing", False) and self.training: + + def create_custom_forward(module): + # bog standard checkpoint + def custom_forward(*inputs): + return module(*inputs, output_attentions = output_attentions) + + return custom_forward + + layer_outputs = torch.utils.checkpoint.checkpoint( + create_custom_forward(layer_module), + hidden_states, + attention_mask, + head_mask[i] if head_mask is not None else None, + position_bias, + use_reentrant = True, # fix for torch 2.9 + ) + else: + # original code from here on + layer_outputs = layer_module( + hidden_states, + attention_mask, + head_mask[i] if head_mask is not None else None, + position_bias, + output_attentions = output_attentions, + **kwargs, + ) + + hidden_states = layer_outputs[0] + + if output_attentions: + all_attentions = all_attentions + (layer_outputs[1],) + + if output_hidden_states: + all_hidden_states = all_hidden_states + (hidden_states,) + + if not return_dict: + return tuple( + v + for v in [hidden_states, all_hidden_states, all_attentions] + if v is not None + ) + return BaseModelOutput( + last_hidden_state = hidden_states, + hidden_states = all_hidden_states, + attentions = all_attentions, + ) + + # assign the patched forward + modeling_mpnet.MPNetEncoder.forward = forward + + @staticmethod + def _patch_mpnet_v5(): + """ + Patch the MPNetModel to support gradient checkpointing. + Supports transformers 5. + """ + from transformers.models.mpnet import modeling_mpnet + + # add supports_gradient_checkpointing flag + modeling_mpnet.MPNetModel.supports_gradient_checkpointing = True + + # add _set_gradient_checkpointing method + def _set_gradient_checkpointing(self, module = None, value = True): + if module is None: + module = self.encoder + if isinstance(module, modeling_mpnet.MPNetEncoder): + module.gradient_checkpointing = value + + modeling_mpnet.MPNetModel._set_gradient_checkpointing = ( + _set_gradient_checkpointing + ) + + # patch MPNetEncoder.forward to support checkpointing + # based on: + # https://github.com/huggingface/transformers/blob/v5.0.0rc1/src/transformers/models/mpnet/modeling_mpnet.py#L284 + def forward( + self, + hidden_states: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + output_attentions: bool = False, + output_hidden_states: bool = False, + return_dict: bool = False, + **kwargs, + ): + position_bias = self.compute_position_bias(hidden_states) + all_hidden_states = () if output_hidden_states else None + all_attentions = () if output_attentions else None + + for i, layer_module in enumerate(self.layer): + if output_hidden_states: + all_hidden_states = all_hidden_states + (hidden_states,) + + # do gradient checkpointing if enabled and training + if getattr(self, "gradient_checkpointing", False) and self.training: + + def create_custom_forward(module): + # checkpoint + def custom_forward(*inputs): + return module(*inputs, output_attentions = output_attentions) + + return custom_forward + + layer_outputs = torch.utils.checkpoint.checkpoint( + create_custom_forward(layer_module), + hidden_states, + attention_mask, + position_bias, + use_reentrant = True, # required for torch >= 2.9 + ) + else: + # original code from here on + layer_outputs = layer_module( + hidden_states, + attention_mask, + position_bias, + output_attentions, + **kwargs, + ) + + hidden_states = layer_outputs[0] + + if output_attentions: + all_attentions = all_attentions + (layer_outputs[1],) + + if output_hidden_states: + all_hidden_states = all_hidden_states + (hidden_states,) + + if not return_dict: + return tuple( + v + for v in [hidden_states, all_hidden_states, all_attentions] + if v is not None + ) + return BaseModelOutput( + last_hidden_state = hidden_states, + hidden_states = all_hidden_states, + attentions = all_attentions, + ) + + modeling_mpnet.MPNetEncoder.forward = forward + + @staticmethod + def _patch_distilbert_v4(): + # change kwargs to positional args to be compatible with peft_utils + """ + Patch the forward method of the DistilBertModel to use positional arguments instead of keyword arguments. + Transformers 4 version. + """ + + # based on: + # https://github.com/huggingface/transformers/blob/v4.57.3/src/transformers/models/distilbert/modeling_distilbert.py#L666 + # original code from here on: + def forward( + self, + input_ids: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + head_mask: Optional[torch.Tensor] = None, + inputs_embeds: Optional[torch.Tensor] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + ): + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + if input_ids is not None and inputs_embeds is not None: + raise ValueError( + "You cannot specify both input_ids and inputs_embeds at the same time" + ) + elif input_ids is not None: + self.warn_if_padding_and_no_attention_mask(input_ids, attention_mask) + input_shape = input_ids.size() + elif inputs_embeds is not None: + input_shape = inputs_embeds.size()[:-1] + else: + raise ValueError( + "You have to specify either input_ids or inputs_embeds" + ) + + device = input_ids.device if input_ids is not None else inputs_embeds.device + + head_mask_is_none = head_mask is None + # Prepare head mask if needed + head_mask = self.get_head_mask(head_mask, self.config.num_hidden_layers) + + embeddings = self.embeddings( + input_ids, inputs_embeds + ) # (bs, seq_length, dim) + + if self.config._attn_implementation == "flash_attention_2": + attention_mask = ( + attention_mask + if (attention_mask is not None and 0 in attention_mask) + else None + ) + else: + if attention_mask is None: + attention_mask = torch.ones( + input_shape, device = device + ) # (bs, seq_length) + + if ( + self.config._attn_implementation == "sdpa" + and head_mask_is_none + and not output_attentions + ): + attention_mask = _prepare_4d_attention_mask_for_sdpa( + attention_mask, embeddings.dtype, tgt_len = input_shape[1] + ) + # patch here, change kwargs to positional args: + return self.transformer( + embeddings, + attention_mask, + head_mask, + output_attentions, + output_hidden_states, + return_dict, + ) + + modeling_distilbert.DistilBertModel.forward = forward + + @staticmethod + def _has_add_pooling_layer(config, auto_model_class = None): + """ + Checks if the model class supports the `add_pooling_layer` argument + """ + try: + if auto_model_class is None: + auto_model_class = AutoModel + # try to resolve the class + model_class = _get_model_class(config, auto_model_class._model_mapping) + + if model_class: + sig = inspect.signature(model_class.__init__) + return "add_pooling_layer" in sig.parameters + except: + pass + + return False + + @staticmethod + def _patch_distilbert_v5(): + """ + Patch the forward method of the DistilBertModel to use positional arguments instead of keyword arguments. + Transformers 5 version. + """ + # based on: + # https://github.com/huggingface/transformers/blob/v5.0.0rc1/src/transformers/models/distilbert/modeling_distilbert.py#L386 + # original code from here on: + from transformers.masking_utils import create_bidirectional_mask + + def forward( + self, + input_ids: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + inputs_embeds: Optional[torch.Tensor] = None, + position_ids: Optional[torch.Tensor] = None, + **kwargs, + ): + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError( + "You must specify exactly one of input_ids or inputs_embeds" + ) + + embeddings = self.embeddings(input_ids, inputs_embeds, position_ids) + + attention_mask = create_bidirectional_mask( + config = self.config, + input_embeds = embeddings, + attention_mask = attention_mask, + ) + + # patch here: unsloth gradient checkpointing hook needs positional arguments + return self.transformer( + embeddings, + attention_mask, + **kwargs, + ) + + modeling_distilbert.DistilBertModel.forward = forward + + @staticmethod + def _add_unsloth_tags(repo_id, token, tags = None): + """ + Add Unsloth and sentence-transformers tags to the Hugging Face Hub repository. + """ + from huggingface_hub import HfApi + + api = HfApi(token = token) + if tags is None: + tags = [] + tags.extend(["unsloth", "sentence-transformers"]) + try: + api.add_tags( + repo_id = repo_id, + tags = tags, + repo_type = "model", + ) + except: + pass + + @staticmethod + def _add_unsloth_branding(save_directory): + """ + Add Unsloth branding to the README.md file generated by sentence-transformers. + """ + readme_path = os.path.join(save_directory, "README.md") + if not os.path.exists(readme_path): + return + + with open(readme_path, "r", encoding = "utf-8") as f: + content = f.read() + + # add unsloth tag to frontmatter + if "---\ntags:\n" in content: + content = content.replace("---\ntags:\n", "---\ntags:\n- unsloth\n") + else: + # if tags exist but not right at start, use regex to append + pattern = r"(^tags:\s*\n)" + if re.search(pattern, content, re.MULTILINE): + content = re.sub( + pattern, r"\1- unsloth\n", content, count = 1, flags = re.MULTILINE + ) + + # add branding badge and text + branding = ( + "\n\nThis model was finetuned with [Unsloth](https://github.com/unslothai/unsloth).\n\n" + '[](https://github.com/unslothai/unsloth)\n' + ) + + # add to description + if "# SentenceTransformer" in content: + parts = content.split("# SentenceTransformer", 1) + content = parts[0] + "# SentenceTransformer" + branding + parts[1] + else: + content += branding + + with open(readme_path, "w", encoding = "utf-8") as f: + f.write(content) + + @staticmethod + def _module_path(model_name, token = None): + """ + Returns the path to the modules.json file or None + """ + try: + if os.path.exists(model_name) and os.path.isdir(model_name): + path = os.path.join(model_name, "modules.json") + return path if os.path.exists(path) else None + else: + try: + return hf_hub_download(model_name, "modules.json", token = token) + except: + return None + except: + return None + + @staticmethod + def _create_transformer_module( + model_name, + model, + tokenizer, + max_seq_length, + trust_remote_code, + ): + """Helper to create and configure a Transformer module.""" + from sentence_transformers.models import Transformer + + # prevents sentence-transformers from loading the model a second time, thanks Etherl + original_from_pretrained = AutoModel.from_pretrained + + def return_existing_model(*args, **kwargs): + return model + + try: + # Temporarily redirect AutoModel loading to return our pre-loaded model + AutoModel.from_pretrained = return_existing_model + + # Initialize Transformer + transformer_module = Transformer( + model_name, + max_seq_length = max_seq_length, + model_args = {"trust_remote_code": trust_remote_code}, + config_args = {"trust_remote_code": trust_remote_code}, + ) + finally: + # Restore original functionality immediately + AutoModel.from_pretrained = original_from_pretrained + + transformer_module.tokenizer = tokenizer + transformer_module.do_lower_case = getattr(tokenizer, "do_lower_case", False) + + # sentence-transformers only passes along known keys to model.forward + model_forward_params = list(inspect.signature(model.forward).parameters) + transformer_module.model_forward_params = set(model_forward_params) | { + "input_ids", + "attention_mask", + "token_type_ids", + "inputs_embeds", + } + + # determine max_seq_length if not provided + if max_seq_length is None: + if hasattr(model, "config") and hasattr( + model.config, "max_position_embeddings" + ): + max_seq_length = model.config.max_position_embeddings + elif hasattr(tokenizer, "model_max_length"): + max_seq_length = tokenizer.model_max_length + else: + max_seq_length = 512 + + transformer_module.max_seq_length = max_seq_length + transformer_module.config_keys = ["max_seq_length", "do_lower_case"] + transformer_module.save_in_root = True + + if hasattr(model, "config"): + model.config.tokenizer_class = tokenizer.__class__.__name__ + + return transformer_module + + @staticmethod + def _load_modules( + model_name, + token, + model, + tokenizer, + max_seq_length, + pooling_mode, + trust_remote_code = False, + ) -> tuple[OrderedDict, bool]: + """ + Load modules from modules.json if available, otherwise fallback to hard-coded modules. + + Returns: + tuple[OrderedDict, bool]: (modules, no_modules_json) + """ + from sentence_transformers.util import import_from_string, load_dir_path + from sentence_transformers.models import Pooling, Normalize + + modules = OrderedDict() + modules_json_path = FastSentenceTransformer._module_path(model_name, token) + + if modules_json_path: + with open(modules_json_path, encoding = "utf8") as f: + modules_config = json.load(f) + + for module_config in modules_config: + class_ref = module_config["type"] + name = module_config.get( + "name", str(module_config.get("idx", len(modules))) + ) + + if class_ref == "sentence_transformers.models.Transformer": + transformer_module = ( + FastSentenceTransformer._create_transformer_module( + model_name, + model, + tokenizer, + max_seq_length, + trust_remote_code, + ) + ) + modules[name] = transformer_module + else: + # load other modules (Pooling, Normalize, etc.) + module_path = module_config["path"] + if os.path.isdir(model_name): + load_path = os.path.join(model_name, module_path) + else: + try: + load_path = load_dir_path( + model_name, module_path, token = token + ) + except Exception as e: + print( + f"Unsloth Warning: Could not download module {module_path}: {e}" + ) + continue + + module_class = import_from_string(class_ref) + try: + module = module_class.load(load_path) + modules[name] = module + except Exception as e: + print( + f"Unsloth Warning: Failed to load module {name} ({class_ref}): {e}" + ) + + return modules, False + + # fallback if no modules.json (non sentence-transformers models) + print( + "Unsloth: No modules.json found, falling back to [Transformer, Pooling, Normalize]. This may or may not work." + ) + + transformer_module = FastSentenceTransformer._create_transformer_module( + model_name, model, tokenizer, max_seq_length, trust_remote_code + ) + modules["0"] = transformer_module + + hidden_size = getattr(model.config, "hidden_size", 768) + + if pooling_mode == "mean": + pooling_mode = FastSentenceTransformer._read_pooling_mode(model_name, token) + + modules["1"] = Pooling( + word_embedding_dimension = hidden_size, pooling_mode = pooling_mode + ) + modules["2"] = Normalize() + + return modules, True + + # Encoder model types that benefit from native torch.compile instead of Unsloth patching + ENCODER_MODEL_TYPES = { + "mpnet", + "bert", + "distilbert", + "modernbert", + "roberta", + "xlm-roberta", + "albert", + "electra", + } + + @staticmethod + def _estimate_compile_threshold( + model, + batch_size = None, + grad_accum = None, + max_seq_length = None, + ): + """ + Estimate the minimum training steps needed for torch.compile to be beneficial. + Returns the threshold with a 1.2x safety margin built in. + + Based on empirical benchmarks: + - Larger models have lower breakeven (more time saved per step) + - Warmup time scales with model size but speedup also increases + + Optional inputs (batch_size, grad_accum, max_seq_length) allow + a coarse pre-run adjustment. These are intentionally conservative + and avoid any runtime measurements. + """ + # Get parameter count from inner model + if hasattr(model, "__getitem__"): + try: + inner = model[0].auto_model + params = sum(p.numel() for p in inner.parameters()) + except: + params = 100_000_000 # Default to 100M if can't determine + else: + params = sum(p.numel() for p in model.parameters()) + + model_type = None + try: + if "inner" in locals(): + model_type = getattr(getattr(inner, "config", None), "model_type", None) + except Exception: + model_type = None + if isinstance(model_type, str): + model_type = model_type.lower() + + params_m = params / 1e6 + + # Empirical formula based on benchmarks with batch_size=2, grad_accum=4 + # Small models: high fixed overhead, lower speedup + # Large models: warmup scales but speedup is significant + if params_m < 50: + estimated_warmup = 35 + params_m * 0.3 + base_speedup = 1.35 + elif params_m < 200: + estimated_warmup = 12 + params_m * 0.03 + base_speedup = 1.75 + else: + estimated_warmup = 15 + params_m * 0.04 + base_speedup = 1.60 + + # Estimate time per step (ms) and time saved + naive_ms = 50 + params_m * 1.0 + compiled_ms = naive_ms / base_speedup + time_saved_per_step_s = (naive_ms - compiled_ms) / 1000 + + if time_saved_per_step_s > 0: + breakeven = estimated_warmup / time_saved_per_step_s + else: + breakeven = float("inf") + + # Return threshold with 1.2x safety margin + threshold = breakeven * 1.2 + + # Optional adjustment based on expected work per step. + # This uses only pre-run information (batch size, grad accum, seq length). + generic_scale = 1.0 + fast_scale = 1.0 + if ( + batch_size is not None + or grad_accum is not None + or max_seq_length is not None + ): + try: + bs = int(batch_size) if batch_size is not None else 2 + ga = int(grad_accum) if grad_accum is not None else 4 + seq = int(max_seq_length) if max_seq_length is not None else 512 + except Exception: + bs, ga, seq = 2, 4, 512 + + bs = max(1, bs) + ga = max(1, ga) + # Guard against unbounded tokenizer.model_max_length + seq = max(64, min(seq, 8192)) + + ref_bs, ref_ga, ref_seq = 2, 4, 512 + + # Generic path: lighter scaling, less conservative than params-only. + ga_scale = (ref_ga / ga) ** 1.0 + bs_seq_scale = ((ref_bs * ref_seq) / (bs * seq)) ** 0.15 + generic_scale = 0.35 * ga_scale * bs_seq_scale + generic_scale = max(0.05, min(generic_scale, 5.0)) + + # Fast encoder path: stronger scaling based on observed behavior. + fast_ga_scale = (ref_ga / ga) ** 1.5 + fast_bs_seq_scale = ((ref_bs * ref_seq) / (bs * seq)) ** 0.25 + fast_scale = 0.2 * fast_ga_scale * fast_bs_seq_scale + fast_scale = max(0.05, min(fast_scale, 5.0)) + + # Conservative safety factors: generic is less conservative than fast. + generic_threshold = threshold * generic_scale * 1.25 + + is_fast_type = ( + isinstance(model_type, str) + and model_type in FastSentenceTransformer.ENCODER_MODEL_TYPES + ) + if is_fast_type: + fast_threshold = threshold * fast_scale * 1.5 + # Prefer the smaller (less conservative) of the two estimates. + final_threshold = min(generic_threshold, fast_threshold) + else: + final_threshold = generic_threshold + + # Reduce mpnet overestimation slightly. + if model_type == "mpnet": + final_threshold *= 0.7 + + # Lower bound to avoid compiling on extremely short runs. + return int(max(20, final_threshold)) + + @staticmethod + def _apply_torch_compile(model, mode = "default"): + """ + Apply torch.compile to a SentenceTransformer model. + Includes workaround for accelerate's unwrap_model bug. + """ + if hasattr(model, "__getitem__"): + inner_model = model[0].auto_model + compiled = torch.compile(inner_model, mode = mode) + model[0].auto_model = compiled + # Fix for accelerate unwrap_model bug: + # When SentenceTransformer contains a compiled inner model, + # accelerate checks has_compiled_regions() which returns True, + # then tries to access model.__dict__["_orig_mod"] which fails. + # This workaround sets _orig_mod to satisfy accelerate. + model.__dict__["_orig_mod"] = model + else: + model = torch.compile(model, mode = mode) + return model + + @staticmethod + def from_pretrained( + model_name, + max_seq_length = None, + dtype = None, + load_in_4bit = False, # Changed default: 4-bit is slow for encoders + load_in_8bit = False, + load_in_16bit = True, # Changed default: 16-bit is optimal for encoders + full_finetuning = False, + token = None, + device_map = "sequential", + rope_scaling = None, + fix_tokenizer = True, + trust_remote_code = False, + use_gradient_checkpointing = False, # Changed default: conflicts with torch.compile + resize_model_vocab = None, + revision = None, + use_exact_model_name = False, + offload_embedding = False, + random_state = 3407, + max_lora_rank = 64, + disable_log_stats = True, + qat_scheme = None, + unsloth_tiled_mlp = False, + pooling_mode = "mean", + for_inference = False, + **kwargs, + ): + try: + from sentence_transformers import SentenceTransformer + from sentence_transformers.models import Transformer, Pooling, Normalize + except ImportError: + raise ImportError( + "Unsloth: To use `FastSentenceTransformer`, you must install `sentence-transformers`.\n" + "Run `pip install sentence-transformers` to install it." + ) + + # if for_inference == True, skip Unsloth optimizations to avoid torch compile issues + if for_inference: + st_device = device_map + if isinstance(st_device, dict) or ( + isinstance(st_device, str) and st_device in ["auto", "sequential"] + ): + st_device = None + + # this was added because when loading for inference it was defaulting to float32 + # propagate dtype to model_kwargs, default to "auto" + model_kwargs = kwargs.get("model_kwargs", {}) + model_kwargs["dtype"] = dtype if dtype is not None else "auto" + + # filter kwargs for SentenceTransformer + st_kwargs = { + "device": st_device, + "trust_remote_code": trust_remote_code, + "token": token, + "revision": revision, + "model_kwargs": model_kwargs, + } + + # add other known kwargs if present + known_keys = [ + "cache_folder", + "truncate_dim", + "tokenizer_kwargs", + "config_kwargs", + ] + for k in known_keys: + if k in kwargs: + st_kwargs[k] = kwargs[k] + + st_model = SentenceTransformer(model_name, **st_kwargs) + return st_model + + # sanity check, thanks Etherl: + if full_finetuning and (load_in_4bit or load_in_8bit): + print( + "Unsloth: You selected full finetuning support, but 4bit / 8bit is enabled - disabling LoRA / QLoRA." + ) + load_in_4bit = False + load_in_8bit = False + load_in_fp8 = False + load_in_16bit = False + + if int(load_in_4bit) + int(load_in_8bit) + int(load_in_16bit) >= 2: + raise RuntimeError( + "Unsloth: Can only load in 4bit or 8bit or 16bit, not a combination!\n" + "Also, we by default set `load_in_16bit = True`.\n" + "If you want 4bit LoRA finetuning, set `load_in_16bit = False` and `load_in_4bit = True`\n" + "If you want 8bit finetuning, set both `load_in_16bit = False` and `load_in_8bit = True`" + ) + + if "auto_model" not in kwargs: + kwargs["auto_model"] = AutoModel + + transformers4 = Version(transformers.__version__).major < 5 + model_type = "" + config = None + try: + config = AutoConfig.from_pretrained( + model_name, token = token, trust_remote_code = trust_remote_code + ) + model_type = getattr(config, "model_type", "") + except: + pass + + # Fast encoder path: Use native torch.compile for encoder models (6x speedup) + # This bypasses Unsloth's auto-compiler which adds @torch.compiler.disable decorators + # that interfere with torch.compile and cause runtime errors for encoder models. + # NOTE: The old Unsloth path is BROKEN for encoder models with torch 2.9+ due to + # conflicting @torch.compile and @torch.compiler.disable decorators. + # Set UNSLOTH_COMPILE_DISABLE=1 to disable torch.compile and use the old path. + is_encoder_model = ( + model_type.lower() in FastSentenceTransformer.ENCODER_MODEL_TYPES + ) + use_fast_encoder = os.environ.get("UNSLOTH_COMPILE_DISABLE", "0") != "1" + if use_fast_encoder and is_encoder_model: + # torch.compile mode: "default" is safest for PEFT/LoRA training + # Note: "reduce-overhead" uses CUDA Graphs which is incompatible with PEFT + compile_mode = "default" + + # Determine dtype - handle float16 machines that don't support bfloat16 + if dtype is None: + if load_in_16bit: + dtype = torch.float16 if not SUPPORTS_BFLOAT16 else torch.bfloat16 + else: + dtype = torch.float32 + elif dtype == torch.bfloat16 and not SUPPORTS_BFLOAT16: + print( + "Unsloth: Device does not support bfloat16. Using float16 instead." + ) + dtype = torch.float16 + + # Determine device + st_device = device_map + if isinstance(st_device, dict) or ( + isinstance(st_device, str) and st_device in ["auto", "sequential"] + ): + st_device = "cuda" + + # Check if model supports SDPA (Scaled Dot Product Attention) for extra speedup + supports_sdpa = False + if config is not None: + try: + model_class = _get_model_class( + config, kwargs.get("auto_model", AutoModel)._model_mapping + ) + supports_sdpa = getattr(model_class, "_supports_sdpa", False) + except: + pass + + # Build model_kwargs for SentenceTransformer + model_kwargs = {"torch_dtype": dtype} + + # Enable SDPA if supported (1.2x extra speedup on top of torch.compile) + if supports_sdpa: + model_kwargs["attn_implementation"] = "sdpa" + + # Print optimization status + sdpa_str = " + SDPA" if supports_sdpa else "" + if load_in_4bit: + print( + f"Unsloth: Using fast encoder path for {model_type} with 4-bit quantization{sdpa_str}" + ) + else: + print( + f"Unsloth: Using fast encoder path for {model_type} (torch.compile{sdpa_str})" + ) + + # Handle 4-bit quantization via BitsAndBytesConfig + if load_in_4bit: + from transformers import BitsAndBytesConfig + + bnb_config = BitsAndBytesConfig( + load_in_4bit = True, + bnb_4bit_compute_dtype = dtype, + bnb_4bit_quant_type = "nf4", + bnb_4bit_use_double_quant = True, + ) + model_kwargs["quantization_config"] = bnb_config + # When using quantization, device must be handled by accelerate + st_device = None + + # Handle gradient checkpointing - warn user it conflicts with torch.compile + _use_gc = use_gradient_checkpointing + if _use_gc and _use_gc != False: + print( + "Unsloth Warning: Gradient checkpointing is incompatible with torch.compile." + ) + print("Disabling torch.compile to enable gradient checkpointing.") + compile_mode = None # Disable compilation + + is_mpnet = "mpnet" == model_type.lower() + + if is_mpnet and transformers4: + FastSentenceTransformer._patch_mpnet_v4() + elif is_mpnet: + FastSentenceTransformer._patch_mpnet_v5() + + # Load via native SentenceTransformer (bypasses Unsloth patching) + st_model = SentenceTransformer( + model_name, + device = st_device, + trust_remote_code = trust_remote_code, + token = token, + revision = revision, + model_kwargs = model_kwargs, + ) + + # Store metadata for get_peft_model + st_model._unsloth_fast_encoder = True + st_model._compile_mode = compile_mode + st_model._dtype = dtype + st_model._load_in_4bit = load_in_4bit + st_model.no_modules = False + + # Add save methods + def _save_pretrained_merged(self, save_directory, **save_kwargs): + self.save_pretrained(save_directory) + tokenizer = save_kwargs.pop("tokenizer", self.tokenizer) + if hasattr(self[0], "auto_model"): + inner = self[0].auto_model + # Handle compiled model + if hasattr(inner, "_orig_mod"): + inner = inner._orig_mod + if hasattr(inner, "merge_and_unload"): + merged = inner.merge_and_unload() + merged.save_pretrained(save_directory) + elif hasattr(inner, "save_pretrained"): + inner.save_pretrained(save_directory) + if tokenizer is not None: + tokenizer.save_pretrained(save_directory) + FastSentenceTransformer._add_unsloth_branding(save_directory) + + st_model.save_pretrained_merged = types.MethodType( + _save_pretrained_merged, st_model + ) + + st_model.save_pretrained_torchao = types.MethodType( + _save_pretrained_torchao, st_model + ) + + st_model.save_pretrained_gguf = types.MethodType( + _save_pretrained_gguf, st_model + ) + + def _push_to_hub_merged(self, repo_id, **push_kwargs): + hub_token = push_kwargs.get("token", None) or get_token() + if hub_token is None: + raise ValueError("No HF token provided") + api = HfApi(token = hub_token) + try: + api.create_repo( + repo_id = repo_id, + private = push_kwargs.get("private"), + exist_ok = True, + repo_type = "model", + ) + except: + pass + FastSentenceTransformer._add_unsloth_tags(repo_id, hub_token) + with tempfile.TemporaryDirectory() as temp_dir: + self.save_pretrained_merged(temp_dir, **push_kwargs) + api.upload_folder( + folder_path = temp_dir, + repo_id = repo_id, + commit_message = push_kwargs.get( + "commit_message", "Upload model" + ), + ) + print(f"Unsloth: Pushed to https://huggingface.co/{repo_id}") + + st_model.push_to_hub_merged = types.MethodType( + _push_to_hub_merged, st_model + ) + + return st_model + + # Warn if using 4-bit with encoder (slow due to dequantization overhead) + if is_encoder_model and load_in_4bit: + print( + "Unsloth Warning: 4-bit quantization adds ~2.3x overhead for encoder models." + ) + print("Consider using load_in_16bit=True for better performance.") + + # check if the model supports add_pooling_layer + if "add_pooling_layer" not in kwargs: + supported = FastSentenceTransformer._has_add_pooling_layer( + config, kwargs.get("auto_model", AutoModel) + ) + if supported: + kwargs["add_pooling_layer"] = False + + # forces fp8 to be False since it's not supported + fp8 = kwargs.pop("load_in_fp8", None) + if fp8: + logging.info("Unsloth: Disabling fp8 for model") + load_in_fp8 = False + + # this is a fix for Snowflake/snowflake-arctic-embed-l-v2.0 + # it has pooler weights which we don't care about for training, + # however unsloth throws an exception if "UNSLOTH_WARN_UNINITIALIZED" == 1 and it sees unused weights + old_environ = os.environ.get("UNSLOTH_WARN_UNINITIALIZED", "1") + os.environ["UNSLOTH_WARN_UNINITIALIZED"] = "0" + + is_distilbert = "distilbert" == model_type.lower() + is_mpnet = "mpnet" == model_type.lower() + + if is_distilbert and transformers4: + FastSentenceTransformer._patch_distilbert_v4() + elif is_distilbert: + FastSentenceTransformer._patch_distilbert_v5() + elif is_mpnet and transformers4: + FastSentenceTransformer._patch_mpnet_v4() + elif is_mpnet: + FastSentenceTransformer._patch_mpnet_v5() + + # check if modules.json exists - if not, force 16-bit training + # why? because i have to implement saving myself for these models, and i don't feel like adding dequantization + # to the save_pretrained_merged for a model that really should be trained in 16-bit anyway + has_modules_json = ( + FastSentenceTransformer._module_path(model_name, token) is not None + ) + + if not has_modules_json and load_in_4bit: + print( + "Unsloth: No modules.json found. This is not a sentence-transformers model.\n" + "Forcing 16-bit loading to simplify merged model saving." + ) + load_in_4bit = False + load_in_16bit = True + + try: + model, tokenizer = FastModel.from_pretrained( + model_name = model_name, + max_seq_length = max_seq_length, + dtype = dtype, + load_in_4bit = load_in_4bit, + load_in_8bit = load_in_8bit, + load_in_16bit = load_in_16bit, + full_finetuning = full_finetuning, + token = token, + device_map = device_map, + rope_scaling = rope_scaling, + fix_tokenizer = fix_tokenizer, + trust_remote_code = trust_remote_code, + use_gradient_checkpointing = use_gradient_checkpointing, + resize_model_vocab = resize_model_vocab, + revision = revision, + return_logits = False, + use_exact_model_name = use_exact_model_name, + offload_embedding = offload_embedding, + random_state = random_state, + max_lora_rank = max_lora_rank, + disable_log_stats = disable_log_stats, + qat_scheme = qat_scheme, + load_in_fp8 = load_in_fp8, + unsloth_tiled_mlp = unsloth_tiled_mlp, + **kwargs, + ) + finally: + os.environ["UNSLOTH_WARN_UNINITIALIZED"] = old_environ + + # try to load modules, otherwise fallback to old hard-coded modules + from sentence_transformers import SentenceTransformer + + modules, no_modules = FastSentenceTransformer._load_modules( + model_name, + token, + model, + tokenizer, + max_seq_length, + pooling_mode, + trust_remote_code = trust_remote_code, + ) + + st_device = device_map + if isinstance(st_device, dict) or ( + isinstance(st_device, str) and st_device in ["auto", "sequential"] + ): + st_device = None + + st_model = SentenceTransformer(modules = modules, device = st_device) + st_model.no_modules = no_modules + + def _save_pretrained_merged(self, save_directory, **kwargs): + # check which adapter files exist before save_pretrained + adapter_files = ["adapter_model.safetensors", "adapter_config.json"] + existing_before = { + f + for f in adapter_files + if os.path.exists(os.path.join(save_directory, f)) + } + + # sentence-transformers config and modules only get saved if we call save_pretrained + self.save_pretrained(save_directory) + + # remove LoRA adapters only if they were created by save_pretrained (not pre-existing) + for file in adapter_files: + if file not in existing_before: + try: + os.remove(os.path.join(save_directory, file)) + except: + pass + + tokenizer = kwargs.pop("tokenizer", self.tokenizer) + if self.no_modules: + # fallback for non-sentence-transformers models + print( + "Unsloth: No modules detected. Using standard merge_and_unload for saving..." + ) + safe_kwargs = kwargs.copy() + # filter out Unsloth-specific args that are not in huggingface's save_pretrained + unsloth_args = [ + "save_method", + "temporary_location", + "maximum_memory_usage", + ] + for k in unsloth_args: + safe_kwargs.pop(k, None) + + merged_model = self[0].auto_model.merge_and_unload() + merged_model.save_pretrained(save_directory, **safe_kwargs) + if tokenizer is not None: + tokenizer.save_pretrained(save_directory) + else: + self[0].auto_model.save_pretrained_merged( + save_directory, tokenizer = tokenizer, **kwargs + ) + + # add Unsloth branding to the generated README + try: + FastSentenceTransformer._add_unsloth_branding(save_directory) + except Exception as e: + print(f"Unsloth Warning: Failed to add branding to README: {e}") + + st_model.save_pretrained_merged = types.MethodType( + _save_pretrained_merged, st_model + ) + + st_model.save_pretrained_torchao = types.MethodType( + _save_pretrained_torchao, st_model + ) + + st_model.save_pretrained_gguf = types.MethodType( + _save_pretrained_gguf, st_model + ) + + def _push_to_hub_merged(self, repo_id, **kwargs): + token = kwargs.get("token", None) or get_token() + if token is None: + raise ValueError( + "No HF token provided. Please provide a token or login with `hf auth login`" + ) + private = kwargs.get("private", None) + commit_message = kwargs.get("commit_message", "Upload model") + + from huggingface_hub import HfApi + + api = HfApi(token = token) + try: + api.create_repo( + repo_id = repo_id, + private = private, + exist_ok = True, + repo_type = "model", + ) + except: + pass + + # order doesn't seem to matter for this after repo creation... + FastSentenceTransformer._add_unsloth_tags(repo_id, token) + + with tempfile.TemporaryDirectory() as temp_dir: + self.save_pretrained_merged(temp_dir, **kwargs) + api.upload_folder( + folder_path = temp_dir, + repo_id = repo_id, + commit_message = commit_message, + ) + print( + f"Unsloth: Successfully pushed merged model to https://huggingface.co/{repo_id}" + ) + + st_model.push_to_hub_merged = types.MethodType(_push_to_hub_merged, st_model) + return st_model + + @staticmethod + def get_peft_model( + model, + r = 16, + target_modules = [ + "query", + "key", + "value", + "dense", + ], + lora_alpha = 16, + lora_dropout = 0.0, + bias = "none", + layers_to_transform = None, + layers_pattern = None, + use_gradient_checkpointing = False, # Changed default: conflicts with torch.compile + random_state = 3407, + max_seq_length = 2048, + use_rslora = False, + modules_to_save = None, + init_lora_weights = True, + loftq_config = {}, + **kwargs, + ): + from sentence_transformers import SentenceTransformer + from peft import LoraConfig, get_peft_model as peft_get_peft_model + + if "task_type" not in kwargs: + kwargs["task_type"] = "FEATURE_EXTRACTION" + print("Setting task_type to FEATURE_EXTRACTION") + + if isinstance(model, SentenceTransformer): + # Check if this is a fast encoder model (uses torch.compile instead of Unsloth patching) + is_fast_encoder = getattr(model, "_unsloth_fast_encoder", False) + + if is_fast_encoder: + # Fast encoder path: Use native PEFT + torch.compile (6x speedup) + transformer_module = model[0] + inner_model = transformer_module.auto_model + + # Check if model is quantized (4-bit/8-bit) + is_quantized = ( + getattr(inner_model, "is_quantized", False) + or getattr(inner_model.config, "quantization_config", None) + is not None + ) + + # Track if gradient checkpointing was actually enabled + gc_enabled = False + + # this is needed when from_pretrained was called without gradient + # checkpointing but get_peft_model requests it + if use_gradient_checkpointing and use_gradient_checkpointing != False: + import transformers + from packaging.version import Version + + transformers4 = Version(transformers.__version__).major < 5 + model_type = getattr(inner_model.config, "model_type", "").lower() + + if model_type == "mpnet" and transformers4: + FastSentenceTransformer._patch_mpnet_v4() + elif model_type == "mpnet": + FastSentenceTransformer._patch_mpnet_v5() + + # Prepare for k-bit training if quantized + if is_quantized: + from ._utils import prepare_model_for_kbit_training + + _gc_for_kbit = ( + use_gradient_checkpointing + if use_gradient_checkpointing + else False + ) + try: + inner_model = prepare_model_for_kbit_training( + inner_model, + use_gradient_checkpointing = _gc_for_kbit, + ) + print("Unsloth: Prepared quantized model for k-bit training") + gc_enabled = bool(_gc_for_kbit) + except ValueError as e: + if "does not support gradient checkpointing" in str(e): + # Model doesn't support gradient checkpointing, disable it + print( + f"Unsloth Warning: {inner_model.__class__.__name__} does not support gradient checkpointing. Skipping." + ) + inner_model = prepare_model_for_kbit_training( + inner_model, + use_gradient_checkpointing = False, + ) + print( + "Unsloth: Prepared quantized model for k-bit training (without gradient checkpointing)" + ) + else: + raise + + # Enable gradient checkpointing if requested (only for non-quantized, since prepare_model handles it) + elif use_gradient_checkpointing and use_gradient_checkpointing != False: + if hasattr(inner_model, "gradient_checkpointing_enable"): + try: + inner_model.gradient_checkpointing_enable() + print("Unsloth: Enabled gradient checkpointing") + gc_enabled = True + except ValueError as e: + if "does not support gradient checkpointing" in str(e): + print( + f"Unsloth Warning: {inner_model.__class__.__name__} does not support gradient checkpointing. Skipping." + ) + + # Create LoRA config + lora_config = LoraConfig( + r = r, + lora_alpha = lora_alpha, + target_modules = target_modules, + lora_dropout = lora_dropout, + bias = bias, + task_type = kwargs.get("task_type", "FEATURE_EXTRACTION"), + ) + + # Apply PEFT directly (not through FastModel) + peft_model = peft_get_peft_model(inner_model, lora_config) + + # Apply QAT if specified + qat_scheme = kwargs.get("qat_scheme", None) + if qat_scheme is not None: + from ._utils import _prepare_model_for_qat + + peft_model = _prepare_model_for_qat(peft_model, qat_scheme) + + # Determine compile mode (only if not using gradient checkpointing) + compile_mode = getattr(model, "_compile_mode", "default") + # Re-enable torch.compile if gradient checkpointing was requested but couldn't be enabled + if compile_mode is None and not gc_enabled: + compile_mode = "default" + print( + "Unsloth: Re-enabling torch.compile since gradient checkpointing is not supported" + ) + + # Re-assign the peft model back to the transformer module + transformer_module.auto_model = peft_model + + # Store compile info for auto-compile at trainer time + # torch.compile is deferred until training starts so we can check max_steps + if compile_mode is not None: + model._compile_mode = compile_mode + model._compile_threshold = ( + FastSentenceTransformer._estimate_compile_threshold(model) + ) + # Flag to indicate compile has not been applied yet + model._compile_pending = True + print( + f"Unsloth: torch.compile will be applied automatically if max_steps > {model._compile_threshold}" + ) + else: + model._compile_mode = None + model._compile_pending = False + print( + "Unsloth: torch.compile disabled (gradient checkpointing enabled)" + ) + + return model + + # Original path for non-fast-encoder models + transformer_module = model[0] + inner_model = transformer_module.auto_model + + peft_model = FastModel.get_peft_model( + model = inner_model, + r = r, + target_modules = target_modules, + lora_alpha = lora_alpha, + lora_dropout = lora_dropout, + bias = bias, + layers_to_transform = layers_to_transform, + layers_pattern = layers_pattern, + use_gradient_checkpointing = use_gradient_checkpointing, + random_state = random_state, + max_seq_length = max_seq_length, + use_rslora = use_rslora, + modules_to_save = modules_to_save, + init_lora_weights = init_lora_weights, + loftq_config = loftq_config, + **kwargs, + ) + + # re-assign the peft model back to the transformer module + transformer_module.auto_model = peft_model + return model + else: + return FastModel.get_peft_model( + model = model, + r = r, + target_modules = target_modules, + lora_alpha = lora_alpha, + lora_dropout = lora_dropout, + bias = bias, + layers_to_transform = layers_to_transform, + layers_pattern = layers_pattern, + use_gradient_checkpointing = use_gradient_checkpointing, + random_state = random_state, + max_seq_length = max_seq_length, + use_rslora = use_rslora, + modules_to_save = modules_to_save, + init_lora_weights = init_lora_weights, + loftq_config = loftq_config, + **kwargs, + ) + + +def _patch_sentence_transformer_trainer(): + """ + Patch SentenceTransformerTrainer to automatically apply torch.compile + when training steps exceed the breakeven threshold. + + This is called automatically when this module is imported. + """ + try: + from sentence_transformers import SentenceTransformerTrainer + except ImportError: + return # sentence_transformers not installed + + if getattr(SentenceTransformerTrainer, "_unsloth_auto_compile_patched", False): + return # Already patched + + from functools import wraps + + _original_init = SentenceTransformerTrainer.__init__ + + @wraps(_original_init) + def _patched_init(self, *args, **kwargs): + # Extract model and training_args + model = kwargs.get("model") or (args[0] if args else None) + training_args = kwargs.get("args") or (args[1] if len(args) > 1 else None) + + # Check if model has pending compile + if ( + model is not None + and training_args is not None + and getattr(model, "_compile_pending", False) + ): + max_steps = getattr(training_args, "max_steps", -1) + compile_mode = getattr(model, "_compile_mode", "default") + + # Re-estimate threshold now that training args are available + batch_size = getattr(training_args, "per_device_train_batch_size", None) + grad_accum = getattr(training_args, "gradient_accumulation_steps", None) + max_seq_length = getattr(model, "max_seq_length", None) + if max_seq_length is None and hasattr(model, "__getitem__"): + try: + max_seq_length = getattr(model[0], "max_seq_length", None) + except Exception: + max_seq_length = None + if max_seq_length is None: + tokenizer = getattr(model, "tokenizer", None) + max_seq_length = ( + getattr(tokenizer, "model_max_length", None) + if tokenizer is not None + else None + ) + + threshold = FastSentenceTransformer._estimate_compile_threshold( + model, + batch_size = batch_size, + grad_accum = grad_accum, + max_seq_length = max_seq_length, + ) + model._compile_threshold = threshold + + if max_steps > 0 and max_steps >= threshold: + print( + f"Unsloth: Auto-compiling model ({max_steps} steps >= {threshold} threshold)" + ) + FastSentenceTransformer._apply_torch_compile(model, mode = compile_mode) + model._compile_pending = False + elif max_steps > 0: + print( + f"Unsloth: Skipping torch.compile ({max_steps} steps < {threshold} threshold)" + ) + model._compile_pending = False + + # Call original __init__ + _original_init(self, *args, **kwargs) + + SentenceTransformerTrainer.__init__ = _patched_init + SentenceTransformerTrainer._unsloth_auto_compile_patched = True + + +# Auto-patch trainer on module import +_patch_sentence_transformer_trainer() From e0bca1692a72bbf00ac5e3d38b30e241b1c4fb28 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 22 Jan 2026 07:40:51 -0800 Subject: [PATCH 164/167] Update vision.py --- unsloth/models/vision.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index a77cf715fc..6835f2e986 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -117,8 +117,6 @@ except: HAS_TORCH_DTYPE = "torch_dtype" in PretrainedConfig.__doc__ -from transformers import GenerationConfig, CompileConfig - _compile_config = CompileConfig( fullgraph = False, dynamic = None, From 4f8a8c04a60a80f321b33c3628b6ac3736c88dc6 Mon Sep 17 00:00:00 2001 From: Michael Han <107991372+shimmyshimmer@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:22:03 -0800 Subject: [PATCH 165/167] Embedding model support --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ff8dcdeef6..8f1e1693fb 100644 --- a/README.md +++ b/README.md @@ -23,18 +23,18 @@ Notebooks are beginner friendly. Read our [guide](https://unsloth.ai/docs/get-st | Model | Free Notebooks | Performance | Memory use | |-----------|---------|--------|----------| | **gpt-oss (20B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/gpt-oss-(20B)-Fine-tuning.ipynb) | 1.5x faster | 70% less | -| **Mistral Ministral 3 (3B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Ministral_3_VL_(3B)_Vision.ipynb) | 1.5x faster | 60% less | | **gpt-oss (20B): GRPO** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/gpt-oss-(20B)-GRPO.ipynb) | 2x faster | 80% less | | **Qwen3: Advanced GRPO** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_(4B)-GRPO.ipynb) | 2x faster | 50% less | | **Qwen3-VL (8B): GSPO** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_VL_(8B)-Vision-GRPO.ipynb) | 1.5x faster | 80% less | -| **Gemma 3 (270M)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Gemma3_(270M).ipynb) | 1.7x faster | 60% less | -| **Gemma 3n (4B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Gemma3N_(4B)-Conversational.ipynb) | 1.5x faster | 50% less | -| **DeepSeek-OCR (3B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Deepseek_OCR_(3B).ipynb) | 1.5x faster | 30% less | +| **Gemma 3 (4B) Vision** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Gemma3_(4B)-Vision.ipynb) | 1.7x faster | 60% less | +| **Gemma 3n (e4B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Gemma3N_(4B)-Conversational.ipynb) | 1.5x faster | 50% less | +| **embeddinggemma (300M)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/EmbeddingGemma_(300M).ipynb) | 2x faster | 20% less | +| **Mistral Ministral 3 (3B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Ministral_3_VL_(3B)_Vision.ipynb) | 1.5x faster | 60% less | | **Llama 3.1 (8B) Alpaca** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Llama3.1_(8B)-Alpaca.ipynb) | 2x faster | 70% less | | **Llama 3.2 Conversational** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Llama3.2_(1B_and_3B)-Conversational.ipynb) | 2x faster | 70% less | | **Orpheus-TTS (3B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Orpheus_(3B)-TTS.ipynb) | 1.5x faster | 50% less | -- See all our notebooks for: [Kaggle](https://github.com/unslothai/notebooks?tab=readme-ov-file#-kaggle-notebooks), [GRPO](https://unsloth.ai/docs/get-started/unsloth-notebooks#grpo-reasoning-rl-notebooks), [TTS](https://unsloth.ai/docs/get-started/unsloth-notebooks#text-to-speech-tts-notebooks) & [Vision](https://unsloth.ai/docs/get-started/unsloth-notebooks#vision-multimodal-notebooks) +- See all our notebooks for: [Kaggle](https://github.com/unslothai/notebooks?tab=readme-ov-file#-kaggle-notebooks), [GRPO](https://unsloth.ai/docs/get-started/unsloth-notebooks#grpo-reasoning-rl-notebooks), [TTS](https://unsloth.ai/docs/get-started/unsloth-notebooks#text-to-speech-tts-notebooks), [embedding](https://unsloth.ai/docs/new/embedding-finetuning) & [Vision](https://unsloth.ai/docs/get-started/unsloth-notebooks#vision-multimodal-notebooks) - See [all our models](https://unsloth.ai/docs/get-started/unsloth-model-catalog) and [all our notebooks](https://unsloth.ai/docs/get-started/unsloth-notebooks) - See detailed documentation for Unsloth [here](https://unsloth.ai/docs) @@ -53,7 +53,8 @@ Use our official [Unsloth Docker image](https://hub.docker.com/r/unsloth/unsloth For RTX 50x, B200, 6000 GPUs: `pip install unsloth`. Read our [Blackwell Guide](https://unsloth.ai/docs/basics/fine-tuning-llms-with-blackwell-rtx-50-series-and-unsloth) and [DGX Spark Guide](https://unsloth.ai/docs/basics/fine-tuning-llms-with-nvidia-dgx-spark-and-unsloth) for more details. ## 🦥 Unsloth News -- New 7x longer context reinforcement learning vs. all other setups, via our new batching algorithms. [Blog](https://unsloth.ai/docs/new/grpo-long-context) +- **Embedding models**: Unsloth now supports ~1.8-3.3x faster embedding fine-tuning. [Blog](https://unsloth.ai/docs/new/embedding-finetuning) • [Notebooks](https://unsloth.ai/docs/get-started/unsloth-notebooks#embedding-models) +- New **7x longer context RL** vs. all other setups, via our new batching algorithms. [Blog](https://unsloth.ai/docs/new/grpo-long-context) - New RoPE & MLP **Triton Kernels** & **Padding Free + Packing**: 3x faster training & 30% less VRAM. [Blog](https://unsloth.ai/docs/new/3x-faster-training-packing) - **Mistral 3**: Run Ministral 3 or Devstral 2 and fine-tune with vision/RL sodoku notebooks. [Guide](https://unsloth.ai/docs/models/ministral-3) • [Notebooks](https://unsloth.ai/docs/models/ministral-3#fine-tuning-ministral-3) - **500K Context**: Training a 20B model with >500K context is now possible on an 80GB GPU. [Blog](https://unsloth.ai/docs/new/500k-context-length-fine-tuning) @@ -99,7 +100,7 @@ For RTX 50x, B200, 6000 GPUs: `pip install unsloth`. Read our [Blackwell Guide]( ## ⭐ Key Features * Supports **full-finetuning**, pretraining, 4b-bit, 16-bit and **FP8** training -* Supports **all models** including [TTS](https://unsloth.ai/docs/basics/text-to-speech-tts-fine-tuning), multimodal, [BERT](https://unsloth.ai/docs/get-started/unsloth-notebooks#other-important-notebooks) and more! Any model that works in transformers, works in Unsloth. +* Supports **all models** including [TTS](https://unsloth.ai/docs/basics/text-to-speech-tts-fine-tuning), multimodal, [embedding](https://unsloth.ai/docs/new/embedding-finetuning) and more! Any model that works in transformers, works in Unsloth. * The most efficient library for [Reinforcement Learning (RL)](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide), using 80% less VRAM. Supports GRPO, GSPO, DrGRPO, DAPO etc. * **0% loss in accuracy** - no approximation methods - all exact. * Export and [deploy your model](https://unsloth.ai/docs/basics/inference-and-deployment) to GGUF, llama.cpp, vLLM, SGLang and Hugging Face. From 0c50a51e0becc16ea029c22ff9574d7b0bb01a81 Mon Sep 17 00:00:00 2001 From: Rachel Li Date: Thu, 22 Jan 2026 18:46:08 -0500 Subject: [PATCH 166/167] Guard torch.compile on ROCm when triton_key is missing (#3923) * Guard torch.compile on ROCm when triton_key missing * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update unsloth/import_fixes.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Tighten ROCm Triton import handling * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Rachel Li Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- unsloth/__init__.py | 3 +++ unsloth/import_fixes.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/unsloth/__init__.py b/unsloth/__init__.py index d3093cf4c0..0b819a546e 100644 --- a/unsloth/__init__.py +++ b/unsloth/__init__.py @@ -127,6 +127,7 @@ from .import_fixes import ( fix_vllm_aimv2_issue, fix_vllm_guided_decoding_params, fix_vllm_pdl_blackwell, + fix_rocm_triton_key_error, ignore_logger_messages, patch_ipykernel_hf_xet, patch_trackio, @@ -141,6 +142,7 @@ fix_xformers_performance_issue() fix_vllm_aimv2_issue() fix_vllm_guided_decoding_params() fix_vllm_pdl_blackwell() +fix_rocm_triton_key_error() ignore_logger_messages() patch_ipykernel_hf_xet() patch_trackio() @@ -154,6 +156,7 @@ del fix_xformers_performance_issue del fix_vllm_aimv2_issue del fix_vllm_guided_decoding_params del fix_vllm_pdl_blackwell +del fix_rocm_triton_key_error del ignore_logger_messages del patch_ipykernel_hf_xet del patch_trackio diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 4f88808c2a..89fd152857 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -666,6 +666,39 @@ def fix_huggingface_hub(): ) +def fix_rocm_triton_key_error(): + """ + ROCm + torch.compile can fail if Triton lacks `triton_key`. + Disable Inductor/compile only on ROCm when that symbol is missing. + """ + try: + import torch + except (ImportError, ModuleNotFoundError): + return + + if not getattr(torch.version, "hip", None): + return + + try: + import triton + except (ImportError, ModuleNotFoundError): + return + + try: + from triton.runtime import triton_key # noqa: F401 + + return + except ImportError: + pass + + os.environ.setdefault("TORCHINDUCTOR_DISABLE", "1") + os.environ.setdefault("TORCH_COMPILE_DISABLE", "1") + logger.info( + "Unsloth: ROCm detected and Triton lacks triton_key; " + "disabling torch.compile/Inductor to avoid backend crash." + ) + + def fix_vllm_pdl_blackwell(): """ Fix vLLM PDL (Programmatic Dependent Launch) bug on Blackwell GPUs (SM100). From 66ec24937936f70074f890926aae4291ae4c8d5c Mon Sep 17 00:00:00 2001 From: Michael Han <107991372+shimmyshimmer@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:35:46 -0800 Subject: [PATCH 167/167] Embedding model fine-tuning support --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8f1e1693fb..8ddfa80c35 100644 --- a/README.md +++ b/README.md @@ -56,20 +56,19 @@ For RTX 50x, B200, 6000 GPUs: `pip install unsloth`. Read our [Blackwell Guide]( - **Embedding models**: Unsloth now supports ~1.8-3.3x faster embedding fine-tuning. [Blog](https://unsloth.ai/docs/new/embedding-finetuning) • [Notebooks](https://unsloth.ai/docs/get-started/unsloth-notebooks#embedding-models) - New **7x longer context RL** vs. all other setups, via our new batching algorithms. [Blog](https://unsloth.ai/docs/new/grpo-long-context) - New RoPE & MLP **Triton Kernels** & **Padding Free + Packing**: 3x faster training & 30% less VRAM. [Blog](https://unsloth.ai/docs/new/3x-faster-training-packing) -- **Mistral 3**: Run Ministral 3 or Devstral 2 and fine-tune with vision/RL sodoku notebooks. [Guide](https://unsloth.ai/docs/models/ministral-3) • [Notebooks](https://unsloth.ai/docs/models/ministral-3#fine-tuning-ministral-3) - **500K Context**: Training a 20B model with >500K context is now possible on an 80GB GPU. [Blog](https://unsloth.ai/docs/new/500k-context-length-fine-tuning) - **FP8 Reinforcement Learning**: You can now do FP8 GRPO on consumer GPUs. [Blog](https://unsloth.ai/docs/new/fp8-reinforcement-learning) • [Notebook](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_8B_FP8_GRPO.ipynb) - **DeepSeek-OCR**: Fine-tune to improve language understanding by 89%. [Guide](https://unsloth.ai/docs/models/deepseek-ocr-how-to-run-and-fine-tune) • [Notebook](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Deepseek_OCR_(3B).ipynb) - **Docker**: Use Unsloth with no setup & environment issues with our new image. [Guide](https://unsloth.ai/docs/new/how-to-fine-tune-llms-with-unsloth-and-docker) • [Docker image](https://hub.docker.com/r/unsloth/unsloth) -- **gpt-oss RL**: Introducing the fastest possible inference for gpt-oss RL! [Read blog](https://unsloth.ai/docs/models/gpt-oss-how-to-run-and-fine-tune/gpt-oss-reinforcement-learning) - **Vision RL**: You can now train VLMs with GRPO or GSPO in Unsloth! [Read guide](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide/vision-reinforcement-learning-vlm-rl) -- **gpt-oss** by OpenAI: Read our [Unsloth Flex Attention](https://unsloth.ai/docs/models/gpt-oss-how-to-run-and-fine-tune/long-context-gpt-oss-training) blog and [gpt-oss Guide](https://unsloth.ai/docs/models/gpt-oss-how-to-run-and-fine-tune). 20B works on 14GB VRAM. 120B on 65GB. +- **gpt-oss** by OpenAI: Read our [RL blog](https://unsloth.ai/docs/models/gpt-oss-how-to-run-and-fine-tune/gpt-oss-reinforcement-learning), [Flex Attention](https://unsloth.ai/docs/models/gpt-oss-how-to-run-and-fine-tune/long-context-gpt-oss-training) blog and [gpt-oss Guide](https://unsloth.ai/docs/models/gpt-oss-how-to-run-and-fine-tune). 20B works on 14GB VRAM. 120B on 65GB.
Click for more news - **Quantization-Aware Training**: We collabed with Pytorch, recovering ~70% accuracy. [Read blog](https://unsloth.ai/docs/basics/quantization-aware-training-qat) - **Memory-efficient RL**: We're introducing even better RL. Our new kernels & algos allows faster RL with 50% less VRAM & 10× more context. [Read blog](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide/memory-efficient-rl) +- **Mistral 3**: Run Ministral 3 or Devstral 2 and fine-tune with vision/RL sodoku notebooks. [Guide](https://unsloth.ai/docs/models/ministral-3) • [Notebooks](https://unsloth.ai/docs/models/ministral-3#fine-tuning-ministral-3) - **Gemma 3n** by Google: [Read Blog](https://unsloth.ai/docs/models/gemma-3-how-to-run-and-fine-tune/gemma-3n-how-to-run-and-fine-tune). We [uploaded GGUFs, 4-bit models](https://huggingface.co/collections/unsloth/gemma-3n-685d3874830e49e1c93f9339). - **[Text-to-Speech (TTS)](https://unsloth.ai/docs/basics/text-to-speech-tts-fine-tuning)** is now supported, including `sesame/csm-1b` and STT `openai/whisper-large-v3`. - **[Qwen3](https://unsloth.ai/docs/models/qwen3-how-to-run-and-fine-tune)** is now supported. Qwen3-30B-A3B fits on 17.5GB VRAM.