diff --git a/.gitignore b/.gitignore index e07aa496b8..25a5ba54a4 100755 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ unsloth_compiled_cache/ # ML artifacts (large files) outputs/ +exports/ *.gguf *.safetensors diff --git a/setup.sh b/setup.sh index 98d878421f..c493d357ee 100755 --- a/setup.sh +++ b/setup.sh @@ -153,23 +153,54 @@ if [ "$IS_COLAB" = true ]; then # Colab: install packages directly without venv run_quiet "pip upgrade" pip install --upgrade pip echo " Installing unsloth-zoo + unsloth..." - run_quiet "pip install unsloth" pip install unsloth-zoo unsloth + run_quiet "pip install unsloth" pip install -r "$SCRIPT_DIR/studio/backend/requirements/base.txt" + echo " Installing additional unsloth dependencies..." + run_quiet "pip install extras" pip install --no-cache-dir -r "$SCRIPT_DIR/studio/backend/requirements/extras.txt" + run_quiet "pip install extras" pip install --no-deps --no-cache-dir -r "$SCRIPT_DIR/studio/backend/requirements/extras-no-deps.txt" + run_quiet "pip install torchao+transformers" pip install --force-reinstall --no-cache-dir -r "$SCRIPT_DIR/studio/backend/requirements/overrides.txt" + run_quiet "pip install triton_kernels" pip install --no-deps -r "$SCRIPT_DIR/studio/backend/requirements/triton-kernels.txt" + # Patch: override llama_cpp.py with fix from unsloth-zoo branch + LLAMA_CPP_DST="$(pip show unsloth-zoo | grep -i '^Location:' | awk '{print $2}')/unsloth_zoo/llama_cpp.py" + curl -sSL "https://raw.githubusercontent.com/unslothai/unsloth-zoo/refs/heads/main/unsloth_zoo/llama_cpp.py" \ + -o "$LLAMA_CPP_DST" echo " Installing studio dependencies..." - run_quiet "pip install extras" pip install typer fastapi uvicorn pydantic matplotlib pandas nest_asyncio "datasets==4.3.0" pyjwt easydict addict + run_quiet "pip install studio" pip install -r "$SCRIPT_DIR/studio/backend/requirements/studio.txt" echo "✅ Python dependencies installed" else - # Local: create venv + # Local: create venv (always start fresh to preserve correct install order) + rm -rf .venv "$BEST_PY" -m venv .venv source .venv/bin/activate run_quiet "pip upgrade" pip install --upgrade pip echo " Installing unsloth-zoo + unsloth..." - run_quiet "pip install unsloth" pip install unsloth-zoo unsloth + run_quiet "pip install unsloth" pip install -r "$SCRIPT_DIR/studio/backend/requirements/base.txt" + echo " Installing additional unsloth dependencies..." + run_quiet "pip install extras" pip install --no-cache-dir -r "$SCRIPT_DIR/studio/backend/requirements/extras.txt" + run_quiet "pip install extras" pip install --no-deps --no-cache-dir -r "$SCRIPT_DIR/studio/backend/requirements/extras-no-deps.txt" + run_quiet "pip install torchao+transformers" pip install --force-reinstall --no-cache-dir -r "$SCRIPT_DIR/studio/backend/requirements/overrides.txt" + run_quiet "pip install triton_kernels" pip install --no-deps -r "$SCRIPT_DIR/studio/backend/requirements/triton-kernels.txt" + # Patch: override llama_cpp.py with fix from unsloth-zoo branch + LLAMA_CPP_DST="$(pip show unsloth-zoo | grep -i '^Location:' | awk '{print $2}')/unsloth_zoo/llama_cpp.py" + curl -sSL "https://raw.githubusercontent.com/unslothai/unsloth-zoo/refs/heads/main/unsloth_zoo/llama_cpp.py" \ + -o "$LLAMA_CPP_DST" echo " Installing studio dependencies..." - run_quiet "pip install extras" pip install typer fastapi uvicorn pydantic matplotlib pandas nest_asyncio "datasets==4.3.0" pyjwt easydict addict + run_quiet "pip install studio" pip install -r "$SCRIPT_DIR/studio/backend/requirements/studio.txt" echo "✅ Python dependencies installed" + + # ── 7. WSL: pre-install GGUF build dependencies ── + # On WSL, sudo requires a password and can't be entered during GGUF export + # (runs in a non-interactive subprocess). Install build deps here instead. + if grep -qi microsoft /proc/version 2>/dev/null; then + echo "" + echo "⚠️ WSL detected — installing build dependencies for GGUF export..." + echo " You may be prompted for your password." + sudo apt-get update -y + sudo apt-get install -y build-essential cmake curl git libcurl4-openssl-dev + echo "✅ GGUF build dependencies installed" + fi fi -# ── 7. Add shell alias (skip in Colab) ── +# ── 8. Add shell alias (skip in Colab) ── # Note: venv activation does NOT persist across terminal sessions. # This alias hardcodes the venv python path so users don't need to activate. if [ "$IS_COLAB" = false ]; then diff --git a/studio/backend/assets/configs/model_defaults/default.yaml b/studio/backend/assets/configs/model_defaults/default.yaml index 3a2563a6f9..7b918b9387 100644 --- a/studio/backend/assets/configs/model_defaults/default.yaml +++ b/studio/backend/assets/configs/model_defaults/default.yaml @@ -3,13 +3,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 5e-5 batch_size: 2 gradient_accumulation_steps: 4 warmup_ratio: 0.1 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.01 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/ernie/unsloth_ERNIE-4.5-21B-A3B-PT.yaml b/studio/backend/assets/configs/model_defaults/ernie/unsloth_ERNIE-4.5-21B-A3B-PT.yaml index 627d71b3fa..79ee761784 100644 --- a/studio/backend/assets/configs/model_defaults/ernie/unsloth_ERNIE-4.5-21B-A3B-PT.yaml +++ b/studio/backend/assets/configs/model_defaults/ernie/unsloth_ERNIE-4.5-21B-A3B-PT.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 4 gradient_accumulation_steps: 2 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/ernie/unsloth_ERNIE-4.5-VL-28B-A3B-PT.yaml b/studio/backend/assets/configs/model_defaults/ernie/unsloth_ERNIE-4.5-VL-28B-A3B-PT.yaml index d2aeb9492e..ef417c8410 100644 --- a/studio/backend/assets/configs/model_defaults/ernie/unsloth_ERNIE-4.5-VL-28B-A3B-PT.yaml +++ b/studio/backend/assets/configs/model_defaults/ernie/unsloth_ERNIE-4.5-VL-28B-A3B-PT.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 2 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/falcon/tiiuae_Falcon-H1-0.5B-Instruct.yaml b/studio/backend/assets/configs/model_defaults/falcon/tiiuae_Falcon-H1-0.5B-Instruct.yaml index 2d3cfc1325..d45526bcf7 100644 --- a/studio/backend/assets/configs/model_defaults/falcon/tiiuae_Falcon-H1-0.5B-Instruct.yaml +++ b/studio/backend/assets/configs/model_defaults/falcon/tiiuae_Falcon-H1-0.5B-Instruct.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 8 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.01 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/gemma/unsloth_codegemma-7b-bnb-4bit.yaml b/studio/backend/assets/configs/model_defaults/gemma/unsloth_codegemma-7b-bnb-4bit.yaml index c6bef5077e..185b91ecf4 100644 --- a/studio/backend/assets/configs/model_defaults/gemma/unsloth_codegemma-7b-bnb-4bit.yaml +++ b/studio/backend/assets/configs/model_defaults/gemma/unsloth_codegemma-7b-bnb-4bit.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 4096 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 1 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/gemma/unsloth_functiongemma-270m-it.yaml b/studio/backend/assets/configs/model_defaults/gemma/unsloth_functiongemma-270m-it.yaml index d67877d167..2ef5798946 100644 --- a/studio/backend/assets/configs/model_defaults/gemma/unsloth_functiongemma-270m-it.yaml +++ b/studio/backend/assets/configs/model_defaults/gemma/unsloth_functiongemma-270m-it.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 4096 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 4 gradient_accumulation_steps: 2 warmup_steps: 10 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-2-27b-bnb-4bit.yaml b/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-2-27b-bnb-4bit.yaml index 899cdc01c7..f3ed24fd2e 100644 --- a/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-2-27b-bnb-4bit.yaml +++ b/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-2-27b-bnb-4bit.yaml @@ -3,13 +3,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-2-2b.yaml b/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-2-2b.yaml index d56d64e9de..70c86dad1b 100644 --- a/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-2-2b.yaml +++ b/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-2-2b.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.01 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3-270m-it.yaml b/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3-270m-it.yaml index d4108fefb0..ccdc19111e 100644 --- a/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3-270m-it.yaml +++ b/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3-270m-it.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 5e-5 batch_size: 4 gradient_accumulation_steps: 1 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3-27b-it.yaml b/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3-27b-it.yaml index 0f213edb70..db1db080a8 100644 --- a/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3-27b-it.yaml +++ b/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3-27b-it.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3-4b-it.yaml b/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3-4b-it.yaml index d7da7a2873..a4130eb8b7 100644 --- a/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3-4b-it.yaml +++ b/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3-4b-it.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3-4b-pt.yaml b/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3-4b-pt.yaml index b0833e6e30..f679b86b81 100644 --- a/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3-4b-pt.yaml +++ b/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3-4b-pt.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 2 + # num_epochs: 2 + num_epochs: 0 learning_rate: 2e-4 batch_size: 1 gradient_accumulation_steps: 4 warmup_ratio: 0.03 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3n-E4B-it.yaml b/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3n-E4B-it.yaml index 7fe1fed620..23ef35603d 100644 --- a/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3n-E4B-it.yaml +++ b/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3n-E4B-it.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 1024 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 1 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3n-E4B.yaml b/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3n-E4B.yaml index 3b1fab2a60..ebad334d5e 100644 --- a/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3n-E4B.yaml +++ b/studio/backend/assets/configs/model_defaults/gemma/unsloth_gemma-3n-E4B.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 2 + # num_epochs: 2 + num_epochs: 0 learning_rate: 2e-4 batch_size: 1 gradient_accumulation_steps: 4 warmup_ratio: 0.03 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/gpt-oss/unsloth_gpt-oss-120b.yaml b/studio/backend/assets/configs/model_defaults/gpt-oss/unsloth_gpt-oss-120b.yaml index 93a8d8b274..da41fb3009 100644 --- a/studio/backend/assets/configs/model_defaults/gpt-oss/unsloth_gpt-oss-120b.yaml +++ b/studio/backend/assets/configs/model_defaults/gpt-oss/unsloth_gpt-oss-120b.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 4096 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 4 gradient_accumulation_steps: 1 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/gpt-oss/unsloth_gpt-oss-20b.yaml b/studio/backend/assets/configs/model_defaults/gpt-oss/unsloth_gpt-oss-20b.yaml index 18f8f7bf47..00bacb118a 100644 --- a/studio/backend/assets/configs/model_defaults/gpt-oss/unsloth_gpt-oss-20b.yaml +++ b/studio/backend/assets/configs/model_defaults/gpt-oss/unsloth_gpt-oss-20b.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 1024 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 1 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/granite/unsloth_granite-4.0-350m-unsloth-bnb-4bit.yaml b/studio/backend/assets/configs/model_defaults/granite/unsloth_granite-4.0-350m-unsloth-bnb-4bit.yaml index 9b91ec531a..dd3a652529 100644 --- a/studio/backend/assets/configs/model_defaults/granite/unsloth_granite-4.0-350m-unsloth-bnb-4bit.yaml +++ b/studio/backend/assets/configs/model_defaults/granite/unsloth_granite-4.0-350m-unsloth-bnb-4bit.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/granite/unsloth_granite-4.0-h-micro.yaml b/studio/backend/assets/configs/model_defaults/granite/unsloth_granite-4.0-h-micro.yaml index e6603b0cea..064b9b0ceb 100644 --- a/studio/backend/assets/configs/model_defaults/granite/unsloth_granite-4.0-h-micro.yaml +++ b/studio/backend/assets/configs/model_defaults/granite/unsloth_granite-4.0-h-micro.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/llama/unsloth_Llama-3.2-11B-Vision-Instruct.yaml b/studio/backend/assets/configs/model_defaults/llama/unsloth_Llama-3.2-11B-Vision-Instruct.yaml index 640ab85b14..6d9f2c8ccb 100644 --- a/studio/backend/assets/configs/model_defaults/llama/unsloth_Llama-3.2-11B-Vision-Instruct.yaml +++ b/studio/backend/assets/configs/model_defaults/llama/unsloth_Llama-3.2-11B-Vision-Instruct.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/llama/unsloth_Llama-3.2-1B-Instruct.yaml b/studio/backend/assets/configs/model_defaults/llama/unsloth_Llama-3.2-1B-Instruct.yaml index 1622d42bef..e00d5b4cb4 100644 --- a/studio/backend/assets/configs/model_defaults/llama/unsloth_Llama-3.2-1B-Instruct.yaml +++ b/studio/backend/assets/configs/model_defaults/llama/unsloth_Llama-3.2-1B-Instruct.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 2048 - num_epochs: 5 + # num_epochs: 5 + num_epochs: 0 learning_rate: 2e-5 batch_size: 1 gradient_accumulation_steps: 8 warmup_steps: 0 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.01 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/llama/unsloth_Llama-3.2-3B-Instruct.yaml b/studio/backend/assets/configs/model_defaults/llama/unsloth_Llama-3.2-3B-Instruct.yaml index 18e9428e5e..ad427a345a 100644 --- a/studio/backend/assets/configs/model_defaults/llama/unsloth_Llama-3.2-3B-Instruct.yaml +++ b/studio/backend/assets/configs/model_defaults/llama/unsloth_Llama-3.2-3B-Instruct.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/llama/unsloth_Llama-3.3-70B-Instruct.yaml b/studio/backend/assets/configs/model_defaults/llama/unsloth_Llama-3.3-70B-Instruct.yaml index 8bd5c2f4dc..8d676bbbc9 100644 --- a/studio/backend/assets/configs/model_defaults/llama/unsloth_Llama-3.3-70B-Instruct.yaml +++ b/studio/backend/assets/configs/model_defaults/llama/unsloth_Llama-3.3-70B-Instruct.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/llama/unsloth_Meta-Llama-3.1-70B-bnb-4bit.yaml b/studio/backend/assets/configs/model_defaults/llama/unsloth_Meta-Llama-3.1-70B-bnb-4bit.yaml index 411d7c2df8..6b3158da02 100644 --- a/studio/backend/assets/configs/model_defaults/llama/unsloth_Meta-Llama-3.1-70B-bnb-4bit.yaml +++ b/studio/backend/assets/configs/model_defaults/llama/unsloth_Meta-Llama-3.1-70B-bnb-4bit.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/llama/unsloth_Meta-Llama-3.1-8B-Instruct-bnb-4bit.yaml b/studio/backend/assets/configs/model_defaults/llama/unsloth_Meta-Llama-3.1-8B-Instruct-bnb-4bit.yaml index 5ba7d7b827..dcf5a5f702 100644 --- a/studio/backend/assets/configs/model_defaults/llama/unsloth_Meta-Llama-3.1-8B-Instruct-bnb-4bit.yaml +++ b/studio/backend/assets/configs/model_defaults/llama/unsloth_Meta-Llama-3.1-8B-Instruct-bnb-4bit.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 8192 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/llama/unsloth_llama-3-8b-Instruct-bnb-4bit.yaml b/studio/backend/assets/configs/model_defaults/llama/unsloth_llama-3-8b-Instruct-bnb-4bit.yaml index aa5aeb178b..d2e398d03d 100644 --- a/studio/backend/assets/configs/model_defaults/llama/unsloth_llama-3-8b-Instruct-bnb-4bit.yaml +++ b/studio/backend/assets/configs/model_defaults/llama/unsloth_llama-3-8b-Instruct-bnb-4bit.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/llama/unsloth_llama-3-8b-bnb-4bit.yaml b/studio/backend/assets/configs/model_defaults/llama/unsloth_llama-3-8b-bnb-4bit.yaml index be515b0bc3..ad3bd0b879 100644 --- a/studio/backend/assets/configs/model_defaults/llama/unsloth_llama-3-8b-bnb-4bit.yaml +++ b/studio/backend/assets/configs/model_defaults/llama/unsloth_llama-3-8b-bnb-4bit.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/llasa/unsloth_Llasa-3B.yaml b/studio/backend/assets/configs/model_defaults/llasa/unsloth_Llasa-3B.yaml index 627adfe491..41217c6c9f 100644 --- a/studio/backend/assets/configs/model_defaults/llasa/unsloth_Llasa-3B.yaml +++ b/studio/backend/assets/configs/model_defaults/llasa/unsloth_Llasa-3B.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 5e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/mistral/unsloth_Magistral-Small-2509-unsloth-bnb-4bit.yaml b/studio/backend/assets/configs/model_defaults/mistral/unsloth_Magistral-Small-2509-unsloth-bnb-4bit.yaml index 9af6fbd3a0..882395745c 100644 --- a/studio/backend/assets/configs/model_defaults/mistral/unsloth_Magistral-Small-2509-unsloth-bnb-4bit.yaml +++ b/studio/backend/assets/configs/model_defaults/mistral/unsloth_Magistral-Small-2509-unsloth-bnb-4bit.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 2 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/mistral/unsloth_Ministral-3-3B-Instruct-2512.yaml b/studio/backend/assets/configs/model_defaults/mistral/unsloth_Ministral-3-3B-Instruct-2512.yaml index d701390da9..4c7ead9985 100644 --- a/studio/backend/assets/configs/model_defaults/mistral/unsloth_Ministral-3-3B-Instruct-2512.yaml +++ b/studio/backend/assets/configs/model_defaults/mistral/unsloth_Ministral-3-3B-Instruct-2512.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 4 gradient_accumulation_steps: 2 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/mistral/unsloth_Mistral-Nemo-Base-2407-bnb-4bit.yaml b/studio/backend/assets/configs/model_defaults/mistral/unsloth_Mistral-Nemo-Base-2407-bnb-4bit.yaml index d1374cc49e..99fc67dcff 100644 --- a/studio/backend/assets/configs/model_defaults/mistral/unsloth_Mistral-Nemo-Base-2407-bnb-4bit.yaml +++ b/studio/backend/assets/configs/model_defaults/mistral/unsloth_Mistral-Nemo-Base-2407-bnb-4bit.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/mistral/unsloth_Mistral-Small-Instruct-2409.yaml b/studio/backend/assets/configs/model_defaults/mistral/unsloth_Mistral-Small-Instruct-2409.yaml index 597cf612fe..e0146c374e 100644 --- a/studio/backend/assets/configs/model_defaults/mistral/unsloth_Mistral-Small-Instruct-2409.yaml +++ b/studio/backend/assets/configs/model_defaults/mistral/unsloth_Mistral-Small-Instruct-2409.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 1 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/mistral/unsloth_Pixtral-12B-2409.yaml b/studio/backend/assets/configs/model_defaults/mistral/unsloth_Pixtral-12B-2409.yaml index 9cb15ea2ff..828ba79c76 100644 --- a/studio/backend/assets/configs/model_defaults/mistral/unsloth_Pixtral-12B-2409.yaml +++ b/studio/backend/assets/configs/model_defaults/mistral/unsloth_Pixtral-12B-2409.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 1 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/mistral/unsloth_mistral-7b-instruct-v0.3-bnb-4bit.yaml b/studio/backend/assets/configs/model_defaults/mistral/unsloth_mistral-7b-instruct-v0.3-bnb-4bit.yaml index 384de103d0..74527b21af 100644 --- a/studio/backend/assets/configs/model_defaults/mistral/unsloth_mistral-7b-instruct-v0.3-bnb-4bit.yaml +++ b/studio/backend/assets/configs/model_defaults/mistral/unsloth_mistral-7b-instruct-v0.3-bnb-4bit.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/mistral/unsloth_mistral-7b-v0.3-bnb-4bit.yaml b/studio/backend/assets/configs/model_defaults/mistral/unsloth_mistral-7b-v0.3-bnb-4bit.yaml index fe288786aa..26810445bf 100644 --- a/studio/backend/assets/configs/model_defaults/mistral/unsloth_mistral-7b-v0.3-bnb-4bit.yaml +++ b/studio/backend/assets/configs/model_defaults/mistral/unsloth_mistral-7b-v0.3-bnb-4bit.yaml @@ -3,13 +3,14 @@ # Also applies to: "unsloth/mistral-7b-v0.3", "mistralai/Mistral-7B-v0.3", training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/other/OuteAI_Llama-OuteTTS-1.0-1B.yaml b/studio/backend/assets/configs/model_defaults/other/OuteAI_Llama-OuteTTS-1.0-1B.yaml index fe0940ddc4..9db476f4dc 100644 --- a/studio/backend/assets/configs/model_defaults/other/OuteAI_Llama-OuteTTS-1.0-1B.yaml +++ b/studio/backend/assets/configs/model_defaults/other/OuteAI_Llama-OuteTTS-1.0-1B.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/other/Spark-TTS-0.5B_LLM.yaml b/studio/backend/assets/configs/model_defaults/other/Spark-TTS-0.5B_LLM.yaml index 02fb108b95..bf6b1f7247 100644 --- a/studio/backend/assets/configs/model_defaults/other/Spark-TTS-0.5B_LLM.yaml +++ b/studio/backend/assets/configs/model_defaults/other/Spark-TTS-0.5B_LLM.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/other/sesame_csm-1b.yaml b/studio/backend/assets/configs/model_defaults/other/sesame_csm-1b.yaml index a01e10971b..803eeebb75 100644 --- a/studio/backend/assets/configs/model_defaults/other/sesame_csm-1b.yaml +++ b/studio/backend/assets/configs/model_defaults/other/sesame_csm-1b.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/other/unsloth_LFM2-1.2B.yaml b/studio/backend/assets/configs/model_defaults/other/unsloth_LFM2-1.2B.yaml index 2eb6246c23..598d80da55 100644 --- a/studio/backend/assets/configs/model_defaults/other/unsloth_LFM2-1.2B.yaml +++ b/studio/backend/assets/configs/model_defaults/other/unsloth_LFM2-1.2B.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/other/unsloth_Nemotron-3-Nano-30B-A3B.yaml b/studio/backend/assets/configs/model_defaults/other/unsloth_Nemotron-3-Nano-30B-A3B.yaml index 0b1b60feac..c212b0508a 100644 --- a/studio/backend/assets/configs/model_defaults/other/unsloth_Nemotron-3-Nano-30B-A3B.yaml +++ b/studio/backend/assets/configs/model_defaults/other/unsloth_Nemotron-3-Nano-30B-A3B.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 4 gradient_accumulation_steps: 2 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/other/unsloth_PaddleOCR-VL.yaml b/studio/backend/assets/configs/model_defaults/other/unsloth_PaddleOCR-VL.yaml index 916333c174..a971f080b6 100644 --- a/studio/backend/assets/configs/model_defaults/other/unsloth_PaddleOCR-VL.yaml +++ b/studio/backend/assets/configs/model_defaults/other/unsloth_PaddleOCR-VL.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 5e-5 batch_size: 4 gradient_accumulation_steps: 2 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/other/unsloth_answerdotai_ModernBERT-large.yaml b/studio/backend/assets/configs/model_defaults/other/unsloth_answerdotai_ModernBERT-large.yaml index 3b886fb3b9..c79841390d 100644 --- a/studio/backend/assets/configs/model_defaults/other/unsloth_answerdotai_ModernBERT-large.yaml +++ b/studio/backend/assets/configs/model_defaults/other/unsloth_answerdotai_ModernBERT-large.yaml @@ -3,13 +3,14 @@ training: max_seq_length: 2048 - num_epochs: 1 + # num_epochs: 1 + num_epochs: 0 learning_rate: 5e-5 batch_size: 32 gradient_accumulation_steps: 1 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/other/unsloth_orpheus-3b-0.1-ft.yaml b/studio/backend/assets/configs/model_defaults/other/unsloth_orpheus-3b-0.1-ft.yaml index 3079b407df..de5fb640ba 100644 --- a/studio/backend/assets/configs/model_defaults/other/unsloth_orpheus-3b-0.1-ft.yaml +++ b/studio/backend/assets/configs/model_defaults/other/unsloth_orpheus-3b-0.1-ft.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 1 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/other/unsloth_tinyllama-bnb-4bit.yaml b/studio/backend/assets/configs/model_defaults/other/unsloth_tinyllama-bnb-4bit.yaml index f5f7ae79c3..dd4525bba3 100644 --- a/studio/backend/assets/configs/model_defaults/other/unsloth_tinyllama-bnb-4bit.yaml +++ b/studio/backend/assets/configs/model_defaults/other/unsloth_tinyllama-bnb-4bit.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 4096 - num_epochs: 1 + # num_epochs: 1 + num_epochs: 0 learning_rate: 2e-5 batch_size: 2 gradient_accumulation_steps: 4 warmup_ratio: 0.1 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.1 random_seed: 3407 packing: true diff --git a/studio/backend/assets/configs/model_defaults/other/unsloth_whisper-large-v3.yaml b/studio/backend/assets/configs/model_defaults/other/unsloth_whisper-large-v3.yaml index 3b1c82ab0a..f0c49f363d 100644 --- a/studio/backend/assets/configs/model_defaults/other/unsloth_whisper-large-v3.yaml +++ b/studio/backend/assets/configs/model_defaults/other/unsloth_whisper-large-v3.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 448 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 1e-4 batch_size: 1 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/phi/unsloth_Phi-3-medium-4k-instruct.yaml b/studio/backend/assets/configs/model_defaults/phi/unsloth_Phi-3-medium-4k-instruct.yaml index 5968e9aea8..c39368fbd2 100644 --- a/studio/backend/assets/configs/model_defaults/phi/unsloth_Phi-3-medium-4k-instruct.yaml +++ b/studio/backend/assets/configs/model_defaults/phi/unsloth_Phi-3-medium-4k-instruct.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/phi/unsloth_Phi-3.5-mini-instruct.yaml b/studio/backend/assets/configs/model_defaults/phi/unsloth_Phi-3.5-mini-instruct.yaml index 8a2e2ca26e..076165c12a 100644 --- a/studio/backend/assets/configs/model_defaults/phi/unsloth_Phi-3.5-mini-instruct.yaml +++ b/studio/backend/assets/configs/model_defaults/phi/unsloth_Phi-3.5-mini-instruct.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/phi/unsloth_Phi-4-14B-Instruct.yaml b/studio/backend/assets/configs/model_defaults/phi/unsloth_Phi-4-14B-Instruct.yaml index 6b41ca81f2..91d53180fa 100644 --- a/studio/backend/assets/configs/model_defaults/phi/unsloth_Phi-4-14B-Instruct.yaml +++ b/studio/backend/assets/configs/model_defaults/phi/unsloth_Phi-4-14B-Instruct.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/phi/unsloth_Phi-4.yaml b/studio/backend/assets/configs/model_defaults/phi/unsloth_Phi-4.yaml index b13fa9a0b7..3b829649a8 100644 --- a/studio/backend/assets/configs/model_defaults/phi/unsloth_Phi-4.yaml +++ b/studio/backend/assets/configs/model_defaults/phi/unsloth_Phi-4.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2-7B.yaml b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2-7B.yaml index 69dbd27626..56d4f35998 100644 --- a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2-7B.yaml +++ b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2-7B.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2-VL-7B-Instruct.yaml b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2-VL-7B-Instruct.yaml index b83ca586f5..86cb03ff25 100644 --- a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2-VL-7B-Instruct.yaml +++ b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2-VL-7B-Instruct.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-1.5B-Instruct.yaml b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-1.5B-Instruct.yaml index 1f08adf6d2..3a3259c8fd 100644 --- a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-1.5B-Instruct.yaml +++ b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-1.5B-Instruct.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 4096 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 1e-5 batch_size: 1 gradient_accumulation_steps: 64 warmup_ratio: 0.1 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 42 packing: false diff --git a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-7B.yaml b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-7B.yaml index 31ab069907..2a8f023e76 100644 --- a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-7B.yaml +++ b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-7B.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-Coder-1.5B-Instruct.yaml b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-Coder-1.5B-Instruct.yaml index e340ea4573..a92d0d6047 100644 --- a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-Coder-1.5B-Instruct.yaml +++ b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-Coder-1.5B-Instruct.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-Coder-14B-Instruct.yaml b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-Coder-14B-Instruct.yaml index f0a27ac90c..eff25af51c 100644 --- a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-Coder-14B-Instruct.yaml +++ b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-Coder-14B-Instruct.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 1 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-Coder-7B-Instruct-bnb-4bit.yaml b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-Coder-7B-Instruct-bnb-4bit.yaml index ae62611432..3ab14078a9 100644 --- a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-Coder-7B-Instruct-bnb-4bit.yaml +++ b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-Coder-7B-Instruct-bnb-4bit.yaml @@ -4,13 +4,14 @@ training: max_seq_length: 32768 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-VL-7B-Instruct-bnb-4bit.yaml b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-VL-7B-Instruct-bnb-4bit.yaml index 930f352dd6..4cfa83f853 100644 --- a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-VL-7B-Instruct-bnb-4bit.yaml +++ b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen2.5-VL-7B-Instruct-bnb-4bit.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-0.6B.yaml b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-0.6B.yaml index bf19fad0bd..d8f9d3a73b 100644 --- a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-0.6B.yaml +++ b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-0.6B.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 1024 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 5e-5 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-14B-Base-unsloth-bnb-4bit.yaml b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-14B-Base-unsloth-bnb-4bit.yaml index fe519f49a7..7f66711413 100644 --- a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-14B-Base-unsloth-bnb-4bit.yaml +++ b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-14B-Base-unsloth-bnb-4bit.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-14B.yaml b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-14B.yaml index 662b96c5fe..0f3403e09e 100644 --- a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-14B.yaml +++ b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-14B.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-32B.yaml b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-32B.yaml index 83ac11efda..f019922017 100644 --- a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-32B.yaml +++ b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-32B.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-4B-Instruct-2507.yaml b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-4B-Instruct-2507.yaml index 7178b86e7a..a8552cc85c 100644 --- a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-4B-Instruct-2507.yaml +++ b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-4B-Instruct-2507.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-4B-Thinking-2507.yaml b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-4B-Thinking-2507.yaml index 0385f1d00d..0808510982 100644 --- a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-4B-Thinking-2507.yaml +++ b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-4B-Thinking-2507.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-VL-8B-Instruct-unsloth-bnb-4bit.yaml b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-VL-8B-Instruct-unsloth-bnb-4bit.yaml index 8db73bbdc2..5530047541 100644 --- a/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-VL-8B-Instruct-unsloth-bnb-4bit.yaml +++ b/studio/backend/assets/configs/model_defaults/qwen/unsloth_Qwen3-VL-8B-Instruct-unsloth-bnb-4bit.yaml @@ -5,13 +5,14 @@ training: max_seq_length: 2048 - num_epochs: 4 + # num_epochs: 4 + num_epochs: 0 learning_rate: 2e-4 batch_size: 2 gradient_accumulation_steps: 4 warmup_steps: 5 - max_steps: 0 - save_steps: 0 + max_steps: 30 + save_steps: 30 weight_decay: 0.001 random_seed: 3407 packing: false diff --git a/studio/backend/core/export/export.py b/studio/backend/core/export/export.py index 66df893f6c..da5b11c60d 100644 --- a/studio/backend/core/export/export.py +++ b/studio/backend/core/export/export.py @@ -18,6 +18,45 @@ from core.inference import get_inference_backend logger = logging.getLogger(__name__) + +def _is_wsl(): + """Detect if running under Windows Subsystem for Linux.""" + try: + return "microsoft" in open("/proc/version").read().lower() + except Exception: + return False + + +def _apply_wsl_sudo_patch(): + """On WSL, monkey-patch do_we_need_sudo() to return False. + + WSL doesn't have passwordless sudo, and do_we_need_sudo() runs + `sudo apt-get update` which hangs waiting for a stdin password + inside a non-interactive subprocess. setup.sh pre-installs the + build dependencies on WSL, so sudo is not needed at runtime. + """ + if not _is_wsl(): + return + + try: + import unsloth_zoo.llama_cpp as llama_cpp_module + + def _wsl_do_we_need_sudo(system_type="debian"): + logger.info( + "WSL detected — skipping sudo check " + "(build deps pre-installed by setup.sh)" + ) + return False + + llama_cpp_module.do_we_need_sudo = _wsl_do_we_need_sudo + logger.info( + "Applied WSL sudo patch to " + "unsloth_zoo.llama_cpp.do_we_need_sudo" + ) + except Exception as e: + logger.warning(f"Could not apply WSL sudo patch: {e}") + + # Model card template MODEL_CARD = \ """--- @@ -236,7 +275,8 @@ class ExportBackend: push_to_hub: bool = False, repo_id: Optional[str] = None, hf_token: Optional[str] = None, - private: bool = False) -> Tuple[bool, str]: + private: bool = False, + base_model_id: Optional[str] = None) -> Tuple[bool, str]: """ Export base model (for non-PEFT models). @@ -266,8 +306,8 @@ class ExportBackend: logger.info(f"Pushing base model to Hub: {repo_id}") - # Get base model name - base_model = self.current_model.config._name_or_path + # Get base model name from request or model config + base_model = base_model_id or self.current_model.config._name_or_path or "unknown" # Create repo hf_api = HfApi(token=hf_token) @@ -352,6 +392,9 @@ class ExportBackend: os.chdir(save_directory) logger.info(f"Changed directory to: {save_directory}") + # On WSL, patch out sudo check before llama.cpp build + _apply_wsl_sudo_patch() + # Now save (will save in current directory) self.current_model.save_pretrained_gguf( "model", # Base filename diff --git a/studio/backend/core/training/training.py b/studio/backend/core/training/training.py index 51d411be78..9b589d215d 100644 --- a/studio/backend/core/training/training.py +++ b/studio/backend/core/training/training.py @@ -4,6 +4,7 @@ Training backend for FastAPI integration import matplotlib.pyplot as plt from typing import Any, Generator, Tuple import logging +import math from .trainer import get_trainer, TrainingProgress from utils.hardware import clear_gpu_cache @@ -28,6 +29,8 @@ class TrainingBackend: self.loss_history = [] self.lr_history = [] self.step_history = [] + self.grad_norm_history = [] + self.grad_norm_step_history = [] self.eval_loss_history = [] self.eval_step_history = [] self.eval_enabled = False @@ -43,6 +46,14 @@ class TrainingBackend: self.loss_history.append(progress.loss) self.lr_history.append(progress.learning_rate) self.step_history.append(progress.step) + if progress.step >= 0 and progress.grad_norm is not None: + try: + grad_norm = float(progress.grad_norm) + except (TypeError, ValueError): + grad_norm = None + if grad_norm is not None and math.isfinite(grad_norm): + self.grad_norm_history.append(grad_norm) + self.grad_norm_step_history.append(progress.step) if progress.eval_loss is not None: self.eval_loss_history.append(progress.eval_loss) self.eval_step_history.append(progress.step) @@ -144,6 +155,8 @@ class TrainingBackend: self.loss_history = [] self.lr_history = [] self.step_history = [] + self.grad_norm_history = [] + self.grad_norm_step_history = [] self.eval_loss_history = [] self.eval_step_history = [] self.eval_enabled = False diff --git a/studio/backend/models/__init__.py b/studio/backend/models/__init__.py index b66efe7093..fe21d525a4 100644 --- a/studio/backend/models/__init__.py +++ b/studio/backend/models/__init__.py @@ -12,6 +12,8 @@ from .models import ( ModelCheckpoints, CheckpointListResponse, ModelDetails, + LocalModelInfo, + LocalModelListResponse, LoRAInfo, LoRAScanResponse, ModelListResponse, @@ -59,6 +61,8 @@ __all__ = [ "TrainingProgress", # Model management schemas "ModelDetails", + "LocalModelInfo", + "LocalModelListResponse", "LoRAInfo", "LoRAScanResponse", "ModelListResponse", diff --git a/studio/backend/models/export.py b/studio/backend/models/export.py index 4a3dd664ee..a0526e80db 100644 --- a/studio/backend/models/export.py +++ b/studio/backend/models/export.py @@ -72,6 +72,10 @@ class ExportCommonOptions(BaseModel): False, description="If True, create a private repository on the Hub (where applicable)", ) + base_model_id: Optional[str] = Field( + None, + description="HuggingFace model ID of the base model (for model card metadata)", + ) class ExportMergedModelRequest(ExportCommonOptions): diff --git a/studio/backend/models/models.py b/studio/backend/models/models.py index ef883013ad..5542db76d5 100644 --- a/studio/backend/models/models.py +++ b/studio/backend/models/models.py @@ -2,7 +2,7 @@ Pydantic schemas for Model Management API """ from pydantic import BaseModel, Field -from typing import Optional, List, Dict, Any +from typing import Optional, List, Dict, Any, Literal class CheckpointInfo(BaseModel): @@ -21,6 +21,18 @@ class ModelCheckpoints(BaseModel): default_factory=list, description="List of checkpoints for this training run (final + intermediate)", ) + base_model: Optional[str] = Field( + None, + description="Base model name from adapter_config.json or config.json", + ) + peft_type: Optional[str] = Field( + None, + description="PEFT type (e.g. LORA) if adapter training, None for full fine-tune", + ) + lora_rank: Optional[int] = Field( + None, + description="LoRA rank (r) if applicable", + ) class CheckpointListResponse(BaseModel): @@ -62,3 +74,34 @@ class ModelListResponse(BaseModel): models: List[ModelDetails] = Field(default_factory=list, description="List of models") default_models: List[str] = Field(default_factory=list, description="List of default model IDs") + +class LocalModelInfo(BaseModel): + """Discovered local model candidate.""" + id: str = Field(..., description="Identifier to use for loading/training") + display_name: str = Field(..., description="Display label") + path: str = Field(..., description="Local path where model data was discovered") + source: Literal["models_dir", "hf_cache"] = Field( + ..., + description="Discovery source", + ) + model_id: Optional[str] = Field( + None, + description="HF repo id for cached models, e.g. org/model", + ) + updated_at: Optional[float] = Field( + None, + description="Unix timestamp of latest observed update", + ) + + +class LocalModelListResponse(BaseModel): + """Response schema for listing local/cached models.""" + models_dir: str = Field(..., description="Directory scanned for custom local models") + hf_cache_dir: Optional[str] = Field( + None, + description="HF cache root that was scanned", + ) + models: List[LocalModelInfo] = Field( + default_factory=list, + description="Discovered local/cached models", + ) diff --git a/studio/backend/models/responses.py b/studio/backend/models/responses.py index 2aa798c5c9..c72dfc54dd 100644 --- a/studio/backend/models/responses.py +++ b/studio/backend/models/responses.py @@ -19,6 +19,8 @@ class TrainingMetricsResponse(BaseModel): loss_history: List[float] = Field(default_factory=list, description="Loss values per step") lr_history: List[float] = Field(default_factory=list, description="Learning rate per step") step_history: List[int] = Field(default_factory=list, description="Step numbers") + grad_norm_history: List[float] = Field(default_factory=list, description="Gradient norm values") + grad_norm_step_history: List[int] = Field(default_factory=list, description="Step numbers for gradient norm values") current_loss: Optional[float] = Field(None, description="Most recent loss value") current_lr: Optional[float] = Field(None, description="Most recent learning rate") current_step: Optional[int] = Field(None, description="Most recent step number") diff --git a/studio/backend/models/training.py b/studio/backend/models/training.py index fd04baf3a0..2b989e6a82 100644 --- a/studio/backend/models/training.py +++ b/studio/backend/models/training.py @@ -104,7 +104,7 @@ class TrainingStatus(BaseModel): metric_history: Optional[dict] = Field( None, description="Full metric history arrays for chart recovery after SSE reconnection. " - "Keys: 'steps', 'loss', 'lr' — each a list of numeric values.", + "Keys: 'steps', 'loss', 'lr', 'grad_norm', 'grad_norm_steps' — each a list of numeric values.", ) @@ -122,4 +122,3 @@ class TrainingProgress(BaseModel): grad_norm: Optional[float] = Field(None, description="L2 norm of gradients, computed before gradient clipping") num_tokens: Optional[int] = Field(None, description="Total number of tokens processed so far") eval_loss: Optional[float] = Field(None, description="Eval loss from the most recent evaluation step") - diff --git a/studio/backend/requirements.txt b/studio/backend/requirements.txt deleted file mode 100644 index 3f97cca66c..0000000000 --- a/studio/backend/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -fastapi>=0.100.0 -uvicorn>=0.27.0 -pydantic>=2.0 -torch -psutil -nest-asyncio>=1.5.8 - diff --git a/studio/backend/requirements/base.txt b/studio/backend/requirements/base.txt new file mode 100644 index 0000000000..407ae01b52 --- /dev/null +++ b/studio/backend/requirements/base.txt @@ -0,0 +1,3 @@ +# Core unsloth packages +unsloth-zoo +unsloth diff --git a/studio/backend/requirements/extras-no-deps.txt b/studio/backend/requirements/extras-no-deps.txt new file mode 100644 index 0000000000..c2a9c6bad8 --- /dev/null +++ b/studio/backend/requirements/extras-no-deps.txt @@ -0,0 +1,13 @@ +# Audio extras (installed with --no-deps --no-cache-dir) +descript-audio-codec +descript-audiotools +julius +torchcodec +snac + +# TRL and related packages +trl==0.23.1 +git+https://github.com/meta-pytorch/OpenEnv.git +executorch==1.0.1 +torch-c-dlpack-ext +sentence_transformers==5.2.0 diff --git a/studio/backend/requirements/extras.txt b/studio/backend/requirements/extras.txt new file mode 100644 index 0000000000..3ed20faa8b --- /dev/null +++ b/studio/backend/requirements/extras.txt @@ -0,0 +1,56 @@ +# OpenEnv dependencies +tomli +tomli-w + +# ExecuTorch dependencies +ruamel.yaml +coremltools +expecttest +flatbuffers +hydra-core +hypothesis +kgb +parameterized +pytest<9.0 +pytest-json-report +pytest-rerunfailures==15.1 +pytest-xdist +# Also needed by sentence_transformers +scikit-learn==1.7.1 + +# Additional extras +pybind11 +langid +jiwer +omegaconf +einx +pyloudnorm +openai-whisper +uroman +MeCab +loguru +flatten_dict +ffmpy +randomname +argbind +tiktoken +ftfy +importlib-resources +librosa +markdown2 +matplotlib +pystoi +soundfile +tensorboard +torch-stoi +evaluate +timm +transformers-cfg +open_spiel +addict +easydict +einops +tabulate +fastmcp>=2.0.0 +openai>=2.7.2 +websockets>=13.0,<14 diff --git a/studio/backend/requirements/overrides.txt b/studio/backend/requirements/overrides.txt new file mode 100644 index 0000000000..02770f3953 --- /dev/null +++ b/studio/backend/requirements/overrides.txt @@ -0,0 +1,7 @@ +# Torch AO overrides (installed with --force-reinstall --no-cache-dir) +torchao==0.14.0 +transformers==4.57.1 +pytorch_tokenizers + +# Kernel packages +kernels diff --git a/studio/backend/requirements/studio.txt b/studio/backend/requirements/studio.txt new file mode 100644 index 0000000000..fd7da2626d --- /dev/null +++ b/studio/backend/requirements/studio.txt @@ -0,0 +1,13 @@ +# Studio UI backend dependencies +typer +fastapi +uvicorn +pydantic +matplotlib +pandas +nest_asyncio +datasets==4.3.0 +pyjwt +easydict +addict +gradio>=4.0.0 diff --git a/studio/backend/requirements/triton-kernels.txt b/studio/backend/requirements/triton-kernels.txt new file mode 100644 index 0000000000..17e265b35e --- /dev/null +++ b/studio/backend/requirements/triton-kernels.txt @@ -0,0 +1,2 @@ +# Triton kernels (installed with --no-deps, from source) +triton_kernels @ git+https://github.com/triton-lang/triton.git@release/3.6.x#subdirectory=python/triton_kernels diff --git a/studio/backend/routes/export.py b/studio/backend/routes/export.py index 162a71b2c4..6616c9fbd8 100644 --- a/studio/backend/routes/export.py +++ b/studio/backend/routes/export.py @@ -190,6 +190,7 @@ async def export_base_model( repo_id=request.repo_id, hf_token=request.hf_token, private=request.private, + base_model_id=request.base_model_id, ) if not success: diff --git a/studio/backend/routes/models.py b/studio/backend/routes/models.py index a5de630fee..cc964d3ea3 100644 --- a/studio/backend/routes/models.py +++ b/studio/backend/routes/models.py @@ -43,6 +43,8 @@ except ImportError: from models import ( CheckpointInfo, CheckpointListResponse, + LocalModelInfo, + LocalModelListResponse, ModelCheckpoints, ModelDetails, LoRAScanResponse, @@ -64,6 +66,116 @@ if not logger.handlers: logger.setLevel(logging.INFO) +def _resolve_hf_cache_dir() -> Path: + """Resolve local HF cache root used by hub downloads.""" + try: + from huggingface_hub.constants import HF_HUB_CACHE + return Path(HF_HUB_CACHE) + except Exception: + return Path.home() / ".cache" / "huggingface" / "hub" + + +def _scan_models_dir(models_dir: Path) -> List[LocalModelInfo]: + if not models_dir.exists() or not models_dir.is_dir(): + return [] + + found: List[LocalModelInfo] = [] + for child in models_dir.iterdir(): + if not child.is_dir(): + continue + has_model_files = ( + (child / "config.json").exists() + or (child / "adapter_config.json").exists() + or any(child.glob("*.safetensors")) + or any(child.glob("*.bin")) + ) + if not has_model_files: + continue + try: + updated_at = child.stat().st_mtime + except OSError: + updated_at = None + found.append( + LocalModelInfo( + id=str(child), + display_name=child.name, + path=str(child), + source="models_dir", + updated_at=updated_at, + ), + ) + return found + + +def _scan_hf_cache(cache_dir: Path) -> List[LocalModelInfo]: + if not cache_dir.exists() or not cache_dir.is_dir(): + return [] + + found: List[LocalModelInfo] = [] + for repo_dir in cache_dir.glob("models--*"): + if not repo_dir.is_dir(): + continue + + repo_name = repo_dir.name[len("models--"):] + if not repo_name: + continue + model_id = repo_name.replace("--", "/") + + try: + updated_at = repo_dir.stat().st_mtime + except OSError: + updated_at = None + + found.append( + LocalModelInfo( + id=model_id, + model_id=model_id, + display_name=model_id.split("/")[-1], + path=str(repo_dir), + source="hf_cache", + updated_at=updated_at, + ), + ) + return found + + +@router.get("/local", response_model=LocalModelListResponse) +async def list_local_models( + models_dir: str = Query(default="./models", description="Directory to scan for local model folders"), + current_subject: str = Depends(get_current_subject), +): + """ + List local model candidates from custom models dir and HF cache. + """ + try: + models_root = Path(models_dir).expanduser().resolve() + hf_cache_dir = _resolve_hf_cache_dir() + local_models = _scan_models_dir(models_root) + _scan_hf_cache(hf_cache_dir) + + deduped: dict[str, LocalModelInfo] = {} + for model in local_models: + if model.id not in deduped: + deduped[model.id] = model + + models = sorted( + deduped.values(), + key=lambda item: (item.updated_at or 0), + reverse=True, + ) + + return LocalModelListResponse( + models_dir=str(models_root), + hf_cache_dir=str(hf_cache_dir), + models=models, + ) + except Exception as e: + logger.error(f"Error listing local models: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"Failed to list local models: {str(e)}", + ) + + @router.get("/list") @@ -294,8 +406,11 @@ async def list_checkpoints( CheckpointInfo(display_name=display_name, path=path, loss=loss) for display_name, path, loss in checkpoints ], + base_model=metadata.get("base_model"), + peft_type=metadata.get("peft_type"), + lora_rank=metadata.get("lora_rank"), ) - for model_name, checkpoints in raw_models + for model_name, checkpoints, metadata in raw_models ] return CheckpointListResponse( diff --git a/studio/backend/routes/training.py b/studio/backend/routes/training.py index c09b814fa1..1770999284 100644 --- a/studio/backend/routes/training.py +++ b/studio/backend/routes/training.py @@ -325,6 +325,8 @@ async def reset_training( backend.loss_history = [] backend.lr_history = [] backend.step_history = [] + backend.grad_norm_history = [] + backend.grad_norm_step_history = [] return {"status": "ok"} except Exception as e: logger.error(f"Error resetting training: {e}", exc_info=True) @@ -408,6 +410,8 @@ async def get_training_status( "steps": list(backend.step_history), "loss": list(backend.loss_history), "lr": list(backend.lr_history), + "grad_norm": list(getattr(backend, "grad_norm_history", [])), + "grad_norm_steps": list(getattr(backend, "grad_norm_step_history", [])), "eval_loss": list(backend.eval_loss_history), "eval_steps": list(backend.eval_step_history), } @@ -445,6 +449,8 @@ async def get_training_metrics( loss_history = backend.loss_history lr_history = backend.lr_history step_history = backend.step_history + grad_norm_history = getattr(backend, "grad_norm_history", []) + grad_norm_step_history = getattr(backend, "grad_norm_step_history", []) # Get current values current_loss = loss_history[-1] if loss_history else None @@ -455,6 +461,8 @@ async def get_training_metrics( loss_history=loss_history, lr_history=lr_history, step_history=step_history, + grad_norm_history=grad_norm_history, + grad_norm_step_history=grad_norm_step_history, current_loss=current_loss, current_lr=current_lr, current_step=current_step, @@ -505,6 +513,8 @@ async def stream_training_progress( total_steps: int, epoch: Optional[float] = None, progress: Optional[Any] = None, + grad_norm_override: Optional[float] = None, + eval_loss_override: Optional[float] = None, ) -> TrainingProgress: total = max(total_steps, 0) if step < 0 or total == 0: @@ -517,9 +527,13 @@ async def stream_training_progress( # Get actual values from progress object if available elapsed_seconds = getattr(progress, 'elapsed_seconds', None) if progress else None eta_seconds = getattr(progress, 'eta_seconds', None) if progress else None - grad_norm = getattr(progress, 'grad_norm', None) if progress else None + grad_norm = grad_norm_override + if grad_norm is None and progress: + grad_norm = getattr(progress, 'grad_norm', None) num_tokens = getattr(progress, 'num_tokens', None) if progress else None - eval_loss = getattr(progress, 'eval_loss', None) if progress else None + eval_loss = eval_loss_override + if eval_loss is None and progress: + eval_loss = getattr(progress, 'eval_loss', None) return TrainingProgress( job_id=job_id, @@ -558,6 +572,13 @@ async def stream_training_progress( # ── Replay missed steps on reconnect ───────────────────── if resume_from_step is not None and backend.step_history: replayed = 0 + grad_norm_by_step = { + step_val: grad_val + for step_val, grad_val in zip( + getattr(backend, "grad_norm_step_history", []), + getattr(backend, "grad_norm_history", []), + ) + } for i, step_val in enumerate(backend.step_history): if step_val > resume_from_step: loss_val = backend.loss_history[i] if i < len(backend.loss_history) else 0.0 @@ -567,7 +588,15 @@ async def stream_training_progress( ) total_replay = getattr(tp_replay, "total_steps", step_val) if tp_replay else step_val epoch_replay = getattr(tp_replay, "epoch", None) if tp_replay else None - payload = build_progress(step_val, loss_val, lr_val, total_replay, epoch_replay, progress=tp_replay) + payload = build_progress( + step_val, + loss_val, + lr_val, + total_replay, + epoch_replay, + progress=tp_replay, + grad_norm_override=grad_norm_by_step.get(step_val), + ) yield format_sse(payload.model_dump_json(), event="progress", event_id=step_val) replayed += 1 if replayed: diff --git a/studio/backend/utils/models/checkpoints.py b/studio/backend/utils/models/checkpoints.py index cbc6c69d95..7a50d0900d 100644 --- a/studio/backend/utils/models/checkpoints.py +++ b/studio/backend/utils/models/checkpoints.py @@ -31,12 +31,13 @@ def _read_checkpoint_loss(checkpoint_path: Path) -> Optional[float]: def scan_checkpoints( outputs_dir: str = "./outputs", -) -> List[Tuple[str, List[Tuple[str, str, Optional[float]]]]]: +) -> List[Tuple[str, List[Tuple[str, str, Optional[float]]], dict]]: """ Scan outputs folder for training runs and their checkpoints. Returns: - List of tuples: [(model_name, [(display_name, checkpoint_path, loss), ...]), ...] + List of tuples: [(model_name, [(display_name, checkpoint_path, loss), ...], metadata), ...] + metadata keys: base_model, peft_type, lora_rank (all optional) The first entry in each checkpoint list is the main adapter; its loss is set to the loss of the last (highest-step) intermediate checkpoint. """ @@ -58,6 +59,32 @@ def scan_checkpoints( if not (config_file.exists() or adapter_config.exists()): continue + # Extract training metadata from adapter_config.json / config.json + metadata: dict = {} + try: + if adapter_config.exists(): + cfg = json.loads(adapter_config.read_text()) + metadata["base_model"] = cfg.get("base_model_name_or_path") + metadata["peft_type"] = cfg.get("peft_type") + metadata["lora_rank"] = cfg.get("r") + elif config_file.exists(): + cfg = json.loads(config_file.read_text()) + metadata["base_model"] = cfg.get("_name_or_path") + except Exception: + pass + + # Fallback: extract base model name from folder name + # e.g. "unsloth_Llama-3.2-3B-Instruct_1771227800" → "unsloth/Llama-3.2-3B-Instruct" + if not metadata.get("base_model"): + parts = item.name.rsplit("_", 1) + if len(parts) == 2 and parts[1].isdigit(): + name_part = parts[0] + idx = name_part.find("_") + if idx > 0: + metadata["base_model"] = name_part[:idx] + "/" + name_part[idx + 1:] + else: + metadata["base_model"] = name_part + # This is a valid training run checkpoints = [] @@ -79,7 +106,7 @@ def scan_checkpoints( last_checkpoint_loss = checkpoints[-1][2] checkpoints[0] = (checkpoints[0][0], checkpoints[0][1], last_checkpoint_loss) - models.append((item.name, checkpoints)) + models.append((item.name, checkpoints, metadata)) logger.debug(f"Found model: {item.name} with {len(checkpoints)} checkpoint(s)") # Sort by modification time (newest first) diff --git a/studio/frontend/src/components/assistant-ui/model-selector.tsx b/studio/frontend/src/components/assistant-ui/model-selector.tsx index 33dea79627..bf5f219558 100644 --- a/studio/frontend/src/components/assistant-ui/model-selector.tsx +++ b/studio/frontend/src/components/assistant-ui/model-selector.tsx @@ -112,7 +112,10 @@ function ModelSelectorContent({ diff --git a/studio/frontend/src/components/navbar.tsx b/studio/frontend/src/components/navbar.tsx index 28fc80499c..6a8fd5d7ca 100644 --- a/studio/frontend/src/components/navbar.tsx +++ b/studio/frontend/src/components/navbar.tsx @@ -3,6 +3,13 @@ import { HoverCardContent, HoverCardTrigger, } from "@/components/ui/hover-card"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; import { cn } from "@/lib/utils"; import { AiChat02Icon, @@ -29,6 +36,7 @@ const NAV_ITEMS = [ export function Navbar() { const pathname = useRouterState({ select: (s) => s.location.pathname }); const [logoHovered, setLogoHovered] = useState(false); + const [mobileOpen, setMobileOpen] = useState(false); const tourId = pathname === "/studio" @@ -39,9 +47,16 @@ export function Navbar() { ? "export" : null; + const openTour = () => { + if (!tourId) return; + window.dispatchEvent( + new CustomEvent(TOUR_OPEN_EVENT, { detail: { id: tourId } }), + ); + }; + return (
-
+
{/* Left: logo */}
- + unsloth @@ -76,7 +91,7 @@ export function Navbar() { {/* Center: pill nav */} - {/* Right: docs link */} -
+ {/* Right: docs/tour desktop */} + + + {/* Right: mobile */} +
); diff --git a/studio/frontend/src/components/ui/alert-dialog.tsx b/studio/frontend/src/components/ui/alert-dialog.tsx index b39a310c49..61327e525e 100644 --- a/studio/frontend/src/components/ui/alert-dialog.tsx +++ b/studio/frontend/src/components/ui/alert-dialog.tsx @@ -42,19 +42,21 @@ function AlertDialogOverlay({ ); } -function AlertDialogContent({ - className, - size = "default", - ...props -}: React.ComponentProps & { - size?: "default" | "sm"; -}) { - return ( - - - & { + size?: "default" | "sm"; + overlayClassName?: string; +}) { + return ( + + + +
-
+ -
+
); } diff --git a/studio/frontend/src/features/auth/signup-page.tsx b/studio/frontend/src/features/auth/signup-page.tsx index 5b71fe52ee..9d83b0bc66 100644 --- a/studio/frontend/src/features/auth/signup-page.tsx +++ b/studio/frontend/src/features/auth/signup-page.tsx @@ -1,9 +1,10 @@ import { LightRays } from "@/components/ui/light-rays"; +import { Card } from "@/components/ui/card"; import { AuthForm } from "./components/auth-form"; export function SignupPage() { return ( -
+
-
+ -
+
); } diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index c79f4b387c..34443ec87f 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -6,6 +6,13 @@ import { import { Thread } from "@/components/assistant-ui/thread"; import { Button } from "@/components/ui/button"; import { SidebarProvider, SidebarTrigger, useSidebar } from "@/components/ui/sidebar"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { @@ -29,6 +36,10 @@ import { GuidedTour, useGuidedTourController } from "@/features/tour"; import { ChatSettingsPanel } from "./chat-settings-sheet"; import { db } from "./db"; import { useChatModelRuntime } from "./hooks/use-chat-model-runtime"; +import { + clearTrainingCompareHandoff, + getTrainingCompareHandoff, +} from "./lib/training-compare-handoff"; import { ChatRuntimeProvider } from "./runtime-provider"; import { useChatRuntimeStore } from "./stores/chat-runtime-store"; import { @@ -41,6 +52,43 @@ import { ThreadSidebar } from "./thread-sidebar"; import type { ChatView } from "./types"; import { buildChatTourSteps } from "./tour"; +type LoraCandidate = { + id: string; + baseModel: string; + updatedAt?: number; +}; + +function normalizeModelRef(value: string | null | undefined): string { + return value?.trim().toLowerCase() ?? ""; +} + +function pickBestLoraForBase( + loras: LoraCandidate[], + baseModel: string | null, +): LoraCandidate | null { + if (loras.length === 0) return null; + const sorted = [...loras].sort( + (a, b) => (b.updatedAt ?? -1) - (a.updatedAt ?? -1), + ); + const normalizedBase = normalizeModelRef(baseModel); + if (!normalizedBase) return sorted[0]; + + const exact = sorted.find( + (lora) => normalizeModelRef(lora.baseModel) === normalizedBase, + ); + if (exact) return exact; + + const partial = sorted.find((lora) => { + const normalizedLoraBase = normalizeModelRef(lora.baseModel); + if (!normalizedLoraBase) return false; + return ( + normalizedLoraBase.includes(normalizedBase) || + normalizedBase.includes(normalizedLoraBase) + ); + }); + return partial ?? sorted[0]; +} + const SingleContent = memo(function SingleContent({ threadId, newThreadNonce, @@ -86,7 +134,10 @@ const CompareContent = memo(function CompareContent({ return (
-
+
@@ -104,8 +155,8 @@ const CompareContent = memo(function CompareContent({
-
-
+
+
Fine-tuned (LoRA) @@ -137,8 +188,23 @@ function InlineSidebar({ children: ReactNode; side?: "left" | "right"; }) { - const { state } = useSidebar(); + const { state, isMobile, openMobile, setOpenMobile } = useSidebar(); const collapsed = state === "collapsed"; + + if (isMobile) { + return ( + + + + Chat sidebar + Chat threads and actions + +
{children}
+
+
+ ); + } + return (
(null); + const [viewBeforeCompare, setViewBeforeCompare] = useState( + null, + ); const inferenceParams = useChatRuntimeStore((state) => state.params); const setInferenceParams = useChatRuntimeStore((state) => state.setParams); const autoTitle = useChatRuntimeStore((state) => state.autoTitle); @@ -216,6 +284,13 @@ export function ChatPage(): ReactElement { const lorasFromStore = useChatRuntimeStore((state) => state.loras); const modelsError = useChatRuntimeStore((state) => state.modelsError); const { refresh, selectModel, ejectModel } = useChatModelRuntime(); + const refreshRef = useRef(refresh); + const selectModelRef = useRef(selectModel); + + useEffect(() => { + refreshRef.current = refresh; + selectModelRef.current = selectModel; + }, [refresh, selectModel]); const canCompare = useMemo(() => { const selected = inferenceParams.checkpoint; if (!selected) return false; @@ -262,18 +337,15 @@ export function ChatPage(): ReactElement { const openSidebar = useCallback(() => setSidebarOpen(true), []); const enterCompare = useCallback(() => { - if (viewBeforeCompareRef.current == null) { - viewBeforeCompareRef.current = view; - } + setViewBeforeCompare((prev) => prev ?? view); setView({ mode: "compare", pairId: crypto.randomUUID() }); }, [view]); const exitCompare = useCallback(() => { - const prev = viewBeforeCompareRef.current; - if (!prev) return; - viewBeforeCompareRef.current = null; - setView(prev); - }, []); + if (!viewBeforeCompare) return; + setView(viewBeforeCompare); + setViewBeforeCompare(null); + }, [viewBeforeCompare]); const models = useMemo( () => @@ -297,9 +369,69 @@ export function ChatPage(): ReactElement { ); useEffect(() => { + if (getTrainingCompareHandoff()) return; void refresh(); }, [refresh]); + useEffect(() => { + const handoff = getTrainingCompareHandoff(); + if (!handoff) return; + console.info("[chat-handoff] received", handoff); + function clearHandoff(): void { + clearTrainingCompareHandoff(); + } + + let canceled = false; + void (async () => { + try { + console.info("[chat-handoff] refreshing models+loras"); + await refreshRef.current(); + if (canceled) return; + + const state = useChatRuntimeStore.getState(); + const targetLora = pickBestLoraForBase(state.loras, handoff.baseModel); + if (targetLora) { + console.info("[chat-handoff] loading lora", { + id: targetLora.id, + baseModel: targetLora.baseModel, + }); + await selectModelRef.current({ id: targetLora.id, isLora: true }); + if (canceled) return; + setView({ mode: "compare", pairId: crypto.randomUUID() }); + clearHandoff(); + console.info("[chat-handoff] loaded lora + opened compare"); + return; + } + + if ( + handoff.baseModel && + state.models.some((model) => model.id === handoff.baseModel) + ) { + console.info("[chat-handoff] no lora match, loading base", { + id: handoff.baseModel, + }); + await selectModelRef.current({ id: handoff.baseModel, isLora: false }); + if (canceled) return; + } else { + console.warn("[chat-handoff] no lora/base match found", { + requestedBaseModel: handoff.baseModel, + loraCount: state.loras.length, + modelCount: state.models.length, + }); + } + clearHandoff(); + console.info("[chat-handoff] completed"); + } catch (error) { + console.error("[chat-handoff] failed", error); + clearHandoff(); + } + })(); + + return () => { + canceled = true; + }; + }, []); + const tourSteps = useMemo( () => buildChatTourSteps({ @@ -332,18 +464,21 @@ export function ChatPage(): ReactElement { useEffect(() => { if (tour.open) return; if (!modelSelectorLocked) return; - setModelSelectorLocked(false); - setModelSelectorOpen(false); + const timeoutId = window.setTimeout(() => { + setModelSelectorLocked(false); + setModelSelectorOpen(false); + }, 0); + return () => window.clearTimeout(timeoutId); }, [modelSelectorLocked, tour.open]); return ( -
+
-
+
{modelsError && ( diff --git a/studio/frontend/src/features/chat/index.ts b/studio/frontend/src/features/chat/index.ts index b7eaaf83ae..521a0fe27f 100644 --- a/studio/frontend/src/features/chat/index.ts +++ b/studio/frontend/src/features/chat/index.ts @@ -7,3 +7,4 @@ export { } from "./chat-settings-sheet"; export { useChatRuntimeStore } from "./stores/chat-runtime-store"; export { useChatModelRuntime } from "./hooks/use-chat-model-runtime"; +export { setTrainingCompareHandoff } from "./lib/training-compare-handoff"; diff --git a/studio/frontend/src/features/chat/lib/training-compare-handoff.ts b/studio/frontend/src/features/chat/lib/training-compare-handoff.ts new file mode 100644 index 0000000000..1ceaf7a00b --- /dev/null +++ b/studio/frontend/src/features/chat/lib/training-compare-handoff.ts @@ -0,0 +1,53 @@ +const TRAINING_COMPARE_HANDOFF_KEY = "chat:training-compare-handoff:v1"; +const HANDOFF_MAX_AGE_MS = 15 * 60 * 1000; + +export type TrainingCompareHandoff = { + intent: "compare"; + baseModel: string | null; + requestedAt: number; +}; + +export function setTrainingCompareHandoff(baseModel: string | null): void { + if (typeof window === "undefined") return; + + const payload: TrainingCompareHandoff = { + intent: "compare", + baseModel, + requestedAt: Date.now(), + }; + window.sessionStorage.setItem( + TRAINING_COMPARE_HANDOFF_KEY, + JSON.stringify(payload), + ); +} + +export function getTrainingCompareHandoff(): TrainingCompareHandoff | null { + if (typeof window === "undefined") return null; + + const raw = window.sessionStorage.getItem(TRAINING_COMPARE_HANDOFF_KEY); + if (!raw) return null; + + try { + const parsed = JSON.parse(raw) as Partial; + if (parsed.intent !== "compare") return null; + if (typeof parsed.requestedAt !== "number") return null; + if (Date.now() - parsed.requestedAt > HANDOFF_MAX_AGE_MS) { + clearTrainingCompareHandoff(); + return null; + } + return { + intent: "compare", + baseModel: + typeof parsed.baseModel === "string" ? parsed.baseModel : null, + requestedAt: parsed.requestedAt, + }; + } catch { + clearTrainingCompareHandoff(); + return null; + } +} + +export function clearTrainingCompareHandoff(): void { + if (typeof window === "undefined") return; + window.sessionStorage.removeItem(TRAINING_COMPARE_HANDOFF_KEY); +} diff --git a/studio/frontend/src/features/export/api/export-api.ts b/studio/frontend/src/features/export/api/export-api.ts new file mode 100644 index 0000000000..cbaa4a8145 --- /dev/null +++ b/studio/frontend/src/features/export/api/export-api.ts @@ -0,0 +1,127 @@ +import { authFetch } from "@/features/auth"; + +async function readError(response: Response): Promise { + try { + const payload = (await response.json()) as { detail?: string; message?: string }; + return payload.detail || payload.message || `Request failed (${response.status})`; + } catch { + return `Request failed (${response.status})`; + } +} + +async function parseJson(response: Response): Promise { + if (!response.ok) { + throw new Error(await readError(response)); + } + return (await response.json()) as T; +} + +export interface CheckpointInfo { + display_name: string; + path: string; + loss?: number | null; +} + +export interface ModelCheckpoints { + name: string; + checkpoints: CheckpointInfo[]; + base_model?: string | null; + peft_type?: string | null; + lora_rank?: number | null; +} + +export interface CheckpointListResponse { + outputs_dir: string; + models: ModelCheckpoints[]; +} + +export interface ExportOperationResponse { + success: boolean; + message: string; + details?: Record | null; +} + +export async function fetchCheckpoints(): Promise { + const response = await authFetch("/api/models/checkpoints"); + return parseJson(response); +} + +export async function loadCheckpoint(params: { + checkpoint_path: string; + max_seq_length?: number; + load_in_4bit?: boolean; +}): Promise { + const response = await authFetch("/api/export/load-checkpoint", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(params), + }); + return parseJson(response); +} + +export async function exportMerged(params: { + save_directory: string; + format_type?: string; + push_to_hub?: boolean; + repo_id?: string | null; + hf_token?: string | null; + private?: boolean; +}): Promise { + const response = await authFetch("/api/export/export/merged", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(params), + }); + return parseJson(response); +} + +export async function exportBase(params: { + save_directory: string; + push_to_hub?: boolean; + repo_id?: string | null; + hf_token?: string | null; + private?: boolean; + base_model_id?: string | null; +}): Promise { + const response = await authFetch("/api/export/export/base", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(params), + }); + return parseJson(response); +} + +export async function exportGGUF(params: { + save_directory: string; + quantization_method: string; + push_to_hub?: boolean; + repo_id?: string | null; + hf_token?: string | null; +}): Promise { + const response = await authFetch("/api/export/export/gguf", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(params), + }); + return parseJson(response); +} + +export async function exportLoRA(params: { + save_directory: string; + push_to_hub?: boolean; + repo_id?: string | null; + hf_token?: string | null; + private?: boolean; +}): Promise { + const response = await authFetch("/api/export/export/lora", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(params), + }); + return parseJson(response); +} + +export async function cleanupExport(): Promise { + const response = await authFetch("/api/export/cleanup", { method: "POST" }); + return parseJson(response); +} diff --git a/studio/frontend/src/features/export/components/export-dialog.tsx b/studio/frontend/src/features/export/components/export-dialog.tsx index 4f66048270..0cbd31fdb4 100644 --- a/studio/frontend/src/features/export/components/export-dialog.tsx +++ b/studio/frontend/src/features/export/components/export-dialog.tsx @@ -13,8 +13,9 @@ import { InputGroupAddon, InputGroupInput, } from "@/components/ui/input-group"; +import { Spinner } from "@/components/ui/spinner"; import { Switch } from "@/components/ui/switch"; -import { ArrowRight01Icon, Key01Icon } from "@hugeicons/core-free-icons"; +import { AlertCircleIcon, ArrowRight01Icon, CheckmarkCircle02Icon, Key01Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { AnimatePresence, motion } from "motion/react"; import { collapseAnim } from "../anim"; @@ -41,6 +42,10 @@ interface ExportDialogProps { onHfTokenChange: (v: string) => void; privateRepo: boolean; onPrivateRepoChange: (v: boolean) => void; + onExport: () => void; + exporting: boolean; + exportError: string | null; + exportSuccess: boolean; } export function ExportDialog({ @@ -49,7 +54,7 @@ export function ExportDialog({ checkpoint, exportMethod, quantLevels, - estimatedSize, + estimatedSize: _estimatedSize, baseModelName, isAdapter, destination, @@ -62,150 +67,211 @@ export function ExportDialog({ onHfTokenChange, privateRepo, onPrivateRepoChange, + onExport, + exporting, + exportError, + exportSuccess, }: ExportDialogProps) { return ( - - - - Export Model - - Choose where to save your exported model. - - - -
- - -
- - - {destination === "hub" && ( - -
-
-
- - onHfUsernameChange(e.target.value)} - /> -
-
- - onModelNameChange(e.target.value)} - /> -
-
- -
-
- - - Get token - - -
- - - - - onHfTokenChange(e.target.value)} - /> - -

- Leave empty if already logged in via CLI. -

-
- -
- - -
+ { + if (exporting) return; + onOpenChange(v); + }} + > + { if (exporting) e.preventDefault(); }}> + {exportSuccess ? ( + <> +
+
+ +
+
+

Export Complete

+

+ {destination === "hub" + ? "Model successfully pushed to Hugging Face Hub." + : "Model saved locally."} +

- - )} - - - {/* Summary */} -
-
- Base Model - {baseModelName} -
-
- {isAdapter ? "Checkpoint" : "Model"} - {checkpoint} -
-
- Export Method - - {EXPORT_METHODS.find((m) => m.value === exportMethod)?.title} - -
- {exportMethod === "gguf" && quantLevels.length > 0 && ( -
- Quantizations - - {quantLevels.join(", ")} -
- )} -
+ + + + + ) : ( + <> + + Export Model + + Choose where to save your exported model. + + + +
+ + +
+ + + {destination === "hub" && ( + +
+
+
+ + onHfUsernameChange(e.target.value)} + disabled={exporting} + /> +
+
+ + onModelNameChange(e.target.value)} + disabled={exporting} + /> +
+
+ +
+
+ + + Get token + + +
+ + + + + onHfTokenChange(e.target.value)} + disabled={exporting} + /> + +

+ Leave empty if already logged in via CLI. +

+
+ +
+ + +
+
+
+ )} +
+ + {/* Error banner */} + {exportError && ( +
+ + {exportError} +
+ )} + + {/* Summary */} +
+
+ Base Model + {baseModelName} +
+
+ {isAdapter ? "Checkpoint" : "Model"} + {checkpoint} +
+
+ Export Method + + {EXPORT_METHODS.find((m) => m.value === exportMethod)?.title} + +
+ {exportMethod === "gguf" && quantLevels.length > 0 && ( +
+ Quantizations + + {quantLevels.join(", ")} + +
+ )} + {/* TODO: unhide once estimated size comes from the backend API */} + {/*
Est. size {estimatedSize} -
-
+
*/} +
- - - - + + + + + + )}
); diff --git a/studio/frontend/src/features/export/export-page.tsx b/studio/frontend/src/features/export/export-page.tsx index 96ca3589eb..9d3e5053de 100644 --- a/studio/frontend/src/features/export/export-page.tsx +++ b/studio/frontend/src/features/export/export-page.tsx @@ -8,80 +8,56 @@ import { SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; +import { Spinner } from "@/components/ui/spinner"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; -import { useTrainingRuntimeStore } from "@/features/training"; import { useTrainingConfigStore } from "@/features/training"; -import { isAdapterMethod } from "@/types/training"; -import { InformationCircleIcon, PackageIcon } from "@hugeicons/core-free-icons"; +import { AlertCircleIcon, InformationCircleIcon, PackageIcon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { AnimatePresence, motion } from "motion/react"; -import { useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useShallow } from "zustand/react/shallow"; import { collapseAnim } from "./anim"; +import type { ModelCheckpoints } from "./api/export-api"; +import { + cleanupExport, + exportBase, + exportGGUF, + exportLoRA, + exportMerged, + fetchCheckpoints, + loadCheckpoint, +} from "./api/export-api"; import { ExportDialog } from "./components/export-dialog"; import { MethodPicker } from "./components/method-picker"; import { QuantPicker } from "./components/quant-picker"; import { type ExportMethod, GUIDE_STEPS, - METHOD_LABELS, getEstimatedSize, } from "./constants"; import { GuidedTour, useGuidedTourController } from "@/features/tour"; import { exportTourSteps } from "./tour"; export function ExportPage() { - const { - trainingMethod, - selectedModel, - saveSteps, - epochs, - loraRank, - hfToken, - setHfToken, - } = useTrainingConfigStore( + const { hfToken, setHfToken } = useTrainingConfigStore( useShallow((s) => ({ - trainingMethod: s.trainingMethod, - selectedModel: s.selectedModel, - saveSteps: s.saveSteps, - epochs: s.epochs, - loraRank: s.loraRank, hfToken: s.hfToken, setHfToken: s.setHfToken, })), ); - const totalSteps = useTrainingRuntimeStore((state) => state.totalSteps); - const isAdapter = isAdapterMethod(trainingMethod); - const checkpoints = useMemo(() => { - if (isAdapter) { - const interval = saveSteps > 0 ? saveSteps : 100; - const total = totalSteps > 0 ? totalSteps : 500; - const entries: { value: string; label: string; detail: string }[] = []; - for (let step = interval; step <= total; step += interval) { - const loss = (1.5 - (step / total) * 0.7).toFixed(2); - entries.push({ - value: `checkpoint-${step}`, - label: `checkpoint-${step}`, - detail: step === total ? `Best Loss: ${loss}` : `Loss: ${loss}`, - }); - } - return entries.reverse(); - } - return [ - { - value: "final-model", - label: "Final Model", - detail: "Full fine-tuned weights", - }, - ]; - }, [isAdapter, saveSteps, totalSteps]); + // ---- API-driven checkpoint state ---- + const [models, setModels] = useState([]); + const [loadingCheckpoints, setLoadingCheckpoints] = useState(true); + const [checkpointError, setCheckpointError] = useState(null); + const [selectedModelIdx, setSelectedModelIdx] = useState(null); const [checkpoint, setCheckpoint] = useState(null); + const [exportMethod, setExportMethod] = useState(null); const [quantLevels, setQuantLevels] = useState([]); const [dialogOpen, setDialogOpen] = useState(false); @@ -91,11 +67,68 @@ export function ExportPage() { const [modelName, setModelName] = useState(""); const [privateRepo, setPrivateRepo] = useState(false); + const [exporting, setExporting] = useState(false); + const [exportError, setExportError] = useState(null); + const [exportSuccess, setExportSuccess] = useState(false); + const tour = useGuidedTourController({ id: "export", steps: exportTourSteps, }); + // ---- Fetch checkpoints on mount ---- + useEffect(() => { + let cancelled = false; + setLoadingCheckpoints(true); + setCheckpointError(null); + fetchCheckpoints() + .then((data) => { + if (!cancelled) { + setModels(data.models); + } + }) + .catch((err) => { + if (!cancelled) { + setCheckpointError( + err instanceof Error ? err.message : "Failed to load checkpoints", + ); + } + }) + .finally(() => { + if (!cancelled) setLoadingCheckpoints(false); + }); + return () => { + cancelled = true; + }; + }, []); + + // ---- Derived state ---- + const selectedModelData = useMemo( + () => + selectedModelIdx != null + ? models.find((m) => m.name === selectedModelIdx) ?? null + : null, + [models, selectedModelIdx], + ); + + const checkpointsForModel = useMemo( + () => selectedModelData?.checkpoints ?? [], + [selectedModelData], + ); + + // Derive training info from selected model's API metadata + const baseModelName = selectedModelData?.base_model ?? "—"; + const isAdapter = !!selectedModelData?.peft_type; + const loraRank = selectedModelData?.lora_rank ?? null; + const trainingMethodLabel = selectedModelData?.peft_type + ? "LoRA / QLoRA" + : "Full Fine-tune"; + + // Reset checkpoint when the selected model changes + useEffect(() => { + setCheckpoint(null); + }, [selectedModelIdx]); + const handleMethodChange = (method: ExportMethod) => { setExportMethod(method); if (method !== "gguf") { @@ -108,11 +141,103 @@ export function ExportPage() { checkpoint && exportMethod && (exportMethod !== "gguf" || quantLevels.length > 0); - const baseModelName = selectedModel ?? "—"; + // ---- Export handler ---- + const handleExport = useCallback(async () => { + if (!checkpoint) return; + + const selectedCp = checkpointsForModel.find( + (cp) => cp.display_name === checkpoint, + ); + if (!selectedCp) return; + + setExporting(true); + setExportError(null); + setExportSuccess(false); + + const saveDir = `./exports/${selectedModelIdx ?? "model"}/${checkpoint}`; + const pushToHub = destination === "hub"; + const repoId = pushToHub && hfUsername && modelName + ? `${hfUsername}/${modelName}` + : undefined; + const token = pushToHub && hfToken ? hfToken : undefined; + + try { + // 1. Load checkpoint + await loadCheckpoint({ checkpoint_path: selectedCp.path }); + + // 2. Run export based on method + if (exportMethod === "merged") { + if (isAdapter) { + await exportMerged({ + save_directory: saveDir, + push_to_hub: pushToHub, + repo_id: repoId, + hf_token: token, + private: privateRepo, + }); + } else { + await exportBase({ + save_directory: saveDir, + push_to_hub: pushToHub, + repo_id: repoId, + hf_token: token, + private: privateRepo, + base_model_id: selectedModelData?.base_model, + }); + } + } else if (exportMethod === "gguf") { + for (const quant of quantLevels) { + await exportGGUF({ + save_directory: saveDir, + quantization_method: quant, + push_to_hub: pushToHub, + repo_id: repoId, + hf_token: token, + }); + } + } else if (exportMethod === "lora") { + await exportLoRA({ + save_directory: saveDir, + push_to_hub: pushToHub, + repo_id: repoId, + hf_token: token, + private: privateRepo, + }); + } + + setExportSuccess(true); + } catch (err) { + setExportError( + err instanceof Error ? err.message : "Export failed", + ); + } finally { + try { + await cleanupExport(); + } catch { + // cleanup is best-effort + } + setExporting(false); + } + }, [ + checkpoint, + checkpointsForModel, + selectedModelIdx, + selectedModelData, + exportMethod, + isAdapter, + quantLevels, + destination, + hfUsername, + modelName, + hfToken, + privateRepo, + ]); + + // ---- Render ---- return (
-
+
@@ -132,141 +257,236 @@ export function ExportPage() { featured={true} className="shadow-border ring-1 ring-border" > - {/* Top row: Checkpoint + metadata | Guide */} -
-
-
- - -
+ {/* Loading / error states */} + {loadingCheckpoints && ( +
+ + Loading checkpoints… +
+ )} -
- - Training Info - -
-
- Base Model - {baseModelName} + {checkpointError && ( +
+ + {checkpointError} +
+ )} + + {!loadingCheckpoints && !checkpointError && ( + <> + {/* Top row: Dropdowns + metadata | Guide */} +
+
+ {/* Training run dropdown */} +
+ +
-
- Method - - {METHOD_LABELS[trainingMethod] ?? trainingMethod} + + {/* Checkpoint dropdown */} +
+ + +
+ +
+ + Training Info -
-
- Checkpoints - {checkpoints.length} -
-
- Epochs - {epochs} -
- {isAdapter && ( -
- LoRA Rank - {loraRank} +
+
+ Base Model + {baseModelName} +
+
+ Method + + {trainingMethodLabel} + +
+
+ Checkpoints + + {checkpointsForModel.length} + +
+ {isAdapter && ( +
+ LoRA Rank + {loraRank} +
+ )}
- )} +
+
+ +
+ + Quick Guide + +
    + {GUIDE_STEPS.map((step, i) => ( +
  1. + + {i + 1} + + {step} +
  2. + ))} +
-
-
- - Quick Guide - -
    - {GUIDE_STEPS.map((step, i) => ( -
  1. - - {i + 1} - - {step} -
  2. - ))} -
-
-
+ - + + {exportMethod === "gguf" && ( + + + + )} + - - {exportMethod === "gguf" && ( - - - - )} - - - -
-
- - Est. size: {estimatedSize} · Free disk space: 120 GB -
- -
+ +
+ {/* TODO: unhide once estimated size comes from the backend API */} + {/*
+ + Est. size: {estimatedSize} · Free disk space: 120 GB +
*/} + +
+ + )}
@@ -289,6 +509,10 @@ export function ExportPage() { onHfTokenChange={setHfToken} privateRepo={privateRepo} onPrivateRepoChange={setPrivateRepo} + onExport={handleExport} + exporting={exporting} + exportError={exportError} + exportSuccess={exportSuccess} />
); diff --git a/studio/frontend/src/features/export/tour/steps.tsx b/studio/frontend/src/features/export/tour/steps.tsx index 63cfe36eb8..559524894b 100644 --- a/studio/frontend/src/features/export/tour/steps.tsx +++ b/studio/frontend/src/features/export/tour/steps.tsx @@ -1,6 +1,17 @@ import type { TourStep } from "@/features/tour"; export const exportTourSteps: TourStep[] = [ + { + id: "training-run", + target: "export-training-run", + title: "Pick training run", + body: ( + <> + Start by selecting the training run. Each run groups the checkpoints + produced by that specific fine-tuning job. + + ), + }, { id: "checkpoint", target: "export-checkpoint", diff --git a/studio/frontend/src/features/onboarding/components/splash-screen.tsx b/studio/frontend/src/features/onboarding/components/splash-screen.tsx index f79775b6b9..9839d967af 100644 --- a/studio/frontend/src/features/onboarding/components/splash-screen.tsx +++ b/studio/frontend/src/features/onboarding/components/splash-screen.tsx @@ -1,4 +1,5 @@ import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; import { motion } from "motion/react"; interface SplashScreenProps { @@ -11,57 +12,61 @@ export function SplashScreen({ onGoToStudio, }: SplashScreenProps) { return ( -
- {/* Mascot */} - +
+ + {/* Mascot */} +
+ +
- {/* Brand text */} - -

- Unsloth Studio -

-

Fine-tune LLMs faster

-
+ {/* Brand text */} + +

+ Unsloth Studio +

+

Fine-tune LLMs faster

+
- {/* Buttons */} - - - - + {/* Buttons */} + + + + +
); } diff --git a/studio/frontend/src/features/onboarding/components/steps/hyperparameters-step.tsx b/studio/frontend/src/features/onboarding/components/steps/hyperparameters-step.tsx index 45f9c82878..9ad9c31e0a 100644 --- a/studio/frontend/src/features/onboarding/components/steps/hyperparameters-step.tsx +++ b/studio/frontend/src/features/onboarding/components/steps/hyperparameters-step.tsx @@ -28,6 +28,8 @@ import { useShallow } from "zustand/react/shallow"; export function HyperparametersStep() { const { trainingMethod, + maxSteps, + setMaxSteps, epochs, setEpochs, contextLength, @@ -43,6 +45,8 @@ export function HyperparametersStep() { } = useTrainingConfigStore( useShallow((s) => ({ trainingMethod: s.trainingMethod, + maxSteps: s.maxSteps, + setMaxSteps: s.setMaxSteps, epochs: s.epochs, setEpochs: s.setEpochs, contextLength: s.contextLength, @@ -60,6 +64,8 @@ export function HyperparametersStep() { const showLoraParams = trainingMethod === "lora" || trainingMethod === "qlora"; + const maxStepsSliderMax = Math.max(500, maxSteps, 30); + const epochsSliderMax = Math.max(10, epochs, 1); return ( @@ -68,7 +74,7 @@ export function HyperparametersStep() {
- Epochs + Max Steps
@@ -196,6 +202,56 @@ export function HyperparametersStep() { className="w-32 font-mono" />
+ +
+ + Epochs + + + + + + Number of full passes over the dataset. Set 0 to run by max + steps.{" "} + + Read more + + + + +
+ setEpochs(v)} + min={0} + max={epochsSliderMax} + step={1} + className="w-40" + /> + setEpochs(Number(e.target.value))} + min={0} + max={epochsSliderMax} + step={1} + className="w-12 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" + /> +
+
diff --git a/studio/frontend/src/features/onboarding/components/steps/model-selection-step.tsx b/studio/frontend/src/features/onboarding/components/steps/model-selection-step.tsx index d11ff6bac0..3f63775ff9 100644 --- a/studio/frontend/src/features/onboarding/components/steps/model-selection-step.tsx +++ b/studio/frontend/src/features/onboarding/components/steps/model-selection-step.tsx @@ -45,7 +45,7 @@ import { Search01Icon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; -import { useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useShallow } from "zustand/react/shallow"; export function ModelSelectionStep() { @@ -53,6 +53,7 @@ export function ModelSelectionStep() { modelType, selectedModel, setSelectedModel, + ensureModelDefaultsLoaded, trainingMethod, setTrainingMethod, hfToken, @@ -62,6 +63,7 @@ export function ModelSelectionStep() { modelType: s.modelType, selectedModel: s.selectedModel, setSelectedModel: s.setSelectedModel, + ensureModelDefaultsLoaded: s.ensureModelDefaultsLoaded, trainingMethod: s.trainingMethod, setTrainingMethod: s.setTrainingMethod, hfToken: s.hfToken, @@ -91,6 +93,10 @@ export function ModelSelectionStep() { hfResults.length, ); + useEffect(() => { + ensureModelDefaultsLoaded(); + }, [selectedModel, ensureModelDefaultsLoaded]); + return ( diff --git a/studio/frontend/src/features/onboarding/components/wizard-content.tsx b/studio/frontend/src/features/onboarding/components/wizard-content.tsx index 2f10be0fcd..20e37f7d58 100644 --- a/studio/frontend/src/features/onboarding/components/wizard-content.tsx +++ b/studio/frontend/src/features/onboarding/components/wizard-content.tsx @@ -31,19 +31,19 @@ export function WizardContent() { return (
-
- Unsloth mascot +
+ Unsloth mascot
-

{stepConfig.title}

+

{stepConfig.title}

{stepConfig.description}

-

+

Step {currentStep} of {STEPS.length}

-
+
diff --git a/studio/frontend/src/features/onboarding/components/wizard-layout.tsx b/studio/frontend/src/features/onboarding/components/wizard-layout.tsx index 3a38035fe1..8153922395 100644 --- a/studio/frontend/src/features/onboarding/components/wizard-layout.tsx +++ b/studio/frontend/src/features/onboarding/components/wizard-layout.tsx @@ -54,7 +54,7 @@ export function WizardLayout() { }, [isFinalStep]); return ( -
+
{showSplash && ( setShowSplash(false)} @@ -81,7 +81,7 @@ export function WizardLayout() { ease: [0.165, 0.84, 0.44, 1], }} > - +
diff --git a/studio/frontend/src/features/onboarding/components/wizard-sidebar.tsx b/studio/frontend/src/features/onboarding/components/wizard-sidebar.tsx index 6cd5c36a6f..734c8c166d 100644 --- a/studio/frontend/src/features/onboarding/components/wizard-sidebar.tsx +++ b/studio/frontend/src/features/onboarding/components/wizard-sidebar.tsx @@ -8,8 +8,8 @@ export function WizardSidebar() { const progress = ((currentStep - 1) / (STEPS.length - 1)) * 100; return ( -