Merge nightly into feature/colab-notebook - resolved setup.sh conflicts
This commit is contained in:
commit
84b9a8aef6
124 changed files with 3875 additions and 1487 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -18,6 +18,7 @@ unsloth_compiled_cache/
|
|||
|
||||
# ML artifacts (large files)
|
||||
outputs/
|
||||
exports/
|
||||
*.gguf
|
||||
*.safetensors
|
||||
|
||||
|
|
|
|||
43
setup.sh
43
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
fastapi>=0.100.0
|
||||
uvicorn>=0.27.0
|
||||
pydantic>=2.0
|
||||
torch
|
||||
psutil
|
||||
nest-asyncio>=1.5.8
|
||||
|
||||
3
studio/backend/requirements/base.txt
Normal file
3
studio/backend/requirements/base.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Core unsloth packages
|
||||
unsloth-zoo
|
||||
unsloth
|
||||
13
studio/backend/requirements/extras-no-deps.txt
Normal file
13
studio/backend/requirements/extras-no-deps.txt
Normal file
|
|
@ -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
|
||||
56
studio/backend/requirements/extras.txt
Normal file
56
studio/backend/requirements/extras.txt
Normal file
|
|
@ -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
|
||||
7
studio/backend/requirements/overrides.txt
Normal file
7
studio/backend/requirements/overrides.txt
Normal file
|
|
@ -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
|
||||
13
studio/backend/requirements/studio.txt
Normal file
13
studio/backend/requirements/studio.txt
Normal file
|
|
@ -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
|
||||
2
studio/backend/requirements/triton-kernels.txt
Normal file
2
studio/backend/requirements/triton-kernels.txt
Normal file
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -112,7 +112,10 @@ function ModelSelectorContent({
|
|||
<PopoverContent
|
||||
align="start"
|
||||
data-tour={dataTour}
|
||||
className={cn("w-[440px] min-w-[440px] gap-0 p-2", className)}
|
||||
className={cn(
|
||||
"w-[min(440px,calc(100vw-1rem))] max-w-[calc(100vw-1rem)] min-w-0 gap-0 p-2",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Tabs defaultValue="hub" className="w-full">
|
||||
<TabsList className="mb-2 w-full">
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<header className="top-0 z-40 h-16 w-full">
|
||||
<div className="mx-auto flex h-full max-w-7xl items-center justify-between px-6">
|
||||
<div className="mx-auto flex h-full max-w-7xl items-center justify-between px-4 sm:px-6">
|
||||
{/* Left: logo */}
|
||||
<div
|
||||
className="relative flex items-center gap-2.5 cursor-pointer select-none"
|
||||
|
|
@ -55,7 +70,7 @@ export function Navbar() {
|
|||
animate={{ rotate: logoHovered ? 360 : 0 }}
|
||||
transition={{ duration: 0.5, ease: [0.165, 0.84, 0.44, 1] }}
|
||||
/>
|
||||
<span className="text-2xl font-bold tracking-wide font-heading">
|
||||
<span className="text-xl font-bold tracking-wide font-heading sm:text-2xl">
|
||||
unsloth
|
||||
</span>
|
||||
<AnimatePresence>
|
||||
|
|
@ -76,7 +91,7 @@ export function Navbar() {
|
|||
{/* Center: pill nav */}
|
||||
<nav
|
||||
data-tour="navbar"
|
||||
className="flex items-center rounded-full border border-border bg-card p-1 ring-1 ring-foreground/5"
|
||||
className="hidden items-center rounded-full border border-border bg-card p-1 ring-1 ring-foreground/5 md:flex"
|
||||
>
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const active = pathname === item.href;
|
||||
|
|
@ -138,8 +153,8 @@ export function Navbar() {
|
|||
})}
|
||||
</nav>
|
||||
|
||||
{/* Right: docs link */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Right: docs/tour desktop */}
|
||||
<div className="hidden items-center gap-2 md:flex">
|
||||
<HoverCard openDelay={200} closeDelay={100}>
|
||||
<HoverCardTrigger asChild={true}>
|
||||
<a
|
||||
|
|
@ -177,11 +192,7 @@ export function Navbar() {
|
|||
{tourId ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(TOUR_OPEN_EVENT, { detail: { id: tourId } }),
|
||||
);
|
||||
}}
|
||||
onClick={openTour}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
title="Tour"
|
||||
>
|
||||
|
|
@ -189,6 +200,77 @@ export function Navbar() {
|
|||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Right: mobile */}
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
{tourId ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openTour}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
title="Tour"
|
||||
>
|
||||
<HugeiconsIcon icon={CursorInfo02Icon} className="size-4" />
|
||||
</button>
|
||||
) : null}
|
||||
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
|
||||
<SheetTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground"
|
||||
aria-label="Open navigation menu"
|
||||
>
|
||||
Menu
|
||||
</button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="right" className="w-[300px] p-4">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Navigate</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="mt-6 flex flex-col gap-2">
|
||||
{NAV_ITEMS.filter((item) => item.enabled).map((item) => {
|
||||
const active = pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
to={item.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className={cn(
|
||||
"rounded-md border px-3 py-2 text-sm font-medium",
|
||||
active
|
||||
? "border-foreground bg-foreground text-background"
|
||||
: "border-border text-foreground hover:bg-accent",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
<a
|
||||
href="https://unsloth.ai/docs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-2 rounded-md border border-border px-3 py-2 text-sm font-medium text-foreground hover:bg-accent"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
>
|
||||
Learn more (Docs)
|
||||
</a>
|
||||
{tourId ? (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-border px-3 py-2 text-left text-sm font-medium text-foreground hover:bg-accent"
|
||||
onClick={() => {
|
||||
openTour();
|
||||
setMobileOpen(false);
|
||||
}}
|
||||
>
|
||||
Start tour
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -42,19 +42,21 @@ function AlertDialogOverlay({
|
|||
);
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
|
||||
size?: "default" | "sm";
|
||||
}) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
data-size={size}
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
size = "default",
|
||||
overlayClassName,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
|
||||
size?: "default" | "sm";
|
||||
overlayClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay className={overlayClassName} />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/5 gap-6 rounded-4xl p-6 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-md group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none",
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -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 LoginPage() {
|
||||
return (
|
||||
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-6 py-10 md:px-10">
|
||||
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4 py-8 sm:px-6 sm:py-10 md:px-10">
|
||||
<LightRays
|
||||
count={6}
|
||||
color="rgba(34, 197, 94, 0.25)"
|
||||
|
|
@ -12,9 +13,9 @@ export function LoginPage() {
|
|||
length="70vh"
|
||||
style={{ opacity: 0.4 }}
|
||||
/>
|
||||
<div className="relative z-10 w-full max-w-sm">
|
||||
<Card className="relative z-10 w-full max-w-sm px-5 py-6 shadow-border ring-1 ring-border sm:px-6 sm:py-8">
|
||||
<AuthForm mode="login" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-6 py-10 md:px-10">
|
||||
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4 py-8 sm:px-6 sm:py-10 md:px-10">
|
||||
<LightRays
|
||||
count={6}
|
||||
color="rgba(34, 197, 94, 0.25)"
|
||||
|
|
@ -12,9 +13,9 @@ export function SignupPage() {
|
|||
length="70vh"
|
||||
style={{ opacity: 0.4 }}
|
||||
/>
|
||||
<div className="relative z-10 w-full max-w-sm">
|
||||
<Card className="relative z-10 w-full max-w-sm px-5 py-6 shadow-border ring-1 ring-border sm:px-6 sm:py-8">
|
||||
<AuthForm mode="signup" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<CompareHandlesProvider handlesRef={handlesRef}>
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<div data-tour="chat-compare-view" className="grid min-h-0 flex-1 grid-cols-2 px-0">
|
||||
<div
|
||||
data-tour="chat-compare-view"
|
||||
className="grid min-h-0 flex-1 grid-cols-1 px-0 md:grid-cols-2"
|
||||
>
|
||||
<div className="flex min-h-0 flex-col">
|
||||
<div className="px-3 py-1.5">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
|
|
@ -104,8 +155,8 @@ const CompareContent = memo(function CompareContent({
|
|||
</ChatRuntimeProvider>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-col">
|
||||
<div className="text-end px-3 py-1.5">
|
||||
<div className="flex min-h-0 flex-col border-t border-border/60 md:border-t-0 md:border-l">
|
||||
<div className="px-3 py-1.5 text-start md:text-end">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-primary">
|
||||
Fine-tuned (LoRA)
|
||||
</span>
|
||||
|
|
@ -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 (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile}>
|
||||
<SheetContent side={side} className="w-[18rem] p-0">
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Chat sidebar</SheetTitle>
|
||||
<SheetDescription>Chat threads and actions</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="h-full overflow-auto">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group shrink-0 h-full"
|
||||
|
|
@ -207,7 +273,9 @@ export function ChatPage(): ReactElement {
|
|||
const [modelSelectorOpen, setModelSelectorOpen] = useState(false);
|
||||
const [modelSelectorLocked, setModelSelectorLocked] = useState(false);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const viewBeforeCompareRef = useRef<ChatView | null>(null);
|
||||
const [viewBeforeCompare, setViewBeforeCompare] = useState<ChatView | null>(
|
||||
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<ModelOption[]>(
|
||||
() =>
|
||||
|
|
@ -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 (
|
||||
<div className="h-[calc(100vh-4rem)] bg-background overflow-hidden">
|
||||
<div className="h-[calc(100dvh-4rem)] bg-background overflow-hidden">
|
||||
<GuidedTour {...tour.tourProps} />
|
||||
<SidebarProvider
|
||||
defaultOpen={true}
|
||||
open={sidebarOpen}
|
||||
onOpenChange={setSidebarOpen}
|
||||
className="!min-h-0 h-full max-w-7xl mx-auto px-4"
|
||||
className="!min-h-0 h-full w-full max-w-7xl mx-auto px-2 sm:px-4"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": "14rem",
|
||||
|
|
@ -362,7 +497,7 @@ export function ChatPage(): ReactElement {
|
|||
</InlineSidebar>
|
||||
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<div className="flex h-11 shrink-0 items-center px-2">
|
||||
<div className="flex h-11 shrink-0 items-center px-1.5 sm:px-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<SidebarTrigger />
|
||||
<TopBarActions
|
||||
|
|
@ -381,6 +516,7 @@ export function ChatPage(): ReactElement {
|
|||
onOpenChange={handleModelSelectorOpenChange}
|
||||
triggerDataTour="chat-model-selector"
|
||||
contentDataTour="chat-model-selector-popover"
|
||||
className="max-w-[62vw] sm:max-w-none"
|
||||
/>
|
||||
</div>
|
||||
{modelsError && (
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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<TrainingCompareHandoff>;
|
||||
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);
|
||||
}
|
||||
127
studio/frontend/src/features/export/api/export-api.ts
Normal file
127
studio/frontend/src/features/export/api/export-api.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { authFetch } from "@/features/auth";
|
||||
|
||||
async function readError(response: Response): Promise<string> {
|
||||
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<T>(response: Response): Promise<T> {
|
||||
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<string, unknown> | null;
|
||||
}
|
||||
|
||||
export async function fetchCheckpoints(): Promise<CheckpointListResponse> {
|
||||
const response = await authFetch("/api/models/checkpoints");
|
||||
return parseJson<CheckpointListResponse>(response);
|
||||
}
|
||||
|
||||
export async function loadCheckpoint(params: {
|
||||
checkpoint_path: string;
|
||||
max_seq_length?: number;
|
||||
load_in_4bit?: boolean;
|
||||
}): Promise<ExportOperationResponse> {
|
||||
const response = await authFetch("/api/export/load-checkpoint", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
return parseJson<ExportOperationResponse>(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<ExportOperationResponse> {
|
||||
const response = await authFetch("/api/export/export/merged", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
return parseJson<ExportOperationResponse>(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<ExportOperationResponse> {
|
||||
const response = await authFetch("/api/export/export/base", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
return parseJson<ExportOperationResponse>(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<ExportOperationResponse> {
|
||||
const response = await authFetch("/api/export/export/gguf", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
return parseJson<ExportOperationResponse>(response);
|
||||
}
|
||||
|
||||
export async function exportLoRA(params: {
|
||||
save_directory: string;
|
||||
push_to_hub?: boolean;
|
||||
repo_id?: string | null;
|
||||
hf_token?: string | null;
|
||||
private?: boolean;
|
||||
}): Promise<ExportOperationResponse> {
|
||||
const response = await authFetch("/api/export/export/lora", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
return parseJson<ExportOperationResponse>(response);
|
||||
}
|
||||
|
||||
export async function cleanupExport(): Promise<ExportOperationResponse> {
|
||||
const response = await authFetch("/api/export/cleanup", { method: "POST" });
|
||||
return parseJson<ExportOperationResponse>(response);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Export Model</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose where to save your exported model.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={destination === "local" ? "dark" : "outline"}
|
||||
onClick={() => onDestinationChange("local")}
|
||||
className="flex-1"
|
||||
>
|
||||
Save Locally
|
||||
</Button>
|
||||
<Button
|
||||
variant={destination === "hub" ? "dark" : "outline"}
|
||||
onClick={() => onDestinationChange("hub")}
|
||||
className="flex-1"
|
||||
>
|
||||
Push to Hub
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{destination === "hub" && (
|
||||
<motion.div {...collapseAnim} className="overflow-hidden">
|
||||
<div className="flex flex-col gap-4 px-0.5">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
Username / Org
|
||||
</label>
|
||||
<Input
|
||||
placeholder="your-username"
|
||||
value={hfUsername}
|
||||
onChange={(e) => onHfUsernameChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
Model Name
|
||||
</label>
|
||||
<Input
|
||||
placeholder="my-model-gguf"
|
||||
value={modelName}
|
||||
onChange={(e) => onModelNameChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
HF Write Token
|
||||
</label>
|
||||
<a
|
||||
href="https://huggingface.co/settings/tokens"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-[11px] text-emerald-600 hover:text-emerald-700 transition-colors"
|
||||
>
|
||||
Get token
|
||||
<HugeiconsIcon
|
||||
icon={ArrowRight01Icon}
|
||||
className="size-3"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<HugeiconsIcon icon={Key01Icon} className="size-4" />
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
type="password"
|
||||
placeholder="hf_..."
|
||||
value={hfToken}
|
||||
onChange={(e) => onHfTokenChange(e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
<p className="text-[11px] text-muted-foreground/70">
|
||||
Leave empty if already logged in via CLI.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch
|
||||
id="private-repo"
|
||||
size="sm"
|
||||
checked={privateRepo}
|
||||
onCheckedChange={onPrivateRepoChange}
|
||||
/>
|
||||
<label
|
||||
htmlFor="private-repo"
|
||||
className="text-xs font-medium cursor-pointer"
|
||||
>
|
||||
Private Repository
|
||||
</label>
|
||||
</div>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(v) => {
|
||||
if (exporting) return;
|
||||
onOpenChange(v);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-lg" onInteractOutside={(e) => { if (exporting) e.preventDefault(); }}>
|
||||
{exportSuccess ? (
|
||||
<>
|
||||
<div className="flex flex-col items-center gap-3 py-6">
|
||||
<div className="flex size-12 items-center justify-center rounded-full bg-emerald-500/10">
|
||||
<HugeiconsIcon icon={CheckmarkCircle02Icon} className="size-6 text-emerald-500" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold">Export Complete</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{destination === "hub"
|
||||
? "Model successfully pushed to Hugging Face Hub."
|
||||
: "Model saved locally."}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="rounded-xl bg-muted/50 p-3 text-xs text-muted-foreground flex flex-col gap-1">
|
||||
<div className="flex justify-between">
|
||||
<span>Base Model</span>
|
||||
<span className="font-medium text-foreground">{baseModelName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>{isAdapter ? "Checkpoint" : "Model"}</span>
|
||||
<span className="font-medium text-foreground">{checkpoint}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Export Method</span>
|
||||
<span className="font-medium text-foreground">
|
||||
{EXPORT_METHODS.find((m) => m.value === exportMethod)?.title}
|
||||
</span>
|
||||
</div>
|
||||
{exportMethod === "gguf" && quantLevels.length > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span>Quantizations</span>
|
||||
<span className="font-medium text-foreground">
|
||||
{quantLevels.join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
<DialogFooter>
|
||||
<Button onClick={() => onOpenChange(false)}>Done</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Export Model</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose where to save your exported model.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={destination === "local" ? "dark" : "outline"}
|
||||
onClick={() => onDestinationChange("local")}
|
||||
disabled={exporting}
|
||||
className="flex-1"
|
||||
>
|
||||
Save Locally
|
||||
</Button>
|
||||
<Button
|
||||
variant={destination === "hub" ? "dark" : "outline"}
|
||||
onClick={() => onDestinationChange("hub")}
|
||||
disabled={exporting}
|
||||
className="flex-1"
|
||||
>
|
||||
Push to Hub
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{destination === "hub" && (
|
||||
<motion.div {...collapseAnim} className="overflow-hidden">
|
||||
<div className="flex flex-col gap-4 px-0.5">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
Username / Org
|
||||
</label>
|
||||
<Input
|
||||
placeholder="your-username"
|
||||
value={hfUsername}
|
||||
onChange={(e) => onHfUsernameChange(e.target.value)}
|
||||
disabled={exporting}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
Model Name
|
||||
</label>
|
||||
<Input
|
||||
placeholder="my-model-gguf"
|
||||
value={modelName}
|
||||
onChange={(e) => onModelNameChange(e.target.value)}
|
||||
disabled={exporting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
HF Write Token
|
||||
</label>
|
||||
<a
|
||||
href="https://huggingface.co/settings/tokens"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-[11px] text-emerald-600 hover:text-emerald-700 transition-colors"
|
||||
>
|
||||
Get token
|
||||
<HugeiconsIcon
|
||||
icon={ArrowRight01Icon}
|
||||
className="size-3"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<HugeiconsIcon icon={Key01Icon} className="size-4" />
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
type="password"
|
||||
placeholder="hf_..."
|
||||
value={hfToken}
|
||||
onChange={(e) => onHfTokenChange(e.target.value)}
|
||||
disabled={exporting}
|
||||
/>
|
||||
</InputGroup>
|
||||
<p className="text-[11px] text-muted-foreground/70">
|
||||
Leave empty if already logged in via CLI.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch
|
||||
id="private-repo"
|
||||
size="sm"
|
||||
checked={privateRepo}
|
||||
onCheckedChange={onPrivateRepoChange}
|
||||
disabled={exporting}
|
||||
/>
|
||||
<label
|
||||
htmlFor="private-repo"
|
||||
className="text-xs font-medium cursor-pointer"
|
||||
>
|
||||
Private Repository
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Error banner */}
|
||||
{exportError && (
|
||||
<div className="flex items-start gap-2 rounded-lg bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<HugeiconsIcon icon={AlertCircleIcon} className="size-4 mt-0.5 shrink-0" />
|
||||
<span>{exportError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary */}
|
||||
<div className="rounded-xl bg-muted/50 p-3 text-xs text-muted-foreground flex flex-col gap-1">
|
||||
<div className="flex justify-between">
|
||||
<span>Base Model</span>
|
||||
<span className="font-medium text-foreground">{baseModelName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>{isAdapter ? "Checkpoint" : "Model"}</span>
|
||||
<span className="font-medium text-foreground">{checkpoint}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Export Method</span>
|
||||
<span className="font-medium text-foreground">
|
||||
{EXPORT_METHODS.find((m) => m.value === exportMethod)?.title}
|
||||
</span>
|
||||
</div>
|
||||
{exportMethod === "gguf" && quantLevels.length > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span>Quantizations</span>
|
||||
<span className="font-medium text-foreground">
|
||||
{quantLevels.join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* TODO: unhide once estimated size comes from the backend API */}
|
||||
{/* <div className="flex justify-between">
|
||||
<span>Est. size</span>
|
||||
<span className="font-medium text-foreground">{estimatedSize}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => onOpenChange(false)}>Start Export</Button>
|
||||
</DialogFooter>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={exporting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onExport} disabled={exporting}>
|
||||
{exporting ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<Spinner className="size-4" />
|
||||
Exporting…
|
||||
</span>
|
||||
) : (
|
||||
"Start Export"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<ModelCheckpoints[]>([]);
|
||||
const [loadingCheckpoints, setLoadingCheckpoints] = useState(true);
|
||||
const [checkpointError, setCheckpointError] = useState<string | null>(null);
|
||||
|
||||
const [selectedModelIdx, setSelectedModelIdx] = useState<string | null>(null);
|
||||
const [checkpoint, setCheckpoint] = useState<string | null>(null);
|
||||
|
||||
const [exportMethod, setExportMethod] = useState<ExportMethod | null>(null);
|
||||
const [quantLevels, setQuantLevels] = useState<string[]>([]);
|
||||
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<string | null>(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 (
|
||||
<div className="min-h-screen bg-background">
|
||||
<main className="mx-auto max-w-7xl px-6 py-4">
|
||||
<main className="mx-auto max-w-7xl px-4 py-4 sm:px-6">
|
||||
<GuidedTour {...tour.tourProps} />
|
||||
|
||||
<div className="mb-8 flex flex-col gap-0.5">
|
||||
|
|
@ -132,141 +257,236 @@ export function ExportPage() {
|
|||
featured={true}
|
||||
className="shadow-border ring-1 ring-border"
|
||||
>
|
||||
{/* Top row: Checkpoint + metadata | Guide */}
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<div className="flex flex-col gap-4 ">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
||||
{isAdapter ? "Checkpoint" : "Model"}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
className="text-foreground/70 hover:text-foreground"
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={InformationCircleIcon}
|
||||
className="size-3"
|
||||
/>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Choose a saved checkpoint to export. Lower loss generally
|
||||
means better quality.{" "}
|
||||
<a
|
||||
href="https://unsloth.ai/docs/basics/inference-and-deployment"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline"
|
||||
>
|
||||
Read more
|
||||
</a>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Select value={checkpoint ?? ""} onValueChange={setCheckpoint}>
|
||||
<SelectTrigger data-tour="export-checkpoint" className="w-full">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
isAdapter ? "Select a checkpoint…" : "Select model…"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{checkpoints.map((cp) => (
|
||||
<SelectItem key={cp.value} value={cp.value}>
|
||||
<span className="flex items-center gap-2">
|
||||
{cp.label}
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{cp.detail}
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* Loading / error states */}
|
||||
{loadingCheckpoints && (
|
||||
<div className="flex items-center gap-2 py-6 justify-center text-sm text-muted-foreground">
|
||||
<Spinner className="size-4" />
|
||||
Loading checkpoints…
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-xl bg-muted/50 p-3 flex flex-col gap-2">
|
||||
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Training Info
|
||||
</span>
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-1.5 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Base Model</span>
|
||||
<span className="font-medium">{baseModelName}</span>
|
||||
{checkpointError && (
|
||||
<div className="flex items-center gap-2 py-6 justify-center text-sm text-destructive">
|
||||
<HugeiconsIcon icon={AlertCircleIcon} className="size-4" />
|
||||
{checkpointError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loadingCheckpoints && !checkpointError && (
|
||||
<>
|
||||
{/* Top row: Dropdowns + metadata | Guide */}
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 md:gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Training run dropdown */}
|
||||
<div data-tour="export-training-run" className="flex flex-col gap-2">
|
||||
<label className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
||||
Training Run
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
className="text-foreground/70 hover:text-foreground"
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={InformationCircleIcon}
|
||||
className="size-3"
|
||||
/>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Select the training run that produced the checkpoints
|
||||
you want to export.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Select
|
||||
value={selectedModelIdx ?? ""}
|
||||
onValueChange={setSelectedModelIdx}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
models.length === 0
|
||||
? "No training runs found"
|
||||
: "Select a training run…"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{models.map((m) => {
|
||||
const tsMatch = m.name.match(/_(\d{10,})$/);
|
||||
const displayName = tsMatch ? m.name.slice(0, tsMatch.index) : m.name;
|
||||
const timeStr = tsMatch
|
||||
? new Date(Number(tsMatch[1]) * 1000).toLocaleString(undefined, {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
})
|
||||
: null;
|
||||
return (
|
||||
<SelectItem key={m.name} value={m.name}>
|
||||
<span className="flex items-center gap-2">
|
||||
{displayName}
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{m.checkpoints.length} checkpoint
|
||||
{m.checkpoints.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
{timeStr && (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
· {timeStr}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Method</span>
|
||||
<span className="font-medium">
|
||||
{METHOD_LABELS[trainingMethod] ?? trainingMethod}
|
||||
|
||||
{/* Checkpoint dropdown */}
|
||||
<div data-tour="export-checkpoint" className="flex flex-col gap-2">
|
||||
<label className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
||||
Checkpoint
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
className="text-foreground/70 hover:text-foreground"
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={InformationCircleIcon}
|
||||
className="size-3"
|
||||
/>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Choose a saved checkpoint to export. Lower loss
|
||||
generally means better quality.{" "}
|
||||
<a
|
||||
href="https://unsloth.ai/docs/basics/inference-and-deployment"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline"
|
||||
>
|
||||
Read more
|
||||
</a>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Select
|
||||
value={checkpoint ?? ""}
|
||||
onValueChange={setCheckpoint}
|
||||
disabled={!selectedModelIdx}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
!selectedModelIdx
|
||||
? "Select a training run first"
|
||||
: checkpointsForModel.length === 0
|
||||
? "No checkpoints found"
|
||||
: "Select a checkpoint…"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{checkpointsForModel.map((cp) => (
|
||||
<SelectItem key={cp.path} value={cp.display_name}>
|
||||
<span className="flex items-center gap-2">
|
||||
{cp.display_name}
|
||||
{cp.loss != null && (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
loss: {cp.loss.toFixed(4)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl bg-muted/50 p-3 flex flex-col gap-2">
|
||||
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Training Info
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Checkpoints</span>
|
||||
<span className="font-medium">{checkpoints.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Epochs</span>
|
||||
<span className="font-medium">{epochs}</span>
|
||||
</div>
|
||||
{isAdapter && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">LoRA Rank</span>
|
||||
<span className="font-medium">{loraRank}</span>
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-1.5 text-xs sm:grid-cols-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Base Model</span>
|
||||
<span className="font-medium">{baseModelName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Method</span>
|
||||
<span className="font-medium">
|
||||
{trainingMethodLabel}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Checkpoints</span>
|
||||
<span className="font-medium">
|
||||
{checkpointsForModel.length}
|
||||
</span>
|
||||
</div>
|
||||
{isAdapter && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">LoRA Rank</span>
|
||||
<span className="font-medium">{loraRank}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
Quick Guide
|
||||
</span>
|
||||
<ol className="flex flex-col gap-3">
|
||||
{GUIDE_STEPS.map((step, i) => (
|
||||
<li
|
||||
key={step}
|
||||
className="flex items-start gap-2 text-xs text-muted-foreground"
|
||||
>
|
||||
<span className="flex size-5 shrink-0 items-center justify-center rounded-full bg-muted text-[10px] font-semibold">
|
||||
{i + 1}
|
||||
</span>
|
||||
{step}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
Quick Guide
|
||||
</span>
|
||||
<ol className="flex flex-col gap-3">
|
||||
{GUIDE_STEPS.map((step, i) => (
|
||||
<li
|
||||
key={step}
|
||||
className="flex items-start gap-2 text-xs text-muted-foreground"
|
||||
>
|
||||
<span className="flex size-5 shrink-0 items-center justify-center rounded-full bg-muted text-[10px] font-semibold">
|
||||
{i + 1}
|
||||
</span>
|
||||
{step}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<MethodPicker value={exportMethod} onChange={handleMethodChange} />
|
||||
|
||||
<MethodPicker value={exportMethod} onChange={handleMethodChange} />
|
||||
<AnimatePresence>
|
||||
{exportMethod === "gguf" && (
|
||||
<motion.div {...collapseAnim} className="overflow-hidden">
|
||||
<QuantPicker value={quantLevels} onChange={setQuantLevels} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{exportMethod === "gguf" && (
|
||||
<motion.div {...collapseAnim} className="overflow-hidden">
|
||||
<QuantPicker value={quantLevels} onChange={setQuantLevels} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<HugeiconsIcon
|
||||
icon={InformationCircleIcon}
|
||||
className="size-3.5"
|
||||
/>
|
||||
<span>Est. size: {estimatedSize} · Free disk space: 120 GB</span>
|
||||
</div>
|
||||
<Button
|
||||
data-tour="export-cta"
|
||||
disabled={!canExport}
|
||||
onClick={() => setDialogOpen(true)}
|
||||
>
|
||||
Export Model
|
||||
</Button>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-end">
|
||||
{/* TODO: unhide once estimated size comes from the backend API */}
|
||||
{/* <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<HugeiconsIcon
|
||||
icon={InformationCircleIcon}
|
||||
className="size-3.5"
|
||||
/>
|
||||
<span>Est. size: {estimatedSize} · Free disk space: 120 GB</span>
|
||||
</div> */}
|
||||
<Button
|
||||
data-tour="export-cta"
|
||||
disabled={!canExport}
|
||||
onClick={() => { setExportSuccess(false); setExportError(null); setDialogOpen(true); }}
|
||||
>
|
||||
Export Model
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</SectionCard>
|
||||
</main>
|
||||
|
||||
|
|
@ -289,6 +509,10 @@ export function ExportPage() {
|
|||
onHfTokenChange={setHfToken}
|
||||
privateRepo={privateRepo}
|
||||
onPrivateRepoChange={setPrivateRepo}
|
||||
onExport={handleExport}
|
||||
exporting={exporting}
|
||||
exportError={exportError}
|
||||
exportSuccess={exportSuccess}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-gradient-to-b from-background via-background to-primary/5">
|
||||
{/* Mascot */}
|
||||
<motion.img
|
||||
src="/Sloth emojis/Sloth loca pc.png"
|
||||
alt="Sloth mascot"
|
||||
className="size-30"
|
||||
initial={{ opacity: 0, y: 40, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
duration: 0.7,
|
||||
bounce: 0.3,
|
||||
delay: 0.1,
|
||||
}}
|
||||
/>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gradient-to-b from-background via-background to-primary/5 p-6">
|
||||
<Card className="w-full max-w-md px-8 py-8 shadow-border ring-1 ring-border">
|
||||
{/* Mascot */}
|
||||
<div className="flex justify-center">
|
||||
<motion.img
|
||||
src="/Sloth emojis/Sloth loca pc.png"
|
||||
alt="Sloth mascot"
|
||||
className="size-30"
|
||||
initial={{ opacity: 0, y: 40, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
duration: 0.7,
|
||||
bounce: 0.3,
|
||||
delay: 0.1,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Brand text */}
|
||||
<motion.div
|
||||
className="flex flex-col items-center gap-1 mt-4"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
ease: [0.165, 0.84, 0.44, 1],
|
||||
delay: 0.4,
|
||||
}}
|
||||
>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Unsloth Studio
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">Fine-tune LLMs faster</p>
|
||||
</motion.div>
|
||||
{/* Brand text */}
|
||||
<motion.div
|
||||
className="mt-4 flex flex-col items-center gap-1"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
ease: [0.165, 0.84, 0.44, 1],
|
||||
delay: 0.4,
|
||||
}}
|
||||
>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Unsloth Studio
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">Fine-tune LLMs faster</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Buttons */}
|
||||
<motion.div
|
||||
className="flex flex-col gap-3 mt-8"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
ease: [0.165, 0.84, 0.44, 1],
|
||||
delay: 0.8,
|
||||
}}
|
||||
>
|
||||
<Button size="lg" onClick={onStartOnboarding}>
|
||||
Start Onboarding
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" onClick={onGoToStudio}>
|
||||
Skip Onboarding
|
||||
</Button>
|
||||
</motion.div>
|
||||
{/* Buttons */}
|
||||
<motion.div
|
||||
className="mt-8 flex flex-col gap-3"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
ease: [0.165, 0.84, 0.44, 1],
|
||||
delay: 0.8,
|
||||
}}
|
||||
>
|
||||
<Button size="lg" onClick={onStartOnboarding}>
|
||||
Start Onboarding
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" onClick={onGoToStudio}>
|
||||
Skip Onboarding
|
||||
</Button>
|
||||
</motion.div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<FieldGroup>
|
||||
|
|
@ -68,7 +74,7 @@ export function HyperparametersStep() {
|
|||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<FieldLabel className="flex items-center gap-1.5 !text-sm text-muted-foreground">
|
||||
Epochs
|
||||
Max Steps
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<button
|
||||
|
|
@ -82,7 +88,7 @@ export function HyperparametersStep() {
|
|||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Number of times to iterate over the entire dataset.{" "}
|
||||
Override total steps. Set 0 to use epochs instead.{" "}
|
||||
<a
|
||||
href="https://unsloth.ai/docs/get-started/fine-tuning-llms-guide/lora-hyperparameters-guide"
|
||||
target="_blank"
|
||||
|
|
@ -96,21 +102,21 @@ export function HyperparametersStep() {
|
|||
</FieldLabel>
|
||||
<div className="flex items-center gap-3">
|
||||
<Slider
|
||||
value={[epochs]}
|
||||
onValueChange={([v]) => setEpochs(v)}
|
||||
min={1}
|
||||
max={10}
|
||||
value={[Math.min(maxStepsSliderMax, Math.max(0, maxSteps))]}
|
||||
onValueChange={([v]) => setMaxSteps(v)}
|
||||
min={0}
|
||||
max={maxStepsSliderMax}
|
||||
step={1}
|
||||
className="w-40"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={epochs}
|
||||
onChange={(e) => setEpochs(Number(e.target.value))}
|
||||
min={1}
|
||||
max={10}
|
||||
value={maxSteps}
|
||||
onChange={(e) => setMaxSteps(Number(e.target.value))}
|
||||
min={0}
|
||||
max={maxStepsSliderMax}
|
||||
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"
|
||||
className="w-16 text-right font-mono text-xs font-medium bg-muted/50 border border-border rounded-lg px-1.5 py-0.5 focus:outline-none focus:ring-1 focus:ring-primary/30 [&::-webkit-inner-spin-button]:appearance-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -196,6 +202,56 @@ export function HyperparametersStep() {
|
|||
className="w-32 font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<FieldLabel className="flex items-center gap-1.5 !text-sm text-muted-foreground">
|
||||
Epochs
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
className="text-muted-foreground/50 hover:text-muted-foreground"
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={InformationCircleIcon}
|
||||
className="size-3.5"
|
||||
/>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Number of full passes over the dataset. Set 0 to run by max
|
||||
steps.{" "}
|
||||
<a
|
||||
href="https://unsloth.ai/docs/get-started/fine-tuning-llms-guide/lora-hyperparameters-guide"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline"
|
||||
>
|
||||
Read more
|
||||
</a>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</FieldLabel>
|
||||
<div className="flex items-center gap-3">
|
||||
<Slider
|
||||
value={[Math.min(epochsSliderMax, Math.max(0, epochs))]}
|
||||
onValueChange={([v]) => setEpochs(v)}
|
||||
min={0}
|
||||
max={epochsSliderMax}
|
||||
step={1}
|
||||
className="w-40"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={epochs}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
|
|
|
|||
|
|
@ -31,19 +31,19 @@ export function WizardContent() {
|
|||
|
||||
return (
|
||||
<main className="flex-1 flex flex-col overflow-y-auto">
|
||||
<header className="flex items-center gap-4 p-6 pb-4">
|
||||
<img src={mascotSrc} alt="Unsloth mascot" className="size-14" />
|
||||
<header className="flex flex-wrap items-start gap-3 p-4 pb-3 sm:p-6 sm:pb-4">
|
||||
<img src={mascotSrc} alt="Unsloth mascot" className="size-12 sm:size-14" />
|
||||
<div className="flex flex-col min-w-0">
|
||||
<h1 className="text-xl font-semibold">{stepConfig.title}</h1>
|
||||
<h1 className="text-lg font-semibold sm:text-xl">{stepConfig.title}</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{stepConfig.description}
|
||||
</p>
|
||||
</div>
|
||||
<p className="ml-auto shrink-0 text-xs text-muted-foreground uppercase tracking-wider">
|
||||
<p className="ml-auto hidden shrink-0 text-xs text-muted-foreground uppercase tracking-wider md:block">
|
||||
Step {currentStep} of {STEPS.length}
|
||||
</p>
|
||||
</header>
|
||||
<div className="flex-1 p-6 pt-2">
|
||||
<div className="flex-1 p-4 pt-1.5 sm:p-6 sm:pt-2">
|
||||
<StepComponent />
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export function WizardLayout() {
|
|||
}, [isFinalStep]);
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen flex items-center justify-center p-8 bg-gradient-to-br from-primary/5 via-background to-primary/3 overflow-hidden">
|
||||
<div className="relative min-h-screen flex items-center justify-center overflow-hidden bg-gradient-to-br from-primary/5 via-background to-primary/3 p-4 sm:p-6 md:p-8">
|
||||
{showSplash && (
|
||||
<SplashScreen
|
||||
onStartOnboarding={() => setShowSplash(false)}
|
||||
|
|
@ -81,7 +81,7 @@ export function WizardLayout() {
|
|||
ease: [0.165, 0.84, 0.44, 1],
|
||||
}}
|
||||
>
|
||||
<Card className="relative z-10 w-full !gap-0 h-[640px] flex flex-row overflow-hidden !p-0 !m-0 shadow-border ring-1 ring-border">
|
||||
<Card className="relative z-10 w-full !gap-0 !m-0 !p-0 flex min-h-[560px] flex-col overflow-hidden shadow-border ring-1 ring-border md:min-h-[620px] md:flex-row lg:h-[660px]">
|
||||
<WizardSidebar />
|
||||
<div className="flex-1 flex flex-col">
|
||||
<WizardContent />
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue