diff --git a/studio/backend/core/training/trainer.py b/studio/backend/core/training/trainer.py
index 804baa33c0..43cb5ff1a1 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)
@@ -543,7 +548,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 +748,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..9123d36b39 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.
@@ -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
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..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 && (
-
-
-
-
- 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)
- }
- />
-
-
- ))}
-
- )}
-
- {/* 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) => (
-
+
+
+
+
+ {CONTEXT_LENGTHS.map((len) => (
+
+ {len.toLocaleString()}
+
))}
-
-
+
+
+
+ Max sequence length for training samples
+
- )}
- {/* Training Hyperparams */}
-
-
-
- Training Hyperparameters
-
-
-
-
-
- Optimization
-
-
- Schedule
-
-
- Memory
-
-
-
-
-
- Optimization algorithm. 8-bit variants reduce memory usage.
- Fused is recommended for vision models.{" "}
-
- Read more
-
- >
- }
- >
-