From cdeed53a978a833a4c4c421146c9fb328076e69c Mon Sep 17 00:00:00 2001 From: Leo Borcherding Date: Mon, 23 Feb 2026 13:07:47 -0600 Subject: [PATCH 1/3] fix: disable eval by default, set eval_steps to 0.0 - Changed default eval_steps from 0.01 to 0.0 across backend and frontend - Fixed UI to allow eval_steps=0 (removed min=0.001 constraint) - Added conditional eval logic with helpful console messages - Updated tooltip to explain how to disable evaluation - Tested: confirmed eval disabled by default with eval_steps=0.0 --- studio/backend/core/training/trainer.py | 16 ++++++++++------ studio/backend/core/training/training.py | 2 +- studio/backend/models/training.py | 2 +- studio/frontend/src/config/training.ts | 2 +- .../features/studio/sections/params-section.tsx | 4 ++-- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/studio/backend/core/training/trainer.py b/studio/backend/core/training/trainer.py index 063d87ead6..ff5b66485a 100644 --- a/studio/backend/core/training/trainer.py +++ b/studio/backend/core/training/trainer.py @@ -543,7 +543,7 @@ class UnslothTrainer: def start_training(self, dataset: Dataset, eval_dataset: Dataset = None, - eval_steps: float = 0.01, + eval_steps: float = 0.00, output_dir: str = "./outputs", num_epochs: int = 3, learning_rate: float = 5e-5, @@ -743,12 +743,16 @@ class UnslothTrainer: # ========== EVAL CONFIGURATION ========== eval_dataset = training_args.get('eval_dataset', None) - eval_steps_val = training_args.get('eval_steps', 0.01) + eval_steps_val = training_args.get('eval_steps', 0.00) if eval_dataset is not None: - config_args["eval_strategy"] = "steps" - config_args["eval_steps"] = eval_steps_val - print(f"Evaluation enabled: eval_steps={eval_steps_val} (fraction of total steps)\n") - print(f"Eval dataset: {len(eval_dataset)} rows\n") + if eval_steps_val > 0: + config_args["eval_strategy"] = "steps" + config_args["eval_steps"] = eval_steps_val + print(f"✅ Evaluation enabled: eval_steps={eval_steps_val} (fraction of total steps)\n") + print(f"Eval dataset: {len(eval_dataset)} rows\n") + else: + print(f"⚠️ Eval dataset provided but eval_steps={eval_steps_val} (disabled)\n") + print("To enable evaluation, set eval_steps > 0.0\n") else: print("No eval dataset — evaluation disabled\n") diff --git a/studio/backend/core/training/training.py b/studio/backend/core/training/training.py index 6fe08c2b9e..f5d9be63c1 100644 --- a/studio/backend/core/training/training.py +++ b/studio/backend/core/training/training.py @@ -115,7 +115,7 @@ class TrainingBackend: subset: str = None, train_split: str = "train", eval_split: str = None, - eval_steps: float = 0.01, + eval_steps: float = 0.00, is_dataset_multimodal: bool = False) -> bool: """ Start training. diff --git a/studio/backend/models/training.py b/studio/backend/models/training.py index 2b989e6a82..54de974100 100644 --- a/studio/backend/models/training.py +++ b/studio/backend/models/training.py @@ -21,7 +21,7 @@ class TrainingStartRequest(BaseModel): subset: Optional[str] = None train_split: Optional[str] = Field("train", description="Training split name") eval_split: Optional[str] = Field(None, description="Eval split name. None = auto-detect") - eval_steps: float = Field(0.01, description="Fraction of total steps between evals (0-1)") + eval_steps: float = Field(0.00, description="Fraction of total steps between evals (0-1)") @model_validator(mode="before") @classmethod diff --git a/studio/frontend/src/config/training.ts b/studio/frontend/src/config/training.ts index da60328d40..33249a044a 100644 --- a/studio/frontend/src/config/training.ts +++ b/studio/frontend/src/config/training.ts @@ -103,7 +103,7 @@ export const DEFAULT_HYPERPARAMS = { warmupSteps: 5, maxSteps: 0, saveSteps: 0, - evalSteps: 0.01, + evalSteps: 0.00, packing: false, trainOnCompletions: false, gradientCheckpointing: "unsloth" as const, diff --git a/studio/frontend/src/features/studio/sections/params-section.tsx b/studio/frontend/src/features/studio/sections/params-section.tsx index de82fb0fb5..7c9bf63b77 100644 --- a/studio/frontend/src/features/studio/sections/params-section.tsx +++ b/studio/frontend/src/features/studio/sections/params-section.tsx @@ -727,12 +727,12 @@ export function ParamsSection(): ReactElement { store.setEvalSteps(Number(e.target.value))} From 2be29338460a94cafa9013c27c0ec618f481232b Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Tue, 24 Feb 2026 09:26:54 +0000 Subject: [PATCH 2/3] skip eval split and HF split detection when eval_steps is disabled --- studio/backend/core/training/trainer.py | 41 +++++++++++++----------- studio/backend/core/training/training.py | 5 +-- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/studio/backend/core/training/trainer.py b/studio/backend/core/training/trainer.py index ff5b66485a..d433d2575a 100644 --- a/studio/backend/core/training/trainer.py +++ b/studio/backend/core/training/trainer.py @@ -343,7 +343,8 @@ class UnslothTrainer: custom_format_mapping: dict = None, subset: str = None, train_split: str = "train", - eval_split: str = None) -> Optional[tuple]: + eval_split: str = None, + eval_steps: float = 0.00) -> Optional[tuple]: """ Load and prepare dataset for training. @@ -358,6 +359,7 @@ class UnslothTrainer: dataset = None eval_dataset = None has_separate_eval_source = False # True if eval comes from a separate HF split + eval_enabled = eval_steps is not None and eval_steps > 0 if local_datasets: # Load local datasets @@ -410,23 +412,26 @@ class UnslothTrainer: print(f"Loaded dataset from Hugging Face: {dataset_source}\n") # Resolve eval split from a separate HF split (explicit or auto-detected) - if eval_split: - # Explicit eval split provided - load it directly - print(f"Loading explicit eval split: '{eval_split}'\n") - eval_load_kwargs = {"path": dataset_source, "split": eval_split} - if subset: - eval_load_kwargs["name"] = subset - eval_dataset = load_dataset(**eval_load_kwargs) - has_separate_eval_source = True - print(f"Loaded eval split '{eval_split}' with {len(eval_dataset)} rows\n") - else: - # Auto-detect eval split from HF (returns a separate dataset, or None) - eval_dataset = self._auto_detect_eval_split_from_hf( - dataset_source=dataset_source, - subset=subset, - ) - if eval_dataset is not None: + if eval_enabled: + if eval_split: + # Explicit eval split provided - load it directly + print(f"Loading explicit eval split: '{eval_split}'\n") + eval_load_kwargs = {"path": dataset_source, "split": eval_split} + if subset: + eval_load_kwargs["name"] = subset + eval_dataset = load_dataset(**eval_load_kwargs) has_separate_eval_source = True + print(f"Loaded eval split '{eval_split}' with {len(eval_dataset)} rows\n") + else: + # Auto-detect eval split from HF (returns a separate dataset, or None) + eval_dataset = self._auto_detect_eval_split_from_hf( + dataset_source=dataset_source, + subset=subset, + ) + if eval_dataset is not None: + has_separate_eval_source = True + else: + print("Eval disabled (eval_steps <= 0), skipping eval split detection\n") if dataset is None: raise ValueError("No dataset provided") @@ -472,7 +477,7 @@ class UnslothTrainer: ) eval_dataset = eval_info["dataset"] print(f"Eval dataset formatted successfully\n") - elif not has_separate_eval_source: + elif eval_enabled and not has_separate_eval_source: # No separate eval source — split the already-formatted dataset formatted_dataset = dataset_info["dataset"] split_result = self._resolve_eval_split_from_dataset(formatted_dataset) diff --git a/studio/backend/core/training/training.py b/studio/backend/core/training/training.py index f5d9be63c1..9123d36b39 100644 --- a/studio/backend/core/training/training.py +++ b/studio/backend/core/training/training.py @@ -223,6 +223,7 @@ class TrainingBackend: subset=subset, train_split=train_split, eval_split=eval_split, + eval_steps=eval_steps, ) # Unpack: load_and_format_dataset returns (dataset, eval_dataset) @@ -232,10 +233,6 @@ class TrainingBackend: dataset = dataset_result eval_dataset = None - # If user set eval_steps to 0, disable evaluation entirely - if eval_steps is not None and float(eval_steps) <= 0: - eval_dataset = None - # Track whether eval is enabled for status reporting self.eval_enabled = eval_dataset is not None From f5057d86ed685aace62536f1d622cce15714636e Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Tue, 24 Feb 2026 09:31:30 +0000 Subject: [PATCH 3/3] =?UTF-8?q?use=20explicit=20float=20bounds=20for=20eva?= =?UTF-8?q?l=5Fsteps=20input=20(0.0=E2=80=931.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studio/sections/params-section.tsx | 1302 ++++++++--------- 1 file changed, 650 insertions(+), 652 deletions(-) diff --git a/studio/frontend/src/features/studio/sections/params-section.tsx b/studio/frontend/src/features/studio/sections/params-section.tsx index 7c9bf63b77..144a1f34d7 100644 --- a/studio/frontend/src/features/studio/sections/params-section.tsx +++ b/studio/frontend/src/features/studio/sections/params-section.tsx @@ -130,11 +130,62 @@ export function ParamsSection(): ReactElement { className="md:min-h-[450px]" >
- {/* Max Steps */} -
-
+ {/* Max Steps */} +
+
+ + Max Steps + + + + + + Override total steps. Set 0 to use epochs instead.{" "} + + Read more + + + + + store.setMaxSteps(Number(e.target.value))} + min={0} + max={maxStepsSliderMax} + step={1} + className="w-16 text-right font-mono text-xs font-medium bg-muted/50 border border-border rounded-lg px-1.5 py-0.5 focus:outline-none focus:ring-1 focus:ring-primary/30 [&::-webkit-inner-spin-button]:appearance-none" + /> +
+ store.setMaxSteps(v)} + min={0} + max={maxStepsSliderMax} + step={1} + /> +

+ Total optimizer steps. Use 0 to run by epochs. +

+
+ + {/* Context length */} + - store.setMaxSteps(v)} - min={0} - max={maxStepsSliderMax} - step={1} - /> -

- Total optimizer steps. Use 0 to run by epochs. -

-
- - {/* Context length */} -
- - Context Length - - - - - - Maximum number of tokens per training sample.{" "} - - Read more - - - - - -

- Max sequence length for training samples -

-
- - {/* Learning Rate */} -
- - Learning Rate - - - - - - Step size for weight updates. Lower values train slower but more - stably.{" "} - - Read more - - - - - store.setLearningRate(Number(e.target.value))} - className="w-full font-mono" - /> -

- Recommended: 2e-4 for LoRA, 2e-5 for full fine-tune -

-
- - {/* LoRA Settings */} - {isLora && ( -
-
- )} - {/* Training Hyperparams */} - - - - Training Hyperparameters - - - - - - Optimization - - - Schedule - - - Memory - - - - - - Optimization algorithm. 8-bit variants reduce memory usage. - Fused is recommended for vision models.{" "} - - Read more - - - } - > - - - - How the learning rate changes over training. Linear decays - steadily; cosine decays in a curve.{" "} - - Read more - - - } - > - - - - Samples processed per step. Higher uses more VRAM.{" "} - - Read more - - - } - value={store.batchSize} - onChange={store.setBatchSize} - min={1} - max={32} - step={1} - /> - - Simulates larger batch sizes without extra VRAM.{" "} - - Read more - - - } - value={store.gradientAccumulation} - onChange={store.setGradientAccumulation} - min={1} - max={64} - step={1} - /> - - L2 regularization to prevent overfitting.{" "} - - Read more - - - } - > - - store.setWeightDecay(Number(e.target.value)) - } - className="w-28 font-mono" - /> - - - - - - Gradually increase LR at training start for stability.{" "} - - Read more - - - } - value={store.warmupSteps} - onChange={store.setWarmupSteps} - min={0} - max={100} - step={1} - /> - - Number of full passes over the dataset. Set 0 to run by - max steps.{" "} - - Read more - - - } - value={store.epochs} - onChange={store.setEpochs} - min={0} - max={epochsSliderMax} - step={1} - /> - - Save a checkpoint every N steps. 0 to disable.{" "} - - Read more - - - } - > - store.setSaveSteps(Number(e.target.value))} - className="w-28 font-mono" - /> - - - store.setEvalSteps(Number(e.target.value))} - className="w-28 font-mono" - /> - - - - store.setRandomSeed(Number(e.target.value)) - } - className="w-28 font-mono" - /> - - - - - - Trade compute for memory by recomputing activations.{" "} - - Read more - - - } - > - - - {!showVisionLora && ( -
- store.setPacking(!!v)} + - + + + + Step size for weight updates. Lower values train slower but more + stably.{" "} + + Read more + + + + + store.setLearningRate(Number(e.target.value))} + className="w-full font-mono" + /> +

+ Recommended: 2e-4 for LoRA, 2e-5 for full fine-tune +

+
+ + {/* LoRA Settings */} + {isLora && ( +
+ +
+ + Dimension of the low-rank matrices. Higher = more capacity.{" "} + + Read more + + + } + value={store.loraRank} + onChange={store.setLoraRank} + min={4} + max={128} + step={4} + /> + + Scaling factor for LoRA updates. Usually 2x rank.{" "} + + Read more + + + } + value={store.loraAlpha} + onChange={store.setLoraAlpha} + min={4} + max={256} + step={4} + /> + + Dropout probability for LoRA layers to reduce overfitting.{" "} + + Read more + + + } + value={store.loraDropout} + onChange={store.setLoraDropout} + min={0} + max={0.5} + step={0.01} + format={(v) => v.toFixed(2)} + /> + + {/* Vision checkboxes */} + {showVisionLora && ( +
+ {( + [ + [ + "finetuneVisionLayers", + "Vision layers", + store.finetuneVisionLayers, + store.setFinetuneVisionLayers, + ], + [ + "finetuneLanguageLayers", + "Language layers", + store.finetuneLanguageLayers, + store.setFinetuneLanguageLayers, + ], + [ + "finetuneAttentionModules", + "Attention modules", + store.finetuneAttentionModules, + store.setFinetuneAttentionModules, + ], + [ + "finetuneMLPModules", + "MLP modules", + store.finetuneMLPModules, + store.setFinetuneMLPModules, + ], + ] as const + ).map(([key, label, value, setter]) => ( +
+ + (setter as (v: boolean) => void)(!!v) + } + /> + +
+ ))}
)} -
- store.setTrainOnCompletions(!!v)} - /> - + + {/* Text target modules */} + {!showVisionLora && ( +
+ + Target Modules + +
+ {TARGET_MODULES.map((mod) => { + const active = store.targetModules.includes(mod); + return ( + + ); + })} +
+
+ )} + + {/* LoRA variant */} +
+ {( + [ + { + value: "lora", + label: "Enable LoRA", + desc: "Train with LoRA", + }, + { value: "rslora", label: "RS-LoRA", desc: "Stable Rank" }, + { + value: "loftq", + label: "LoftQ", + desc: "Memory Efficient", + }, + ] as const + ).map((opt) => ( + + ))}
- - - - +
+
+ )} + + {/* Training Hyperparams */} + + + + Training Hyperparameters + + + + + + Optimization + + + Schedule + + + Memory + + + + + + Optimization algorithm. 8-bit variants reduce memory usage. + Fused is recommended for vision models.{" "} + + Read more + + + } + > + + + + How the learning rate changes over training. Linear decays + steadily; cosine decays in a curve.{" "} + + Read more + + + } + > + + + + Samples processed per step. Higher uses more VRAM.{" "} + + Read more + + + } + value={store.batchSize} + onChange={store.setBatchSize} + min={1} + max={32} + step={1} + /> + + Simulates larger batch sizes without extra VRAM.{" "} + + Read more + + + } + value={store.gradientAccumulation} + onChange={store.setGradientAccumulation} + min={1} + max={64} + step={1} + /> + + L2 regularization to prevent overfitting.{" "} + + Read more + + + } + > + + store.setWeightDecay(Number(e.target.value)) + } + className="w-28 font-mono" + /> + + + + + + Gradually increase LR at training start for stability.{" "} + + Read more + + + } + value={store.warmupSteps} + onChange={store.setWarmupSteps} + min={0} + max={100} + step={1} + /> + + Number of full passes over the dataset. Set 0 to run by + max steps.{" "} + + Read more + + + } + value={store.epochs} + onChange={store.setEpochs} + min={0} + max={epochsSliderMax} + step={1} + /> + + Save a checkpoint every N steps. 0 to disable.{" "} + + Read more + + + } + > + store.setSaveSteps(Number(e.target.value))} + className="w-28 font-mono" + /> + + + store.setEvalSteps(Number(e.target.value))} + className="w-28 font-mono" + /> + + + + store.setRandomSeed(Number(e.target.value)) + } + className="w-28 font-mono" + /> + + + + + + Trade compute for memory by recomputing activations.{" "} + + Read more + + + } + > + + + {!showVisionLora && ( +
+ store.setPacking(!!v)} + /> + +
+ )} +
+ store.setTrainOnCompletions(!!v)} + /> + +
+
+
+
+