Merge branch 'nightly' into fix/dropdown-menu-prefill
This commit is contained in:
commit
e5f9ae5c9f
83 changed files with 781 additions and 238 deletions
|
|
@ -1,19 +1,34 @@
|
|||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
|
||||
def studio(
|
||||
port: int = typer.Option(8000, "--port", "-p", help="Port to run the UI server on."),
|
||||
host: str = typer.Option("0.0.0.0", "--host", "-H", help="Host address to bind to."),
|
||||
share: bool = typer.Option(True, "--share", "-s", help="Create a public Gradio share link."),
|
||||
frontend: Optional[Path] = typer.Option(None, "--frontend", "-f", help="Path to frontend build directory."),
|
||||
silent: bool = typer.Option(False, "--silent", "-q", help="Suppress startup messages."),
|
||||
):
|
||||
"""Launch the Unsloth web UI for training, inference, and export."""
|
||||
from app import demo, script_dir
|
||||
"""Launch the Unsloth web UI backend server."""
|
||||
from studio.backend.run import run_server
|
||||
|
||||
typer.echo(f"Starting Unsloth UI on http://{host}:{port}")
|
||||
if not silent:
|
||||
from studio.backend.run import _resolve_external_ip
|
||||
display_host = _resolve_external_ip() if host == "0.0.0.0" else host
|
||||
typer.echo(f"Starting Unsloth Studio on http://{display_host}:{port}")
|
||||
|
||||
demo.launch(
|
||||
share=share,
|
||||
server_port=port,
|
||||
server_name=host,
|
||||
favicon_path=f"{script_dir}/assets/favicon-32x32.png",
|
||||
run_server(
|
||||
host=host,
|
||||
port=port,
|
||||
frontend_path=frontend,
|
||||
silent=silent,
|
||||
)
|
||||
|
||||
# Keep running until interrupted
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
typer.echo("\nShutting down...")
|
||||
|
|
|
|||
20
setup.sh
20
setup.sh
|
|
@ -212,38 +212,42 @@ USER_SHELL="$(basename "${SHELL:-/bin/bash}")"
|
|||
case "$USER_SHELL" in
|
||||
zsh)
|
||||
SHELL_RC="$HOME/.zshrc"
|
||||
ALIAS_BLOCK="alias unsloth-ui='${REPO_DIR}/.venv/bin/python ${REPO_DIR}/cli.py ui -f ${REPO_DIR}/studio/frontend/dist'"
|
||||
ALIAS_BLOCK="alias unsloth-studio='${REPO_DIR}/.venv/bin/python ${REPO_DIR}/cli.py studio -f ${REPO_DIR}/studio/frontend/dist'
|
||||
alias unsloth-ui='${REPO_DIR}/.venv/bin/python ${REPO_DIR}/cli.py studio -f ${REPO_DIR}/studio/frontend/dist'"
|
||||
;;
|
||||
fish)
|
||||
SHELL_RC="$HOME/.config/fish/config.fish"
|
||||
# fish uses 'abbr' or 'function'; a simple alias works via 'alias' in config.fish
|
||||
ALIAS_BLOCK="alias unsloth-ui '${REPO_DIR}/.venv/bin/python ${REPO_DIR}/cli.py ui -f ${REPO_DIR}/studio/frontend/dist'"
|
||||
ALIAS_BLOCK="alias unsloth-studio '${REPO_DIR}/.venv/bin/python ${REPO_DIR}/cli.py studio -f ${REPO_DIR}/studio/frontend/dist'
|
||||
alias unsloth-ui '${REPO_DIR}/.venv/bin/python ${REPO_DIR}/cli.py studio -f ${REPO_DIR}/studio/frontend/dist'"
|
||||
;;
|
||||
ksh)
|
||||
SHELL_RC="$HOME/.kshrc"
|
||||
ALIAS_BLOCK="alias unsloth-ui='${REPO_DIR}/.venv/bin/python ${REPO_DIR}/cli.py ui -f ${REPO_DIR}/studio/frontend/dist'"
|
||||
ALIAS_BLOCK="alias unsloth-studio='${REPO_DIR}/.venv/bin/python ${REPO_DIR}/cli.py studio -f ${REPO_DIR}/studio/frontend/dist'
|
||||
alias unsloth-ui='${REPO_DIR}/.venv/bin/python ${REPO_DIR}/cli.py studio -f ${REPO_DIR}/studio/frontend/dist'"
|
||||
;;
|
||||
*)
|
||||
# Default to bash for bash and any other POSIX-compatible shell
|
||||
SHELL_RC="$HOME/.bashrc"
|
||||
ALIAS_BLOCK="alias unsloth-ui='${REPO_DIR}/.venv/bin/python ${REPO_DIR}/cli.py ui -f ${REPO_DIR}/studio/frontend/dist'"
|
||||
ALIAS_BLOCK="alias unsloth-studio='${REPO_DIR}/.venv/bin/python ${REPO_DIR}/cli.py studio -f ${REPO_DIR}/studio/frontend/dist'
|
||||
alias unsloth-ui='${REPO_DIR}/.venv/bin/python ${REPO_DIR}/cli.py studio -f ${REPO_DIR}/studio/frontend/dist'"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo " Detected shell: $USER_SHELL → $SHELL_RC"
|
||||
|
||||
ALIAS_ADDED=false
|
||||
if ! grep -qF "unsloth-ui" "$SHELL_RC" 2>/dev/null; then
|
||||
if ! grep -qF "unsloth-studio" "$SHELL_RC" 2>/dev/null; then
|
||||
mkdir -p "$(dirname "$SHELL_RC")" # needed for fish's nested config path
|
||||
cat >> "$SHELL_RC" <<UNSLOTH_EOF
|
||||
|
||||
# Unsloth Studio launcher
|
||||
$ALIAS_BLOCK
|
||||
UNSLOTH_EOF
|
||||
echo "✅ Alias 'unsloth-ui' added to $SHELL_RC"
|
||||
echo "✅ Aliases 'unsloth-studio' and 'unsloth-ui' added to $SHELL_RC"
|
||||
ALIAS_ADDED=true
|
||||
else
|
||||
echo "✅ Alias 'unsloth-ui' already exists in $SHELL_RC"
|
||||
echo "✅ Aliases 'unsloth-studio' and 'unsloth-ui' already exist in $SHELL_RC"
|
||||
fi
|
||||
|
||||
fi # End of "if not Colab" for shell alias setup
|
||||
|
|
@ -267,6 +271,6 @@ else
|
|||
echo "║ Launch with: ║"
|
||||
fi
|
||||
echo "║ ║"
|
||||
echo "║ unsloth-ui -H 0.0.0.0 -p 8000 ║"
|
||||
echo "║ unsloth-studio -H 0.0.0.0 -p 8000 ║"
|
||||
echo "╚══════════════════════════════════════╝"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ training:
|
|||
weight_decay: 0.01
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.01
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: false
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.01
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: true
|
||||
optim: "adamw_torch_fused"
|
||||
lr_scheduler_type: "cosine"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: true
|
||||
optim: "adamw_torch_fused"
|
||||
lr_scheduler_type: "cosine"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.01
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: true
|
||||
optim: "adamw_torch"
|
||||
lr_scheduler_type: "cosine"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "paged_adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.1
|
||||
random_seed: 3407
|
||||
packing: true
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 42
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "paged_adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ training:
|
|||
weight_decay: 0.001
|
||||
random_seed: 3407
|
||||
packing: false
|
||||
train_on_completions: false
|
||||
train_on_completions: true
|
||||
gradient_checkpointing: "unsloth"
|
||||
optim: "adamw_8bit"
|
||||
lr_scheduler_type: "linear"
|
||||
|
|
|
|||
|
|
@ -38,9 +38,13 @@ class InferenceBackend:
|
|||
]
|
||||
self.device = get_device().value
|
||||
|
||||
# Thread safety
|
||||
# Thread safety — _generation_lock serializes model.generate() calls.
|
||||
# Must be a regular Lock (NOT RLock) because in async FastAPI, multiple
|
||||
# requests share the same event-loop thread, so RLock reentrancy lets
|
||||
# concurrent compare-mode requests race on the GPU. The lock is
|
||||
# acquired by the *background generation thread*, not the event-loop.
|
||||
import threading
|
||||
self._generation_lock = threading.RLock()
|
||||
self._generation_lock = threading.Lock()
|
||||
self._model_state_lock = threading.Lock()
|
||||
|
||||
logger.info(f"InferenceBackend initialized on {self.device}")
|
||||
|
|
@ -448,9 +452,10 @@ class InferenceBackend:
|
|||
"""
|
||||
Apply adapter state before generation. Must be called under _generation_lock.
|
||||
|
||||
Uses revert_to_base_model() / activate_lora_adapter() which work correctly
|
||||
for models loaded by Unsloth as complete PeftModels (via model.unload() /
|
||||
model.load_adapter()), matching the proven pattern from the Gradio eval page.
|
||||
Uses PEFT's disable_adapter_layers() / enable_adapter_layers() which toggle
|
||||
a boolean flag on each LoRA layer. Unsloth's fast_linear_forward checks this
|
||||
flag (proj.disable_adapters) and skips LoRA computation when True.
|
||||
This is non-destructive — no model unloading/reloading needed.
|
||||
|
||||
Args:
|
||||
use_adapter: None = no change, False = disable (base model),
|
||||
|
|
@ -464,32 +469,34 @@ class InferenceBackend:
|
|||
return
|
||||
|
||||
model_info = self.models[base]
|
||||
model = model_info.get("model")
|
||||
if model is None:
|
||||
return
|
||||
|
||||
if use_adapter is False:
|
||||
# Revert to pure base model by unloading adapter weights
|
||||
logger.info(f"Compare mode: reverting '{base}' to base model for generation")
|
||||
self.revert_to_base_model(base)
|
||||
# Disable LoRA layers → base model output
|
||||
if isinstance(model, (PeftModel, PeftModelForCausalLM)):
|
||||
logger.info(f"Compare mode: disabling adapters on '{base}' for base model generation")
|
||||
model.base_model.disable_adapter_layers()
|
||||
else:
|
||||
logger.info(f"Compare mode: model '{base}' is not a PeftModel, already base")
|
||||
|
||||
elif use_adapter is True:
|
||||
# Activate the LoRA adapter from the original model path
|
||||
lora_path = model_info.get("model_path")
|
||||
if lora_path and model_info.get("is_lora"):
|
||||
logger.info(f"Compare mode: activating LoRA adapter from '{lora_path}' on '{base}'")
|
||||
self.activate_lora_adapter(base, lora_path)
|
||||
# Re-enable LoRA layers → adapter output
|
||||
if isinstance(model, (PeftModel, PeftModelForCausalLM)):
|
||||
logger.info(f"Compare mode: enabling adapters on '{base}' for LoRA generation")
|
||||
model.base_model.enable_adapter_layers()
|
||||
else:
|
||||
# Fallback for dynamically attached adapters
|
||||
loaded = model_info.get("loaded_adapters", {})
|
||||
if loaded:
|
||||
adapter_name = list(loaded.keys())[-1]
|
||||
logger.info(f"Compare mode: enabling adapter '{adapter_name}' on '{base}'")
|
||||
self.set_active_adapter(base, adapter_name)
|
||||
else:
|
||||
logger.warning("use_adapter=true but no adapter path/adapters on model")
|
||||
logger.warning("use_adapter=true but model is not a PeftModel")
|
||||
|
||||
elif isinstance(use_adapter, str):
|
||||
# Activate a specific adapter by path
|
||||
logger.info(f"Compare mode: activating specific adapter '{use_adapter}' on '{base}'")
|
||||
self.activate_lora_adapter(base, use_adapter)
|
||||
# Enable adapters and set the specific one active
|
||||
if isinstance(model, (PeftModel, PeftModelForCausalLM)):
|
||||
logger.info(f"Compare mode: enabling adapter '{use_adapter}' on '{base}'")
|
||||
model.base_model.enable_adapter_layers()
|
||||
self.set_active_adapter(base, use_adapter)
|
||||
else:
|
||||
logger.warning(f"use_adapter='{use_adapter}' but model is not a PeftModel")
|
||||
|
||||
def generate_with_adapter_control(
|
||||
self,
|
||||
|
|
@ -500,18 +507,18 @@ class InferenceBackend:
|
|||
"""
|
||||
Thread-safe generation with optional adapter toggling.
|
||||
|
||||
Acquires the generation lock, applies adapter state, then generates.
|
||||
This ensures adapter toggle + generation are atomic — critical for
|
||||
compare mode where base and LoRA panes fire concurrently.
|
||||
The adapter toggle + model.generate() are serialized by _generation_lock
|
||||
inside the background generation thread — NOT in the event-loop thread.
|
||||
This prevents the RLock-reentrant race that occurs when two async SSE
|
||||
handlers share the same event-loop thread.
|
||||
|
||||
Args:
|
||||
use_adapter: Adapter control (None/False/True/str). See _apply_adapter_state.
|
||||
**gen_kwargs: Forwarded to generate_chat_response.
|
||||
"""
|
||||
with self._generation_lock:
|
||||
self._apply_adapter_state(use_adapter)
|
||||
# Delegate to the lock-free generation path
|
||||
yield from self._generate_chat_response_inner(cancel_event=cancel_event, **gen_kwargs)
|
||||
yield from self._generate_chat_response_inner(
|
||||
cancel_event=cancel_event, _adapter_state=use_adapter, **gen_kwargs
|
||||
)
|
||||
|
||||
def generate_chat_response(self,
|
||||
messages: list,
|
||||
|
|
@ -526,22 +533,20 @@ class InferenceBackend:
|
|||
cancel_event=None) -> Generator[str, None, None]:
|
||||
"""
|
||||
Generate response for text or vision models.
|
||||
Acquires the generation lock. For adapter-controlled generation,
|
||||
use generate_with_adapter_control() instead.
|
||||
The generation lock is acquired by the background generation thread.
|
||||
"""
|
||||
with self._generation_lock:
|
||||
yield from self._generate_chat_response_inner(
|
||||
messages=messages,
|
||||
system_prompt=system_prompt,
|
||||
image=image,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
top_k=top_k,
|
||||
min_p=min_p,
|
||||
max_new_tokens=max_new_tokens,
|
||||
repetition_penalty=repetition_penalty,
|
||||
cancel_event=cancel_event,
|
||||
)
|
||||
yield from self._generate_chat_response_inner(
|
||||
messages=messages,
|
||||
system_prompt=system_prompt,
|
||||
image=image,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
top_k=top_k,
|
||||
min_p=min_p,
|
||||
max_new_tokens=max_new_tokens,
|
||||
repetition_penalty=repetition_penalty,
|
||||
cancel_event=cancel_event,
|
||||
)
|
||||
|
||||
def _generate_chat_response_inner(self,
|
||||
messages: list,
|
||||
|
|
@ -553,10 +558,14 @@ class InferenceBackend:
|
|||
min_p: float = 0.0,
|
||||
max_new_tokens: int = 256,
|
||||
repetition_penalty: float = 1.1,
|
||||
cancel_event=None) -> Generator[str, None, None]:
|
||||
cancel_event=None,
|
||||
_adapter_state=None) -> Generator[str, None, None]:
|
||||
"""
|
||||
Inner generation logic (no lock). Called by both generate_chat_response
|
||||
Inner generation logic. Called by both generate_chat_response
|
||||
and generate_with_adapter_control.
|
||||
|
||||
_adapter_state is passed to generate_stream/vision so the background
|
||||
thread can toggle adapters under the generation lock.
|
||||
"""
|
||||
if not self.active_model_name:
|
||||
yield "Error: No active model"
|
||||
|
|
@ -616,6 +625,7 @@ class InferenceBackend:
|
|||
yield from self.generate_stream(
|
||||
formatted_prompt, temperature, top_p, top_k, min_p, max_new_tokens, repetition_penalty,
|
||||
cancel_event=cancel_event,
|
||||
_adapter_state=_adapter_state,
|
||||
)
|
||||
|
||||
def _generate_vision_response(self, messages, system_prompt, image,
|
||||
|
|
@ -677,6 +687,7 @@ class InferenceBackend:
|
|||
streamer=streamer,
|
||||
max_new_tokens=max_new_tokens,
|
||||
use_cache=True,
|
||||
do_sample=temperature > 0,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
top_k=top_k,
|
||||
|
|
@ -686,16 +697,17 @@ class InferenceBackend:
|
|||
err: dict[str, str] = {}
|
||||
|
||||
def generate_fn():
|
||||
try:
|
||||
model.generate(**generation_kwargs)
|
||||
except Exception as e:
|
||||
err["msg"] = str(e)
|
||||
logger.error(f"Vision generation error in thread: {e}")
|
||||
finally:
|
||||
with self._generation_lock:
|
||||
try:
|
||||
streamer.end()
|
||||
except Exception:
|
||||
pass
|
||||
model.generate(**generation_kwargs)
|
||||
except Exception as e:
|
||||
err["msg"] = str(e)
|
||||
logger.error(f"Vision generation error in thread: {e}")
|
||||
finally:
|
||||
try:
|
||||
streamer.end()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
thread = threading.Thread(target=generate_fn)
|
||||
thread.start()
|
||||
|
|
@ -741,8 +753,13 @@ class InferenceBackend:
|
|||
min_p: float = 0.0,
|
||||
max_new_tokens: int = 256,
|
||||
repetition_penalty: float = 1.1,
|
||||
cancel_event=None) -> Generator[str, None, None]:
|
||||
"""Generate streaming text response (text models only)."""
|
||||
cancel_event=None,
|
||||
_adapter_state=None) -> Generator[str, None, None]:
|
||||
"""Generate streaming text response (text models only).
|
||||
|
||||
_adapter_state: if not None, the background thread toggles adapters
|
||||
before model.generate(), all under _generation_lock.
|
||||
"""
|
||||
if not self.active_model_name:
|
||||
yield "Error: No active model"
|
||||
return
|
||||
|
|
@ -773,7 +790,7 @@ class InferenceBackend:
|
|||
top_k=top_k,
|
||||
min_p=min_p,
|
||||
repetition_penalty=repetition_penalty,
|
||||
do_sample=True,
|
||||
do_sample=temperature > 0,
|
||||
eos_token_id=tokenizer.eos_token_id,
|
||||
pad_token_id=tokenizer.eos_token_id if tokenizer.pad_token_id is None else tokenizer.pad_token_id,
|
||||
)
|
||||
|
|
@ -795,16 +812,19 @@ class InferenceBackend:
|
|||
)
|
||||
|
||||
def generate_fn():
|
||||
try:
|
||||
model.generate(**generation_kwargs)
|
||||
except Exception as e:
|
||||
err["msg"] = str(e)
|
||||
logger.error(f"Generation error: {e}")
|
||||
finally:
|
||||
with self._generation_lock:
|
||||
try:
|
||||
streamer.end()
|
||||
except Exception:
|
||||
pass
|
||||
if _adapter_state is not None:
|
||||
self._apply_adapter_state(_adapter_state)
|
||||
model.generate(**generation_kwargs)
|
||||
except Exception as e:
|
||||
err["msg"] = str(e)
|
||||
logger.error(f"Generation error: {e}")
|
||||
finally:
|
||||
try:
|
||||
streamer.end()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
err: dict[str, str] = {}
|
||||
thread = threading.Thread(target=generate_fn)
|
||||
|
|
|
|||
|
|
@ -867,6 +867,7 @@ class UnslothTrainer:
|
|||
self.trainer,
|
||||
instruction_part=instruction_part,
|
||||
response_part=response_part,
|
||||
num_proc=config_args.get("dataset_num_proc", max(1, os.cpu_count() // 4)),
|
||||
)
|
||||
print("Train on responses only configured successfully\n")
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -70,33 +70,48 @@ def _serialize_preview_rows(rows):
|
|||
|
||||
# --- Endpoints ---
|
||||
|
||||
# Recognized data-file extensions for the single-file fallback approach.
|
||||
DATA_EXTS = (
|
||||
'.parquet',
|
||||
'.json', '.jsonl',
|
||||
'.csv', '.tsv',
|
||||
'.txt',
|
||||
'.arrow',
|
||||
'.tar', '.tar.gz', '.tgz',
|
||||
'.gz', '.zst',
|
||||
'.zip',
|
||||
)
|
||||
|
||||
|
||||
@router.post("/check-format", response_model=CheckFormatResponse)
|
||||
async def check_format(request: CheckFormatRequest):
|
||||
def check_format(request: CheckFormatRequest):
|
||||
"""
|
||||
Check if a dataset requires manual column mapping.
|
||||
|
||||
This is a lightweight check that streams only the first N rows,
|
||||
runs format detection, and (if processable) returns processed
|
||||
preview samples. The full dataset is re-processed at training time.
|
||||
|
||||
For HuggingFace datasets we use streaming mode so we never download
|
||||
the entire dataset — only the rows we actually need are fetched.
|
||||
|
||||
Strategy for HuggingFace datasets:
|
||||
1. list_repo_files → pick the first data file → load_dataset(data_files=[…])
|
||||
Avoids resolving thousands of files; typically ~2-4 s.
|
||||
2. Full streaming load_dataset as a last-resort fallback.
|
||||
|
||||
Local files are loaded directly.
|
||||
|
||||
Using a plain `def` (not async) so FastAPI runs this in a thread-pool,
|
||||
preventing any blocking IO from freezing the event loop.
|
||||
"""
|
||||
try:
|
||||
from itertools import islice
|
||||
from datasets import Dataset, load_dataset
|
||||
from utils.datasets import format_dataset
|
||||
|
||||
|
||||
PREVIEW_SIZE = 10
|
||||
|
||||
|
||||
logger.info(f"Checking format for dataset: {request.dataset_name}")
|
||||
|
||||
# Load dataset
|
||||
|
||||
dataset_path = Path(request.dataset_name)
|
||||
total_rows = None
|
||||
|
||||
|
||||
if dataset_path.exists():
|
||||
# Local dataset — direct load is fine (files are local)
|
||||
# ── Local file ──────────────────────────────────────────
|
||||
if dataset_path.suffix in ['.json', '.jsonl']:
|
||||
dataset = load_dataset('json', data_files=str(dataset_path), split=request.train_split)
|
||||
elif dataset_path.suffix == '.csv':
|
||||
|
|
@ -111,54 +126,83 @@ async def check_format(request: CheckFormatRequest):
|
|||
total_rows = len(dataset)
|
||||
preview_slice = dataset.select(range(min(PREVIEW_SIZE, total_rows)))
|
||||
else:
|
||||
# HuggingFace dataset — use STREAMING to avoid downloading everything
|
||||
load_kwargs = {"path": request.dataset_name, "split": request.train_split, "streaming": True}
|
||||
if request.subset:
|
||||
load_kwargs["name"] = request.subset
|
||||
if request.hf_token:
|
||||
load_kwargs["token"] = request.hf_token
|
||||
|
||||
streamed_ds = load_dataset(**load_kwargs)
|
||||
|
||||
# Take only the first PREVIEW_SIZE rows from the stream
|
||||
rows = list(islice(streamed_ds, PREVIEW_SIZE))
|
||||
if not rows:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Dataset appears to be empty or could not be streamed"
|
||||
# ── HuggingFace dataset ─────────────────────────────────
|
||||
# Tier 1: list_repo_files → load only the first data file
|
||||
preview_slice = None
|
||||
|
||||
try:
|
||||
from huggingface_hub import HfApi
|
||||
api = HfApi()
|
||||
repo_files = api.list_repo_files(
|
||||
request.dataset_name,
|
||||
repo_type="dataset",
|
||||
token=request.hf_token or None,
|
||||
)
|
||||
|
||||
# Convert list-of-dicts into a proper Dataset for downstream compat
|
||||
preview_slice = Dataset.from_list(rows)
|
||||
# total_rows unknown in streaming mode
|
||||
data_files = [f for f in repo_files if any(f.endswith(ext) for ext in DATA_EXTS)]
|
||||
|
||||
if data_files:
|
||||
first_file = data_files[0]
|
||||
logger.info(f"Tier 1: loading single file {first_file}")
|
||||
load_kwargs = {
|
||||
"path": request.dataset_name,
|
||||
"data_files": [first_file],
|
||||
"split": "train",
|
||||
"streaming": True,
|
||||
}
|
||||
if request.hf_token:
|
||||
load_kwargs["token"] = request.hf_token
|
||||
|
||||
streamed_ds = load_dataset(**load_kwargs)
|
||||
rows = list(islice(streamed_ds, PREVIEW_SIZE))
|
||||
if rows:
|
||||
preview_slice = Dataset.from_list(rows)
|
||||
except Exception as e:
|
||||
logger.warning(f"Tier 1 (single-file) failed: {e}")
|
||||
|
||||
if preview_slice is None:
|
||||
# Tier 2: full streaming (resolves all files — slow for large repos)
|
||||
logger.info("Tier 2: falling back to full streaming load_dataset")
|
||||
load_kwargs = {"path": request.dataset_name, "split": request.train_split, "streaming": True}
|
||||
if request.subset:
|
||||
load_kwargs["name"] = request.subset
|
||||
if request.hf_token:
|
||||
load_kwargs["token"] = request.hf_token
|
||||
|
||||
streamed_ds = load_dataset(**load_kwargs)
|
||||
|
||||
rows = list(islice(streamed_ds, PREVIEW_SIZE))
|
||||
if not rows:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Dataset appears to be empty or could not be streamed"
|
||||
)
|
||||
|
||||
preview_slice = Dataset.from_list(rows)
|
||||
total_rows = None
|
||||
|
||||
|
||||
# Run lightweight format check on the preview slice
|
||||
result = check_dataset_format(preview_slice, is_vlm=request.is_vlm)
|
||||
|
||||
logger.info(f"Format check result: requires_mapping={result['requires_manual_mapping']}, format={result['detected_format']}")
|
||||
|
||||
|
||||
logger.info(f"Format check result: requires_mapping={result['requires_manual_mapping']}, format={result['detected_format']}, is_multimodal={result.get('is_multimodal', False)}")
|
||||
|
||||
# Generate preview samples
|
||||
preview_samples = None
|
||||
if not result["requires_manual_mapping"]:
|
||||
# Format detected — return processed preview
|
||||
try:
|
||||
format_result = format_dataset(
|
||||
preview_slice,
|
||||
format_type="auto",
|
||||
custom_format_mapping=result.get("suggested_mapping"),
|
||||
num_proc=1, # Only 10 preview rows — no need for multiprocessing
|
||||
)
|
||||
processed = format_result["dataset"]
|
||||
preview_samples = _serialize_preview_rows(processed)
|
||||
except Exception as e:
|
||||
logger.warning(f"Processed preview generation failed (non-fatal): {e}")
|
||||
# Fall back to raw samples so frontend still has something
|
||||
preview_samples = _serialize_preview_rows(preview_slice)
|
||||
else:
|
||||
# Format detection failed — return raw samples so user can
|
||||
# see actual data and map columns in the frontend
|
||||
preview_samples = _serialize_preview_rows(preview_slice)
|
||||
|
||||
|
||||
return CheckFormatResponse(
|
||||
requires_manual_mapping=result["requires_manual_mapping"],
|
||||
detected_format=result["detected_format"],
|
||||
|
|
@ -171,7 +215,7 @@ async def check_format(request: CheckFormatRequest):
|
|||
preview_samples=preview_samples,
|
||||
total_rows=total_rows,
|
||||
)
|
||||
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -328,6 +328,11 @@ def detect_multimodal_dataset(dataset):
|
|||
"""
|
||||
Detects if dataset contains multimodal data (images/vision).
|
||||
|
||||
Two-pass approach:
|
||||
1. Column-name heuristic (fast): checks for keywords like 'image', 'img', 'pixel'.
|
||||
2. Value-type inspection (reliable): checks if actual values are PIL Images,
|
||||
bytes with image headers, or HF Image-feature dicts.
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
"is_multimodal": bool,
|
||||
|
|
@ -339,11 +344,16 @@ def detect_multimodal_dataset(dataset):
|
|||
column_names = list(sample.keys())
|
||||
|
||||
# Keywords that indicate multimodal/image data
|
||||
multimodal_keywords = ['image', 'img', 'pixel']
|
||||
multimodal_keywords = [
|
||||
'image', 'img', 'pixel',
|
||||
'jpg', 'jpeg', 'png', 'webp', 'bmp', 'gif', 'tiff', 'svg',
|
||||
'photo', 'pic', 'picture', 'visual',
|
||||
]
|
||||
|
||||
multimodal_columns = []
|
||||
modality_types = set()
|
||||
|
||||
# ── Pass 1: column-name heuristic ───────────────────────
|
||||
for col_name in column_names:
|
||||
col_lower = col_name.lower()
|
||||
|
||||
|
|
@ -353,6 +363,17 @@ def detect_multimodal_dataset(dataset):
|
|||
modality_types.add(keyword)
|
||||
break # Don't check other keywords for this column
|
||||
|
||||
# ── Pass 2: inspect actual values ───────────────────────
|
||||
# Catches columns with non-obvious names (e.g. "jpg", "photo", "pic")
|
||||
already_detected = set(multimodal_columns)
|
||||
for col_name in column_names:
|
||||
if col_name in already_detected:
|
||||
continue
|
||||
value = sample[col_name]
|
||||
if _is_image_value(value):
|
||||
multimodal_columns.append(col_name)
|
||||
modality_types.add("image")
|
||||
|
||||
return {
|
||||
"is_multimodal": len(multimodal_columns) > 0,
|
||||
"multimodal_columns": multimodal_columns,
|
||||
|
|
@ -360,6 +381,54 @@ def detect_multimodal_dataset(dataset):
|
|||
}
|
||||
|
||||
|
||||
def _is_image_value(value) -> bool:
|
||||
"""Check if a single sample value looks like image data."""
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
# PIL Image instance
|
||||
try:
|
||||
from PIL.Image import Image as PILImage
|
||||
if isinstance(value, PILImage):
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# HF datasets Image feature stores decoded images as PIL or dicts with
|
||||
# {"bytes": b"...", "path": "..."} when not yet decoded
|
||||
if isinstance(value, dict):
|
||||
if "bytes" in value and "path" in value:
|
||||
return True
|
||||
|
||||
# Raw bytes with a known image magic header
|
||||
if isinstance(value, (bytes, bytearray)):
|
||||
return _has_image_header(value)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _has_image_header(data: bytes) -> bool:
|
||||
"""Quick magic-byte check for common image formats."""
|
||||
if len(data) < 4:
|
||||
return False
|
||||
# JPEG
|
||||
if data[:2] == b'\xff\xd8':
|
||||
return True
|
||||
# PNG
|
||||
if data[:4] == b'\x89PNG':
|
||||
return True
|
||||
# GIF
|
||||
if data[:3] == b'GIF':
|
||||
return True
|
||||
# WebP
|
||||
if data[:4] == b'RIFF' and len(data) >= 12 and data[8:12] == b'WEBP':
|
||||
return True
|
||||
# BMP
|
||||
if data[:2] == b'BM':
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def detect_vlm_dataset_structure(dataset):
|
||||
"""
|
||||
Detects if VLM dataset is:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { Reasoning, ReasoningGroup } from "@/components/assistant-ui/reasoning";
|
|||
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
|
||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { copyToClipboard } from "@/lib/copy-to-clipboard";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
ActionBarMorePrimitive,
|
||||
|
|
@ -38,7 +39,7 @@ import {
|
|||
RefreshCwIcon,
|
||||
SquareIcon,
|
||||
} from "lucide-react";
|
||||
import { type FC, useRef } from "react";
|
||||
import { type FC, useRef, useState } from "react";
|
||||
|
||||
export const Thread: FC<{ hideComposer?: boolean; hideWelcome?: boolean }> = ({
|
||||
hideComposer,
|
||||
|
|
@ -275,6 +276,32 @@ const AssistantMessage: FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const COPY_RESET_MS = 2000;
|
||||
|
||||
const CopyButton: FC = () => {
|
||||
const aui = useAui();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const resetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const handleCopy = () => {
|
||||
const text = aui.message().getCopyText();
|
||||
if (copyToClipboard(text)) {
|
||||
setCopied(true);
|
||||
if (resetTimeoutRef.current) clearTimeout(resetTimeoutRef.current);
|
||||
resetTimeoutRef.current = setTimeout(() => {
|
||||
setCopied(false);
|
||||
resetTimeoutRef.current = null;
|
||||
}, COPY_RESET_MS);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipIconButton tooltip="Copy" onClick={handleCopy}>
|
||||
{copied ? <CheckIcon /> : <CopyIcon />}
|
||||
</TooltipIconButton>
|
||||
);
|
||||
};
|
||||
|
||||
const AssistantActionBar: FC = () => {
|
||||
return (
|
||||
<ActionBarPrimitive.Root
|
||||
|
|
@ -283,16 +310,7 @@ const AssistantActionBar: FC = () => {
|
|||
autohideFloat="single-branch"
|
||||
className="aui-assistant-action-bar-root col-start-3 row-start-2 -ml-1 flex gap-1 text-muted-foreground data-floating:absolute data-floating:rounded-md data-floating:border data-floating:bg-background data-floating:p-1 data-floating:shadow-sm"
|
||||
>
|
||||
<ActionBarPrimitive.Copy asChild={true}>
|
||||
<TooltipIconButton tooltip="Copy">
|
||||
<AuiIf condition={({ message }) => message.isCopied}>
|
||||
<CheckIcon />
|
||||
</AuiIf>
|
||||
<AuiIf condition={({ message }) => !message.isCopied}>
|
||||
<CopyIcon />
|
||||
</AuiIf>
|
||||
</TooltipIconButton>
|
||||
</ActionBarPrimitive.Copy>
|
||||
<CopyButton />
|
||||
<ActionBarPrimitive.Reload asChild={true}>
|
||||
<TooltipIconButton tooltip="Refresh">
|
||||
<RefreshCwIcon />
|
||||
|
|
@ -352,16 +370,7 @@ const UserActionBar: FC = () => {
|
|||
autohide="not-last"
|
||||
className="aui-user-action-bar-root flex items-center"
|
||||
>
|
||||
<ActionBarPrimitive.Copy asChild={true}>
|
||||
<TooltipIconButton tooltip="Copy">
|
||||
<AuiIf condition={({ message }) => message.isCopied}>
|
||||
<CheckIcon />
|
||||
</AuiIf>
|
||||
<AuiIf condition={({ message }) => !message.isCopied}>
|
||||
<CopyIcon />
|
||||
</AuiIf>
|
||||
</TooltipIconButton>
|
||||
</ActionBarPrimitive.Copy>
|
||||
<CopyButton />
|
||||
<ActionBarPrimitive.Edit asChild={true}>
|
||||
<TooltipIconButton tooltip="Edit" className="aui-user-action-edit">
|
||||
<PencilIcon />
|
||||
|
|
|
|||
|
|
@ -73,10 +73,26 @@ export const TARGET_MODULES = [
|
|||
"down_proj",
|
||||
];
|
||||
|
||||
export const OPTIMIZER_OPTIONS: ReadonlyArray<{ value: string; label: string }> = [
|
||||
{ value: "adamw_8bit", label: "AdamW 8-bit" },
|
||||
{ value: "paged_adamw_8bit", label: "Paged AdamW 8-bit" },
|
||||
{ value: "adamw_bnb_8bit", label: "AdamW BNB 8-bit" },
|
||||
{ value: "paged_adamw_32bit", label: "Paged AdamW 32-bit" },
|
||||
{ value: "adamw_torch", label: "AdamW (PyTorch)" },
|
||||
{ value: "adamw_torch_fused", label: "AdamW (PyTorch Fused)" },
|
||||
];
|
||||
|
||||
export const LR_SCHEDULER_OPTIONS: ReadonlyArray<{ value: string; label: string }> = [
|
||||
{ value: "linear", label: "Linear" },
|
||||
{ value: "cosine", label: "Cosine" },
|
||||
];
|
||||
|
||||
export const DEFAULT_HYPERPARAMS = {
|
||||
epochs: 3,
|
||||
contextLength: 2048,
|
||||
learningRate: 2e-4,
|
||||
optimizerType: "adamw_8bit",
|
||||
lrSchedulerType: "linear",
|
||||
loraRank: 16,
|
||||
loraAlpha: 32,
|
||||
loraDropout: 0.05,
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
|||
if (abortSignal.aborted) return;
|
||||
warmupToastShown = true;
|
||||
toast.promise(firstTokenPromise, {
|
||||
loading: "Warming up model",
|
||||
loading: "Generating",
|
||||
success: "Generating",
|
||||
error: (err) =>
|
||||
err instanceof Error && err.message ? err.message : "Generation failed",
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import {
|
|||
import {
|
||||
useDebouncedValue,
|
||||
useHfDatasetSearch,
|
||||
useHfTokenValidation,
|
||||
useInfiniteScroll,
|
||||
} from "@/hooks";
|
||||
import { cn, formatCompact } from "@/lib/utils";
|
||||
|
|
@ -103,10 +104,14 @@ export function DatasetStep() {
|
|||
isLoading,
|
||||
isLoadingMore,
|
||||
fetchMore,
|
||||
error: hfSearchError,
|
||||
} = useHfDatasetSearch(debouncedQuery, {
|
||||
accessToken: hfToken || undefined,
|
||||
});
|
||||
|
||||
const { error: tokenValidationError, isChecking: isCheckingToken } =
|
||||
useHfTokenValidation(hfToken);
|
||||
|
||||
const resultIds = useMemo(() => hfResults.map((r) => r.id), [hfResults]);
|
||||
|
||||
const comboboxAnchorRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -179,6 +184,23 @@ export function DatasetStep() {
|
|||
onChange={(e) => setHfToken(e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
{(tokenValidationError ?? hfSearchError) && (
|
||||
<p className="text-xs text-destructive">
|
||||
{tokenValidationError ?? hfSearchError}
|
||||
{" — "}
|
||||
<a
|
||||
href="https://huggingface.co/settings/tokens"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Get or update token
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{isCheckingToken && (
|
||||
<p className="text-xs text-muted-foreground">Checking token…</p>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { MODEL_TYPE_TO_HF_TASK } from "@/config/training";
|
|||
import {
|
||||
useDebouncedValue,
|
||||
useHfModelSearch,
|
||||
useHfTokenValidation,
|
||||
useInfiniteScroll,
|
||||
} from "@/hooks";
|
||||
import { formatCompact } from "@/lib/utils";
|
||||
|
|
@ -80,11 +81,15 @@ export function ModelSelectionStep() {
|
|||
isLoading,
|
||||
isLoadingMore,
|
||||
fetchMore,
|
||||
error: hfSearchError,
|
||||
} = useHfModelSearch(debouncedQuery, {
|
||||
task,
|
||||
accessToken: hfToken || undefined,
|
||||
});
|
||||
|
||||
const { error: tokenValidationError, isChecking: isCheckingToken } =
|
||||
useHfTokenValidation(hfToken);
|
||||
|
||||
const resultIds = useMemo(() => hfResults.map((r) => r.id), [hfResults]);
|
||||
|
||||
const comboboxAnchorRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -126,6 +131,23 @@ export function ModelSelectionStep() {
|
|||
onChange={(e) => setHfToken(e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
{(tokenValidationError ?? hfSearchError) && (
|
||||
<p className="text-xs text-destructive">
|
||||
{tokenValidationError ?? hfSearchError}
|
||||
{" — "}
|
||||
<a
|
||||
href="https://huggingface.co/settings/tokens"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Get or update token
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{isCheckingToken && (
|
||||
<p className="text-xs text-muted-foreground">Checking token…</p>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ export function DatasetPreviewDialog({
|
|||
const mappingOk = !!manualMapping.input && !!manualMapping.output;
|
||||
const leftLabel = isVlm ? "Image" : "Input";
|
||||
const rightLabel = isVlm ? "Text" : "Output";
|
||||
const isHfDataset = !!datasetName && datasetName.includes("/");
|
||||
|
||||
useEffect(() => {
|
||||
if (!manualMapping.input || !manualMapping.output) return;
|
||||
|
|
@ -266,8 +267,13 @@ export function DatasetPreviewDialog({
|
|||
<Spinner className="size-5 text-primary" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground font-medium">
|
||||
Loading preview...
|
||||
{isHfDataset ? "Fetching dataset preview from Hugging Face..." : "Loading preview..."}
|
||||
</p>
|
||||
{isHfDataset && (
|
||||
<p className="text-xs text-muted-foreground/60">
|
||||
This may take a moment for large datasets
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
import {
|
||||
useDebouncedValue,
|
||||
useHfDatasetSearch,
|
||||
useHfTokenValidation,
|
||||
useInfiniteScroll,
|
||||
} from "@/hooks";
|
||||
import { formatCompact } from "@/lib/utils";
|
||||
|
|
@ -102,10 +103,14 @@ export function DatasetSection() {
|
|||
isLoading,
|
||||
isLoadingMore,
|
||||
fetchMore,
|
||||
error: hfSearchError,
|
||||
} = useHfDatasetSearch(debouncedQuery, {
|
||||
accessToken: hfToken || undefined,
|
||||
});
|
||||
|
||||
const { error: tokenValidationError, isChecking: isCheckingToken } =
|
||||
useHfTokenValidation(hfToken);
|
||||
|
||||
const resultIds = useMemo(() => {
|
||||
const ids = hfResults.map((r) => r.id);
|
||||
if (dataset && !ids.includes(dataset)) {
|
||||
|
|
@ -249,6 +254,23 @@ export function DatasetSection() {
|
|||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
{(tokenValidationError ?? hfSearchError) && (
|
||||
<p className="text-xs text-destructive">
|
||||
{tokenValidationError ?? hfSearchError}
|
||||
{" — "}
|
||||
<a
|
||||
href="https://huggingface.co/settings/tokens"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Get or update token
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{isCheckingToken && (
|
||||
<p className="text-xs text-muted-foreground">Checking token…</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<HfDatasetSubsetSplitSelectors
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
useDebouncedValue,
|
||||
useGpuInfo,
|
||||
useHfModelSearch,
|
||||
useHfTokenValidation,
|
||||
useInfiniteScroll,
|
||||
} from "@/hooks";
|
||||
import { formatCompact } from "@/lib/utils";
|
||||
|
|
@ -152,11 +153,15 @@ export function ModelSection() {
|
|||
isLoading,
|
||||
isLoadingMore,
|
||||
fetchMore,
|
||||
error: hfSearchError,
|
||||
} = useHfModelSearch(debouncedQuery, {
|
||||
task,
|
||||
accessToken: hfToken || undefined,
|
||||
});
|
||||
|
||||
const { error: tokenValidationError, isChecking: isCheckingToken } =
|
||||
useHfTokenValidation(hfToken);
|
||||
|
||||
const resultIds = useMemo(() => {
|
||||
const ids = hfResults.map((r) => r.id);
|
||||
if (selectedModel && !ids.includes(selectedModel)) {
|
||||
|
|
@ -568,6 +573,23 @@ export function ModelSection() {
|
|||
onChange={(e) => setHfToken(e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
{(tokenValidationError ?? hfSearchError) && (
|
||||
<p className="text-xs text-destructive">
|
||||
{tokenValidationError ?? hfSearchError}
|
||||
{" — "}
|
||||
<a
|
||||
href="https://huggingface.co/settings/tokens"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Get or update token
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{isCheckingToken && (
|
||||
<p className="text-xs text-muted-foreground">Checking token…</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,12 @@ import {
|
|||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { CONTEXT_LENGTHS, TARGET_MODULES } from "@/config/training";
|
||||
import {
|
||||
CONTEXT_LENGTHS,
|
||||
LR_SCHEDULER_OPTIONS,
|
||||
OPTIMIZER_OPTIONS,
|
||||
TARGET_MODULES,
|
||||
} from "@/config/training";
|
||||
import { useTrainingConfigStore } from "@/features/training";
|
||||
import type { GradientCheckpointing } from "@/types/training";
|
||||
import {
|
||||
|
|
@ -508,6 +513,78 @@ export function ParamsSection(): ReactElement {
|
|||
value="optimization"
|
||||
className="mt-3 flex flex-col gap-3"
|
||||
>
|
||||
<Row
|
||||
label="Optimizer"
|
||||
tooltip={
|
||||
<>
|
||||
Optimization algorithm. 8-bit variants reduce memory usage.
|
||||
Fused is recommended for vision models.{" "}
|
||||
<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>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
value={store.optimizerType}
|
||||
onValueChange={(v) => store.setOptimizerType(v)}
|
||||
>
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{OPTIMIZER_OPTIONS.map((opt) => (
|
||||
<SelectItem
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Row>
|
||||
<Row
|
||||
label="LR scheduler"
|
||||
tooltip={
|
||||
<>
|
||||
How the learning rate changes over training. Linear decays
|
||||
steadily; cosine decays in a curve.{" "}
|
||||
<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>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
value={store.lrSchedulerType}
|
||||
onValueChange={(v) => store.setLrSchedulerType(v)}
|
||||
>
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LR_SCHEDULER_OPTIONS.map((opt) => (
|
||||
<SelectItem
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Row>
|
||||
<SliderRow
|
||||
label="Batch Size"
|
||||
tooltip={
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import type { TrainingPhase } from "@/features/training";
|
|||
|
||||
export const phaseLabel: Record<TrainingPhase, string> = {
|
||||
idle: "Idle",
|
||||
downloading_model: "Downloading model",
|
||||
downloading_dataset: "Downloading dataset",
|
||||
loading_model: "Loading model",
|
||||
loading_dataset: "Loading dataset",
|
||||
configuring: "Configuring",
|
||||
|
|
@ -13,6 +15,10 @@ export const phaseLabel: Record<TrainingPhase, string> = {
|
|||
|
||||
export const phaseColors: Record<TrainingPhase, string> = {
|
||||
idle: "bg-muted text-muted-foreground",
|
||||
downloading_model:
|
||||
"bg-sky-100 text-sky-700 dark:bg-sky-900 dark:text-sky-300",
|
||||
downloading_dataset:
|
||||
"bg-sky-100 text-sky-700 dark:bg-sky-900 dark:text-sky-300",
|
||||
loading_model:
|
||||
"bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300",
|
||||
loading_dataset:
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import { Link, useNavigate } from "@tanstack/react-router";
|
|||
import { useShallow } from "zustand/react/shallow";
|
||||
import { useGpuUtilization } from "@/hooks";
|
||||
import { setTrainingCompareHandoff } from "@/features/chat";
|
||||
import { OPTIMIZER_OPTIONS } from "@/config/training";
|
||||
import { formatDuration, formatNumber, phaseColors, phaseLabel } from "./progress-section-lib";
|
||||
|
||||
export function ProgressSection(): ReactElement {
|
||||
|
|
@ -71,6 +72,7 @@ export function ProgressSection(): ReactElement {
|
|||
maxSteps: state.maxSteps,
|
||||
contextLength: state.contextLength,
|
||||
warmupSteps: state.warmupSteps,
|
||||
optimizerType: state.optimizerType,
|
||||
loraRank: state.loraRank,
|
||||
loraAlpha: state.loraAlpha,
|
||||
loraDropout: state.loraDropout,
|
||||
|
|
@ -126,6 +128,10 @@ export function ProgressSection(): ReactElement {
|
|||
? runtime.currentGradNorm
|
||||
: lastNonZeroValue(runtime.gradNormHistory) ?? runtime.currentGradNorm;
|
||||
|
||||
const optimizerLabel =
|
||||
OPTIMIZER_OPTIONS.find((o) => o.value === config.optimizerType)?.label ??
|
||||
config.optimizerType;
|
||||
|
||||
const configItems = [
|
||||
{
|
||||
section: "Hyperparams",
|
||||
|
|
@ -133,6 +139,7 @@ export function ProgressSection(): ReactElement {
|
|||
["Epochs", config.epochs],
|
||||
["Batch size", config.batchSize],
|
||||
["Learning rate", config.learningRate],
|
||||
["Optimizer", optimizerLabel],
|
||||
["Max steps", config.maxSteps],
|
||||
["Context length", config.contextLength],
|
||||
["Warmup steps", config.warmupSteps],
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ export function StudioPage(): ReactElement {
|
|||
const ensureModelDefaultsLoaded = useTrainingConfigStore(
|
||||
(s) => s.ensureModelDefaultsLoaded,
|
||||
);
|
||||
const ensureDatasetChecked = useTrainingConfigStore(
|
||||
(s) => s.ensureDatasetChecked,
|
||||
);
|
||||
const dialogOpen = useDatasetPreviewDialogStore((s) => s.open);
|
||||
const dialogMode = useDatasetPreviewDialogStore((s) => s.mode);
|
||||
const dialogInitial = useDatasetPreviewDialogStore((s) => s.initialData);
|
||||
|
|
@ -65,7 +68,8 @@ export function StudioPage(): ReactElement {
|
|||
|
||||
useEffect(() => {
|
||||
ensureModelDefaultsLoaded();
|
||||
}, [selectedModel, ensureModelDefaultsLoaded]);
|
||||
ensureDatasetChecked();
|
||||
}, [selectedModel, ensureModelDefaultsLoaded, ensureDatasetChecked]);
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen overflow-hidden bg-background">
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ export function TrainingView(): ReactElement {
|
|||
);
|
||||
|
||||
const isPreparingPhase =
|
||||
runtime.phase === "downloading_model" ||
|
||||
runtime.phase === "downloading_dataset" ||
|
||||
runtime.phase === "loading_model" ||
|
||||
runtime.phase === "loading_dataset" ||
|
||||
runtime.phase === "configuring";
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ export function buildTrainingStartPayload(
|
|||
weight_decay: config.weightDecay,
|
||||
random_seed: config.randomSeed,
|
||||
packing: config.packing,
|
||||
optim: "adamw_8bit",
|
||||
lr_scheduler_type: "linear",
|
||||
optim: config.optimizerType,
|
||||
lr_scheduler_type: config.lrSchedulerType,
|
||||
use_lora: adapterMethod,
|
||||
lora_r: config.loraRank,
|
||||
lora_alpha: config.loraAlpha,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ interface BackendTrainingDefaults {
|
|||
max_seq_length?: number;
|
||||
num_epochs?: number;
|
||||
learning_rate?: number | string;
|
||||
optim?: string;
|
||||
lr_scheduler_type?: string;
|
||||
batch_size?: number;
|
||||
gradient_accumulation_steps?: number;
|
||||
warmup_steps?: number;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ type ModelDefaultsPatch = Partial<
|
|||
| "epochs"
|
||||
| "contextLength"
|
||||
| "learningRate"
|
||||
| "optimizerType"
|
||||
| "lrSchedulerType"
|
||||
| "loraRank"
|
||||
| "loraAlpha"
|
||||
| "loraDropout"
|
||||
|
|
@ -86,6 +88,12 @@ export function mapBackendModelConfigToTrainingPatch(
|
|||
const learningRate = toNumber(training?.learning_rate);
|
||||
if (learningRate !== undefined) patch.learningRate = learningRate;
|
||||
|
||||
const optim = toStringValue(training?.optim);
|
||||
if (optim !== undefined) patch.optimizerType = optim;
|
||||
|
||||
const lrSchedulerType = toStringValue(training?.lr_scheduler_type);
|
||||
if (lrSchedulerType !== undefined) patch.lrSchedulerType = lrSchedulerType;
|
||||
|
||||
const batchSize = toNumber(training?.batch_size);
|
||||
if (batchSize !== undefined) patch.batchSize = batchSize;
|
||||
|
||||
|
|
|
|||
|
|
@ -43,12 +43,19 @@ let _datasetCheckController: AbortController | null = null;
|
|||
// AbortController for in-flight model default loads.
|
||||
let _modelConfigController: AbortController | null = null;
|
||||
|
||||
// Track whether the user has manually toggled trainOnCompletions
|
||||
// since the last auto-set (model load or dataset change).
|
||||
let _trainOnCompletionsManuallySet = false;
|
||||
|
||||
const NON_PERSISTED_STATE_KEYS: ReadonlySet<keyof TrainingConfigState> = new Set([
|
||||
"modelType",
|
||||
"isCheckingVision",
|
||||
"isLoadingModelDefaults",
|
||||
"modelDefaultsError",
|
||||
"modelDefaultsAppliedFor",
|
||||
"isCheckingDataset",
|
||||
"isDatasetMultimodal",
|
||||
"trainOnCompletions",
|
||||
]);
|
||||
|
||||
function partializePersistedState(
|
||||
|
|
@ -102,8 +109,17 @@ export const useTrainingConfigStore = create<TrainingConfigStore>()(
|
|||
if (controller.signal.aborted) return;
|
||||
if (get().selectedModel !== modelName) return;
|
||||
|
||||
_trainOnCompletionsManuallySet = false;
|
||||
const patch = mapBackendModelConfigToTrainingPatch(modelDetails.config);
|
||||
|
||||
// If vision model + multimodal dataset already known, override
|
||||
// trainOnCompletions to false regardless of backend default.
|
||||
if (modelDetails.is_vision && get().isDatasetMultimodal === true) {
|
||||
patch.trainOnCompletions = false;
|
||||
}
|
||||
|
||||
set({
|
||||
...mapBackendModelConfigToTrainingPatch(modelDetails.config),
|
||||
...patch,
|
||||
isVisionModel: modelDetails.is_vision,
|
||||
isLoadingModelDefaults: false,
|
||||
isCheckingVision: false,
|
||||
|
|
@ -139,6 +155,40 @@ export const useTrainingConfigStore = create<TrainingConfigStore>()(
|
|||
});
|
||||
};
|
||||
|
||||
const runDatasetCheck = (datasetName: string, split: string) => {
|
||||
_datasetCheckController?.abort();
|
||||
const controller = new AbortController();
|
||||
_datasetCheckController = controller;
|
||||
set({ isCheckingDataset: true });
|
||||
|
||||
const state = get();
|
||||
checkDatasetFormat({
|
||||
datasetName,
|
||||
hfToken: state.hfToken.trim() || null,
|
||||
subset: state.datasetSubset,
|
||||
split,
|
||||
})
|
||||
.then((res) => {
|
||||
if (controller.signal.aborted) return;
|
||||
const isMultimodal = !!res.is_multimodal;
|
||||
const updates: Record<string, unknown> = {
|
||||
isDatasetMultimodal: isMultimodal,
|
||||
isCheckingDataset: false,
|
||||
};
|
||||
if (!_trainOnCompletionsManuallySet) {
|
||||
const { isVisionModel } = get();
|
||||
if (isVisionModel && isMultimodal) {
|
||||
updates.trainOnCompletions = false;
|
||||
}
|
||||
}
|
||||
set(updates);
|
||||
})
|
||||
.catch(() => {
|
||||
if (controller.signal.aborted) return;
|
||||
set({ isDatasetMultimodal: null, isCheckingDataset: false });
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
...initialState,
|
||||
setStep: (step) => set({ currentStep: step }),
|
||||
|
|
@ -196,6 +246,7 @@ export const useTrainingConfigStore = create<TrainingConfigStore>()(
|
|||
setDataset: (dataset) => {
|
||||
_datasetCheckController?.abort();
|
||||
_datasetCheckController = null;
|
||||
_trainOnCompletionsManuallySet = false;
|
||||
set({
|
||||
dataset,
|
||||
datasetSubset: null,
|
||||
|
|
@ -208,6 +259,7 @@ export const useTrainingConfigStore = create<TrainingConfigStore>()(
|
|||
setDatasetSubset: (datasetSubset) => {
|
||||
_datasetCheckController?.abort();
|
||||
_datasetCheckController = null;
|
||||
_trainOnCompletionsManuallySet = false;
|
||||
set({
|
||||
datasetSubset,
|
||||
datasetSplit: null,
|
||||
|
|
@ -217,8 +269,6 @@ export const useTrainingConfigStore = create<TrainingConfigStore>()(
|
|||
});
|
||||
},
|
||||
setDatasetSplit: (datasetSplit) => {
|
||||
_datasetCheckController?.abort();
|
||||
_datasetCheckController = null;
|
||||
set({
|
||||
datasetSplit,
|
||||
datasetManualMapping: emptyManualMapping(),
|
||||
|
|
@ -233,27 +283,21 @@ export const useTrainingConfigStore = create<TrainingConfigStore>()(
|
|||
: state.uploadedFile;
|
||||
if (!datasetName) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
_datasetCheckController = controller;
|
||||
set({ isCheckingDataset: true });
|
||||
runDatasetCheck(datasetName, datasetSplit || "train");
|
||||
},
|
||||
ensureDatasetChecked: () => {
|
||||
const state = get();
|
||||
if (state.isCheckingDataset) return;
|
||||
if (state.isDatasetMultimodal !== null) return;
|
||||
|
||||
checkDatasetFormat({
|
||||
datasetName,
|
||||
hfToken: state.hfToken.trim() || null,
|
||||
subset: state.datasetSubset,
|
||||
split: datasetSplit || "train",
|
||||
})
|
||||
.then((res) => {
|
||||
if (controller.signal.aborted) return;
|
||||
set({
|
||||
isDatasetMultimodal: !!res.is_multimodal,
|
||||
isCheckingDataset: false,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
if (controller.signal.aborted) return;
|
||||
set({ isDatasetMultimodal: null, isCheckingDataset: false });
|
||||
});
|
||||
const datasetName =
|
||||
state.datasetSource === "huggingface"
|
||||
? state.dataset
|
||||
: state.uploadedFile;
|
||||
if (!datasetName) return;
|
||||
|
||||
const split = state.datasetSplit || "train";
|
||||
runDatasetCheck(datasetName, split);
|
||||
},
|
||||
setDatasetManualMapping: (datasetManualMapping) =>
|
||||
set({ datasetManualMapping }),
|
||||
|
|
@ -261,6 +305,8 @@ export const useTrainingConfigStore = create<TrainingConfigStore>()(
|
|||
setEpochs: (epochs) => set({ epochs }),
|
||||
setContextLength: (contextLength) => set({ contextLength }),
|
||||
setLearningRate: (learningRate) => set({ learningRate }),
|
||||
setOptimizerType: (optimizerType) => set({ optimizerType }),
|
||||
setLrSchedulerType: (lrSchedulerType) => set({ lrSchedulerType }),
|
||||
setLoraRank: (loraRank) => set({ loraRank }),
|
||||
setLoraAlpha: (loraAlpha) => set({ loraAlpha }),
|
||||
setLoraDropout: (loraDropout) => set({ loraDropout }),
|
||||
|
|
@ -274,8 +320,10 @@ export const useTrainingConfigStore = create<TrainingConfigStore>()(
|
|||
setSaveSteps: (saveSteps) => set({ saveSteps }),
|
||||
setEvalSteps: (evalSteps) => set({ evalSteps }),
|
||||
setPacking: (packing) => set({ packing }),
|
||||
setTrainOnCompletions: (trainOnCompletions) =>
|
||||
set({ trainOnCompletions }),
|
||||
setTrainOnCompletions: (trainOnCompletions) => {
|
||||
_trainOnCompletionsManuallySet = true;
|
||||
set({ trainOnCompletions });
|
||||
},
|
||||
setGradientCheckpointing: (gradientCheckpointing) =>
|
||||
set({ gradientCheckpointing }),
|
||||
setRandomSeed: (randomSeed) => set({ randomSeed }),
|
||||
|
|
@ -300,7 +348,7 @@ export const useTrainingConfigStore = create<TrainingConfigStore>()(
|
|||
},
|
||||
{
|
||||
name: "unsloth_training_config_v1",
|
||||
version: 3,
|
||||
version: 5,
|
||||
migrate: (persisted, version) => {
|
||||
const s = persisted as Record<string, unknown>;
|
||||
if (version < 2 && s.datasetSubset == null && s.datasetConfig != null) {
|
||||
|
|
@ -310,6 +358,12 @@ export const useTrainingConfigStore = create<TrainingConfigStore>()(
|
|||
if (version < 3 && s.modelDefaultsAppliedFor == null) {
|
||||
s.modelDefaultsAppliedFor = null;
|
||||
}
|
||||
if (version < 4 && s.optimizerType == null) {
|
||||
s.optimizerType = DEFAULT_HYPERPARAMS.optimizerType;
|
||||
}
|
||||
if (version < 5 && s.lrSchedulerType == null) {
|
||||
s.lrSchedulerType = DEFAULT_HYPERPARAMS.lrSchedulerType;
|
||||
}
|
||||
return s as unknown as TrainingConfigStore;
|
||||
},
|
||||
partialize: partializePersistedState,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ export interface TrainingConfigState {
|
|||
epochs: number;
|
||||
contextLength: number;
|
||||
learningRate: number;
|
||||
optimizerType: string;
|
||||
lrSchedulerType: string;
|
||||
loraRank: number;
|
||||
loraAlpha: number;
|
||||
loraDropout: number;
|
||||
|
|
@ -72,6 +74,7 @@ export interface TrainingConfigActions {
|
|||
setModelType: (type: ModelType) => void;
|
||||
setSelectedModel: (model: string | null) => void;
|
||||
ensureModelDefaultsLoaded: () => void;
|
||||
ensureDatasetChecked: () => void;
|
||||
setTrainingMethod: (method: TrainingMethod) => void;
|
||||
setHfToken: (token: string) => void;
|
||||
setDatasetSource: (source: DatasetSource) => void;
|
||||
|
|
@ -84,6 +87,8 @@ export interface TrainingConfigActions {
|
|||
setEpochs: (epochs: number) => void;
|
||||
setContextLength: (length: number) => void;
|
||||
setLearningRate: (rate: number) => void;
|
||||
setOptimizerType: (value: string) => void;
|
||||
setLrSchedulerType: (value: string) => void;
|
||||
setLoraRank: (rank: number) => void;
|
||||
setLoraAlpha: (alpha: number) => void;
|
||||
setLoraDropout: (dropout: number) => void;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
export type TrainingPhase =
|
||||
| "idle"
|
||||
| "downloading_model"
|
||||
| "downloading_dataset"
|
||||
| "loading_model"
|
||||
| "loading_dataset"
|
||||
| "configuring"
|
||||
|
|
|
|||
|
|
@ -5,4 +5,5 @@ export { useHardwareInfo } from "./use-hardware-info";
|
|||
export { useHfModelSearch } from "./use-hf-model-search";
|
||||
export { useHfDatasetSearch } from "./use-hf-dataset-search";
|
||||
export { useHfDatasetSplits } from "./use-hf-dataset-splits";
|
||||
export { useHfTokenValidation } from "./use-hf-token-validation";
|
||||
export { useInfiniteScroll } from "./use-infinite-scroll";
|
||||
|
|
|
|||
59
studio/frontend/src/hooks/use-hf-token-validation.ts
Normal file
59
studio/frontend/src/hooks/use-hf-token-validation.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { whoAmI } from "@huggingface/hub";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useDebouncedValue } from "./use-debounced-value";
|
||||
|
||||
export interface HfTokenValidationState {
|
||||
isValid: boolean | null;
|
||||
error: string | null;
|
||||
isChecking: boolean;
|
||||
}
|
||||
|
||||
const INITIAL: HfTokenValidationState = {
|
||||
isValid: null,
|
||||
error: null,
|
||||
isChecking: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the Hugging Face token by calling the whoami-v2 API.
|
||||
* Debounces the token to avoid excessive requests while typing.
|
||||
* Returns validation state: isValid (null = not checked), error message, and isChecking.
|
||||
*/
|
||||
export function useHfTokenValidation(token: string): HfTokenValidationState {
|
||||
const debouncedToken = useDebouncedValue(token.trim(), 500);
|
||||
const [state, setState] = useState<HfTokenValidationState>(INITIAL);
|
||||
const versionRef = useRef(0);
|
||||
|
||||
const runCheck = useCallback(async (t: string) => {
|
||||
if (!t) {
|
||||
setState({ isValid: null, error: null, isChecking: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const v = ++versionRef.current;
|
||||
setState((prev) => ({ ...prev, isChecking: true, error: null }));
|
||||
|
||||
try {
|
||||
await whoAmI({ accessToken: t });
|
||||
if (versionRef.current !== v) return;
|
||||
setState({ isValid: true, error: null, isChecking: false });
|
||||
} catch {
|
||||
if (versionRef.current !== v) return;
|
||||
setState({
|
||||
isValid: false,
|
||||
error: "invalid or expired token",
|
||||
isChecking: false,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!debouncedToken) {
|
||||
setState(INITIAL);
|
||||
return;
|
||||
}
|
||||
runCheck(debouncedToken);
|
||||
}, [debouncedToken, runCheck]);
|
||||
|
||||
return state;
|
||||
}
|
||||
44
studio/frontend/src/lib/copy-to-clipboard.ts
Normal file
44
studio/frontend/src/lib/copy-to-clipboard.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Copy text to clipboard in a way that works on Mac/Safari.
|
||||
* Uses a synchronous textarea + execCommand fallback so the copy runs in the
|
||||
* same user gesture as the click (required by Safari's clipboard security).
|
||||
*/
|
||||
export function copyToClipboard(text: string): boolean {
|
||||
if (typeof text !== "string" || text.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Synchronous fallback: works in Safari/Mac when clipboard API fails
|
||||
// because it runs entirely within the user gesture (click) stack.
|
||||
if (document.queryCommandSupported?.("copy") !== false) {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.top = "0";
|
||||
textarea.style.left = "0";
|
||||
textarea.style.opacity = "0";
|
||||
textarea.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus({ preventScroll: true });
|
||||
textarea.select();
|
||||
try {
|
||||
const ok = document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
return ok;
|
||||
} catch {
|
||||
document.body.removeChild(textarea);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Modern API only when fallback not available (e.g. non-browser)
|
||||
if (typeof navigator?.clipboard?.writeText === "function") {
|
||||
navigator.clipboard.writeText(text).then(
|
||||
() => {},
|
||||
() => {}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue