Commit graph

4,819 commits

Author SHA1 Message Date
Daniel Han
2ea8217727
Merge branch 'main' into feat/chat-ui-enhancements 2026-03-25 04:00:36 -07:00
Daniel Han
0a1f0c2e78 fix(chat): address review feedback from Gemini, Codex, and internal review
1. ArtifactPanel textarea: use local state with onChange/onBlur instead
   of empty onChange handler that made the editor unwritable

2. Artifact emission: move store.addArtifact from render body into a
   dedicated ArtifactEmitter component using useEffect

3. searchText for new threads: write searchText during generateTitle
   so post-migration threads are searchable by content

4. PromptLibrarySheet: wire onInsertPrompt with clipboard fallback and
   toast notification; handle prompt() cancel (null) as abort

5. download.ts: defer URL.revokeObjectURL with setTimeout to avoid
   failed downloads on Firefox

6. DB migration: replace serial for-of await loop with Promise.all to
   prevent transaction timeout on large databases; skip empty text

7. handleDeleteFolder: use Dexie transaction with .modify() for atomic
   unfile-then-delete; use undefined instead of "" for unfiled folderId

8. Keyboard shortcuts: remove Cmd+Shift+F from help dialog since the
   handler is not wired

Also:
- Remove unused aui hook, SearchIcon import, and prompt-library imports
- Fix stale artifactPanelOpen closure in command palette toggle
- Fix JSONL export: remove non-standard feedback field from message
  objects; use application/x-ndjson MIME type
- Fix nested button in ArtifactTab: use div with role="tab"
- Use static db import instead of redundant dynamic import in adapter
- Log memory injection errors instead of silent catch
- Add cancelled flag to feedback useEffect to prevent stale setState
- Use functional setState in handleFeedback to avoid stale closure
- Add .catch() on feedback DB write
2026-03-25 10:47:17 +00:00
Daniel Han
be2cd7087a
Add macOS and Linux desktop shortcuts to install.sh (#4568)
* Add macOS and Linux desktop shortcuts to install.sh

Adds create_studio_shortcuts() function that creates platform-native
shortcuts after `unsloth studio setup` completes, mirroring the Windows
shortcut behavior from PR #4558.

Linux: .desktop file in ~/.local/share/applications/ and ~/Desktop/
macOS: .app bundle in ~/Applications/ with Info.plist, exec stub, and
       optional .icns icon built from unsloth-gem.png via sips+iconutil

Both platforms share a Bash launcher script at
~/.local/share/unsloth/launch-studio.sh that provides:
- Health check with service fingerprint verification
- Port scanning (8888-8908) via ss/lsof
- PID-file single-instance guard (no flock dependency)
- Terminal spawning (macOS: Terminal.app; Linux: gnome-terminal etc.)
- Browser open after health poll with 60s timeout

WSL is skipped (no native desktop environment).

* Fix 6 issues found by 10 parallel reviewers

1. [10/10] Health check now supports wget as fallback to curl via
   _http_get() helper, matching the installer's own download() pattern.
   Previously wget-only systems would time out on every launch.

2. [9/10] Exe path substitution now escapes sed metacharacters (&, \, |)
   and shell single-quotes before injection, preventing launcher
   corruption for paths like /opt/R&D/bin/unsloth.

3. [4/10] Linux .desktop Exec= field now quotes the launcher path,
   fixing launches from home directories containing spaces.

4. [3/10] macOS AppleScript command now escapes backslashes and
   double-quotes before interpolation into do script "...", fixing
   Terminal.app launch failures.

5. [3/10] Single-instance guard now uses atomic mkdir instead of
   racy check-then-write PID file, preventing duplicate concurrent
   launches on rapid double-click.

6. [1/10] Launcher now scans for a free port via _find_launch_port()
   instead of always hardcoding -p 8888, so Studio starts correctly
   when another service already occupies port 8888.

Also fixed: `open` command on Linux (openvt) no longer incorrectly
triggers the macOS browser-open path -- now gated on uname=Darwin.

* Fix mktemp guard and exe path escaping from PR review comments

Two real issues identified from automated review comments:

1. Guard mktemp -d failure in macOS icns generation. If mktemp -d
   returned empty, dirname would resolve to / and rm -rf would attempt
   to delete the root directory. Now checks that the temp dir was
   actually created before proceeding.

2. Replace sed-based exe path substitution with a conf file approach.
   The previous sed escaping broke paths containing apostrophes
   (e.g. /home/O'Connor/) because the '\'' escape introduced
   backslashes that were then double-escaped by the metacharacter
   pass. Now writes UNSLOTH_EXE to a separate studio.conf file that
   the launcher sources at runtime, eliminating all sed metacharacter
   and shell quoting interaction issues.

   This also addresses the sed -i.bak portability concern (now moot
   since sed is no longer used on the launcher file).

* Fix unbound variable crash and per-user lock in launcher

- Use ${UNSLOTH_EXE:-} so set -u does not crash before the friendly
  error message when studio.conf is missing or empty.
- Append $(id -u) to the fallback lock path so each user gets their
  own lock directory when XDG_RUNTIME_DIR is unset.

* Mark desktop shortcut as trusted for GNOME/Nautilus

On modern GNOME desktops, chmod +x alone is not sufficient to make
a .desktop file launchable by double-click on ~/Desktop. Nautilus
requires the metadata::trusted attribute to be set via gio, otherwise
it shows a warning dialog instead of launching the application.
2026-03-25 03:37:37 -07:00
Daniel Han
6872c6e850
Remove advanced CodeQL workflow in favor of default setup (#4584)
The repo has both the CodeQL "default setup" (configured in repo
settings) and this advanced workflow file enabled. GitHub does not
allow both simultaneously, causing all PR CI runs to fail with:

  "CodeQL analyses from advanced configurations cannot be processed
   when the default setup is enabled"

Since the default setup already covers the same languages (Python,
JavaScript/TypeScript) with the same build-mode (none), remove the
redundant advanced workflow file.
2026-03-25 03:34:21 -07:00
Daniel Han
78b8ec8194 feat(chat): add search, feedback, export, shortcuts, memory, prompts, folders, artifacts
Comprehensive set of chat UI enhancements for ML power users.

Search: debounced sidebar search filtering threads by title and first
user message content.

Feedback: thumbs up/down buttons on assistant messages, persisted to
IndexedDB for preference dataset collection (DPO/RLHF).

Export: per-thread Markdown, JSON, and JSONL export from the sidebar
dropdown. JSONL uses OpenAI chat format for direct SFT use.

Keyboard shortcuts: Cmd+K command palette (cmdk), Cmd+Shift+N new chat,
Cmd+Shift+C compare, Cmd+Shift+S settings, ? help dialog.

Session memory: persistent context entries injected into system prompt.
CRUD panel in settings with per-entry toggle and token budget indicator.

Prompt library: reusable prompt templates with {{variable}} substitution,
stored in IndexedDB with tag support.

Folders and pinning: collapsible folder groups in sidebar, pin threads
to top, move-to-folder via dropdown menu.

Artifacts panel: auto-detected code blocks (20+ lines), HTML, and SVG
open in a right-side panel with version history and inline editing.

Screen wake lock: prevents screen sleep during long inference runs.

DB changes: Dexie version 4 adds folders, prompts, memory tables and
searchText/folderId/pinned/feedback fields with migration backfill.
2026-03-25 10:21:50 +00:00
dependabot[bot]
38405cc18c
build(deps): bump oxc-parser (#4571)
Bumps the npm-oxc-validator group in /studio/backend/core/data_recipe/oxc-validator with 1 update: [oxc-parser](https://github.com/oxc-project/oxc/tree/HEAD/napi/parser).


Updates `oxc-parser` from 0.116.0 to 0.121.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/napi/parser/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/crates_v0.121.0/napi/parser)

---
updated-dependencies:
- dependency-name: oxc-parser
  dependency-version: 0.121.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-oxc-validator
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 02:44:38 -07:00
dependabot[bot]
f294161e26
build(deps): bump the actions group with 2 updates (#4570)
Bumps the actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `actions/checkout` from 4 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

Updates `github/codeql-action` from 3 to 4
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 02:44:22 -07:00
Pete Kloehn
efedbe9740
Feature/add dependabot and codeql security checks (#4479)
* Add CodeQL analysis workflow configuration

* Add Dependabot configuration for package updates

Configure Dependabot to check for updates in various ecosystems weekly.

* Fix dependabot.yml: bun ecosystem, missing dir, grouping for PR #4479

1. studio/frontend uses bun.lock not package-lock.json, so change npm to bun
2. Add missing studio/backend/requirements/ pip entry (consumed by studio/setup.sh)
3. Add groups with patterns ["*"] to all pip/bun/npm entries to batch updates
   and avoid 30+ individual Dependabot PRs on the first run

* Consolidate pip blocks to fix overlapping directory violation

GitHub Dependabot forbids multiple same-ecosystem entries with
overlapping directories on the same branch. The root "/" directory
overlapped the 3 nested pip dirs. Merge all 4 pip blocks into one
using the `directories:` (plural) key.

Also remove redundant open-pull-requests-limit from the bun block
since grouping with patterns: ["*"] already limits PR count.

---------

Co-authored-by: Daniel Han <danielhanchen@users.noreply.github.com>
2026-03-25 02:41:33 -07:00
Datta Nimmaturi
04359be333
[Studio] Try installing causal-conv1d from prebuilt wheels if avialable (#4547)
* Try installing causal-conv1d from prebuilt wheels if avialable

* Prefer installing mamba-ssm from wheel to speed up things

* undo python stack install changes

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Revert "undo python stack install changes"

This reverts commit d943551092.

* add comments

* Fix wheel installer: model detection, platform tags, torch pin, error handling

- Add nemotron-h (hyphen) and granite-4.0-h / granitemoehybrid to model
  detection for both causal-conv1d and mamba-ssm. These hybrid Mamba models
  were silently skipped since nemotron_h (underscore) never matches real
  HF model IDs like nvidia/Nemotron-H-8B-Base, and granite was missing
  entirely despite being a supported model in model_config.py and loader.py.
- Fix _causal_conv1d_platform_tag to detect linux_aarch64 via
  platform.machine() instead of hardcoding linux_x86_64. Both upstream
  releases publish aarch64 wheels. Drop win_amd64 since neither repo
  publishes Windows wheels (avoids a wasted HTTP probe on every run).
- Pin torch to >=2.6.0,<2.11.0 instead of <=2.10.0 to add a version floor
  and document the wheel coverage range with upstream release links.
- Strip non-numeric suffixes from torch minor version so nightly builds
  like 2.7a0 correctly resolve to wheel tag torch2.7 instead of torch2.7a0.
- Use stderr=_sp.PIPE instead of stderr=_sp.STDOUT in the env probe so
  torch import warnings do not corrupt the JSON output.
- Add timeout=30 to the env probe subprocess to prevent indefinite hangs.
- Catch Exception (not just ImportError) on the existing-install check so
  ABI-broken installs with OSError/RuntimeError are retried rather than
  silently accepted.
- Guard uv invocation with shutil.which("uv") to prevent FileNotFoundError
  crash when uv is not on PATH. Wrap the top-level ensure calls in
  try/except so failures do not kill the training worker.
- Hoist _SSM_MODEL_SUBSTRINGS to module level.
- Remove redundant --torch-backend=auto flag from direct wheel URL install.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add LFM2 to causal-conv1d detection; stop training on install failure

- Add "lfm2" to _model_wants_causal_conv1d so Studio picks up the
  fast kernel path for Liquid Foundation Model 2.
- Replace silent logger.warning on SSM dependency install failure
  with an error event that tells the user to choose another model
  and stops the training job immediately.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Catch subprocess timeout in torch probe; narrow import guard to ImportError

- _probe_causal_conv1d_env: wrap subprocess.run in try/except for
  TimeoutExpired so a slow torch import returns None (falls back to
  PyPI) instead of killing the training job.
- _install_package_wheel_first: narrow except Exception to except
  ImportError on the __import__ check so unexpected errors from a
  broken module still propagate.

* Remove unconditional torch pin from install_python_stack

The torch>=2.6.0,<2.11.0 pin was added to ensure prebuilt
causal-conv1d / mamba-ssm wheels exist, but it runs at install
time for all users regardless of model choice. This can downgrade
or unnecessarily upgrade torch. The worker already handles wheel
compatibility at training time by probing the environment and
falling back to PyPI, so the install-time pin is not needed.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-25 02:22:26 -07:00
Wasim Yousef Said
926e74509d
feat(chat): cleaner tool UI, inline LaTeX, clickable links (#4561)
* feat(chat): ghost-style tool containers

Remove borders and card styling from tool call UI. ToolFallback
uses minimal padding with indented content. ToolGroup defaults
to ghost variant with subtle background for multi-tool grouping.

* feat(chat): compact web search source pills

Switch sources from vertical full-width badges to horizontal
wrapping pills with smaller icons.

* feat(chat): left-accent code and terminal tool UI

Replace bordered card layout with a left border accent for
Python and Terminal tool output. Add timer cleanup on unmount
for the copy button in both components.

* feat(chat): inline latex and clickable links

Enable single-dollar $...$ math rendering via createMathPlugin.
Add styled link component with target=_blank for external links.

* fix(chat): inline generating indicator, static tailwind classes, misc fixes

Move generating indicator from viewport footer into assistant
message using AnimatedShinyText shimmer. Only shows when message
content is empty, hides once tool calls or text appear.

Use static size class map in SourceIcon for Tailwind v4 compat.
Use unique keys for web search sources. Remove px-3 from ghost
tool group variant.

* fix(chat): only show generating indicator while message is running

Hide the shimmer when message is cancelled or errored with no
content, preventing stale loading UI on empty completed messages.

* fix: escape currency dollar signs in LaTeX math rendering and fix TS build error

- Add preprocessLaTeX() in lib/latex.ts to escape currency patterns ($5, $1,000, $5.99, $100K)
  before they reach the math parser, preventing false positives when singleDollarTextMath is enabled.
  Code blocks and already-escaped dollars are left untouched.
- Use preprocessLaTeX via useMemo in markdown-text.tsx so Streamdown receives clean input.
- Fix TS18048 in thread.tsx: message.status?.type (optional chaining) since status can be undefined.

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-25 02:06:03 -07:00
Daniel Han
3998f67680
Bump Data Designer to 0.5.4 (removes litellm dependency) (#4569)
* Bump Data Designer to 0.5.4 (removes litellm dependency)

NVIDIA Data Designer v0.5.4 removes litellm entirely and replaces it
with native OpenAI and Anthropic adapters. This follows the litellm
supply chain incident where versions 1.82.7 and 1.82.8 were compromised
with a credential stealer.

Release notes: https://github.com/NVIDIA-NeMo/DataDesigner/releases/tag/v0.5.4

Changes:
- Bump data-designer, data-designer-config, data-designer-engine to 0.5.4
- Sync data-designer-deps.txt with 0.5.4 engine requirements:
  - Added: chardet, fsspec, mcp
  - Removed: python-json-logger, pymupdf, pymupdf4llm, mammoth
    (these remain in the unstructured-seed plugin which still needs them)
  - duckdb constraint relaxed from <1.5 to <2 (upstream fixed record_batch)
- Bump plugin lower bound to >=0.5.4

* Keep pymupdf, pymupdf4llm, mammoth in data-designer-deps

The unstructured-seed plugin is installed with --no-deps, so its
pyproject.toml dependencies are not auto-resolved. These three
packages are needed by the seed route (studio/backend/routes/
data_recipe/seed.py) and must remain in the explicit deps list.
2026-03-25 02:01:43 -07:00
Avaya Aggarwal
45d0a343b5
feat: Implement Q-GaLore optimizer and custom embedding learning rate… (#4511)
* feat: Implement Q-GaLore optimizer and custom embedding learning rate in the Unsloth trainer.

* feat: Implement QGaLoreAdamW8bit optimizer with 8-bit states, GaLore low-rank gradient projection, and optional INT8 weight quantization, along with supporting projector and tests.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* feat: Introduce Q-GaLore AdamW optimizer with low-rank quantized gradient projection and integrate into the trainer, along with dedicated tests.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* feat: Implement Q-GaLore AdamW optimizer with gradient projection and quantization, including trainer integration and corresponding tests.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix 3 bugs in Q-GaLore optimizer and add weight_quant forward hooks

1. Fix use-after-delete crash: move `del p._saved_data` after the
   weight decay block so decoupled weight decay can reference the
   current weights correctly (p.data).

2. Fix substring matching in make_q_galore_param_groups: split
   parameter names on "." and check exact component matches to
   prevent false positives (e.g. "not_q_proj" matching "q_proj").

3. Implement forward pre-hooks for weight_quant: after the optimizer
   quantizes weights to INT8, replace p.data with a 1-element
   placeholder to free float memory. A register_forward_pre_hook
   dequantizes back to float before each forward pass. The trainer
   calls install_weight_quant_hooks() when weight_quant is enabled.

4. Update test_weight_decay_uses_saved_data to match the fixed code
   path (decoupled decay uses p.data, expected value 2.7). Add
   test_weight_quant_hook_restores_float to verify the INT8-to-float
   hook round-trip.

All 24/24 Q-GaLore tests pass. Benchmarked on Llama-3.2-1B-Instruct
FFT: Q-GaLore saves 32% VRAM (10.63 -> 7.24 GB) with better loss
convergence (1.3 vs 2.0 at step 100). No regressions in 31-notebook
sweep across Llama, Qwen, Mistral, Phi, Gemma, vision, and GRPO.

* Default weight_quant to False in QGaloreConfig

Benchmarks show weight_quant=True adds ~1 GB on Llama-3.2-1B due to
INT8 copy/scale overhead exceeding savings from the placeholder trick.
Users can still opt in explicitly. The optimizer logic is unchanged.

* Optimize Q-GaLore projector and optimizer step performance

Projector (q_galore_projector.py):
- Use torch.svd_lowrank with oversampling p=10 (Halko et al. 2009) instead
  of full SVD for large matrices. Falls back to full SVD when min(m,n) <= 2*rank.
  SVD steps are 6-8x faster on Llama-3.2-1B (22s -> 3s for first step).
- Cache the dequantized ortho matrix between project() and project_back() to
  avoid redundant dequantization when quant=True.
- Replace F.cosine_similarity with torch.dot for 1-D unit vectors in the
  adaptive schedule. Remove unused torch.nn.functional import.
- Use collections.deque(maxlen=queue_size) instead of list with manual pop(0).

Optimizer (q_galore_adamw.py):
- Remove redundant .clone() on dequantized weights (line 151) and on float
  data before re-quantization (line 211). _dequantize already returns a fresh
  tensor and _quantize/_quantize_stochastic only reads its input.
- Consolidate per-group torch.cuda.synchronize() into a single call after
  all param groups complete.
- Use torch.empty instead of torch.zeros for the scalar placeholder tensor
  that is never read.

Verified: 24/24 unit tests pass. Llama-3.2-1B 61-step training produces
losses within 0.24% relative diff (correlation >0.9999) of the original.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-25 01:03:10 -07:00
Krishna Chaitanya
11606c5025
fix: remove auto wandb.finish() after train() to allow post-training evaluate() (#4564)
* fix: remove auto wandb.finish() after train() to allow post-training evaluate()

The prepare_for_training_mode wrapper unconditionally called wandb.finish()
after trainer.train() completed. This terminated the active W&B run, causing
trainer.evaluate() to fail with "You must call wandb.init() before wandb.log()".

Users who need multiple training runs in one session can call wandb.finish()
manually between runs to avoid data overwriting.

Fixes #3954

* fix: defer wandb.finish() to next train() call instead of removing it

Instead of calling wandb.finish() at the end of train() (which breaks
evaluate/log) or removing it entirely (which causes data overwriting on
multiple train() calls), defer it to the start of the next train() call.

This way:
- train() + evaluate() works (run stays open after train)
- train() + train() gets separate W&B runs (previous run finished first)
- train() + evaluate() + train() also works correctly

Also resets HF's WandbCallback._initialized flag so it re-calls
wandb.init() for the new run.

Fixes #3954

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-25 01:00:12 -07:00
Wasim Yousef Said
208862218d
feat(studio): training history persistence and past runs viewer (#4501)
* feat(db): add SQLite storage layer for training history

* feat(api): add training history endpoints and response models

* feat(training): integrate DB persistence into training event loop

* feat(ui): add training history views and card grid

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(studio): address review issues in training history persistence

- Strip hf_token/wandb_token from config before SQLite storage
- Add UUID suffix to job_id for collision resistance
- Use isfinite() for 0.0 metric handling throughout
- Respect _should_stop in error event finalization
- Run schema DDL once per process, not per connection
- Close connection on schema init failure
- Guard cleanup_orphaned_runs at startup
- Cap _metric_buffer at 500 entries
- Make FLUSH_THRESHOLD a class constant
- Map 'running' to 'training' phase in historical view
- Derive LR/GradNorm from history arrays in historical view
- Fix nested button with div[role=button] in history cards
- Guard String(value) against null/undefined in config popover
- Clear selectedHistoryRunId on auto tab switch

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(studio): address round-2 review findings across training backend and frontend

Backend (training.py):
- Move state mutation after proc.start() so a failed spawn does not wedge
  the backend with is_training=True
- Create DB run row eagerly after proc.start() so runs appear in history
  during model loading, not after first metric event
- Rewrite _flush_metrics_to_db() with snapshot-before-insert pattern to
  preserve metrics arriving during the write and retain buffer on failure
- Guard eval_loss with float() coercion and math.isfinite(), matching the
  existing grad_norm guard
- Increase pump thread join timeout from 3s to 8s to cover SQLite's
  default 5s lock timeout

Frontend (studio-page.tsx):
- Fix history navigation: check isTrainingRunning instead of
  showTrainingView in onSelectRun so completed runs are not misrouted
- Replace activeTab state + auto-switch useEffect with derived tab to
  eliminate react-hooks/set-state-in-effect lint violation

Frontend (historical-training-view.tsx):
- Add explicit "running" branch to message ternary so running runs no
  longer fall through to "Training errored"
- Derive loading from detail/error state and move cleanup to effect
  return to eliminate react-hooks/set-state-in-effect lint violation

Frontend (progress-section.tsx):
- Derive stopRequested from isTrainingRunning && stopRequestedLocal to
  eliminate react-hooks/set-state-in-effect lint violation and remove
  unused useEffect import

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(studio): resolve 3 remaining bugs from round-2 review

1. Stuck on Current Run tab [12/20]: Only force "current-run" tab when
   isTrainingRunning is true, not when stale completed-run data exists.
   After training ends, users can freely navigate to Configure.

2. Incomplete metric sanitization [7/20]: Apply float() coercion and
   isfinite() guards to loss and learning_rate, matching the existing
   pattern used by grad_norm and eval_loss. Prevents TypeError from
   string values and NaN leaks into history arrays.

3. Stop button state leak across runs [10/20]: Add key={runtime.jobId}
   to ProgressSection so React remounts it when a new run starts,
   resetting stopRequestedLocal state.

* fix(studio): deduplicate loss/lr sanitization in training event handler

Reuse _safe_loss/_safe_lr from the progress update block instead of
re-sanitizing the same raw event values for metric history.

* fix(studio): restore loss > 0 guard to prevent eval steps injecting 0.0 into metric histories

Round-2/3 fixes relaxed the history append guard from `loss > 0` to
`loss is not None`, which let eval-only log events (where loss defaults
to 0.0) append fake zeros into loss_history and lr_history. Restore the
`loss > 0` check to match the worker's own has_train_loss gate. The
float() coercion and isfinite() sanitization from round-3 remain intact.

* fix(studio): resolve training history bugs — nullable loss/lr, tab nav, sparkline

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-25 00:58:55 -07:00
Daniel Han
3108750bb0
Remove duplicate frontend assets from wheel to reduce package size (#4567)
The wheel currently ships frontend/public/, frontend/src/, and
frontend/*.lock alongside frontend/dist/. These are build-time inputs
that Vite already copies into dist/ during the build step:

- public/ is copied verbatim into dist/ by vite build (28.6 MB duplicate)
- src/ is TSX source compiled into dist/assets/*.js (2.1 MB, not used at runtime)
- *.lock files are package manager lockfiles (0.9 MB, not used at runtime)

The backend only serves from frontend/dist/ (see main.py setup_frontend
and run.py frontend_path). Nothing references public/ or src/ at runtime.

This drops the wheel from ~62.7 MB to ~31 MB.
2026-03-24 23:48:49 -07:00
Lee Jackson
557743f027
studio: windows desktop shortcut launcher (#4558)
* feat(windows): add Studio desktop/Start shortcuts with health-check launcher

* chore(windows): bundle sloth.ico and set shortcut icons when valid

* chore(windows):add images/sloth.ico

* fix(windows): guard PSScriptRoot for Studio shortcut icon in iex installs

* fix(install): high-DPI sloth.ico and relocate to studio/frontend/publi

* chore(studio): update sloth.ico for clearer desktop and shell icons

* chore(studio): use unsloth.ico for Studio shortcut icon

* feat(windows): improve Studio shortcut launcher (fast health + browser UX)

* fix(windows): stable unsloth.ico URL and Unicode-safe Studio launcher scripts

* fix(windows): escape $ in exe path and write launcher UTF-8 with BOM

* fix(windows): skip shortcuts when Desktop or APPDATA paths are missing

* fix(install): log shortcut/icon/port failures and warn early on missing paths

* fix(install): guard missing LOCALAPPDATA before shortcut paths

* fix(install): harden New-StudioShortcuts and improve success messaging

* fix(install): include port 8908 in studio health check

* fix(install): fix launch-studio.ps1  quoting

* Fix launcher edge cases and normalize indentation in install.ps1

- Handle silent timeout: show a message when Studio is still starting
  but did not become healthy within the timeout, instead of exiting
  with no feedback
- Add -NoProfile to the visible PowerShell terminal launch so the
  user profile cannot hang or error before Studio runs
- Add a named mutex (Local\UnslothStudioLauncher) to prevent
  double-click from spawning duplicate terminals; second instance
  polls for health and opens the browser when ready
- Normalize indentation inside New-StudioShortcuts outer try block
  from mixed 8/12-space to consistent 12-space

* Simplify Get-CandidatePorts port dedup with Sort-Object -Unique

Replace the foreach/-notcontains loop with a single pipeline:
  $ports = (@($basePort) + $listening) | Sort-Object -Unique

* Harden health probe and handle abandoned mutex in launcher

- Test-StudioHealth now checks resp.service == 'Unsloth UI Backend' to
  avoid fingerprinting collisions with other local services on the same
  port range.
- Wrap the mutex WaitOne(0) call in a try/catch for
  AbandonedMutexException so the launcher recovers gracefully when a
  previous instance was killed while holding the mutex.

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-24 23:41:02 -07:00
Krishna Chaitanya
9b989ee898
fix: prevent UnicodeEncodeError on Windows CP1252 consoles in studio setup (#4563)
* fix: prevent UnicodeEncodeError on Windows CP1252 consoles in studio setup

On Windows, `unsloth studio setup` crashes with a UnicodeEncodeError
when install_python_stack.py tries to print Unicode status glyphs
(, , ⚠️) to a console that uses a legacy code page like CP1252.

Add a _safe_print() helper that catches UnicodeEncodeError and
gracefully degrades emoji to ASCII equivalents ([OK], [FAIL], [!]).
Replace all print() calls that emit Unicode glyphs with _safe_print().

Fixes #4509

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Replace Unicode dashes with ASCII in install_python_stack.py

Box-drawing (U+2500) and em dash (U+2014) chars in section dividers
and comments are themselves not representable on CP1252 -- replace
with plain ASCII dashes for consistency with the fix.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-24 22:04:09 -07:00
TR-3B
8c94b461fb
Add GRPO resume vLLM cleanup guard (#4411)
* Add GRPO resume vLLM cleanup guard

* Guard GRPO resume sleep on vLLM sleep mode

* Harden GRPO resume vLLM cleanup guard

- Wrap llm.sleep(1) in try/except so a failed sleep does not block
  training resume (best-effort cleanup)
- Also check kwargs["model_path"] which transformers.Trainer.train()
  still accepts and normalizes to resume_from_checkpoint internally

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-24 21:37:45 -07:00
Wasim Yousef Said
085f9529b6
Regroup chat settings sidebar into focused sections (#4551)
* feat(chat): regroup settings sidebar into Model, Sampling, Tools, and Preferences sections

Split the monolithic Settings collapsible into focused sections with
icons. Model section shows context length and KV cache dtype for GGUF
models, trust remote code for non GGUF. Tools section groups auto heal,
max tool calls, and tool call timeout. Preferences section holds auto
title toggle.

* feat(chat): persist collapsible section open/closed state in localStorage

Remember which sections the user expanded or collapsed across sidebar
toggles, mobile sheet reopens, and browser sessions.

* fix(chat): harden collapsible state persistence and restore defaultOpen

- Validate localStorage values are booleans before using them, preventing
  corrupted entries like string "false" from being treated as truthy
- Use Object.hasOwn() instead of `in` operator to avoid prototype chain
  matches on keys like "constructor" or "toString"
- Restore defaultOpen={true} on Model and Preferences sections so they
  are expanded on first visit, matching the old Settings section behavior
- Fix misleading Context Length description to reflect it is read-only
- Downgrade console.error to console.warn for non-critical localStorage
  parse failures

* fix(chat): remove redundant disabled styles on Context Length input

The Input component already applies opacity-50 and cursor-not-allowed
via its disabled: variants. Specifying them unconditionally in the
className is redundant.

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-24 19:39:27 -07:00
Daniel Han
acc881452f
fix: pin unsloth>=2026.3.11 in install.sh and install.ps1 (#4556)
Ensures both install scripts always pull a version that has the
litellm removal fix. Without the pin, stale uv/pip caches could
resolve the older 2026.3.10 which still had litellm in
data-designer-deps.txt, causing setup to fail at step 8/11
while PyPI has litellm quarantined.
2026-03-24 07:44:07 -07:00
Daniel Han
76a2f17470
fix(studio): remove litellm dep (quarantined on PyPI) (#4553)
litellm has been quarantined on PyPI due to a supply chain attack
in version 1.82.8 (malicious credential-stealing .pth file).
No versions are currently installable, which blocks
`unsloth studio setup` at step 8/11 (data-designer deps).

Remove litellm from the single-env data-designer requirements
so setup completes. litellm can be re-added once PyPI lifts the
quarantine.

Ref: https://github.com/BerriAI/litellm/issues/24512
2026-03-24 07:10:26 -07:00
Daniel Han
fac6f7887e Versioning 2026-03-24 06:50:36 -07:00
Daniel Han
95d2748278
fix: give @0xKushwaha git history credit for completion_only_loss fix (#4552)
* Revert "fix: handle prompt/completion datasets in slow-path BOS detection (#4548)"

This reverts commit fca83182af.

* fix: support completion_only_loss=True with prompt/completion dataset columns

When completion_only_loss=True, TRL rejects formatting_func but Unsloth's
patched _prepare_dataset/_prepare_non_packed_dataloader assumed either
formatting_func or dataset_text_field was always set, causing a catch-22.

Now handles prompt/completion columns as a third case for BOS token
detection, with a safe None fallback for all other cases.

(cherry picked from commit 978f78c6f1)

* fix: handle prompt/completion datasets in slow-path BOS detection

The slow-path check_text blocks in rl_replacements.py and
tokenizer_utils.py crash when a prompt/completion dataset is used
because they unconditionally access dataset[0][dataset_text_field]
even when the dataset does not have a text field.

This fixes both files to:
- Default dataset_text_field to None instead of raising when undefined
- Detect prompt/completion columns and concatenate them for BOS check
- Guard with isinstance(str) on both prompt and completion to handle
  conversational format (list of dicts) by setting test_text to None
- Add test_text is not None guard on has_bos_token_already to prevent
  AttributeError on NoneType.startswith()

This is the slow-path complement to unslothai/unsloth-zoo#560 which
fixes the fast-path in sft_prepare_dataset.

Closes #4486

(cherry picked from commit b6ce5786d0)

* fix: preserve chat_template BOS check when test_text is None

The has_bos_token_already guard wrapped both test_text.startswith()
and bos_token in chat_template with test_text is not None, which
disabled the chat_template BOS detection for conversational datasets
where test_text is set to None.

Split the guard so test_text is not None only applies to the
startswith() call, while bos_token in chat_template is always checked.

(cherry picked from commit 40bd8b8917)

---------

Co-authored-by: Ayush Kushwaha <148432773+ayushkushwaha240@users.noreply.github.com>
2026-03-24 06:38:57 -07:00
Daniel Han
fca83182af
fix: handle prompt/completion datasets in slow-path BOS detection (#4548)
* fix: handle prompt/completion datasets in slow-path BOS detection

The slow-path check_text blocks in rl_replacements.py and
tokenizer_utils.py crash when a prompt/completion dataset is used
because they unconditionally access dataset[0][dataset_text_field]
even when the dataset does not have a text field.

This fixes both files to:
- Default dataset_text_field to None instead of raising when undefined
- Detect prompt/completion columns and concatenate them for BOS check
- Guard with isinstance(str) on both prompt and completion to handle
  conversational format (list of dicts) by setting test_text to None
- Add test_text is not None guard on has_bos_token_already to prevent
  AttributeError on NoneType.startswith()

This is the slow-path complement to unslothai/unsloth-zoo#560 which
fixes the fast-path in sft_prepare_dataset.

Closes #4486

* fix: preserve chat_template BOS check when test_text is None

The has_bos_token_already guard wrapped both test_text.startswith()
and bos_token in chat_template with test_text is not None, which
disabled the chat_template BOS detection for conversational datasets
where test_text is set to None.

Split the guard so test_text is not None only applies to the
startswith() call, while bos_token in chat_template is always checked.
2026-03-24 05:27:59 -07:00
Michael Han
a41dbb6ab2
Add r/unsloth Reddit.md 2026-03-24 04:13:38 -07:00
Michael Han
381f509695
Adding Qwen3.5 RL.md 2026-03-24 04:06:23 -07:00
Wasim Yousef Said
c8057d911b
fix: system prompt ignored in unsloth inference (#4528)
* fix: system prompt was dropped in unsloth text and vision inference

* refactor: simplify system prompt message construction

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix: use multimodal typed content parts for vision system message and add fallback

The system message content must use typed content parts
([{"type": "text", "text": ...}]) instead of a plain string to match
the multimodal processor contract (consistent with the audio path).
Plain strings cause some processors (e.g. LLaVA) to silently drop the
system prompt.

Also wraps processor.apply_chat_template in try/except so models that
reject the system role gracefully fall back to no system message with
a warning log.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix: capture and log original exception in vision system prompt fallback

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-24 04:01:33 -07:00
Wasim Yousef Said
3dc212e218
fix: always show chat tool icons (#4525)
* fix: always show chat tool icons, gray out when model doesn't support them

Tool icons (Think, Search, Code) were hidden unless a model was loaded
and supported those features. Now they're always visible so users can
see and pre-select them. If a loaded model doesn't support a feature,
the button gets grayed out and disabled instead of being removed.

* refactor: centralize Qwen thinking params in store

* fix: disable tool buttons when no model is loaded

Change disabled condition from `modelLoaded && !supportsX` to
`!modelLoaded || !supportsX` so buttons are grayed out both when
no model is loaded and when the loaded model lacks the capability.

* Fix Qwen3 param clobbering and restore SuggestionItem capability guards

- Revert setReasoningEnabled() in the store to a pure boolean setter.
  Moving the Qwen3 param logic into it caused reconnect/load/refresh
  paths (which also call setReasoningEnabled) to silently overwrite
  user-customized or server-provided temperature/topP/topK/minP.

- Restore applyQwenThinkingParams() as a standalone function called
  only from explicit user toggle click handlers in thread.tsx and
  shared-composer.tsx, matching the pre-PR behavior.

- Re-add supportsReasoning/supportsTools guards in the SuggestionItem
  click handler so that clicking a suggestion card only activates
  tool toggles the loaded model actually supports.

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-24 03:26:56 -07:00
Daniel Han
77b21333fb
fix(studio): restore scroll lock on reasoning panel collapse (#4545)
PR #4543 removed useScrollLock from ReasoningRoot, causing the thread
viewport to jump when a user collapses a reasoning panel. Restore the
hook to freeze scrollTop during the 200ms collapse animation, matching
the pattern used by tool-fallback.tsx and tool-group.tsx.
2026-03-24 02:27:06 -07:00
Wasim Yousef Said
1129ea44bc
fix(studio): show Windows-specific reset-password command on login error (#4529) 2026-03-23 23:04:00 -07:00
Daniel Han
5916bcb2e3
Fix Studio port conflict detection for loopback addresses (#4532)
* Fix port conflict detection when loopback address is held by another process

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Use getaddrinfo for IPv6 host support, restore emojis in terminal output

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Guard against conn.pid being None in _get_pid_on_port

psutil.net_connections() can return entries with pid=None when the
current user lacks privileges to see the owning process (common on
macOS without root, Windows without admin, and some Linux configs).

psutil.Process(None) does not raise -- it silently returns the
current process, which would make the warning incorrectly blame
Unsloth Studio itself for blocking the port.

Skip entries with pid=None so the caller falls back to the generic
"port is already in use" message instead.

* Update studio/backend/run.py

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-23 22:34:47 -07:00
Lee Jackson
45e4a0473a
studio: stop scroll hijack during generation and fix thinking panel layout shift (#4543)
* fix(chat): stabilize thinking panel and thread scroll during generation

* fix: match ChatGPT scroll and thinking panel behavior

- Remove autoScroll={false} from thread viewport to restore default
  follow-scroll during streaming (pauses when user scrolls up, resumes
  at bottom)
- Rewrite reasoning panel state: auto-opens on stream start, user can
  close during streaming, auto-collapses when reasoning ends, user can
  re-expand after collapse

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-23 22:33:46 -07:00
Lee Jackson
01d7dce3f4
studio: persist system prompt and preset settings across navigation (#4538)
* fix(studio): harden system prompt persistence and storage fallback

* Exclude checkpoint from localStorage persistence for PR #4538

checkpoint is backend-owned state -- refresh() already syncs it from
getInferenceStatus() on every page load. Persisting it to localStorage
causes a stale model ID to survive across backend restarts, which
prevents auto-load from triggering when no model is actually loaded.

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-23 22:21:04 -07:00
金黄色葡萄球君君
2b330e2f24
fix: store embedding_learning_rate on self in UnslothTrainingArguments (#4531)
Fixes #4492

The embedding_learning_rate parameter was assigned to a local variable
instead of self.embedding_learning_rate, causing UnslothTrainer.create_optimizer()
to always get None via getattr and silently fall back to a single param group.

Bug: embedding_learning_rate = embedding_learning_rate (no-op)
Fix: self.embedding_learning_rate = embedding_learning_rate
2026-03-23 21:08:29 -07:00
pre-commit-ci[bot]
a5be6904a6
[pre-commit.ci] pre-commit autoupdate (#4542)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.15.6 → v0.15.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.6...v0.15.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-03-23 14:55:27 -07:00
Datta Nimmaturi
cd65584f19
Update issue template 2026-03-23 10:10:15 +05:30
Daniel Han
1ecb55faa2 Update _utils.py b8475 2026-03-22 08:23:40 -07:00
Daniel Han
797ddd201e
Fix Studio silently exiting on Windows without error output (#4527)
* Fix Studio silently exiting on Windows without error output

On Windows, `unsloth studio` launches a child process via
subprocess.Popen to run the server in the studio venv. If the child
crashes (e.g. due to a missing package), the parent just calls
typer.Exit(rc) with no message -- the user sees "Launching Unsloth
Studio... Please wait..." and then the prompt returns with zero
feedback.

Root cause: `data_designer_unstructured_seed` is imported at the top
level in seed.py. If this package is not installed in the studio venv,
the entire import chain (seed.py -> routes/__init__.py -> main.py ->
run_server()) crashes with ModuleNotFoundError. Since run.py has no
try/except around run_server() and studio.py does not report nonzero
exit codes, the failure is completely silent.

Changes:
- run.py: wrap run_server() in try/except, print clear error with
  traceback to stderr. Also reconfigure stderr encoding on Windows so
  tracebacks with non-ASCII paths do not cause secondary failures.
- studio.py: print an error message when the child process exits with
  a nonzero code on Windows, so the user knows something went wrong.
- seed.py: make data_designer_unstructured_seed import optional with
  a try/except fallback. The server starts normally and only returns
  HTTP 500 if the unstructured seed endpoints are actually called.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Skip Anaconda/Miniconda Python when creating Studio venv on Windows

Conda-bundled CPython ships modified DLL search paths that prevent
torch from loading c10.dll on Windows. The Studio server fails
silently at startup because the venv was created with conda's Python.

Standalone CPython (python.org, winget, uv) does not have this issue.

Both install.ps1 and setup.ps1 now skip any Python binary whose path
contains conda, miniconda, anaconda, miniforge, or mambaforge when
selecting the interpreter for the studio venv. If only conda Python
is available, the scripts print an error with instructions to install
standalone CPython.

* Fix multi-file preview crash and improve setup.ps1 Python discovery

Addresses review findings [10/10] and [8/10]:

1. seed.py: _read_preview_rows_from_multi_files() had a hard import
   of build_multi_file_preview_rows inside the function body, bypassing
   the optional-plugin guard. Moved it into the top-level try/except
   block and added a None guard matching the other functions.

2. setup.ps1: Python discovery now probes py.exe (Python Launcher)
   first, uses Get-Command -All to look past conda entries that shadow
   standalone CPython further down PATH, skips WindowsApps stubs, and
   resolves the actual executable path so venv creation does not
   re-resolve back to a conda interpreter.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Check sys.base_prefix to catch venvs created from conda Python

A venv created from conda Python (e.g. C:\Users\danie\.venv) has a
path that does not contain "conda", but sys.base_prefix still points
to the conda install (e.g. C:\Users\danie\miniconda3). The previous
path-only check missed this case entirely.

Both install.ps1 and setup.ps1 now use a Test-IsConda helper that
checks both the executable path AND sys.base_prefix against the
conda/miniconda/anaconda/miniforge/mambaforge pattern. This catches:
- Direct conda Python executables
- Venvs created from conda Python (base_prefix reveals the origin)

* Fix install.ps1 passing version string to uv venv instead of resolved path

Find-CompatiblePython returned a bare version string (e.g. "3.13")
which was passed to `uv venv --python 3.13`. uv performs its own
interpreter discovery and can resolve that version string back to a
conda Python, defeating the entire conda-skip logic.

Now Find-CompatiblePython returns a hashtable with both .Version (for
display) and .Path (the resolved absolute executable path). The venv
is created with `uv venv --python <absolute-path>`, ensuring uv uses
the exact interpreter we validated.

* Quote resolved Python path in uv venv call for paths with spaces

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-03-22 08:23:03 -07:00
Daniel Han
866cb33ce0 Update _utils.py 2026-03-22 06:14:35 -07:00
NuoFang
4cedeba8c2
fix(studio): prevent ModuleNotFoundError in dataset.map() on Windows (#4473)
* fix(studio): prevent ModuleNotFoundError in dataset.map() on Windows

On Windows, dataset.map() uses "spawn", which requires workers to
import compiled modules from disk. Previously, clear_unsloth_compiled_cache()
deleted the entire directory, causing workers to crash when looking for
UnslothSFTTrainer.py.

Changes:
1. Added `preserve_patterns` to cache cleanup to keep `Unsloth*Trainer.py`
   on Windows while clearing model-specific files.
2. Added the cache directory to PYTHONPATH for spawn workers.
Linux/macOS behavior is unchanged.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix spawn-platform coverage, CWD path mismatch, and race condition for PR #4473

- Extend platform guard from win32-only to include macOS (also uses spawn
  since Python 3.8, same ModuleNotFoundError would occur)
- Replace fragile CWD-based PYTHONPATH registration with centralized
  register_compiled_cache_on_path() that uses the same __file__-relative
  _CACHE_DIRS already used by cache_cleanup -- fixes path mismatch when
  studio is launched from a directory other than the repo root
- Move PYTHONPATH registration to the top of _train_worker(), before any
  dataset.map() call (previously it ran late in config assembly, after
  dataset formatting which also calls dataset.map())
- Update inference.py model-unload to preserve trainer files on spawn
  platforms, preventing a race where unloading a model via inference tab
  would delete UnslothSFTTrainer.py while training workers are importing it

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix cache-dir precedence reversal in register_compiled_cache_on_path()

Iterating _CACHE_DIRS in forward order while calling insert(0) each time
reverses the declared priority: later entries shadow earlier ones. When
multiple compiled-cache directories exist, spawned workers could import a
stale trainer from the wrong cache.

Fix: iterate in reverse so that the highest-priority entry (first in
_CACHE_DIRS) is inserted last and ends up at position 0 in sys.path and
PYTHONPATH.

* fix: harden worker-count helpers against cpu_count=None and desired<=0

- safe_num_proc: guard os.cpu_count() with `or 1`, clamp multi-GPU
  path with max(1, min(4, desired)), clamp return with max(1, desired)
- safe_thread_num_proc: same os.cpu_count() guard and return clamp
- Add regression tests (31 L1 unit + 10 sandbox edge-case tests)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* remove regression tests from PR

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-22 06:11:24 -07:00
Daniel Han
62e3de181f
Update weather dashboard suggestion to request HTML code output (#4523)
The previous prompt "Show me a live weather dashboard, no API key needed"
was too vague. The new wording explicitly asks for HTML code, which
produces more useful and consistent responses.
2026-03-22 06:09:48 -07:00
Leo Borcherding
71c77d4e96
fix(install.ps1): fix non-NVIDIA package resolution — split torch+unsloth install (#4515)
* fix(install.ps1): split torch+unsloth install to fix non-NVIDIA package resolution

--torch-backend=auto on a non-NVIDIA Windows machine causes uv to resolve
unsloth==2024.8 (pre-CLI, no unsloth.exe). Fix: detect GPU robustly (PATH +
hardcoded fallback paths, mirrors setup.ps1), install torch first with an
explicit --index-url (CUDA variant for NVIDIA, CPU for everyone else), then
install unsloth separately without --torch-backend so the solver always picks
a modern release that ships the Studio CLI.

Closes the remaining gap flagged in #4478.

* fix(install.ps1): align warning with setup.ps1, add --upgrade, handle CUDA 11.x

- Match the no-GPU warning message to studio/setup.ps1 wording
  (chat-only GGUF mode, driver download link)
- Add CUDA 11.x floor check in Get-TorchIndexUrl so old drivers
  fall back to CPU wheels instead of silently getting cu124
- Log a warning when nvidia-smi output cannot be parsed
- Add --upgrade to both uv pip install calls so re-runs pick up
  newer package versions

* revert --upgrade from uv pip install calls

uv pip install already resolves to the latest satisfying version;
--upgrade is unnecessary and could force unwanted re-installs.

* fix: replace frozen cu124 fallbacks with cu126, guard CUDA 11.x

cu124 wheels are frozen at torch 2.6.0 -- falling back to them pins
users to an outdated PyTorch.  Three issues fixed in both install.ps1
and setup.ps1:

1. CUDA 12.0-12.5 now maps to cu126 (was cu124).
2. CUDA 11.x and older now falls back to cpu (was cu124, which would
   silently install incompatible GPU wheels).
3. Parse-failure and no-nvidia-smi fallbacks updated to cu126/cpu.

Adds tests/test_cuda_wheel_mapping.py covering the mapping logic,
nvidia-smi parsing, PS1 file sync, PyTorch index URL validation,
and sandbox torch installs.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* remove test file from PR branch

Test file kept locally, not needed in the PR.

* fix: map CUDA 11.x to cu118 instead of cpu

PyTorch still publishes cu118 wheels (up to torch 2.7.1), so CUDA 11.x
users get GPU-accelerated torch rather than being forced to CPU-only.
Only CUDA 10.x and older fall back to cpu.

* fix: revert CUDA 12.0-12.5 to cu124, handle cpu tag in setup.ps1

CUDA 12.0-12.5 drivers only support up to their reported CUDA version,
so cu126 wheels (built with CUDA 12.6) fail to load. Revert the catch-
all for 12.0-12.5 back to cu124.

Also fix setup.ps1 caller: when Get-PytorchCudaTag returns "cpu" (e.g.
CUDA 10.x driver), the installer now correctly skips Triton and prints
"CPU-only" instead of "CUDA support (cpu)".

* fix: add --upgrade to unsloth install for stale venv repair

On reruns against an existing venv, uv pip install unsloth makes no
changes if unsloth==2024.8 is already installed (it satisfies the
constraint). Adding --upgrade only to the unsloth install ensures
stale installs get repaired without forcing a multi-GB torch
re-download.

* fix: use --upgrade-package to avoid clobbering torch CUDA wheels

`--upgrade unsloth` re-resolves torch from default PyPI, stripping the
+cuXXX suffix installed in step 1.  `--upgrade-package unsloth unsloth`
upgrades only unsloth (and pulls missing deps like transformers, trl)
while preserving the pinned torch from the CUDA-specific index.

* docs: explain why split-install and --upgrade-package are needed

Expand the inline comment block to document both design decisions:
1. Why torch is installed separately (solver fallback to 2024.8)
2. Why --upgrade-package is used instead of --upgrade (preserves CUDA wheels)

---------

Co-authored-by: LeoBorcherding <LeoBorcherding@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-03-22 05:41:58 -07:00
Daniel Han
100b8857f2
Fix Studio crash on Anaconda/conda-forge Python (#4484)
* Fix Studio crash on Anaconda Python due to platform._sys_version() parse failure

Anaconda and conda-forge modify sys.version to include distributor
metadata between pipe characters, e.g.:

    3.12.4 | packaged by Anaconda, Inc. | (main, ...) [MSC v.1929 ...]

Python's platform._sys_version() has a hardcoded regex that cannot
parse this format, raising ValueError. CPython closed this as "not
planned" (cpython#102396) since Anaconda modified the binary.

This breaks the import chain: run.py -> structlog -> rich -> attrs,
which calls platform.python_implementation() at module scope.

Fix: before any library imports, strip the pipe segments, parse the
cleaned version string via the standard parser, and cache the result
under the original sys.version key so all subsequent platform calls
hit the cache.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add defensive fallback for unpaired pipe edge cases in version patch

Address Gemini review suggestion: if the paired-pipe regex leaves
residual pipes (hypothetical single-pipe distributor metadata), fall
back to extracting the version number and the parenthesized build
info directly. Wrap the entire patch in try/except so unexpected
version string formats degrade gracefully instead of crashing the
patch itself.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Refactor into shared _platform_compat module, cover colab.py entrypoint

Address reviewer feedback:

1. Extract the Anaconda/conda-forge sys.version fix into a shared
   _platform_compat.py module that wraps platform._sys_version() with
   a retry-on-ValueError fallback. This is more robust than cache-seeding
   because it handles all future platform._sys_version() calls, not just
   the first one.

2. Import the fix from both run.py and colab.py entrypoints, so Studio
   no longer crashes on Anaconda Python regardless of the launch path.

3. The wrapper is idempotent (guarded by a flag) and handles edge cases:
   paired pipes (Anaconda, conda-forge), unpaired pipes (hypothetical),
   and standard CPython strings (no-op since ValueError is never raised).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Replace monkey-patch with cache-prime, fix colab.py duplicate sys.path, cover main.py

- Rewrite _platform_compat.py: replace function-wrapping monkey-patch with
  one-shot cache seed (_seed_sys_version_cache). Parses cleaned sys.version
  once and seeds platform._sys_version_cache so the stdlib parser never sees
  the problematic Anaconda/conda-forge pipe-delimited string. No function
  replacement, no idempotency flag, no reload edge cases.

- colab.py: remove duplicate backend_path sys.path insertion after
  _bootstrap_studio_venv(). The early insertion (before _platform_compat
  import) already covers it. This also fixes backend/ ending up behind
  venv site-packages in sys.path ordering.

- run.py: move PYTHONWARNINGS=ignore before _platform_compat import to
  preserve original intent of suppressing warnings early.

- main.py: add sys.path + _platform_compat import before route imports,
  covering the direct `uvicorn main:app` launch path.

- Add test_platform_compat.py with 7 tests covering Anaconda, conda-forge,
  and standard CPython version strings, plus the loggers import chain.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Remove test_platform_compat.py from PR

* Handle Format B conda-forge version strings with duplicate paren groups

Some conda-forge builds produce sys.version with the build info both
before and after the pipe label (e.g. "3.9.7 (default, ...) | packaged
by conda-forge | (default, ...) \n[GCC 7.5.0]"). After stripping the
pipe segment, two consecutive (...) groups remain, which still fails
platform._sys_version(). Add a second regex pass to drop the duplicate
paren group.

* Guard _sys_version call with try/except to avoid making things worse

If the cleaned version string is still unparseable by the stdlib regex
(e.g. nested parens, exotic multi-pipe formats), silently give up
instead of letting ValueError propagate at import time -- which would
be a worse crash than the original deferred one.

---------

Co-authored-by: Daniel Han <danielhanchen@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-03-22 05:36:55 -07:00
Andrew Barnes
2c5d3c48ec
fix: subprocess crash during map operation on Windows (#4507)
* fix: handle Windows subprocess crash during dataset.map()

Windows uses spawn (not fork) for multiprocessing. Spawned workers
cannot resolve Unsloth's dynamically compiled cache modules from
unsloth_compiled_cache/, causing ModuleNotFoundError and RuntimeError
during dataset.map() tokenization.

Add two platform-guarded patches for sys.platform == "win32":
1. Force HF_DATASETS_MULTITHREADING_MAX_WORKERS=1 and set spawn method
2. Monkey-patch Dataset.map() to force num_proc=None

Fixes #4490

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* address review: extend spawn fix to macOS, add multiprocess fallback

- Change platform checks from sys.platform == "win32" to
  sys.platform != "linux" so macOS (also spawn-based) is covered
- Wrap multiprocess import in try/except falling back to stdlib
  multiprocessing when the multiprocess package isn't installed
- Rename _win32_safe_map to _spawn_safe_map to reflect broader scope

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix: replace global Dataset.map monkey-patch with targeted num_proc routing

The previous approach had issues: Patch 1 set HF_DATASETS_MULTITHREADING_MAX_WORKERS
and forced set_start_method (dead code on platforms already using spawn), and Patch 2
globally monkey-patched Dataset.map() (too broad, missed Dataset.filter()).

Replace with a two-layer fix:

1. Studio layer: Add dataset_map_num_proc() that returns None on spawn platforms
   (Windows, macOS). Unlike num_proc=1 which still creates Pool(1) and spawns a
   worker, num_proc=None runs Dataset.map()/filter() truly in-process.
   Update all dataset.map() callsites to use it. ThreadPoolExecutor callers
   (format_conversion.py) keep using safe_num_proc() since threads are unaffected.

2. Root-cause layer: Propagate UNSLOTH_COMPILE_LOCATION via PYTHONPATH on spawn
   platforms so spawned workers can import compiled modules. Mirrors the .venv_t5
   pattern in worker.py. Does not import unsloth_zoo.compiler (heavy torch/triton
   imports). Completely skipped on Linux.

Also extend safe_num_proc() to return 1 on macOS (was only guarding Windows),
and narrow the transformers 5.x dataloader guard from != "linux" to explicit
("win32", "darwin").

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix: add safe_thread_num_proc() for ThreadPoolExecutor callsites

safe_num_proc() correctly caps to 1 on macOS/Windows for process-based
multiprocessing, but format_conversion.py reuses it for ThreadPoolExecutor
workers. Threads share address space and are unaffected by spawn, so
capping to 1 makes image URL downloads sequential -- a real regression.

Add safe_thread_num_proc() that skips the platform guard but keeps the
cpu_count heuristic, and switch both ThreadPoolExecutor callsites in
format_conversion.py to use it.

* fix: remove double-wrap in dataset_num_proc + fix num_proc=1 in datasets route

- trainer.py:3009: Replace safe_num_proc(max(1, os.cpu_count() // 4))
  with max(1, (os.cpu_count() or 1) // 4) to avoid double-wrapping
  inside dataset_map_num_proc which already calls safe_num_proc
- trainer.py:15-20: Clarify comment on PYTHONPATH propagation
- datasets.py:445: Change num_proc=1 to num_proc=None for 10-row
  preview slice (avoids unnecessary multiprocessing overhead)

* fix: guard os.cpu_count() against None in worker-count helpers

os.cpu_count() can return None on some platforms. Use (os.cpu_count() or 1)
to prevent TypeError in safe_num_proc() and safe_thread_num_proc().

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-22 05:21:09 -07:00
DoubleMathew
4c1a6cb962
gate on min uv version and shortcut python candidate search if known (#4489)
* gate on min uv version and shortcut python candidate search if known

* fix sort -V cross compat issue, run_quiet early exit on llamacpp, autolaunch

* update launch message

* Fix PR comments

* auto launch and find open port

* remove dev install

* Fix review findings: major-version guard, non-fatal port fallback, tty comment, restore local

* Remove autolaunch, clean up dead state and debug noise

- Remove find_open_port, TTY-gated autolaunch, and </dev/tty
  redirection from install.sh; just print launch instructions
- Remove unused BEST_MAJOR variable from studio/setup.sh
- Remove stray "finished finding best python" debug echo
- Fix stale comment "below 3.12" to "below 3.11"

* Reject prerelease uv at exact minimum version boundary

* Remove 2>/dev/null from version_ge numeric comparisons

Let non-numeric version parts surface errors on stderr
instead of being silently swallowed.

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-22 05:20:25 -07:00
Daniel Han
64f9f389a0 Created using Colab 2026-03-22 04:57:26 -07:00
Velsa
981f477e31
fix: reconfigure stdout to UTF-8 on Windows to prevent UnicodeEncodeError on startup (#4493)
* fix: reconfigure stdout UTF-8 on Windows to prevent UnicodeEncodeError from emoji

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix: default frontend_path when None to fix blank page when venv is pre-activated

* Restore Windows UTF-8 stdout fix dropped in earlier commit

The cp1252 console encoding on Windows cannot render emoji characters
used in startup messages (e.g. print(" Frontend loaded ...")).
This causes UnicodeEncodeError and crashes the server before it starts.

Place sys.stdout.reconfigure(encoding="utf-8", errors="replace") at the
top of run_server(), unconditionally before any print() or structlog
call, so all emoji output is covered -- including the frontend status
messages and silent=True paths that the original placement missed.

Guarded by sys.platform == "win32" and hasattr check, so it is a no-op
on Linux/macOS and safe in non-standard stdout environments (Jupyter,
piped IO).

* fix: preserve run_server(None) as headless, fix CLI frontend kwarg

Remove the frontend_path=None fallback in run_server() that changed
None from "headless/API-only" to "mount bundled frontend", breaking
backwards compatibility for embedders.

The blank-page bug was actually caused by the CLI wrappers always
passing frontend_path=frontend (even when frontend=None), which
overrode run_server()'s default. Fix studio.py and ui.py to only
pass frontend_path when the user explicitly sets --frontend.

* fix: use timeout loop for shutdown event in ui command

Match studio_default()'s shutdown loop that uses a 1-second timeout
on Event.wait(). Without a timeout, the bare wait() blocks at the C
level on Linux, preventing Python from delivering SIGINT (Ctrl+C).

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-22 04:49:59 -07:00
Leo Borcherding
96edad9c95
PR: Fix/cuda minimum check and abort (#4517)
* fix: add CUDA minimum version check and abort for llama.cpp (>= 12.4)

- setup.ps1/setup.sh: abort with clear error if CUDA toolkit < 12.4
  (llama.cpp requirement); link to cuda-toolkit-archive for upgrade
- setup.ps1: promote CUDA VS integration copy failure from WARN to
  ERROR + exit 1; remove manual-copy hack instructions per Roland —
  correct fix is re-installing CUDA/MSBuild, not a manual workaround

Fixes: https://github.com/unslothai/unsloth/issues/4437
Reported by: Sebastien

* fix: wipe stale studio venv when torch CUDA tag changes

When the NVIDIA driver is updated, the required PyTorch CUDA tag changes
(e.g. cu124 -> cu130) but setup.ps1 was silently reusing the existing
.venv, leaving the old torch wheel in place and breaking the UI for
everyone on the next setup run.

Before creating/reusing the venv, inspect the installed torch version
string. If its CUDA tag does not match what the current driver requires,
wipe the venv so we always get a clean, correct install.

* Fix CUDA version check: portability, non-fatal fallback, stale venv detection

- setup.sh: Replace grep -oP with POSIX sed for macOS compatibility
- setup.sh: Replace exit 1 with NVCC_PATH="" to fall back to CPU-only build
- setup.sh: Move version check before -DGGML_CUDA=ON append
- setup.sh: Add else branch warning when nvcc version is unparseable
- setup.ps1: Replace exit 1 with $NvccPath=$null for non-fatal CUDA fallback
- setup.ps1: Add driver vs toolkit guidance in version warning
- setup.ps1: Guard CUDA env/VS integration setup with if ($NvccPath)
- setup.ps1: VS integration catch: downgrade to WARN, restore source/dest paths
- setup.ps1: Stale venv: detect CPU torch and untagged wheels, not just +cuNNN
- setup.ps1: Stale venv: rebuild on failed torch import
- setup.ps1: Stale venv: wrap Remove-Item in try/catch for locked files

* Remove incorrect CUDA >= 12.4 check, keep only stale venv detection

llama.cpp has no hard minimum CUDA version -- it builds with CUDA as old
as 11.2 and degrades features gracefully via #if CUDART_VERSION guards.
The 12.4 figure was the default Docker/CI baseline, not a build requirement.

Reverted:
- CUDA version check in setup.sh (entirely removed)
- CUDA version check in setup.ps1 (entirely removed)
- VS integration catch block cosmetic changes (restored to main)
- if ($NvccPath) guard around CUDA env setup (not needed without version check)

Kept:
- Stale venv detection in setup.ps1: detects torch CUDA tag mismatch
  (cu124 vs cu130, cpu vs cuXXX, broken torch import) and rebuilds venv

* Fix stale venv detection: incomplete venvs, timeout, fatal delete failure

- Add 30s timeout for torch import probe via ProcessStartInfo/WaitForExit
- Use Test-Path -PathType Container to reject files masquerading as venv dir
- Trigger rebuild when python.exe is missing (incomplete venv)
- Make Remove-Item failure fatal ([ERROR] + exit 1) instead of warn-and-continue
- Move $expectedTorchTag computation inside -not $shouldRebuild guard

---------

Co-authored-by: LeoBorcherding <LeoBorcherding@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-22 04:46:36 -07:00
Daniel Han
bcf28466c2
fix: exclude .ipynb from ruff pre-commit hook (#4521)
The ruff pre-commit hook runs on all file types by default, including
.ipynb notebooks. Colab notebooks are authored in Colab's editor and
can contain IPython magics (%cd, !git) that ruff cannot parse. This
causes pre-commit.ci to fail on unrelated PRs when a notebook on main
has syntax ruff does not understand.

Add `exclude: '\.ipynb$'` to the ruff hook so notebooks are skipped.
2026-03-22 03:25:58 -07:00
Daniel Han
17dc83dc34 Created using Colab 2026-03-22 01:56:35 -07:00