diff --git a/.github/ISSUE_TEMPLATE/bug---issue.md b/.github/ISSUE_TEMPLATE/bug---issue.md
index 83e0fd73a9..ffa3d3c885 100644
--- a/.github/ISSUE_TEMPLATE/bug---issue.md
+++ b/.github/ISSUE_TEMPLATE/bug---issue.md
@@ -6,7 +6,7 @@ labels: bug
assignees: ''
---
-
+Note: Please do not remove the questions. Answer beside them.
1. Did you update? `pip install --upgrade unsloth unsloth_zoo`
2. `Colab` or `Kaggle` or local / cloud
3. Number GPUs used, use `nvidia-smi`
@@ -16,6 +16,7 @@ assignees: ''
```python
Put Minimal code to reproduce error here ###Remove Hugging Face token###
+###Please make sure to check formatting properly, edit if needed.###
```
🦥 You can also ask via our Reddit page: https://reddit.com/r/unsloth/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 1879186a73..25eeaedd3c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.15.6
+ rev: v0.15.7
hooks:
- id: ruff
args:
diff --git a/README.md b/README.md
index 7cc40c7644..8f783bf661 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ Run and train AI models with a unified local interface.
Quickstart •
Notebooks •
Documentation •
- Discord
+ Reddit
@@ -32,12 +32,12 @@ Unsloth provides several key features for both inference and training:
* We work directly with teams behind [gpt-oss](https://docs.unsloth.ai/new/gpt-oss-how-to-run-and-fine-tune#unsloth-fixes-for-gpt-oss), [Qwen3](https://www.reddit.com/r/LocalLLaMA/comments/1kaodxu/qwen3_unsloth_dynamic_ggufs_128k_context_bug_fixes/), [Llama 4](https://github.com/ggml-org/llama.cpp/pull/12889), [Mistral](models/tutorials/devstral-how-to-run-and-fine-tune.md), [Gemma 1-3](https://news.ycombinator.com/item?id=39671146), and [Phi-4](https://unsloth.ai/blog/phi4), where we’ve fixed bugs that improve model accuracy.
* Upload images, audio, PDFs, code, DOCX and more file types to chat with.
### Training
-* Train **500+ models** up to **2x faster** with up to **70% less VRAM**, with no accuracy loss.
+* Train and RL **500+ models** up to **2x faster** with up to **70% less VRAM**, with no accuracy loss.
* Custom Triton and mathematical **kernels**. See some collabs we did with [PyTorch](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide/fp8-reinforcement-learning) and [Hugging Face](https://unsloth.ai/docs/new/faster-moe).
* **Data Recipes**: [Auto-create datasets](https://unsloth.ai/docs/new/studio/data-recipe) from **PDF, CSV, DOCX** etc. Edit data in a visual-node workflow.
-* Supports full fine-tuning, pretraining, 4-bit, 16-bit and, FP8 training.
+* **[Reinforcement Learning](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide)** (RL): The most efficient [RL](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide) library, using **80% less VRAM** for GRPO, [FP8](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide/fp8-reinforcement-learning) etc.
+* Supports full fine-tuning, RL, pretraining, 4-bit, 16-bit and, FP8 training.
* **Observability**: Monitor training live, track loss and GPU usage and customize graphs.
-* **Reinforcement Learning**: The most efficient [RL](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide) library, using **80% less VRAM** for GRPO, [FP8](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide/fp8-reinforcement-learning) etc.
* [Multi-GPU](https://unsloth.ai/docs/basics/multi-gpu-training-with-unsloth) training is supported, with major improvements coming soon.
## ⚡ Quickstart
@@ -49,7 +49,7 @@ Unsloth Studio (Beta) works on **Windows, Linux, WSL** and **macOS**.
* **CPU:** Supported for Chat and Data Recipes currently
* **NVIDIA:** Training works on RTX 30/40/50, Blackwell, DGX Spark, Station and more
* **macOS:** Currently supports chat and Data Recipes. **MLX training** is coming very soon
-* **AMD:** Chat works. Train with [Unsloth Core](#unsloth-core-code-based). Studio support is coming soon.
+* **AMD:** Chat + Data works. Train with [Unsloth Core](#unsloth-core-code-based). Studio support is out soon.
* **Coming soon:** Training support for Apple MLX, AMD, and Intel.
* **Multi-GPU:** Available now, with a major upgrade on the way
@@ -172,8 +172,9 @@ Train for free with our notebooks. Read our [guide](https://unsloth.ai/docs/get-
|-----------|---------|--------|----------|
| **Qwen3.5 (4B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_5_(4B)_Vision.ipynb) | 1.5x faster | 60% less |
| **gpt-oss (20B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/gpt-oss-(20B)-Fine-tuning.ipynb) | 2x faster | 70% less |
+| **Qwen3.5 GSPO** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_5_(4B)_Vision_GRPO.ipynb) | 2x faster | 70% less |
| **gpt-oss (20B): GRPO** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/gpt-oss-(20B)-GRPO.ipynb) | 2x faster | 80% less |
-| **Qwen3: Advanced GRPO** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_(4B)-GRPO.ipynb) | 2x faster | 50% less |
+| **Qwen3: Advanced GRPO** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_(4B)-GRPO.ipynb) | 2x faster | 70% less |
| **Gemma 3 (4B) Vision** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Gemma3_(4B)-Vision.ipynb) | 1.7x faster | 60% less |
| **embeddinggemma (300M)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/EmbeddingGemma_(300M).ipynb) | 2x faster | 20% less |
| **Mistral Ministral 3 (3B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Ministral_3_VL_(3B)_Vision.ipynb) | 1.5x faster | 60% less |
@@ -196,13 +197,13 @@ Train for free with our notebooks. Read our [guide](https://unsloth.ai/docs/get-
- **FP8 & Vision RL**: You can now do FP8 & VLM GRPO on consumer GPUs. [FP8 Blog](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide/fp8-reinforcement-learning) • [Vision RL](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide/vision-reinforcement-learning-vlm-rl)
- **gpt-oss** by OpenAI: Read our [RL blog](https://unsloth.ai/docs/models/gpt-oss-how-to-run-and-fine-tune/gpt-oss-reinforcement-learning), [Flex Attention](https://unsloth.ai/docs/models/gpt-oss-how-to-run-and-fine-tune/long-context-gpt-oss-training) blog and [Guide](https://unsloth.ai/docs/models/gpt-oss-how-to-run-and-fine-tune).
-## 🔗 Links and Resources
+## 💚 Community and Links
| Type | Links |
| ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
+| **Discord** | [Join Discord server](https://discord.com/invite/unsloth) |
| **r/unsloth Reddit** | [Join Reddit community](https://reddit.com/r/unsloth) |
| 📚 **Documentation & Wiki** | [Read Our Docs](https://unsloth.ai/docs) |
| **Twitter (aka X)** | [Follow us on X](https://twitter.com/unslothai) |
-| 💾 **Installation** | [Pip & Docker Install](https://unsloth.ai/docs/get-started/install) |
| 🔮 **Our Models** | [Unsloth Catalog](https://unsloth.ai/docs/get-started/unsloth-model-catalog) |
| ✍️ **Blog** | [Read our Blogs](https://unsloth.ai/blog) |
diff --git a/studio/backend/core/inference/inference.py b/studio/backend/core/inference/inference.py
index 6cb077f4a9..1a265690ff 100644
--- a/studio/backend/core/inference/inference.py
+++ b/studio/backend/core/inference/inference.py
@@ -927,6 +927,12 @@ class InferenceBackend:
logger.warning(f"Could not apply get_chat_template: {e}")
# Step 2: Format with tokenizer.apply_chat_template()
+ if system_prompt:
+ template_messages = [
+ {"role": "system", "content": system_prompt}
+ ] + messages
+ else:
+ template_messages = messages
try:
if not (hasattr(tokenizer, "chat_template") and tokenizer.chat_template):
raise ValueError(
@@ -937,7 +943,7 @@ class InferenceBackend:
f"one via tokenizer.chat_template before inference."
)
formatted_prompt = tokenizer.apply_chat_template(
- messages, tokenize = False, add_generation_prompt = True
+ template_messages, tokenize = False, add_generation_prompt = True
)
logger.debug(f"Formatted prompt: {formatted_prompt[:200]}...")
except Exception as e:
@@ -992,19 +998,40 @@ class InferenceBackend:
# Prepare vision messages
if image:
- vision_messages = [
- {
- "role": "user",
- "content": [
- {"type": "image"},
- {"type": "text", "text": user_message},
- ],
- }
- ]
+ user_msg = {
+ "role": "user",
+ "content": [
+ {"type": "image"},
+ {"type": "text", "text": user_message},
+ ],
+ }
+ if system_prompt:
+ vision_messages = [
+ {
+ "role": "system",
+ "content": [{"type": "text", "text": system_prompt}],
+ },
+ user_msg,
+ ]
+ else:
+ vision_messages = [user_msg]
- input_text = processor.apply_chat_template(
- vision_messages, add_generation_prompt = True, tokenize = False
- )
+ try:
+ input_text = processor.apply_chat_template(
+ vision_messages, add_generation_prompt = True, tokenize = False
+ )
+ except Exception as e:
+ if system_prompt:
+ logger.warning(
+ f"Vision processor for '{self.active_model_name}' may not support "
+ f"system messages; retrying without. Original error: {e}"
+ )
+ vision_messages = [user_msg]
+ input_text = processor.apply_chat_template(
+ vision_messages, add_generation_prompt = True, tokenize = False
+ )
+ else:
+ raise
inputs = processor(
image,
input_text,
diff --git a/studio/backend/run.py b/studio/backend/run.py
index 5c24c550c7..e32b912c37 100644
--- a/studio/backend/run.py
+++ b/studio/backend/run.py
@@ -73,18 +73,79 @@ def _resolve_external_ip() -> str:
return "0.0.0.0"
+def _get_pid_on_port(port: int) -> "tuple[int, str] | None":
+ """Return (pid, process_name) of the process listening on *port*, or None.
+
+ Uses psutil when available. Falls back gracefully to None so callers
+ can still report the port conflict without process details.
+
+ Works on Windows, macOS, and Linux wherever psutil is installed.
+ """
+ try:
+ import psutil
+ except ImportError:
+ return None
+ try:
+ for conn in psutil.net_connections(kind = "tcp"):
+ if conn.status == "LISTEN" and conn.laddr.port == port:
+ if conn.pid is None:
+ return None
+ try:
+ proc = psutil.Process(conn.pid)
+ return (conn.pid, proc.name())
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
+ return (conn.pid, "")
+ except (psutil.AccessDenied, OSError) as e:
+ # psutil.net_connections() needs elevated privileges on some platforms
+ logger.debug("Failed to scan network connections for port %s: %s", port, e)
+ return None
+
+
def _is_port_free(host: str, port: int) -> bool:
- """Check if a port is available for binding."""
+ """Check if a port is available for binding.
+
+ When *host* is ``0.0.0.0`` (wildcard), we also check whether anything
+ is already listening on ``127.0.0.1`` (and ``::1`` when IPv6 is
+ available). An SSH tunnel or similar process may hold the loopback
+ address while our wildcard bind still succeeds, making Unsloth Studio
+ unreachable via ``localhost``.
+
+ Works on Windows, macOS, and Linux.
+ """
import socket
+ # 1. Can we bind to the requested address?
+ # Use getaddrinfo so both IPv4 ("0.0.0.0") and IPv6 ("::") hosts
+ # resolve to the correct address family automatically.
try:
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ addr_info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
+ family, socktype, proto, _, sockaddr = addr_info[0]
+ with socket.socket(family, socktype, proto) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- s.bind((host, port))
- return True
+ s.bind(sockaddr)
except OSError:
return False
+ # 2. When binding to all interfaces, verify that localhost is not
+ # already claimed by another process (e.g. an SSH -L tunnel).
+ # We attempt a TCP connect -- if it succeeds something is listening.
+ if host in ("0.0.0.0", "::"):
+ for loopback, family in [
+ ("127.0.0.1", socket.AF_INET),
+ ("::1", socket.AF_INET6),
+ ]:
+ try:
+ with socket.socket(family, socket.SOCK_STREAM) as s:
+ s.settimeout(1)
+ if s.connect_ex((loopback, port)) == 0:
+ # Connection succeeded -- port is taken on loopback
+ return False
+ except OSError:
+ # IPv6 disabled or other OS-level restriction -- skip
+ continue
+
+ return True
+
def _find_free_port(host: str, start: int, max_attempts: int = 20) -> int:
"""Find a free port starting from `start`, trying up to max_attempts ports."""
@@ -149,11 +210,11 @@ def _graceful_shutdown(server = None):
logger.info("All subprocesses cleaned up")
-# The uvicorn server instance — set by run_server(), used by callers
+# The uvicorn server instance -- set by run_server(), used by callers
# that need to tell the server to exit (e.g. signal handlers).
_server = None
-# Shutdown event — used to wake the main loop on signal
+# Shutdown event -- used to wake the main loop on signal
_shutdown_event = None
@@ -205,9 +266,22 @@ def run_server(
# Auto-find free port if requested port is in use
if not _is_port_free(host, port):
original_port = port
- port = _find_free_port(host, port)
+ blocker = _get_pid_on_port(port)
+ port = _find_free_port(host, port + 1)
if not silent:
- print(f"Port {original_port} is in use, using port {port} instead")
+ print("")
+ print("=" * 50)
+ if blocker:
+ pid, name = blocker
+ print(
+ f"Port {original_port} is already in use by " f"{name} (PID {pid})."
+ )
+ else:
+ print(f"Port {original_port} is already in use.")
+ print(f"Unsloth Studio will use port {port} instead.")
+ print(f"Open http://localhost:{port} in your browser.")
+ print("=" * 50)
+ print("")
# Setup frontend if path provided
if frontend_path:
@@ -297,7 +371,7 @@ if __name__ == "__main__":
sys.stderr.flush()
sys.exit(1)
- # ── Signal handler — ensures subprocess cleanup on Ctrl+C ────
+ # Signal handler -- ensures subprocess cleanup on Ctrl+C
def _signal_handler(signum, frame):
_graceful_shutdown(_server)
_shutdown_event.set()
diff --git a/studio/frontend/src/components/assistant-ui/reasoning.tsx b/studio/frontend/src/components/assistant-ui/reasoning.tsx
index 4f3f8075a4..0e37f6d433 100644
--- a/studio/frontend/src/components/assistant-ui/reasoning.tsx
+++ b/studio/frontend/src/components/assistant-ui/reasoning.tsx
@@ -151,7 +151,7 @@ function ReasoningTrigger({
& { streaming?: boolean }) {
const scrollRef = useRef(null);
+ const shouldAutoScrollRef = useRef(true);
useEffect(() => {
if (!(streaming && scrollRef.current)) {
return;
}
const el = scrollRef.current;
+ const updateAutoScroll = () => {
+ const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
+ shouldAutoScrollRef.current = distanceFromBottom <= 24;
+ };
const observer = new MutationObserver(() => {
- el.scrollTop = el.scrollHeight;
+ if (shouldAutoScrollRef.current) {
+ el.scrollTop = el.scrollHeight;
+ }
});
+ el.addEventListener("scroll", updateAutoScroll);
observer.observe(el, {
childList: true,
subtree: true,
characterData: true,
});
+ shouldAutoScrollRef.current = true;
el.scrollTop = el.scrollHeight;
- return () => observer.disconnect();
+ return () => {
+ observer.disconnect();
+ el.removeEventListener("scroll", updateAutoScroll);
+ };
}, [streaming]);
return (
@@ -330,6 +342,7 @@ const ReasoningGroupImpl: ReasoningGroupComponent = ({
});
const [manualOpen, setManualOpen] = useState(false);
+ const [dismissedWhileStreaming, setDismissedWhileStreaming] = useState(false);
const [duration, setDuration] = useState(0);
const startTimeRef = useRef(null);
@@ -345,17 +358,23 @@ const ReasoningGroupImpl: ReasoningGroupComponent = ({
}
}, [isReasoningStreaming]);
- const isOpen = isReasoningStreaming || manualOpen;
+ // Reset dismissed flag when a new stream starts
+ useEffect(() => {
+ if (isReasoningStreaming) {
+ setDismissedWhileStreaming(false);
+ }
+ }, [isReasoningStreaming]);
- const variant = isReasoningStreaming
- ? "outline"
- : manualOpen
- ? "outline"
- : "ghost";
+ // Derived: open during streaming (unless dismissed), or if user manually opened after
+ const isOpen = (isReasoningStreaming && !dismissedWhileStreaming) || manualOpen;
+ const variant = isOpen ? "outline" : "ghost";
+ // Allow closing during streaming (matches ChatGPT)
const handleOpenChange = useCallback(
(open: boolean) => {
- if (!isReasoningStreaming) {
+ if (isReasoningStreaming) {
+ setDismissedWhileStreaming(!open);
+ } else {
setManualOpen(open);
}
},
@@ -368,14 +387,17 @@ const ReasoningGroupImpl: ReasoningGroupComponent = ({
onOpenChange={handleOpenChange}
variant={variant}
>
-