Commit graph

1,038 commits

Author SHA1 Message Date
imagineer99
c8d8ff79b1 fix(studio):fix selector ring clipping 2026-03-28 17:30:41 +00:00
imagineer99
b747b7f2a9 feat(studio): add HF/local model selection UI for GGUF export 2026-03-28 17:30:41 +00:00
Daniel Han
82d14b44d3
fix: preserve Windows drive-letter paths on native Windows (#4665)
normalize_path() unconditionally converted Windows paths like
C:\Users\... to WSL format /mnt/c/Users/..., which breaks path
resolution on native Windows. This caused LM Studio GGUF models
to fail detection (detect_gguf_model returned None for the invalid
path), falling through to the Unsloth import path which requires
a GPU.

Now only performs the /mnt/ mapping when actually running under WSL.
On native Windows, drive letters are preserved and backslashes are
normalized to forward slashes.
2026-03-27 08:19:41 -07:00
Roland Tannous
562e54fc6e
Fix HF cache default and show LM Studio models in chat/inference (#4653)
* fix: default HF cache to standard platform path instead of legacy Unsloth cache

* feat: show LM Studio and local models in chat Fine-tuned tab

* feat: show LM Studio models in Hub models tab

* fix: fetch local models after auth refresh completes

* Revert "fix: fetch local models after auth refresh completes"

This reverts commit cfd61f0ac7.

* fix: increase llama-server health check timeout to 600s for large models

* feat: expandable GGUF variant picker for LM Studio local models

* fix: show GGUF variant label for locally loaded LM Studio models

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

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

* fix: show publisher name in LM Studio model labels

* fix: set model_id for loose GGUF files in LM Studio publisher dirs

* fix: show publisher prefix in Fine-tuned tab LM Studio models

* fix: only use model_id for lmstudio source models

* fix: only show LM Studio models in Hub tab on Mac/chat-only mode

* fix: respect XDG_CACHE_HOME, handle Windows paths in isLocalPath, refresh LM Studio on remount

- _setup_cache_env now reads XDG_CACHE_HOME (falls back to ~/.cache)
  instead of hard-coding ~/.cache/huggingface. This follows the standard
  HF cache resolution chain and respects distro/container overrides.

- isLocalPath in GgufVariantExpander uses a regex that covers Windows
  drive letters (C:\, D:/), UNC paths (\\server\share), relative paths
  (./, ../), and tilde (~/) -- not just startsWith("/").

- HubModelPicker.useEffect now calls listLocalModels() before the
  alreadyCached early-return gate so LM Studio models are always
  refreshed on remount. Also seeds useState from _lmStudioCache for
  instant display on re-open.

* fix: add comment explaining isLocalPath regex for Windows/cross-platform paths

* fix: prioritize unsloth publisher in LM Studio model list

* fix: scope unsloth-first sort to LM Studio models on all platforms

* fix: add missing _lmStudioCache module-level declaration

* fix: prioritize unsloth publisher before timestamp sort in LM Studio group

---------

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-27 06:59:27 -07:00
Wasim Yousef Said
73969a1e4f
fix: disable OCR in pymupdf4llm PDF extraction (#4659) 2026-03-27 06:53:33 -07:00
Daniel Han
c4e34c88c8
Fall back to parsing model name when HF API has no param count (#4656)
Some models like unsloth/Qwen3-0.6B have no safetensors metadata
on Hugging Face, so the training model selector showed no parameter
size badge. The chat model picker already had extractParamLabel()
as a fallback that parses sizes like "0.6B" from the model name.

Add the same fallback to the training model selector and the
onboarding model selection step.

Co-authored-by: Daniel Han <danielhanchen@users.noreply.github.com>
2026-03-27 05:57:49 -07:00
Wasim Yousef Said
4ab7fb1f7b
fix: replace navbar shutdown text button with icon-only button (#4655) 2026-03-27 05:44:59 -07:00
Daniel Han
e36f72c685
Detect always-on reasoning models and show Think button as locked-on (#4654)
* Detect always-on reasoning models and show Think button as locked-on

Models with hardcoded <think>/<think> tags or reasoning_content in
their chat template (e.g. distilled reasoning models) always produce
thinking output regardless of any toggle. Previously these models
were not detected as reasoning-capable at all, so the Think button
was grayed out even though the model was actively reasoning.

Backend:
- Detect <think>/<think> and reasoning_content in GGUF chat templates
  as a fallback when enable_thinking is not present
- Add reasoning_always_on flag to LoadResponse and InferenceStatusResponse
- Pass the flag through all GGUF load and status response paths

Frontend:
- Add reasoningAlwaysOn to the chat runtime store and API types
- When reasoning_always_on is true, show the Think button as lit
  (active) but not clickable, with a tooltip explaining the model
  always uses thinking
- Force reasoningEnabled=true when the model always reasons

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

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

* Use pointer-events-none instead of disabled for always-on Think button

The HTML disabled attribute was not fully blocking clicks on the Think
button for always-on reasoning models. Switch to pointer-events-none
CSS class which prevents all mouse interaction at the CSS level.

* Use a static span instead of disabled button for always-on Think

Replace the button element with a plain span when reasoning is
always on. This makes it physically impossible to toggle since
there is no clickable element at all, avoiding any CSS or
disabled-attribute edge cases.

* Simplify always-on Think button to stay lit and remain toggleable

Keep the Think button as a normal toggleable button but ensure it
shows as lit when reasoning_always_on is true. The model always
reasons regardless of the toggle state so there is no need to
block interaction.

---------

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-27 05:42:26 -07:00
Daniel Han
eacaf6827c
fix: no-torch install deps without pulling torch transitively (#4650)
Use --no-deps for ALL packages (unsloth, unsloth-zoo, and runtime deps)
since the current PyPI metadata for unsloth still declares torch as a
hard dependency. Runtime deps (typer, pydantic, safetensors,
transformers, etc.) are installed from no-torch-runtime.txt with
--no-deps to prevent transitive torch resolution from accelerate, peft,
trl, and sentence-transformers.

no-torch-runtime.txt now includes unsloth's own direct deps (typer,
pydantic, pyyaml, nest-asyncio) since --no-deps skips those too.

install.sh installs no-torch-runtime.txt directly (via helper function
_find_no_torch_runtime). install.ps1 does the same via
Find-NoTorchRuntimeFile. SKIP_STUDIO_BASE stays at 1 to avoid setup.sh
fast-path issues.

install_python_stack.py NO_TORCH branch does the same for unsloth
studio update, using package_name instead of hardcoded "unsloth".
2026-03-27 05:19:26 -07:00
Daniel Han
a7c43bc46d
Fix inference failing for transformers 5.x models (trust_remote_code) (#4652)
* Fix inference failing for transformers 5.x models (trust_remote_code)

The training worker in core/training/worker.py auto-enables
trust_remote_code for unsloth/* models that need transformers 5.x
(e.g. NVIDIA-Nemotron-3-Nano-4B). The inference worker did not have
the same logic, so loading these models for chat would fail with
"No config file found" while training worked fine.

Add the same auto-detection to the inference worker so
trust_remote_code is set automatically when needed.

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

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

---------

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-27 04:51:30 -07:00
Wasim Yousef Said
887b8cb1c2
fix: add auth + UX improvements to shutdown button (#4642)
* Studio shutdown button

* fix: add auth to shutdown endpoint and improve UX

- Add JWT auth (Depends(get_current_subject)) to POST /api/shutdown
- Use authFetch instead of bare fetch in shutdown dialog
- Only show beforeunload prompt when training is running
- Remove Ctrl+W/Cmd+W interception (browsers don't allow it)
- Store shutdown task on app.state to prevent GC

---------

Co-authored-by: Datta Nimmaturi <venkatadattasainimmaturi@gmail.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-27 04:36:08 -07:00
Daniel Han
1fb9fe3304
Fix orphan server cleanup killing user's own llama-server (#4622)
* fix: only kill studio-managed llama-server processes, not user's own servers

_kill_orphaned_servers() checked for "unsloth" anywhere in the process
cmdline, which matched the user's own llama-server when serving models
from unsloth/ HF repos (the model path in -m contains "unsloth"). This
caused the user's server to get SIGKILLed on Studio startup, destroying
their prompt cache and forcing full model re-loads.

Narrow the check to only match processes whose binary path lives under
~/.unsloth/llama.cpp/ (the Studio install directory).

* Address review: cover env var paths, move Path.home() inside try block

- Also check LLAMA_SERVER_PATH and UNSLOTH_LLAMA_CPP_PATH so orphans
  from custom install locations are still cleaned up.
- Move studio_dirs construction inside the try/except so a Path.home()
  failure (containers without HOME) does not crash the constructor.

* Address reviewer feedback: proper path ancestry, /proc/pid/exe, legacy paths

Changes based on 10-reviewer consensus:

- Use Path.is_relative_to() instead of substring matching to prevent
  false positives on sibling paths like ~/.unsloth/llama.cpp-backup/.
- Use /proc/<pid>/exe (symlink to real binary) instead of parsing the
  first cmdline token, which breaks on paths with spaces. Falls back
  to cmdline parsing on non-Linux or when /proc is unavailable.
- Add legacy in-tree install paths (project_root/llama.cpp/ and
  project_root/bin/) so orphans from older setup.sh are still cleaned.
- Treat LLAMA_SERVER_PATH as an exact binary match rather than widening
  it to its parent directory, which could match unrelated servers in
  shared locations like /usr/local/bin/.
- Keep everything inside the try/except so Path.home() failures in
  containers do not crash the constructor.

* [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

* Address review: add Linux platform guard and log cleanup errors

- Guard pgrep fallback with sys.platform check so it does not crash
  on Windows/macOS when psutil is unavailable.
- Replace silent except-pass with logger.warning for observability.

* [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>
2026-03-27 04:33:04 -07:00
Daniel Han
b1c3a1e857
fix: replace [huggingfacenotorch] with no-torch-runtime.txt requirements (#4649)
The [huggingfacenotorch] extras only exist in pyproject.toml but are
NOT published on PyPI, so uv pip install "unsloth[huggingfacenotorch]"
fails on fresh installs from the registry.

Fix: add studio/backend/requirements/no-torch-runtime.txt with the
runtime deps (safetensors, transformers, datasets, accelerate, etc.)
that mirror [huggingfacenotorch] from pyproject.toml. In no-torch mode:
1. install.sh/ps1 install unsloth + unsloth-zoo with --no-deps
2. SKIP_STUDIO_BASE=0 so install_python_stack.py's NO_TORCH branch runs
3. install_python_stack.py installs no-torch-runtime.txt
2026-03-27 03:58:51 -07:00
Daniel Han
9d68621614
Streaming tool detection: guard late tool_calls, filter incomplete fragments (#4648)
* Guard against late tool_calls after visible content, filter incomplete fragments

1. If visible content was already emitted (_last_emitted is non-empty)
   when delta.tool_calls arrives, ignore the tool_calls instead of
   reclassifying the turn as a tool call. llama-server never
   interleaves content and tool_calls (they are mutually exclusive),
   but this guard is defensive for other OpenAI-compatible backends.

2. Filter out incomplete structured tool_calls fragments before
   execution. Entries with empty function.name (from truncation by
   max_tokens, disconnect, or interruption) are skipped instead of
   being passed to execute_tool().

* [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>
2026-03-27 03:40:14 -07:00
Wasim Yousef Said
5c7c3883cb
feat: update app icons to rounded logo (#4640)
Replace favicon.png, unsloth-gem.png, and unsloth.ico with rounded.png.
Update install.sh to source rounded.png for Linux/macOS shortcuts.
2026-03-27 03:18:20 -07:00
Daniel Han
79d9bf0c9a
Fix GGUF GPU fit check to account for KV cache VRAM (#4623)
* fix: account for KV cache in GGUF GPU fit check and auto-cap context length

The GPU fit check only compared GGUF file size against free VRAM,
ignoring KV cache memory. Models with large native context lengths
(e.g. Qwen3.5-9B at 262k) would pass the fit check since the GGUF
is only 5.6 GB, but the KV cache at 262k context needs ~40 GB at
f16. This caused llama-server to silently fall back to CPU inference.

Changes:
- Parse block_count, head_count_kv, head_count, and embedding_length
  from GGUF metadata alongside context_length
- Add KV cache VRAM estimation based on architecture params and the
  selected cache quantization type (f16, q8_0, q4_0, etc.)
- Auto-reduce context length to the maximum that fits in available
  GPU VRAM when the native context would exceed it
- Include estimated KV cache size in the _select_gpus total so the
  fit decision reflects actual runtime memory, not just file size

For the reported scenario (Qwen3.5-9B on RTX 3090 with 22415 MiB
free), context is auto-reduced from 262144 to ~63k with f16 KV cache,
keeping the model fully on GPU. With q4_0 KV cache quantization the
context can reach ~226k.

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

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

* fix: resolve 6 bugs in KV cache VRAM estimation and add test harness

- Fix q8_0 BPE constant: 1.125 -> 34/32 (1.0625) to match llama.cpp block size
- Fix _fit_context_to_vram returning min_ctx when weights exceed budget
  (should return requested_ctx unchanged, let --fit handle it)
- Fix binary search inflating below-2048 requests (lo=min_ctx=2048 > hi)
- Fix n_ctx=0 regressing to 4096 when metadata unavailable (preserve sentinel)
- Fix multi-GPU auto-cap using single-GPU budget instead of aggregate
- Fix _context_length being overwritten with capped effective value

Add tests/test_gguf_kv_vram.py: 43 cross-platform pytest tests covering
pure logic, integration (monkeypatched load_model), and real GGUF parsing.
Runs in an isolated uv venv with only pytest -- no GPU/torch/structlog needed.

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

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

* fix: complete _effective_context_length lifecycle

- Initialize _effective_context_length in __init__ (prevents AttributeError)
- Reset _effective_context_length in unload_model (prevents stale values)
- Update context_length property to return effective (capped) value for
  the UI/API, falling back to native _context_length if not set

* fix: multi-GPU selection tries smallest subset first

The previous approach summed all GPUs' memory to cap context, then
selected GPUs afterward. This was overly optimistic for heterogeneous
setups (e.g., 48 GiB + 4 GiB): the context was inflated by the tiny
GPU's contribution, then both GPUs were dragged in.

Now we try GPU subsets from smallest (1 GPU) to largest, capping
context for each. We pick the smallest subset where the model+KV
fits. This prefers single-GPU when possible (simpler, no tensor
split overhead) and avoids pulling in GPUs that barely help.

Add tests: test_multi_gpu_prefers_fewer_gpus,
test_multi_gpu_heterogeneous.

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

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

* fix: prefer fewer GPUs over higher context in GPU selection

Multi-GPU inference is slower due to tensor-split overhead, so we
should prefer fewer GPUs with reduced context over more GPUs with
full context. Now the loop stops at the first GPU subset where the
model fits, rather than continuing to find subsets that allow higher
context. Only if the model can't fit on N GPUs do we try N+1.

This preserves the original behavior: use multi-GPU only when the
model doesn't fit on a single GPU.

* fix: make _kill_orphaned_servers cross-platform via psutil

Replace pgrep + os.kill(SIGKILL) with psutil.process_iter() and
proc.kill(), which work on Linux, macOS, and Windows. Build an
allowlist of install roots matching _find_llama_server_binary so
only studio-managed servers are killed.

* fix: skip KV estimation loop when effective context is unknown

When n_ctx=0 and GGUF metadata lacks context_length, effective_ctx
stays 0. _estimate_kv_cache_bytes(0) returns 0, so a GPU could be
selected with no KV headroom. Guard the loop with effective_ctx > 0
to fall back to file-size-only GPU selection in this case.

* chore: temporarily remove test harness (will add back separately)

* refactor: deduplicate UINT32/UINT64 handling in GGUF parser

Replace duplicated if/elif chains for vtype 4 and 10 with a single
block using setattr. No behavioral change.

* fix: honor explicit n_ctx by using multi-GPU before capping

When the user explicitly sets n_ctx, try to fit the full requested
context using _select_gpus (which adds GPUs as needed). Only cap
context if it doesn't fit on any GPU combination.

When n_ctx=0 (auto/native context), keep the existing behavior:
prefer fewer GPUs with reduced context, since multi-GPU is slower
and the user didn't ask for a specific context length.

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

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

* fix: context_length property returns native value for frontend slider

The frontend uses context_length as the slider max. Returning the
capped effective value prevented users from requesting higher context
on reload (e.g., after switching to q4_0 KV cache). Revert to
returning the native GGUF metadata value -- the backend auto-caps
at load time regardless.

* revert: context_length returns effective (capped) value

The UI slider should show what the server is actually running at,
not the theoretical maximum. Revert to returning the effective
context length.

* fix: raise minimum context floor from 2048 to 4096

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-03-27 03:14:42 -07:00
Daniel Han
e318da21a7
Fix ~1.2s TTFT penalty when tools are enabled in Studio (#4639)
* Fix ~1.2s TTFT penalty when tools are enabled in Studio

When users enable web search, Python execution, or terminal tools,
every message gets a ~1.2s delay before any text appears -- even when
the model does not call any tool. This happens because
generate_chat_completion_with_tools() does a non-streaming detection
pass (stream: False) first, waits for the complete response, then
checks for tool calls. For the ~90% of messages that don't trigger a
tool call, this blocking wait is entirely wasted.

Root cause: the detection pass payload uses stream: False, forcing
llama-server to generate the entire response before returning any
tokens.

Fix: replace the non-streaming detection pass with a streaming pass
(stream: True) and a speculative buffer state machine that detects
tool signals in the first 1-2 SSE chunks:

- BUFFERING: accumulate content tokens, check first chars for tool
  signal prefixes (<tool_call>, <function=)
- STREAMING: no tool detected, yield tokens to caller immediately
- DRAINING: tool signal found, silently accumulate rest of stream

Three detection paths:
1. Structured delta.tool_calls -- detected instantly, transition to
   DRAINING, accumulate fragments, assemble at stream end.
2. XML tool markup in content -- buffer holds up to 32 chars checking
   for <tool_call> or <function= prefix, then transitions to DRAINING.
3. No tool signal -- first non-whitespace, non-XML char triggers
   immediate transition to STREAMING (fast path, ~90% of requests).

Safety net: after any stream ends in STREAMING state, check accumulated
content for XML tool signals. Handles rare "content before tool call"
edge case.

Additional supporting changes:
- Add headers parameter to _stream_with_retry for auth forwarding
- Share _strip_tool_markup and regex patterns between the detection
  pass and the final streaming pass (removes duplication)
- Remove the iteration==0 non-streaming content shortcut (no longer
  needed since all iterations stream directly)
- Keep the final streaming pass as fallback for max_tool_iterations
  exhaustion

Benchmarked on Qwen3.5-4B Q4_K_XL:
- No tools:              TTFT ~112ms (unchanged)
- Tools enabled, no call: TTFT ~112ms (was ~1207ms)
- Decode TPS:            226 (unchanged in all cases)

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

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

* Add unit tests for streaming tool detection state machine

16 tests covering every tool call parsing path:
- Plain text (no tool call) streaming
- Structured delta.tool_calls detection and fragment assembly
- XML <tool_call>JSON</tool_call> detection via buffer
- XML <function=name> tag detection via buffer
- Whitespace before tool XML
- Safety net (content then tool XML)
- Parallel multi-tool calls
- Reasoning token bypass (thinking models)
- Reasoning then tool call
- Empty response handling
- Buffer prefix timeout (HTML not mistaken for tool)
- Non-XML first char instant streaming
- False positive rejection (<tool_tip> vs <tool_call>)
- Arguments split across multiple chunks
- auto_heal_tool_calls=False respects the flag
- Metrics accumulation across tool iterations

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

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

* Fix reasoning-only BUFFERING, pre-tool content emission, and code duplication

Addresses review feedback on the streaming tool detection:

1. Reasoning tokens are no longer yielded during BUFFERING/DRAINING
   states. The consumer in routes/inference.py tracks prev_text across
   tool iterations without resetting it, so yielding reasoning during
   a detection pass that resolves to a tool call would corrupt the
   delta computation for subsequent iterations. Reasoning is now
   silently accumulated during detection (matching the old non-streaming
   behavior) and flushed together with content when the buffer resolves
   to STREAMING.

2. Handle reasoning-only responses in the BUFFERING resolver. When a
   thinking model emits only reasoning_content with no content tokens,
   the stream ends while still in BUFFERING state. The resolver now
   detects this case and yields reasoning as plain text (without
   <think> wrapper), matching the final streaming pass behavior for
   models like Qwen3 in always-think mode.

3. Replace duplicated re.sub calls for stripping tool markup with
   the existing _strip_tool_markup(content_text, final=True) helper,
   removing ~40 lines of redundant regex code.

4. Update tests: adjust reasoning test expectations to match the new
   behavior (reasoning batched with content, not streamed individually
   during BUFFERING). Add test_reasoning_only_no_content for the
   reasoning-only edge case. 17/17 tests pass.

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

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

* Address remaining reviewer findings: late tool_call IDs and XML speculation

1. Late-arriving tool_calls.id: when a provider sends the real ID on a
   later delta chunk (after the initial one with index and function
   name), the accumulator now updates the ID instead of keeping the
   synthetic "call_{idx}" placeholder. (P2, 2/10 reviewers)

2. XML speculation respects auto_heal_tool_calls: when auto_heal is
   explicitly disabled, _TOOL_XML_SIGNALS is empty so the BUFFERING
   state never speculatively holds content for XML prefix detection.
   Content starting with literal "<tool_call>" or "<function=" text
   flows straight through without delay. (P2, 1/10 reviewers)

Skipped: finish_reason="tool_calls" without delta.tool_calls fallback
(P1, 1/10 reviewers). llama-server always sends delta.tool_calls
fragments in streaming mode. A non-streaming fallback for this edge
case would add complexity for a scenario that does not occur in
practice with the supported backend.

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

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

* Check request.is_disconnected() every 20 tokens instead of every token

The disconnect check is an async round-trip that adds overhead on every
loop iteration. Since the cancel watcher in llama_cpp.py already
handles connection teardown (closes the streaming response on cancel),
this route-layer check is a secondary safety net that does not need to
run on every single token.

Check every 20 tokens across all 4 streaming paths:
- gguf_tool_stream (tool-enabled GGUF)
- gguf_stream_chunks (standard GGUF)
- audio_input_generate (audio/whisper input)
- generic backend stream (non-GGUF fallback)

* Fix safety net, DRAINING metadata, and test import path

1. Safety net no longer retroactively executes tools after visible
   content was already emitted to the user. Once _last_emitted is
   non-empty, the stream is committed to normal content mode.
   Retroactive tool execution after visible output would violate the
   streaming contract and corrupt the route-layer cumulative delta
   tracker (prev_text). The tool XML is still stripped by
   _strip_tool_markup so the user sees clean content.

2. DRAINING false-positive path now merges accumulated metrics from
   prior tool iterations instead of dropping them. Uses the same
   merge formula as the STREAMING path.

3. Test import path fixed to use repo root instead of hardcoded
   sibling directory. Works in clean checkouts and CI.

4. Renamed test_content_then_tool_xml_safety_net to
   test_content_then_tool_xml_no_retroactive_execution to reflect
   the corrected behavior.

17/17 tests pass.

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

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

* Redact --api-key value from llama-server startup log

When UNSLOTH_DIRECT_STREAM=1, the generated bearer token was logged
verbatim in the startup command. Replace the secret with <redacted>
before logging.

* Remove test file temporarily

* Revert disconnect throttle, reset prev_text on tool_start, restore XML safety net

Addresses all P1 findings from reviewer round 3 (10 reviewers):

1. Revert disconnect check to every iteration (was every 20th).
   All 10 reviewers flagged this as a correctness regression for
   short streams and sparse tool event loops. The cancel watcher in
   llama_cpp.py is the primary mechanism but the route-layer check
   must remain per-iteration for completeness. [10/10]

2. Reset prev_text on tool_start in gguf_tool_stream. When a tool
   cycle begins after visible content was already streamed, the
   route-layer cumulative delta tracker (prev_text) must be reset
   so the post-tool synthesis response is not truncated or dropped.
   [9/10]

3. Remove the _last_emitted gate from the XML safety net. The gate
   was added to prevent retroactive tool execution after visible
   content, but with prev_text now reset on tool_start (#2), the
   root cause is fixed and the safety net can correctly handle
   content-then-tool-XML responses (matching pre-PR behavior).
   [8/10]

* Use None instead of {} for empty auth headers in TTS methods

* Include accumulated metrics in STREAMING metadata check

* [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>
2026-03-27 03:13:38 -07:00
Lee Jackson
0233fe7f9c
studio: setup log styling (#4494)
* refactor(studio): unify setup terminal output style and add verbose setup mode

* studio(windows): align setup.ps1 banner/steps with setup.sh (ANSI, verbose)

* studio(setup): revert nvcc path reordering to match main

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

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

* studio(setup): restore fail-fast llama.cpp setup flow

* studio(banner): use IPv6 loopback URL when binding :: or ::1

* Fix IPv6 URL bracketing, try_quiet stderr, _step label clamp

- Bracket IPv6 display_host in external_url to produce clickable URLs
- Redirect try_quiet failure log to stderr instead of stdout
- Clamp _step label to column width to prevent negative padding

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

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

* Add sandbox integration tests for PR #4494 UX fixes

Simulation harness (tests/simulate_pr4494.py) creates an isolated uv
venv, copies the real source files into it, and runs subprocess tests
for all three fixes with visual before/after demos and edge cases.

Standalone bash test (tests/test_try_quiet.sh) validates try_quiet
stderr redirect across 8 scenarios including broken-version contrast.

39 integration tests total (14 IPv6 + 15 try_quiet + 10 _step), all
existing 75 unit tests still pass.

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

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

* Truncate step() labels in setup.sh to match PS1 and Python

The %-15s printf format pads short labels but does not truncate long
ones.  Change to %-15.15s so labels wider than 15 chars are clipped,
matching the PowerShell .Substring(0,15) and Python label[:15] logic.

* Remove sandbox integration tests from PR

These test files are not part of the styling fix and should not
ship with this PR.

* Show error output on failure instead of suppressing it

- install_python_stack.py: restore _red for patch_package_file
  warnings (was downgraded to _dim)
- setup.ps1: capture winget output and show on failure for CUDA,
  Node, Python, and OpenSSL installs (was piped to Out-Null)
- setup.ps1: always show git pull failure warning, not just in
  verbose mode

* Show winget error output for Git and CMake installs on failure

Same capture-and-print-on-failure pattern already used for
Node, Python, CUDA, and OpenSSL winget installs.

* fix: preserve stderr for _run_quiet error messages in setup.sh

The step() helper writes to stdout, but _run_quiet's error header
was originally sent to stderr (>&2). Without the redirect, callers
that separate stdout/stderr would miss the failure headline while
still seeing the log body on stderr. Add >&2 to both step calls
inside _run_quiet to match main's behavior.

* feat: add --verbose flag to setup and update commands

Wire UNSLOTH_VERBOSE=1 through _run_setup_script() so that
'unsloth studio update --verbose' (and the deprecated 'setup')
passes the flag to setup.sh / setup.ps1 / install_python_stack.py.

---------

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-27 03:12:48 -07:00
Daniel Han
3c9f0ed149
fix: use unsloth[huggingfacenotorch] instead of --no-deps in no-torch mode (#4647)
The previous --no-deps approach skipped ALL dependencies, not just
torch. This left safetensors, transformers, datasets, accelerate, etc.
missing, causing PackageNotFoundError at runtime.

Fix: in no-torch mode, install unsloth[huggingfacenotorch] (which pulls
all runtime deps except torch), then install unsloth-zoo with --no-deps
(since zoo's published metadata still declares torch as a hard dep).
This gives a working no-torch environment with all non-torch packages.

Applied to all three installer files: install.sh, install.ps1, and
studio/install_python_stack.py.
2026-03-27 02:38:11 -07:00
Daniel Han
e9ac785346
fix: install.sh Mac Intel compatibility + Studio no-torch support (#4624)
* fix: install.sh Mac Intel compatibility + Studio no-torch support (#4621)

On Intel Macs (x86_64), PyTorch has no wheels for torch >= 2.3, so the
installer crashes. Even when torch is absent, Studio crashes on startup
because two files have bare top-level torch imports.

Studio's GGUF inference (llama.cpp) does not need PyTorch. Training and
HF-inference already isolate torch to subprocesses. Only 2 files in the
server startup chain had top-level torch imports preventing startup.

Changes:
- install.sh: detect architecture, default to Python 3.12 on Intel Mac,
  skip torch install, add Python 3.13.8 guard for arm64, pass
  UNSLOTH_NO_TORCH env var to setup.sh
- data_collators.py: remove unused `import torch` (no torch.* refs)
- chat_templates.py: lazy-import IterableDataset into function bodies
- install_python_stack.py: add IS_MACOS/NO_TORCH constants, skip
  torch-dependent packages, skip overrides.txt, skip triton on macOS

No existing working flow changes. Linux/WSL and macOS arm64 behavior is
identical.

* tests: add test suite for Mac Intel compat + no-torch mode

Shell tests (test_mac_intel_compat.sh):
- version_ge edge cases (9 tests)
- Architecture detection for Darwin x86_64/arm64, Linux x86_64/aarch64
- get_torch_index_url returns cpu on simulated Darwin
- UNSLOTH_NO_TORCH propagation to both setup.sh branches

Python unit tests (test_no_torch_filtering.py):
- _filter_requirements with NO_TORCH_SKIP_PACKAGES
- NO_TORCH env var parsing (true/1/TRUE/false/0/unset)
- IS_MACOS constant check
- Overrides skip and triton macOS skip guards

Python import tests (test_studio_import_no_torch.py):
- data_collators.py loads in isolated no-torch venv
- chat_templates.py has no top-level torch imports
- Negative control confirms import torch fails without torch

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

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

* tests: add E2E sandbox tests for Mac Intel no-torch mode

Replace static/synthetic test stubs with real sandbox tests:

- Shell: E2E uv venv creation at Python 3.12, mock uv shim to verify
  torch install is skipped when MAC_INTEL=true, dynamic env propagation
  test for UNSLOTH_NO_TORCH in both local and non-local install paths
- Python filtering: test real extras.txt and extras-no-deps.txt with
  NO_TORCH_SKIP_PACKAGES, subprocess mock of install_python_stack() for
  5 platform configs (NO_TORCH+macOS, Windows+NO_TORCH, normal Linux,
  Windows-only, macOS-only), VCS URL and env marker edge cases
- Python imports: parametrized Python 3.12+3.13 venv fixture, dataclass
  instantiation for all 3 collator classes, chat_templates.py exec with
  stubs, negative controls proving import torch and torchao install fail
  in no-torch venvs

91 total tests, all passing.

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

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

* fix: address reviewer findings for Intel Mac no-torch mode

P1 fixes:
- Auto-infer NO_TORCH in install_python_stack.py via platform.machine()
  so `unsloth studio update` preserves GGUF-only mode without needing
  the UNSLOTH_NO_TORCH env var (6/10 reviewers)
- Add openai-whisper and transformers-cfg to NO_TORCH_SKIP_PACKAGES
  since both have unconditional torch dependencies (4/10 reviewers)
- Skip unsloth-zoo on Intel Mac --local installs (depends on torch)
  in both migrated and fresh install paths (1/10)
- Recreate stale 3.13 venvs as 3.12 on Intel Mac re-runs (1/10)
- Detect Apple Silicon under Rosetta via sysctl hw.optional.arm64
  and warn user to use native arm64 terminal (1/10)

P2 fixes:
- Wire new test files into tests/run_all.sh (4/10 reviewers)
- Add update-path tests (skip_base=False) for Intel Mac
- Add _infer_no_torch tests for platform auto-detection

P3 fixes:
- Fix macOS progress bar total (triton step skipped but was counted)
- Fix temp file leak when Windows + NO_TORCH filters stack

All tests pass: 30 shell, 66 Python (96 total).

* feat: add --python override flag to install.sh

Lets users force a specific Python version, e.g. ./install.sh --python 3.12.
Addresses M2 Mac users whose systems resolve to a problematic 3.13.x patch.
When --python is set, the Intel Mac stale-venv guard and 3.13.8 auto-downgrade
are skipped so the user's choice is respected.

* tests: add comprehensive E2E sandbox tests for no-torch mode

Add test_e2e_no_torch_sandbox.py with 7 test groups (43 tests total)
covering the full no-torch import chain, edge cases, and install logic:

- Group 1: BEFORE vs AFTER import chain comparison (proves the bug
  existed and the fix works by synthetically prepending top-level torch
  imports)
- Group 2: Dataclass instantiation without torch
- Group 3: Edge cases with broken/fake torch modules on sys.path
- Group 4: Hardware detection fallback to CPU without torch
- Group 5: install.sh flag parsing, version resolution, arch detection
- Group 6: install_python_stack.py NO_TORCH filtering
- Group 7: Live server startup without torch (marked @server, skipped
  when studio venv is unavailable)

All 43 tests pass on both Python 3.12 and 3.13 isolated venvs.

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

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

* feat: add --no-torch flag to install.sh/ps1, fix lazy import bug in dataset formatting

- Fix chat_templates.py: narrow torch IterableDataset import into inner
  try/except ImportError so dataset.map() works without torch installed
- Fix format_conversion.py: same lazy import fix for convert_chatml_to_alpaca
  and convert_alpaca_to_chatml
- Add --no-torch flag to install.sh with unified SKIP_TORCH variable
  (driven by --no-torch flag OR MAC_INTEL auto-detection)
- Add --no-torch flag to install.ps1 with $SkipTorch variable
- Print CPU hint when no GPU detected and --no-torch not set
- Replace MAC_INTEL guards with SKIP_TORCH in torch install sections
- Update shell tests (40 pass) and Python tests (90 pass)

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

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

* fix: address reviewer findings for --no-torch installer paths

- Fix migrated-env branch in install.sh and install.ps1: check
  SKIP_TORCH first, then branch on STUDIO_LOCAL_INSTALL. Previously
  SKIP_TORCH+non-local fell into else and installed unsloth-zoo (which
  depends on torch), defeating --no-torch mode.
- Fix $env:UNSLOTH_NO_TORCH leak in install.ps1: always set to "true"
  or "false" instead of only setting on the true branch. Prevents stale
  no-torch state from leaking across runs in the same PS session.
- Fix install_python_stack.py update path: add NO_TORCH guard around
  base.txt install so unsloth studio update does not reinstall
  unsloth-zoo (which depends on torch) in no-torch mode.

* fix: install unsloth + unsloth-zoo with --no-deps in no-torch mode

Instead of skipping unsloth-zoo entirely (which breaks unsloth's
dependency on it), install both packages with --no-deps so they are
present but torch is not pulled in transitively. Applied consistently
across all no-torch paths: migrated-env, fresh-local, fresh-non-local
in install.sh, install.ps1, and install_python_stack.py.

* chore: temporarily remove test files (will be added in a follow-up)

* refactor: deduplicate SKIP_TORCH conditional branches in installers

Collapse if/else blocks that differ only by --no-deps into a single
branch with a conditional flag variable. Applied to migrated-env and
fresh-local paths in install.sh, install.ps1, and install_python_stack.py.

* fix: apply --no-deps to fresh non-local --no-torch install path

The non-local else branch was missing $_no_deps_arg/$noDepsArg, so
uv pip install unsloth would resolve torch from PyPI metadata (the
published unsloth package still declares torch as a hard dep). Now
--no-deps is applied consistently to all SKIP_TORCH code paths.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-03-27 02:09:21 -07:00
Daniel Han
d57a4d993d studio: fix chat CPU spike (#4632)
Inline querier identity changed every render, forcing useLiveQuery to
resubscribe continuously causing CPU spikes. Store querier in a ref and
only re-subscribe when explicit deps change.
2026-03-27 06:20:26 +00:00
Daniel Han
e62085a3d6
Fix repetition_penalty default causing 24% TPS drop in GGUF inference (#4634)
The ChatCompletionRequest Pydantic model defaulted repetition_penalty
to 1.1 when clients omitted the field. This silently forced
llama-server to perform per-token repetition scanning, dropping
streaming throughput from ~225 TPS to ~172 TPS (a 24% penalty).

The Studio frontend always sends repetition_penalty=1.0 explicitly,
so UI users were unaffected. But any API client hitting
/v1/chat/completions without setting the field (curl, third-party
integrations, Open WebUI, etc.) would get the slow path.

Benchmarked on Qwen3.5-4B Q4_K_XL, GPU 0:
- repeat_penalty=1.0: 225.2 TPS
- repeat_penalty=1.1: 172.7 TPS (24% slower)
- LM Studio (which applies rp internally): 170.8 TPS

This aligns the Pydantic default with the frontend default (1.0),
generate_chat_completion's function signature default (1.0), and
llama-server's own default (1.0).
2026-03-26 20:20:53 -07:00
Roland Tannous
e79a178200
Allow install_python_stack to run on Colab (#4633)
* Allow install_python_stack to run on Colab

The _COLAB_NO_VENV flag was setting _SKIP_PYTHON_DEPS=true, which
skipped both the PyPI version check (needs $VENV_DIR/bin/python) and
install_python_stack (uses sys.executable, works without a venv).

Introduce a separate _SKIP_VERSION_CHECK flag for the version check,
so install_python_stack still runs on Colab. The _SKIP_PYTHON_DEPS
flag remains available for the "versions match" fast path.

* Remove colab.py workarounds that broke transformers/hf-hub compatibility

PR #4601 added _pip_install_backend_deps(), _bootstrap_studio_venv(),
and _is_colab() to colab.py as workarounds for install_python_stack
being skipped on Colab. These workarounds:
- Stripped version constraints from studio.txt and installed into system Python
- Upgraded huggingface-hub to >=1.0, breaking Colab's pre-installed
  transformers which requires huggingface-hub<1.0

With install_python_stack now running on Colab (previous commit), these
workarounds are unnecessary — all deps are properly installed by setup.sh.
Restore colab.py to its original PR #4237 structure: just get_colab_url(),
show_link(), and start().

* Remove --local flag from setup.sh in Colab notebook

The --local flag is not needed for the standard Colab flow since
install_python_stack now runs on Colab and installs deps from PyPI.
2026-03-27 00:29:27 +04:00
Wasim Yousef Said
71781272dd
fix: add python-json-logger dependency to data-designer-deps (#4627) 2026-03-26 09:50:51 -07:00
Radouane Elhajali
a6fe743ebe
studio: humanize ETA display for long training runs (#4608)
* studio: humanize ETA display for long training runs

When training takes hours or days, the ETA displayed raw minutes
(e.g. '560m 50s'). This changes the format to:
- Under 1 hour: Xm Ys (unchanged)
- 1-24 hours: Xh Ym Zs
- Over 24 hours: Xd Xh Xm

* Fix formatDuration edge cases and consolidate duplicate for PR #4608

- Guard NaN/Infinity inputs with Number.isFinite() (matches formatNumber in same file)
- Add sub-minute branch so 30s displays as "30s" instead of "0m 30s"
- Accept undefined in type signature to match formatNumber pattern
- Remove duplicate formatDuration from history-card-grid.tsx and import the shared one

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-26 06:55:54 -07:00
Michael Han
937da02f6c
Update Unsloth_Studio_Colab.ipynb 2026-03-26 05:45:30 -07:00
Etherll
b3a3435ac3
fix: Windows installer fails on _yaml.pyd Access Denied (os error 5) (#4617)
* fix: avoid _yaml.pyd lock on Windows during dependency overrides

* fix: move pytorch_tokenizers and kernels to no-deps install to avoid Windows _yaml.pyd loc
2026-03-26 05:15:19 -07:00
Lee Jackson
352455610b
studio: align Dataset/Parameters/Training cards, fix expandable height, animate LoRA settings (#4614)
* fix(studio): align config cards, dynamic height for expanders, LoRA collapsible

* Fix clipping regressions in training, dataset, and params section cards

- training-section: Add hasMessage conditional so the card expands
  (min-h) when startError, vision/audio incompatibility, or config
  validation messages are present instead of always using fixed height
- dataset-section: Expand card when a local dataset is selected via
  upload (datasetSource === "upload" && selectedLocalDataset), not only
  when the Advanced panel is open
- params-section: Guard loraOpen behind isLora so switching to full
  fine-tune collapses the card instead of staying expanded from stale
  React useState

* Fix dataset card clipping for direct file uploads

Use uploadedFile instead of selectedLocalDataset in the card height
condition. selectedLocalDataset is derived from localDatasets.find()
which only resolves for Data Recipe entries, not direct file uploads
(.jsonl, .csv, .parquet, .arrow). The card already renders the Eval
Dataset panel based on uploadedFile (line 750), so the height gate
should match.

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-03-26 04:05:30 -07:00
Wasim Yousef Said
07abcb46de
fix: normalize search matching for recommended models and LoRA picker (#4615)
Recommended models matching the query were filtered from HF results but the Recommended section was hidden during search, causing them to vanish entirely.

- Show filtered recommended models during search by introducing `filteredRecommendedIds`
- Switch `recommendedSet` to use filtered IDs when searching so dedup against HF results is correct
- Hide empty "Hugging Face" label when recommended matches cover the query
- Add `normalizeForSearch` helper to strip separators (spaces, hyphens, underscores, dots) so queries like "llama 3" match "Llama-3.2-1B" and "qwen 2.5" matches "Qwen2.5-7B" in both the recommended model filter and the LoRA adapter filter
2026-03-26 03:40:11 -07:00
Roland Tannous
6b3eb504b2
Fix Colab setup skipping llama.cpp installation (#4618)
* Fix Colab setup skipping llama.cpp installation

The early exit 0 in the Colab no-venv path prevented setup.sh from
ever reaching the llama.cpp install section. Remove the early exit
and instead guard only the venv-dependent Python deps section, so
execution continues through to the llama.cpp prebuilt/source install.

* Simplify _SKIP_PYTHON_DEPS initialization

* Add --local flag to setup.sh in Colab notebook
2026-03-26 13:55:46 +04:00
Daniel Han
baabfa0a6e
Fix Colab huggingface-hub conflict, ensurepip fallback, bump to 2026.3.14 (#4603)
* Fix Colab huggingface-hub conflict, ensurepip fallback, bump to 2026.3.14

- colab.py / setup.sh: relax == pins to >= when installing studio.txt
  on Colab so huggingface-hub does not clobber Colab's bundled version
  (breaks transformers is_offline_mode import)
- install_python_stack.py: when uv is unavailable and pip is missing
  (uv-created venvs), bootstrap via ensurepip before attempting upgrade
- Bump version to 2026.3.14
- Bump installer min version pins to 2026.3.14

* [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>
2026-03-25 09:38:02 -07:00
Daniel Han
23eb7fc0a7
Fix Colab Studio launch and setup.ps1 box alignment (#4601)
* Fix Colab Studio launch and setup.ps1 box alignment

- colab.py: when the Studio venv is missing on Colab, pip-install
  backend dependencies (structlog, fastapi, etc.) from studio.txt
  into the current Python instead of failing with ModuleNotFoundError
- setup.sh: on Colab without a venv, install backend deps into system
  Python and skip venv-dependent sections (Python stack update,
  llama.cpp build) that would otherwise fail
- setup.ps1: use PadRight(47) for the done-line so "Setup Complete!"
  and "Update Complete!" both align with the box border

* [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>
2026-03-25 09:00:08 -07:00
Daniel Han
55d24d7c49
feat(studio): editable context length with Apply/Reset for GGUF settings (#4592)
* feat(studio): editable context length with Apply/Reset for GGUF model settings

Previously the Context Length field was read-only and the backend
hardcoded `-c 0`, ignoring custom values entirely. KV Cache Dtype also
triggered an immediate model reload with no way to cancel.

Backend:
- llama_cpp.py: pass the actual n_ctx value to `-c` instead of always 0
- models/inference.py: relax max_seq_length to 0..1048576 (0 = model
  default) so GGUF models with large context windows are supported

Frontend:
- chat-runtime-store: add customContextLength and loadedKvCacheDtype
  state fields for dirty tracking
- chat-settings-sheet: make Context Length an editable number input,
  stop KV Cache Dtype from auto-reloading, show Apply/Reset buttons
  when either setting has been changed
- use-chat-model-runtime: send customContextLength as max_seq_length
  in the load request, reset after successful load

* fix: preserve maxSeqLength for non-GGUF models in load request

customContextLength ?? 0 sent max_seq_length=0 for non-GGUF models,
breaking the finetuning/inference path that needs the slider value.

Now uses a three-way branch:
- customContextLength set: use it (user edited GGUF context)
- GGUF without custom: 0 (model's native context)
- Non-GGUF: maxSeqLength from the sampling slider

* fix: keep max_seq_length default at 4096 for non-GGUF callers

Only relax the bounds (ge=0 for GGUF's "model default" mode,
le=1048576 for large context windows). The default stays at 4096
so API callers that omit max_seq_length still get a sane value
for non-GGUF models.

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

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

* fix(studio): rename trust remote code toggle and hide when no model selected

- Rename "Trust remote code" to "Enable custom code"
- Shorten subtitle to "Only enable if sure"
- Hide the toggle when no model is loaded (already hidden for GGUFs)

* fix: restore ge=128 for max_seq_length validation

Keep the minimum at 128 so the API rejects nonsensical values.
GGUF path now sends the model's native context length (from
ggufContextLength) instead of 0 when the user has not customized it.
The upper bound stays at 1048576 for large-context GGUF models.

* feat(studio): replace Context Length input with slider

Use a ParamSlider (512 to model's native context, step 512) instead
of a small number input. Shows "Max" when at the model's native
context length. Consistent with the other slider controls in the
settings panel.

* feat(studio): add editable number input alongside Context Length slider

The slider and number input stay synced -- dragging the slider updates
the number, typing a number moves the slider. The input also accepts
values beyond the slider range for power users who need custom context
lengths larger than the model default.

* fix(studio): widen context length input and use 1024 step for slider

Make the number input wider (100px) so large values like 262144 are
fully visible. Change slider step from 512 to 1024 and min from 512
to 1024.

* fix(studio): context length number input increments by 1024

* fix(studio): cap context length input at model's native max

Adds max attribute and clamps typed/incremented values so the context
length cannot exceed the GGUF model's reported context window.

* fix(studio): point "What's new" link to changelog page

Changed from /blog to /docs/new/changelog.

* fix(studio): preserve custom context length after Apply, remove stale subtitle

- After a reload with a custom context length, keep the user's value
  in the UI instead of snapping back to the model's native max.
  ggufContextLength always reports the model's native metadata value
  regardless of what -c was passed, so we need to preserve
  customContextLength when it differs from native.
- Remove "Reload to apply." from KV Cache Dtype subtitle since the
  Apply/Reset buttons now handle this.

* feat(studio): auto-enable Search and Code tools when model supports them

Previously toolsEnabled and codeToolsEnabled stayed false after loading
a model even if it reported supports_tools=true. Now both toggles are
automatically enabled when the loaded model supports tool calling,
matching the existing behavior for reasoning.

* fix(studio): auto-enable tools in autoLoadSmallestModel path

The suggestion cards trigger autoLoadSmallestModel which bypasses
selectModel entirely. It was hardcoding toolsEnabled: false and
codeToolsEnabled: false even when the model supports tool calling.
Now both are set from the load response, matching the selectModel
behavior. Also sets kvCacheDtype/loadedKvCacheDtype for dirty
tracking consistency.

* fix(studio): re-read tool flags after auto-loading model

The runtime state was captured once at the start of the chat adapter's
run(), before autoLoadSmallestModel() executes. After auto-load enables
tools in the store, the request was still built with the stale snapshot
that had toolsEnabled=false. Now re-reads the store after auto-load so
the first message includes tools.

* fix(studio): re-read entire runtime state after auto-load, not just tools

The runtime snapshot (including params.checkpoint, model id, and all
tool/reasoning flags) was captured once before auto-load. After
autoLoadSmallestModel sets the checkpoint and enables tools, the
request was still built with stale params (empty checkpoint, tools
disabled). Now re-reads the full store state after auto-load so the
first message has the correct model, tools, and reasoning flags.

* feat(studio): add Hugging Face token field in Preferences

Adds a password input under Configuration > Preferences for users to
enter their HF token. The token is persisted in localStorage and
passed to all model validate/load/download calls, replacing the
previously hardcoded null. This enables downloading gated and private
models.

* fix(studio): use model native context for GGUF auto-load, show friendly errors

The auto-load paths and selectModel for GGUF were sending
max_seq_length=4096 which now actually limits the context window
(since we fixed the backend to respect n_ctx). Changed to send 0
for GGUF, which means "use model's native context size".

Also replaced generic "An internal error occurred" messages with
user-friendly descriptions for known errors like context size
exceeded and lost connections.

LoadRequest validation changed to ge=0 to allow the GGUF "model
default" signal. The frontend slider still enforces min=128 for
non-GGUF models.

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

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

* fix(studio): filter out FP8 models from model search results

Hide models matching *-FP8-* or *FP8-Dynamic* from both the
recommended list and HF search results. These models are not
yet supported in the inference UI.

---------

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-25 08:32:38 -07:00
Daniel Han
6d6008a1ef
Add PID file tracking and unsloth studio stop command (#4598)
* Add PID file tracking and `unsloth studio stop` command

On macOS the .app shortcut launches Studio via osascript into a
Terminal window, then the launcher script exits. The server process
runs outside of the launcher's context with no PID file, so there
is no straightforward way to find or stop it.

This adds:
- PID file at ~/.unsloth/studio/studio.pid, written after the
  server starts and removed on graceful shutdown or via atexit
- `unsloth studio stop` command that reads the PID file and sends
  SIGTERM (or taskkill on Windows) to shut down the server

The PID file is only removed if it still contains the current
process ID, avoiding races when a new server instance replaces
a crashed one.

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

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

* Move atexit PID cleanup into run_server()

The atexit registration was only in the __main__ block, so it
did not cover the `unsloth studio` CLI path that calls
run_server() directly via studio_default(). Moving it into
run_server() ensures the PID file is cleaned up on unexpected
exit regardless of entry point.

* [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>
2026-03-25 08:27:27 -07:00
Roland Tannous
48a7884584
feat: multi-source model discovery (HF default, legacy cache, LM Studio) (#4591)
* feat: multi-source model discovery (HF default, legacy cache, LM Studio)

* Fix multi-source model discovery bugs

- Fix lmstudio_model_dirs: add ~/.lmstudio/models as default path,
  remove dead sys.platform branch, add dedup via seen set
- Fix _setup_cache_env: preserve legacy HF cache env vars when the
  legacy hub directory exists and is non-empty
- Fix _scan_lmstudio_dir: use absolute path for id field so
  is_local_path() returns True
- Remove LM Studio dirs from allowed_roots (scanned unconditionally)
- Replace bare except passes with logger.warning in legacy cache blocks
- Fix delete_cached_model to search both default and legacy HF caches
- Make lmstudio_dirs non-optional in TS interface (matches Python schema)
- Exclude lmstudio source from trainable model filter
- Remove unused import sys

* Scan HF default cache alongside legacy and active caches

When _setup_cache_env overrides HF_HUB_CACHE to the legacy Unsloth
path, the standard HF default cache (~/.cache/huggingface/hub) was
never scanned, hiding models downloaded before Unsloth Studio was
installed.

Add hf_default_cache_dir() and _all_hf_cache_scans() helper that
deduplicates and scans all three HF cache locations (active, legacy,
default). Used in list_local_models, list_cached_gguf,
list_cached_models, and delete_cached_model.

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

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

---------

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-25 07:48:04 -07:00
Daniel Han
366fb048d4
fix(studio): add bun cache validation to Windows setup.ps1 (#4596)
Port the bun cache corruption fix from setup.sh to setup.ps1.

bun's package cache can become corrupt, storing only package metadata
without actual content. This causes bun install to exit 0 but leave
binaries like tsc missing from node_modules/.bin/.

Changes:
- After bun install, verify tsc and vite exist in node_modules\.bin\
- Check for both bare names and .cmd wrappers (Windows creates both)
- If missing, clear the bun cache and retry once
- Only fall back to npm if the retry also fails
2026-03-25 07:27:08 -07:00
Daniel Han
3efea63e2f
fix(studio): source-build fallback prefers Unsloth's tested tag over upstream latest (#4593)
* fix(studio): source-build fallback prefers Unsloth's tested tag over upstream latest

When the prebuilt install fails and falls back to source build,
--resolve-llama-tag now queries the Unsloth release repo
(unslothai/llama.cpp) first to get the latest tested/approved tag
(e.g. b8508), instead of going straight to ggml-org/llama.cpp which
may return a newer untested tag (e.g. b8514).

This ensures the source-build fallback compiles the same version that
the prebuilt path would have installed, rather than a potentially
incompatible bleeding-edge release.

Resolution order for "latest":
  1. Unsloth release repo (tested/approved)
  2. ggml-org upstream (bleeding-edge)
  3. Raw requested tag string (last resort)

Changes:
- resolve_requested_llama_tag() accepts optional published_repo param
  with docstring explaining the resolution order
- CLI --resolve-llama-tag passes --published-repo through
- setup.sh and setup.ps1 pass --published-repo to --resolve-llama-tag
  with inline comments explaining the preference

* [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>
2026-03-25 07:25:47 -07:00
Daniel Han
2e4569e06a
fix(studio): clear bun cache on failure and retry before falling back to npm (#4594)
bun's package cache can become corrupt, storing only package metadata
(package.json, README) without actual content (bin/, lib/). When this
happens, bun install exits 0 and reports packages as installed, but
binaries like tsc are missing from node_modules/.bin/.

For example, a corrupt typescript cache entry is 64KB (metadata only)
vs 23MB when correctly downloaded.

Changes:
- After bun install, verify tsc and vite exist in node_modules/.bin/
- If missing, clear the bun cache with bun pm cache rm and retry once
- Only fall back to npm if the retry also fails
- Revert bun installation to npm install -g bun (the binary is fine,
  the cache was the problem)
2026-03-25 07:05:02 -07:00
Daniel Han
457c42964f
fix(studio): validate bun install and retry from official source on failure (#4589)
bun install (specifically the npm "bun" shim v1.3.x installed via
npm install -g bun) can exit 0 while silently failing to install
packages. This causes the frontend build to fail with "tsc: not found"
or missing type declarations, since the fallback to npm only triggers
on a non-zero exit code.

Changes:

1. Initial bun install now tries the official bun.sh installer first
   (which gives a real bun runtime), falling back to npm install -g bun
   only if that fails.

2. After bun install reports success, verify that critical binaries
   (tsc, vite) actually exist in node_modules/.bin/. If they are
   missing, reinstall bun from the official source and retry once
   before falling back to npm.

3. Extract the bun install + validation logic into _try_bun_install()
   to avoid duplicating the check/cleanup across both attempts.
2026-03-25 06:38:32 -07:00
Roland Tannous
1f498a73e6 Revert "feat: multi-source model discovery (HF default, legacy cache, LM Studio)"
This reverts commit d56b115bb4.
2026-03-25 13:35:03 +00:00
Roland Tannous
d56b115bb4 feat: multi-source model discovery (HF default, legacy cache, LM Studio) 2026-03-25 13:24:46 +00:00
Daniel Han
ae2b1b97ba
fix(studio): add pip-installed nvidia CUDA libs to LD_LIBRARY_PATH for llama-server (#4590)
The prebuilt llama.cpp binary (cuda13-newer) links against
libcudart.so.13 and libcublas.so.13. When torch is installed via pip,
these libraries live in the venv's site-packages under
nvidia/cu13/lib/, not in /usr/local/cuda/.

The existing LD_LIBRARY_PATH logic only searched /usr/local/cuda*
paths (which have CUDA 12.x), so the CUDA backend failed to load
silently and llama-server fell back to CPU -- even with -ngl -1.

This adds a glob scan of the venv's nvidia package directories
(cu*, cudnn, nvjitlink) to LD_LIBRARY_PATH before launching
llama-server, matching where pip puts the CUDA runtime.

Tested on Colab with RTX PRO 6000 Blackwell (CUDA 13.0, pip torch):
before -- 3 MiB GPU, 0% util, CPU inference
after  -- 13317 MiB GPU, 77% util, full GPU inference

Co-authored-by: Daniel Han <danielhanchen@users.noreply.github.com>
2026-03-25 06:24:40 -07:00
Daniel Han
d87c21aebf
fix(studio): add -ngl -1 when model fits on GPU to enable GPU offloading (#4588)
When _select_gpus determines that a GGUF model fits on the selected
GPU(s), the code sets CUDA_VISIBLE_DEVICES but never passes -ngl
(number of GPU layers) to llama-server. Without -ngl or --fit,
llama-server defaults to 0 GPU layers and runs entirely on CPU.

This adds -ngl -1 (offload all layers) in the elif branch where
gpu_indices is set and use_fit is False, so models that fit in VRAM
actually use the GPU for inference.

Co-authored-by: Daniel Han <danielhanchen@users.noreply.github.com>
2026-03-25 06:14:33 -07:00
DoubleMathew
f4d8a246bf
Use prebuilt llama.cpp for unsloth studio setup (#4562)
* Use prebuilt llama.cpp for unsloth studio setup

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

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

* Fix 3 issues that cause unnecessary fallback to source build

1. Make filelock import optional -- environments without filelock
   (e.g. minimal installs) crashed at import time instead of
   gracefully skipping the lock.

2. Use already-verified converter script from the hydrated source
   tree instead of re-downloading from raw.githubusercontent.com
   with no checksum. Adds symlink with copy fallback for the
   legacy filename.

3. Initialize $SkipPrebuiltInstall in setup.ps1 before first use
   to prevent potential uninitialized variable errors.

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

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

* Keep network fallback in ensure_converter_scripts

Prefer the local verified copy from the hydrated source tree, but
retain the original network download as a fallback if the file is
missing. Create the legacy hyphenated filename as a symlink with a
copy fallback instead of writing a second full copy.

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

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

* Fix 4 bugs in source-build fallback and binary_env paths

- setup.ps1: Replace git pull + checkout FETCH_HEAD with fetch + checkout -B
  to avoid detached HEAD state that breaks re-runs. Use pinned tag in both
  fetch and clone paths.
- setup.sh: Move rm -rf after cmake/git prerequisite checks so a missing
  tool no longer deletes the existing install. Add --branch tag to clone.
- install_llama_prebuilt.py: Add binary_path.parent to Linux LD_LIBRARY_PATH
  in binary_env() so bundled .so files in build/bin are found even without
  RPATH, matching the existing Windows PATH logic.
- Add test for binary_env LD_LIBRARY_PATH on Linux.

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

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

* Handle unresolved "latest" tag in source-build fallback clone

When tag resolution fails and the requested tag is "latest", both
setup scripts now omit --branch from git clone so the default branch
is cloned instead of failing on a nonexistent "latest" branch/tag.
Similarly, the PS1 fetch path fetches the default ref when the tag
is "latest".

* Resolve actual latest ggml-org tag instead of using literal "latest"

When both Python tag resolution attempts fail and the requested tag
is "latest", query the GitHub API for the actual latest release tag
from ggml-org/llama.cpp (e.g. b8508) instead of passing the literal
string "latest" to git clone --branch, which would fail since no
such branch/tag exists.

setup.sh uses curl + python json parsing; setup.ps1 uses
Invoke-RestMethod. Both fall back to the raw requested tag if the
API call also fails.

* Try Unsloth release repo before ggml-org when resolving latest tag

When falling back to the GitHub API to resolve "latest", query the
Unsloth release repo (unslothai/llama.cpp) first since it has the
prebuilt binaries pinned to tested tags. Only fall back to
ggml-org/llama.cpp if the Unsloth repo query fails.

* Add comprehensive sandbox tests for PR #4562 bug fixes

35 tests covering all fixes across platforms:
- binary_env cross-platform (Linux LD_LIBRARY_PATH, Windows PATH,
  macOS DYLD_LIBRARY_PATH) with edge cases (dedup, ordering, existing paths)
- resolve_requested_llama_tag (concrete, latest, None, empty)
- setup.sh logic via subprocess: prereq check ordering (cmake/git missing
  preserves install), pinned tag in clone, fetch+checkout -B pattern,
  fetch failure warns instead of aborting
- "latest" tag resolution fallback chain (Unsloth API -> ggml-org ->
  raw) with mock curl: success, failure, malformed JSON, empty body,
  empty tag_name, env overrides
- Source code pattern verification for both .sh and .ps1 files

All 138 tests pass in isolated uv venv.

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

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

* Add binary_path.parent to macOS DYLD_LIBRARY_PATH in binary_env

macOS prebuilt .dylib files are overlaid into build/bin (same as
Linux), but binary_env only added install_dir to DYLD_LIBRARY_PATH.
Add binary_path.parent so the loader can find sibling dylibs even
without embedded loader paths.

Mirrors the existing fix for Linux LD_LIBRARY_PATH and the Windows
PATH pattern.

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

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

* Guard --branch when resolved tag is "latest"; fix broken test assertion

When all API fallbacks fail and the tag stays as literal "latest",
omit --branch from git clone (clones default branch instead of
failing). Both setup.sh and setup.ps1 now check for "latest" before
passing --branch to git clone/fetch.

Also fix test_setup_ps1_clone_uses_branch_tag which used Python
tuple syntax (assert "x", "y" in z) that always passes. Changed to
assert "x" in z and "y" in z.

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

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

* Fix macOS DYLD trailing colon, install_lock no-op, and debug log

- binary_env macOS: use dedupe_existing_dirs instead of raw string
  concatenation. Eliminates trailing colon in DYLD_LIBRARY_PATH
  (which causes dyld to search CWD for libraries) and deduplicates
  when binary_path.parent == install_dir. Now consistent with the
  Linux and Windows branches.
- install_lock: when filelock is not installed, use os.O_CREAT|O_EXCL
  as a fallback exclusive file lock with timeout, instead of yielding
  with no locking. Prevents concurrent installs from corrupting each
  other's staging directories.
- setup.ps1: remove [DEBUG] log line that printed to every user on
  every Windows setup run.

* Add stale-lock detection and atomic clone-then-swap

install_lock fallback (no filelock): write PID to lock file and
check if the holder process is still alive on contention. Dead PIDs
(ProcessLookupError) and unreadable lock files trigger immediate
cleanup. Live processes owned by other users (PermissionError) are
correctly recognized as alive -- the lock is not removed.

setup.sh/setup.ps1 source-build: clone into a temporary directory
first, then swap into place only on success. If git clone fails,
the existing install is preserved instead of being deleted by the
premature rm -rf.

* Remove redundant upstream_tag != release_tag check

load_approved_release_checksums compared checksums.upstream_tag
against the Unsloth release_tag, which are different namespaces
(upstream ggml-org tag vs Unsloth published tag). This only worked
because both happened to be "b8508" by convention. Would break if
Unsloth ever uses a different release naming scheme.

The existing check at parse_approved_release_checksums (line 950)
already validates the release_tag field correctly.

* Fix lock TOCTOU race and build-in-temp-dir swap

install_lock fallback: add os.fsync(fd) after writing PID to ensure
the PID is visible to racing processes before they check. Treat
empty lock files (PID not yet written) as "wait and retry" instead
of stale, closing the window where two processes could both see an
empty file, both unlink it, and both acquire the lock.

setup.sh/setup.ps1 source-build: clone AND build in a temp directory
(LLAMA_CPP_DIR.build.$$). Only swap into the final LLAMA_CPP_DIR
after the build succeeds. If clone or cmake or build fails, the temp
dir is cleaned up and the existing working install is preserved.
Previously, rm -rf ran after clone but before build, destroying the
existing install even if the build later failed.

---------

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 05:42:43 -07:00
Lee Jackson
cc1be75621
studio: stabilize reasoning panel scroll behavior and prevent composer overlap (#4587)
* fix(studio): reasoning panel scroll and thread footer overlap

* refactor(studio): dedupe reasoning scroll lock teardown
2026-03-25 05:32:31 -07:00
Roland Tannous
19e9c60a8e
Consolidate dual venvs and separate install from update (#4530)
* refactor: consolidate dual venvs into single ~/.unsloth/studio/unsloth_studio

* refactor: separate install.sh (first-time) from setup.sh (smart update with PyPI version check)

* fix: install.sh calls setup.sh directly, keep both setup and update CLI commands

* fix: use importlib.resources.files() directly without _path attribute

* fix: bootstrap uv before pip upgrade to handle uv venvs without pip

* fix: frontend 404 when launched via CLI, add global symlink to ~/.local/bin

* feat: add --local flag to install.sh and unsloth studio update for branch testing

* fix: resolve repo root from script location for --local installs

* feat: add --package flag to install.sh for testing with custom package names

* feat: add --package flag to unsloth studio update

* fix: always nuke venv in install.sh for clean installs

* revert: remove Windows changes, will handle in separate PR

* fix: error when --package is passed without an argument

* revert: restore Windows scripts to current main

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

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

* fix: always explicitly set STUDIO_LOCAL_INSTALL and STUDIO_PACKAGE_NAME env vars

* fix: pass explicit STUDIO_LOCAL_REPO env var for --local installs

* fix: align banner box for Setup vs Update labels

* deprecate: hide 'unsloth studio setup' command, point users to update/install.sh

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

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

* fix: check stdout not stdin for auto-launch detection (curl pipe fix)

* fix: update install URL to unsloth.ai/install.sh

* fix: update install.sh usage comments to unsloth.ai/install.sh

* fix: use --upgrade-package for base deps to preserve existing torch/CUDA installs

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

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

* fix: --local install now also installs unsloth-zoo via base.txt before editable overlay

* fix: don't skip base packages for --local installs (editable needs unsloth-zoo)

* refactor: move --local full dep install to install.sh, keep SKIP_STUDIO_BASE for all paths

* feat: add migration support for old .venv and CWD-based installs in setup.sh

* Revert "feat: add migration support for old .venv and CWD-based installs in setup.sh"

This reverts commit 301291d002.

* feat: migrate old .venv layout in install.sh instead of always nuking

* feat: validate old .venv with torch CUDA test before migration, recovery message on launch failure

* fix: try CUDA then fall back to CPU for migration validation

* fix: upgrade unsloth/unsloth-zoo with --reinstall-package on migration to preserve torch

* remove: delete unused unsloth ui command (use unsloth studio instead)

* Fix Windows venv path mismatch between install.ps1, setup.ps1, and studio.py

install.ps1 was creating the venv CWD-relative ($VenvName = "unsloth_studio"),
setup.ps1 was using an absolute path to ".unsloth\studio\.venv", and studio.py
looks for ".unsloth\studio\unsloth_studio". All three paths were different, so
the Windows installer would never produce a working Studio setup.

install.ps1:
- Use absolute $StudioHome + $VenvDir matching the Linux install.sh layout
- Add 3-way migration: old .venv at STUDIO_HOME, CWD-relative ~/unsloth_studio
  from the previous install.ps1, or fresh creation with torch validation
- For migrated envs, upgrade unsloth while preserving existing torch/CUDA wheels
- Set SKIP_STUDIO_BASE=1 before calling setup.ps1 (matches install.sh behavior)
- Fix launch instructions to use the absolute venv path

setup.ps1:
- Change $VenvDir from ".unsloth\studio\.venv" to ".unsloth\studio\unsloth_studio"
- Add SKIP_STUDIO_BASE guard: error out if venv is missing when called from
  install.ps1 (which should have already created it)
- Differentiate "Setup" vs "Update" in banners based on SKIP_STUDIO_BASE

* setup.ps1: unconditionally error if venv missing, matching setup.sh

setup.sh always errors out if the venv does not exist (line 224-228),
telling the user to run install.sh first. setup.ps1 was conditionally
creating a bare venv with python -m venv when SKIP_STUDIO_BASE was not
set, which would produce an empty venv with no torch or unsloth. Now
setup.ps1 matches setup.sh: always error, always point to install.ps1.

* Fix --torch-backend=auto CPU solver dead-end on Linux, macOS, and Windows

On CPU-only machines, `uv pip install unsloth --torch-backend=auto`
falls back to unsloth==2024.8 because the CPU solver cannot satisfy
newer unsloth's dependencies. install.ps1 already solved this with a
two-step approach; this applies the same fix to install.sh and
install_python_stack.py.

install.sh: add get_torch_index_url() that detects GPU via nvidia-smi
and maps CUDA versions to PyTorch index URLs (matching install.ps1's
Get-TorchIndexUrl). Fresh installs now install torch first via explicit
--index-url, then install unsloth with --upgrade-package to preserve
the pre-installed torch. All 5 --torch-backend=auto removed from
primary paths.

install.ps1: add fallback else-branch when TorchIndexUrl is empty,
using --torch-backend=auto as last resort (matching install.sh).

install_python_stack.py: remove unconditional --torch-backend=auto
from _build_uv_cmd. Torch is pre-installed by install.sh/setup.ps1
by the time this runs. Callers that need it can set UV_TORCH_BACKEND.

Both install.sh and install.ps1 now share the same three-branch logic:
migrated env (upgrade-package only), normal (torch-first + index-url),
and fallback (--torch-backend=auto if URL detection fails).

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

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

* Use --reinstall-package for migrated envs on both Linux and Windows

For migrated environments (moved from legacy venv location),
--reinstall-package is better than --upgrade-package because it forces
a clean reinstall even if the same version is already installed. This
ensures proper .dist-info and .pyc state in the new venv location.

--upgrade-package remains correct for the fresh install path where
torch is already installed and we just want to add unsloth without
re-resolving torch.

* Address review findings: portability, parity, and stale comments

- Replace grep -oP (GNU Perl regex) with POSIX sed in
  get_torch_index_url() so the script works on BSD grep (macOS is
  already guarded by the Darwin early-return, but Alpine/BusyBox
  would silently get the wrong CUDA tag)
- Add LC_ALL=C before nvidia-smi invocation to prevent locale-dependent
  output parsing issues
- Add warning on stderr when nvidia-smi output is unparseable, matching
  install.ps1's [WARN] message
- Add explicit unsloth-zoo positional arg to install.ps1 migrated path,
  matching install.sh (--reinstall-package alone won't install it if it
  was never present in the migrated env)
- Fix stale comment in install_python_stack.py line 392 that still
  claimed --torch-backend=auto is added by _build_uv_cmd
- Add sed to test tools directory (function now uses sed instead of grep)

* Add --index-url to migrated env path to prevent CPU torch resolution

The migrated path runs uv pip install with --reinstall-package for
unsloth/unsloth-zoo. While uv should keep existing torch as satisfied,
the resolver could still re-resolve torch as a transitive dependency.
Without --index-url pointing at the correct CUDA wheel index, the
resolver would fall back to plain PyPI and potentially pull CPU-only
torch. Adding --index-url $TORCH_INDEX_URL ensures CUDA wheels are
available if the resolver needs them.

Applied to both install.sh and install.ps1.

* Revert --index-url on migrated env path

The original install.ps1 on main already handles the migrated path
without --index-url and it works correctly. --reinstall-package only
forces reinstall of the named packages while uv keeps existing torch
as satisfied. No need for the extra flag.

* Fix unsloth studio update --local not installing local checkout

studio.py sets STUDIO_LOCAL_REPO when --local is passed, but
install_python_stack.py never read it. The update path always
installed from PyPI regardless of the --local flag.

Add a local_repo branch that first updates deps from base.txt
(with --upgrade-package to preserve torch), then overlays the
local checkout as an editable install with --no-deps.

---------

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 05:24:21 -07:00
Daniel Han
3446e0c489
Add ROCm (AMD GPU) support to studio setup (#4585)
* Add support for ROCm in studio setup

* Fix ROCm detection bugs: ROCM_PATH resolution, CUDA guard, compiler selection

- Set GPU_BACKEND="cuda" when nvcc is found (CUDA path was unreachable)
- Guard ROCm detection with `if [ -z "$GPU_BACKEND" ]` so CUDA takes
  priority on mixed-toolchain hosts
- Rename ROCM_PATH to ROCM_HIPCC for the hipcc binary; resolve the
  actual ROCm root via readlink -f and hipconfig -R into ROCM_ROOT
- Export both ROCM_PATH and HIP_PATH as the resolved root directory
- Use HIPCXX via hipconfig -l instead of legacy CMAKE_C_COMPILER=hipcc
- Switch grep -oP to grep -oE for portability across Linux distros
- Use GPU_TARGETS (upstream cmake variable) instead of AMDGPU_TARGETS
- Remove stale hardcoded fallback targets; let cmake auto-detect instead

* Fix gfx regex to match gfx90a (MI210/MI250/MI250X)

The grep and bash regex used {3,4} digits after 'gfx', which silently
excluded gfx90a (2 digits + letter 'a') -- the architecture for AMD
Instinct MI210, MI250, and MI250X data-center GPUs. Change to {2,4}
so all real gfx targets from gfx90a through gfx1200 are matched.

---------

Co-authored-by: edamamez <eda.zhou@amd.com>
2026-03-25 04:50:23 -07:00
Etherll
d69d60ff19
perf(studio): upgrade to Vite 8 + auto-install bun for faster frontend builds (#4522)
* perf(studio): upgrade to Vite 8 + auto-install bun for 3x faster frontend builds

* fix(studio): make bun-to-npm fallback actually reachable

setup.sh used run_quiet() for the bun install attempt, but run_quiet
calls exit on failure. This killed the script before the npm fallback
could run, making the "falling back to npm" branch dead code.

Replace the run_quiet call with a direct bun invocation that captures
output to a temp file (same pattern, but returns instead of exiting).

Also clean up partial node_modules left by a failed bun install before
falling back to npm, in both setup.sh and build.sh. Without this, npm
inherits a corrupted node_modules tree from the failed bun run.

* fix(studio): restore commonjsOptions for dagre CJS interop

The previous commit removed build.commonjsOptions, assuming Vite 8's
Rolldown handles CJS natively. While optimizeDeps.include covers the
dev server (pre-bundling), it does NOT apply to production builds.

The resolve.alias still points @dagrejs/dagre to its .cjs.js entry,
so without commonjsOptions the production bundle fails to resolve
the CJS default export. This causes "TypeError: e is not a function"
on /chat after build (while dev mode works fine).

Restore the original commonjsOptions block to fix production builds.

* fix(studio): use motion/react instead of legacy framer-motion import

* fix(studio): address PR review findings for Vite 8 + bun upgrade

Fixes:
  - Remove bun.lock from repo and add to .gitignore (npm is source of truth)
  - Use & bun install *> $null pattern in setup.ps1 for reliable $LASTEXITCODE
  - Add Remove-Item node_modules before npm fallback in setup.ps1
  - Print bun install failure log in setup.sh before discarding
  - Add Refresh-Environment after npm install -g bun in setup.ps1
  - Tighten Node version check to ^20.19.0 || >=22.12.0 (Vite 8 requirement)
  - Add engines field to package.json
  - Use string comparison for _install_ok in build.sh
  - Remove explicit framer-motion ^11.18.2 from package.json (motion pulls
    framer-motion ^12.38.0 as its own dependency — the old pin caused a
    version conflict)

* Fix Colab Node bypass and bun.lock stale-build trigger

Gate the Colab Node shortcut on NODE_OK=true so Colab
environments with a Node version too old for Vite 8 fall
through to the nvm install path instead of silently proceeding.

Exclude bun.lock from the stale-build probe in both setup.sh
and setup.ps1 so it does not force unnecessary frontend rebuilds
on every run.

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: Shine1i <wasimysdev@gmail.com>
2026-03-25 04:27:41 -07: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
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