From 5864dece26995d3553f418e593f0edce5bed4a99 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Tue, 17 Feb 2026 22:53:29 +0000 Subject: [PATCH 01/11] fix: fix: stream HF datasets in check-format endpoint to avoid full downloads; add info logging to model config endpoints --- studio/backend/routes/datasets.py | 37 ++++++++++++++++++++++--------- studio/backend/routes/models.py | 4 ++++ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/studio/backend/routes/datasets.py b/studio/backend/routes/datasets.py index 223f701d8c..8b94ddefdc 100644 --- a/studio/backend/routes/datasets.py +++ b/studio/backend/routes/datasets.py @@ -75,12 +75,16 @@ async def check_format(request: CheckFormatRequest): """ Check if a dataset requires manual column mapping. - This is a lightweight check that loads only the first 10 rows, + This is a lightweight check that streams only the first N rows, runs format detection, and (if processable) returns processed preview samples. The full dataset is re-processed at training time. + + For HuggingFace datasets we use streaming mode so we never download + the entire dataset — only the rows we actually need are fetched. """ try: - from datasets import load_dataset + from itertools import islice + from datasets import Dataset, load_dataset from utils.datasets import format_dataset PREVIEW_SIZE = 10 @@ -89,9 +93,10 @@ async def check_format(request: CheckFormatRequest): # Load dataset dataset_path = Path(request.dataset_name) + total_rows = None if dataset_path.exists(): - # Local dataset + # Local dataset — direct load is fine (files are local) if dataset_path.suffix in ['.json', '.jsonl']: dataset = load_dataset('json', data_files=str(dataset_path), split=request.train_split) elif dataset_path.suffix == '.csv': @@ -103,18 +108,30 @@ async def check_format(request: CheckFormatRequest): status_code=400, detail=f"Unsupported file format: {dataset_path.suffix}" ) + total_rows = len(dataset) + preview_slice = dataset.select(range(min(PREVIEW_SIZE, total_rows))) else: - # HuggingFace dataset - load_kwargs = {"path": request.dataset_name, "split": request.train_split} + # HuggingFace dataset — use STREAMING to avoid downloading everything + load_kwargs = {"path": request.dataset_name, "split": request.train_split, "streaming": True} if request.subset: load_kwargs["name"] = request.subset if request.hf_token: load_kwargs["token"] = request.hf_token - dataset = load_dataset(**load_kwargs) - - # Slice to top N rows — all detection and preview runs on this subset - total_rows = len(dataset) - preview_slice = dataset.select(range(min(PREVIEW_SIZE, total_rows))) + + streamed_ds = load_dataset(**load_kwargs) + + # Take only the first PREVIEW_SIZE rows from the stream + rows = list(islice(streamed_ds, PREVIEW_SIZE)) + if not rows: + raise HTTPException( + status_code=400, + detail="Dataset appears to be empty or could not be streamed" + ) + + # Convert list-of-dicts into a proper Dataset for downstream compat + preview_slice = Dataset.from_list(rows) + # total_rows unknown in streaming mode + total_rows = None # Run lightweight format check on the preview slice result = check_dataset_format(preview_slice, is_vlm=request.is_vlm) diff --git a/studio/backend/routes/models.py b/studio/backend/routes/models.py index cc964d3ea3..761c04d3e7 100644 --- a/studio/backend/routes/models.py +++ b/studio/backend/routes/models.py @@ -248,6 +248,7 @@ async def get_model_config( This endpoint wraps the backend load_model_defaults function. """ try: + logger.info(f"Getting model config for: {model_name}") # Load model defaults from backend config_dict = load_model_defaults(model_name) @@ -267,6 +268,7 @@ async def get_model_config( # If ModelConfig creation fails, use defaults pass + logger.info(f"Model config result for {model_name}: is_vision={is_vision}, is_lora={is_lora}, base_model={base_model}") return ModelDetails( id=model_name, model_name=model_name, @@ -369,8 +371,10 @@ async def check_vision_model( This endpoint wraps the backend is_vision_model function. """ try: + logger.info(f"Checking if vision model: {model_name}") is_vision = is_vision_model(model_name) + logger.info(f"Vision check result for {model_name}: is_vision={is_vision}") return VisionCheckResponse( model_name=model_name, is_vision=is_vision, From 3d0d1c7020147f351b5ee6e1da3d8c83c7de4d0d Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Tue, 17 Feb 2026 23:12:45 +0000 Subject: [PATCH 02/11] fix: cap dataset.map() num_proc to 8 to prevent CUDA fork deadlocks --- studio/backend/utils/datasets/chat_templates.py | 4 ++-- studio/backend/utils/datasets/format_conversion.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/studio/backend/utils/datasets/chat_templates.py b/studio/backend/utils/datasets/chat_templates.py index e3b1bef315..3f6d73377e 100644 --- a/studio/backend/utils/datasets/chat_templates.py +++ b/studio/backend/utils/datasets/chat_templates.py @@ -285,7 +285,7 @@ def apply_chat_template_to_dataset( if not isinstance(dataset, IterableDataset): from multiprocessing import cpu_count if num_proc is None or type(num_proc) is not int: - num_proc = cpu_count() + num_proc = min(cpu_count(), 8) dataset_map_kwargs['num_proc'] = num_proc dataset_map_kwargs['desc'] = "Applying template to Alpaca format" @@ -349,7 +349,7 @@ def apply_chat_template_to_dataset( if not isinstance(dataset, IterableDataset): from multiprocessing import cpu_count if num_proc is None or type(num_proc) is not int: - num_proc = cpu_count() + num_proc = min(cpu_count(), 8) dataset_map_kwargs['num_proc'] = num_proc dataset_map_kwargs['desc'] = f"Applying chat template to {final_format}" diff --git a/studio/backend/utils/datasets/format_conversion.py b/studio/backend/utils/datasets/format_conversion.py index 9367741e8e..df2ff95fc8 100644 --- a/studio/backend/utils/datasets/format_conversion.py +++ b/studio/backend/utils/datasets/format_conversion.py @@ -110,7 +110,7 @@ def standardize_chat_format( from multiprocessing import cpu_count if num_proc is None or type(num_proc) is not int: - num_proc = cpu_count() + num_proc = min(cpu_count(), 8) dataset_map_kwargs['num_proc'] = num_proc dataset_map_kwargs['desc'] = "Standardizing chat format" @@ -176,7 +176,7 @@ def convert_chatml_to_alpaca(dataset, batch_size=1000, num_proc=None): from multiprocessing import cpu_count if num_proc is None or type(num_proc) is not int: - num_proc = cpu_count() + num_proc = min(cpu_count(), 8) dataset_map_kwargs['num_proc'] = num_proc dataset_map_kwargs['desc'] = "Converting ChatML to Alpaca format" @@ -224,7 +224,7 @@ def convert_alpaca_to_chatml(dataset, batch_size=1000, num_proc=None): from multiprocessing import cpu_count if num_proc is None or type(num_proc) is not int: - num_proc = cpu_count() + num_proc = min(cpu_count(), 8) dataset_map_kwargs['num_proc'] = num_proc dataset_map_kwargs['desc'] = "Converting Alpaca to ChatML format" From 6f1b78217202107632bb4f24d9b53e62f06ed6b4 Mon Sep 17 00:00:00 2001 From: Roland Tannous <115670425+rolandtannous@users.noreply.github.com> Date: Wed, 18 Feb 2026 03:58:44 +0400 Subject: [PATCH 03/11] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d2092b3a9..3d2810fae5 100644 --- a/README.md +++ b/README.md @@ -45,12 +45,14 @@ This script will: ### Launch the studio ```bash -# After setup, open a new terminal (or source ~/.bashrc), then: +# After setup, open a new terminal (or source ~/.bashrc), then inside your working directory: unsloth-ui -H 0.0.0.0 -p 8000 ``` On **first launch**, a one-time setup token is printed to the console. Use it in the browser to create your admin account. +As this repo is in continuous development, please make sure to run the setup.sh file everytime you pull new changes from the repo. + ## API Reference All endpoints require a valid JWT `Authorization: Bearer ` header (except `/api/auth/*` and `/api/health`). From 42ee6178ae73a0f71d57a7c47ec183b315dbed27 Mon Sep 17 00:00:00 2001 From: Manan17 Date: Wed, 18 Feb 2026 00:11:27 +0000 Subject: [PATCH 04/11] linear fix --- studio/frontend/src/features/training/api/mappers.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/studio/frontend/src/features/training/api/mappers.ts b/studio/frontend/src/features/training/api/mappers.ts index 510cc542b8..f3b0629754 100644 --- a/studio/frontend/src/features/training/api/mappers.ts +++ b/studio/frontend/src/features/training/api/mappers.ts @@ -46,7 +46,9 @@ export function buildTrainingStartPayload( lora_r: config.loraRank, lora_alpha: config.loraAlpha, lora_dropout: config.loraDropout, - target_modules: adapterMethod ? config.targetModules : [], + target_modules: adapterMethod + ? config.targetModules.filter((m) => m !== "all-linear") + : [], gradient_checkpointing: config.gradientCheckpointing, use_rslora: config.loraVariant === "rslora", use_loftq: config.loraVariant === "loftq", From 064cd56a21de16f548f5dfa7e91e5fa7ac6626d7 Mon Sep 17 00:00:00 2001 From: imagineer99 Date: Tue, 17 Feb 2026 23:27:01 +0000 Subject: [PATCH 05/11] feat: disable navbar navigation while training is active Disable Export and Chat nav items (desktop + mobile) when isTrainingRunning is true, keeping only Studio clickable. --- studio/frontend/src/components/navbar.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/studio/frontend/src/components/navbar.tsx b/studio/frontend/src/components/navbar.tsx index 6a8fd5d7ca..2828cb61f8 100644 --- a/studio/frontend/src/components/navbar.tsx +++ b/studio/frontend/src/components/navbar.tsx @@ -21,6 +21,7 @@ import { ZapIcon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; +import { useTrainingRuntimeStore } from "@/features/training"; import { Link, useRouterState } from "@tanstack/react-router"; import { AnimatePresence, motion } from "motion/react"; import { useState } from "react"; @@ -35,6 +36,7 @@ const NAV_ITEMS = [ export function Navbar() { const pathname = useRouterState({ select: (s) => s.location.pathname }); + const isTrainingRunning = useTrainingRuntimeStore((s) => s.isTrainingRunning); const [logoHovered, setLogoHovered] = useState(false); const [mobileOpen, setMobileOpen] = useState(false); @@ -95,7 +97,9 @@ export function Navbar() { > {NAV_ITEMS.map((item) => { const active = pathname === item.href; - if (!item.enabled) { + const disabledByTraining = + isTrainingRunning && item.href !== "/studio"; + if (!item.enabled || disabledByTraining) { return ( {NAV_ITEMS.filter((item) => item.enabled).map((item) => { const active = pathname === item.href; + const disabledByTraining = + isTrainingRunning && item.href !== "/studio"; + if (disabledByTraining) { + return ( + + {item.label} + + ); + } return ( Date: Wed, 18 Feb 2026 07:08:32 +0000 Subject: [PATCH 06/11] fix the linear path on backend --- studio/backend/core/training/trainer.py | 8 +++++++- studio/frontend/src/features/training/api/mappers.ts | 4 +--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/studio/backend/core/training/trainer.py b/studio/backend/core/training/trainer.py index 9468e282ef..9de384cbe3 100644 --- a/studio/backend/core/training/trainer.py +++ b/studio/backend/core/training/trainer.py @@ -214,7 +214,13 @@ class UnslothTrainer: return True # LoRA/QLoRA mode - apply PEFT - if target_modules is None or (isinstance(target_modules, list) and len(target_modules) == 0): + # "all-linear" is a PEFT keyword that targets every linear layer + if isinstance(target_modules, list) and "all-linear" in target_modules: + if len(target_modules) == 1: + target_modules = "all-linear" + else: + target_modules = [m for m in target_modules if m != "all-linear"] + elif target_modules is None or (isinstance(target_modules, list) and len(target_modules) == 0): target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"] diff --git a/studio/frontend/src/features/training/api/mappers.ts b/studio/frontend/src/features/training/api/mappers.ts index f3b0629754..510cc542b8 100644 --- a/studio/frontend/src/features/training/api/mappers.ts +++ b/studio/frontend/src/features/training/api/mappers.ts @@ -46,9 +46,7 @@ export function buildTrainingStartPayload( lora_r: config.loraRank, lora_alpha: config.loraAlpha, lora_dropout: config.loraDropout, - target_modules: adapterMethod - ? config.targetModules.filter((m) => m !== "all-linear") - : [], + target_modules: adapterMethod ? config.targetModules : [], gradient_checkpointing: config.gradientCheckpointing, use_rslora: config.loraVariant === "rslora", use_loftq: config.loraVariant === "loftq", From c37bf686a69ed69846cf6b89487f6f9ff5fd119c Mon Sep 17 00:00:00 2001 From: Manan17 Date: Wed, 18 Feb 2026 07:59:57 +0000 Subject: [PATCH 07/11] Dividing the total cpu_count // 3 --- studio/backend/core/training/trainer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/studio/backend/core/training/trainer.py b/studio/backend/core/training/trainer.py index 9de384cbe3..df742f64f0 100644 --- a/studio/backend/core/training/trainer.py +++ b/studio/backend/core/training/trainer.py @@ -699,6 +699,7 @@ class UnslothTrainer: "output_dir": output_dir, "report_to": ["wandb"] if training_args.get('enable_wandb', False) else "none", "include_num_input_tokens_seen": True, # Enable token counting + "dataset_num_proc": max(1, os.cpu_count() // 3), } # Add warmup parameter - use warmup_ratio if provided, otherwise warmup_steps From 76cd1dc24c7007c90c7ac7a1f1400329144df3ae Mon Sep 17 00:00:00 2001 From: Manan17 Date: Wed, 18 Feb 2026 08:18:13 +0000 Subject: [PATCH 08/11] fixing the hangup of training after multiple back to back training processes --- studio/backend/core/training/trainer.py | 8 ++++++++ studio/backend/utils/hardware/hardware.py | 1 + 2 files changed, 9 insertions(+) diff --git a/studio/backend/core/training/trainer.py b/studio/backend/core/training/trainer.py index df742f64f0..396c6c4956 100644 --- a/studio/backend/core/training/trainer.py +++ b/studio/backend/core/training/trainer.py @@ -115,6 +115,14 @@ class UnslothTrainer: is_dataset_multimodal: bool = False) -> bool: """Load model for training (supports both text and vision models)""" try: + if self.model is not None: + del self.model + if self.tokenizer is not None: + del self.tokenizer + + if self.trainer is not None: + del self.trainer + print("\nClearing GPU memory before training...") clear_gpu_cache() diff --git a/studio/backend/utils/hardware/hardware.py b/studio/backend/utils/hardware/hardware.py index 754ef6fdae..0d7cc97cfb 100644 --- a/studio/backend/utils/hardware/hardware.py +++ b/studio/backend/utils/hardware/hardware.py @@ -120,6 +120,7 @@ def clear_gpu_cache(): if device == DeviceType.CUDA: import torch + torch.cuda.synchronize() torch.cuda.empty_cache() torch.cuda.ipc_collect() elif device == DeviceType.MLX: From d69431fa57bbfdcf89ef72eb7eb7fdd98dfc30ae Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Wed, 18 Feb 2026 08:38:53 +0000 Subject: [PATCH 09/11] Scale dataset num_proc dynamically to cpu_count//3 instead of hardcap 8 --- studio/backend/utils/datasets/chat_templates.py | 4 ++-- studio/backend/utils/datasets/format_conversion.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/studio/backend/utils/datasets/chat_templates.py b/studio/backend/utils/datasets/chat_templates.py index 3f6d73377e..6420aa899d 100644 --- a/studio/backend/utils/datasets/chat_templates.py +++ b/studio/backend/utils/datasets/chat_templates.py @@ -285,7 +285,7 @@ def apply_chat_template_to_dataset( if not isinstance(dataset, IterableDataset): from multiprocessing import cpu_count if num_proc is None or type(num_proc) is not int: - num_proc = min(cpu_count(), 8) + num_proc = max(1, cpu_count() // 3) dataset_map_kwargs['num_proc'] = num_proc dataset_map_kwargs['desc'] = "Applying template to Alpaca format" @@ -349,7 +349,7 @@ def apply_chat_template_to_dataset( if not isinstance(dataset, IterableDataset): from multiprocessing import cpu_count if num_proc is None or type(num_proc) is not int: - num_proc = min(cpu_count(), 8) + num_proc = max(1, cpu_count() // 3) dataset_map_kwargs['num_proc'] = num_proc dataset_map_kwargs['desc'] = f"Applying chat template to {final_format}" diff --git a/studio/backend/utils/datasets/format_conversion.py b/studio/backend/utils/datasets/format_conversion.py index df2ff95fc8..a97db20ba4 100644 --- a/studio/backend/utils/datasets/format_conversion.py +++ b/studio/backend/utils/datasets/format_conversion.py @@ -110,7 +110,7 @@ def standardize_chat_format( from multiprocessing import cpu_count if num_proc is None or type(num_proc) is not int: - num_proc = min(cpu_count(), 8) + num_proc = max(1, cpu_count() // 3) dataset_map_kwargs['num_proc'] = num_proc dataset_map_kwargs['desc'] = "Standardizing chat format" @@ -176,7 +176,7 @@ def convert_chatml_to_alpaca(dataset, batch_size=1000, num_proc=None): from multiprocessing import cpu_count if num_proc is None or type(num_proc) is not int: - num_proc = min(cpu_count(), 8) + num_proc = max(1, cpu_count() // 3) dataset_map_kwargs['num_proc'] = num_proc dataset_map_kwargs['desc'] = "Converting ChatML to Alpaca format" @@ -224,7 +224,7 @@ def convert_alpaca_to_chatml(dataset, batch_size=1000, num_proc=None): from multiprocessing import cpu_count if num_proc is None or type(num_proc) is not int: - num_proc = min(cpu_count(), 8) + num_proc = max(1, cpu_count() // 3) dataset_map_kwargs['num_proc'] = num_proc dataset_map_kwargs['desc'] = "Converting Alpaca to ChatML format" From 5a02ed4f0f5db71bdc2d664eef0198ac64f11363 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Wed, 18 Feb 2026 08:58:25 +0000 Subject: [PATCH 10/11] Disable flex attention on Blackwell+ GPUs (sm_120+) at startup --- studio/backend/main.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/studio/backend/main.py b/studio/backend/main.py index 3d9e2a0f9a..e027998dcf 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -1,6 +1,7 @@ """ Main FastAPI application for Unsloth UI Backend """ +import os import secrets import shutil from contextlib import asynccontextmanager @@ -15,7 +16,7 @@ from datetime import datetime # Import routers from routes import training_router, models_router, inference_router, datasets_router, auth_router, export_router from auth import storage -from utils.hardware import detect_hardware +from utils.hardware import detect_hardware, get_device, DeviceType import utils.hardware.hardware as _hw_module UNSLOTH_CACHE_DIR = Path(__file__).parent / "unsloth_compiled_cache" @@ -27,6 +28,18 @@ async def lifespan(app: FastAPI): # Detect hardware first — sets DEVICE global used everywhere detect_hardware() + # Disable flex attention on Blackwell+ GPUs (sm_120 and above) + if get_device() == DeviceType.CUDA: + import torch + props = torch.cuda.get_device_properties(0) + sm_version = props.major * 10 + props.minor + if sm_version >= 120: + os.environ["UNSLOTH_FLEX_ATTENTION"] = "0" + import logging + logging.getLogger(__name__).info( + f"GPU sm_{sm_version} detected — setting UNSLOTH_FLEX_ATTENTION=0" + ) + if not storage.is_initialized(): setup_token = secrets.token_urlsafe(32) storage.save_setup_token(setup_token) From d57b2742ab7bebd922fa81b8855ee7365834c02e Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Wed, 18 Feb 2026 09:21:53 +0000 Subject: [PATCH 11/11] renamed UNSLOTH_FLEX_ATTENTION to UNSLOTH_ENABLE_FLEX_ATTENTION --- studio/backend/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/backend/main.py b/studio/backend/main.py index e027998dcf..e7a33750ff 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -34,7 +34,7 @@ async def lifespan(app: FastAPI): props = torch.cuda.get_device_properties(0) sm_version = props.major * 10 + props.minor if sm_version >= 120: - os.environ["UNSLOTH_FLEX_ATTENTION"] = "0" + os.environ["UNSLOTH_ENABLE_FLEX_ATTENTION"] = "0" import logging logging.getLogger(__name__).info( f"GPU sm_{sm_version} detected — setting UNSLOTH_FLEX_ATTENTION=0"