Merge remote-tracking branch 'upstream/main' into fix-oom-labels-vulkan-6414

This commit is contained in:
LeoBorcherding 2026-07-25 00:55:54 -05:00
commit 4e7c0c2a1b
162 changed files with 2884 additions and 418 deletions

View file

@ -1,134 +1,145 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "view-in-github",
"colab_type": "text"
},
"source": [
"<a href=\"https://colab.research.google.com/github/unslothai/unsloth/blob/main/studio/Unsloth_Studio_Colab.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "view-in-github",
"colab_type": "text"
},
"source": [
"<a href=\"https://colab.research.google.com/github/unslothai/unsloth/blob/main/studio/Unsloth_Studio_Colab.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "6b87de59"
},
"source": [
"To run this, press \"*Runtime*\" and press \"*Run all*\" on a **free** Tesla T4 Google Colab instance!\n",
"<div class=\"align-center\">\n",
"<a href=\"https://unsloth.ai/\"><img src=\"https://github.com/unslothai/unsloth/raw/main/images/unsloth%20new%20logo.png\" width=\"115\"></a>\n",
"<a href=\"https://discord.gg/unsloth\"><img src=\"https://github.com/unslothai/unsloth/raw/main/images/Discord button.png\" width=\"145\"></a>\n",
"<a href=\"https://unsloth.ai/docs/\"><img src=\"https://github.com/unslothai/unsloth/blob/main/images/documentation%20green%20button.png?raw=true\" width=\"125\"></a> Join Discord if you need help + ⭐ <i>Star us on <a href=\"https://github.com/unslothai/unsloth\">Github</a> </i> ⭐\n",
"</div>\n",
"\n",
"To install Unsloth Studio on your local device, follow [our guide](https://unsloth.ai/docs/new/unsloth-studio/install). Unsloth Studio is licensed [AGPL-3.0](https://github.com/unslothai/unsloth/blob/main/studio/LICENSE.AGPL-3.0).\n",
"\n",
"### Unsloth Studio\n",
"\n",
"Train and run open models with [**Unsloth Studio**](https://unsloth.ai/docs/new/unsloth-studio/start). NEW! Installation should now only take 2 mins!\n",
"\n",
"\n",
"We are actively working on making Unsloth Studio install on Colab T4 GPUs faster.\n",
"\n",
"[Features](https://unsloth.ai/docs/new/unsloth-studio#features) • [Quickstart](https://unsloth.ai/docs/new/unsloth-studio/start) • [Data Recipes](https://unsloth.ai/docs/new/unsloth-studio/data-recipe) • [Unsloth Chat](https://unsloth.ai/docs/new/unsloth-studio/chat) • [Export](https://unsloth.ai/docs/new/unsloth-studio/export)"
],
"id": "6b87de59"
},
{
"cell_type": "markdown",
"metadata": {
"id": "e4206349"
},
"source": [
"<p align=\"left\"><img src=\"https://github.com/unslothai/unsloth/raw/main/studio/frontend/public/studio%20github%20landscape%20colab%20display.png\" width=\"600\"></p>"
],
"id": "e4206349"
},
{
"cell_type": "markdown",
"metadata": {
"id": "27da2957"
},
"source": [
"### Setup: Clone repo and run setup"
],
"id": "27da2957"
},
{
"cell_type": "code",
"metadata": {
"id": "27e68f91"
},
"source": "!git clone --depth 1 --branch main https://github.com/unslothai/unsloth.git\n%cd /content/unsloth\n!chmod +x studio/setup.sh && ./studio/setup.sh --local",
"execution_count": null,
"outputs": [],
"id": "27e68f91"
},
{
"cell_type": "markdown",
"metadata": {
"id": "3e1771a9"
},
"source": [
"### Start Unsloth Studio"
],
"id": "3e1771a9"
},
{
"cell_type": "code",
"metadata": {
"id": "277e431e"
},
"source": [
"import sys\n",
"sys.path.insert(0, \"/content/unsloth/studio/backend\")\n",
"from colab import start\n",
"\n",
"# On Colab, start() auto-opens a Cloudflare link and prints admin login credentials.\n",
"# Use the Cloudflare link above the ready card to open Studio (in-cell iframes often stay blank).\n",
"start()\n",
"\n",
"# To skip the Cloudflare tunnel and try the in-notebook proxy iframe only:\n",
"# start(cloudflare=False)"
],
"execution_count": null,
"outputs": [],
"id": "277e431e"
},
{
"cell_type": "markdown",
"metadata": {
"id": "f2b0c6a1"
},
"source": [
"And we're done! If you have any questions on Unsloth, we have a [Discord](https://discord.gg/unsloth) channel! If you find any bugs or want to keep updated with the latest LLM stuff, or need help, join projects etc, feel free to join our Discord!\n",
"\n",
"Some other resources:\n",
"1. Looking to use Unsloth locally? Read our [Installation Guide](https://unsloth.ai/docs/get-started/install) for details on installing Unsloth on Windows, Docker, AMD, Intel GPUs.\n",
"2. Learn how to do Reinforcement Learning with our [RL Guide and notebooks](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide).\n",
"3. Read our guides and notebooks for [Text-to-speech (TTS)](https://unsloth.ai/docs/basics/text-to-speech-tts-fine-tuning) and [vision](https://unsloth.ai/docs/basics/vision-fine-tuning) model support.\n",
"4. Explore our [LLM Tutorials Directory](https://unsloth.ai/docs/models/tutorials-how-to-fine-tune-and-run-llms) to find dedicated guides for each model.\n",
"5. Need help with Inference? Read our [Inference & Deployment page](https://unsloth.ai/docs/basics/inference-and-deployment) for details on using vLLM, llama.cpp, Ollama etc.\n",
"\n",
"<div class=\"align-center\">\n",
" <a href=\"https://unsloth.ai\"><img src=\"https://github.com/unslothai/unsloth/raw/main/images/unsloth%20new%20logo.png\" width=\"115\"></a>\n",
" <a href=\"https://discord.gg/unsloth\"><img src=\"https://github.com/unslothai/unsloth/raw/main/images/Discord.png\" width=\"145\"></a>\n",
" <a href=\"https://unsloth.ai/docs/\"><img src=\"https://github.com/unslothai/unsloth/blob/main/images/documentation%20green%20button.png?raw=true\" width=\"125\"></a>\n",
"\n",
" Join Discord if you need help + ⭐️ <i>Star us on <a href=\"https://github.com/unslothai/unsloth\">Github</a> </i> ⭐️\n",
"\n",
" <b>This notebook is licensed <a href=\"https://github.com/unslothai/unsloth/blob/main/studio/LICENSE.AGPL-3.0\">AGPL-3.0</a></b>\n",
"</div>"
],
"id": "f2b0c6a1"
}
],
"metadata": {
"accelerator": "GPU",
"colab": {
"gpuType": "T4",
"provenance": [],
"include_colab_link": true
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
{
"cell_type": "markdown",
"id": "6b87de59",
"metadata": {
"id": "6b87de59"
},
"source": [
"To run this, press \"*Runtime*\" and press \"*Run all*\" on a **free** Tesla T4 Google Colab instance!\n",
"<div class=\"align-center\">\n",
"<a href=\"https://unsloth.ai/\"><img src=\"https://github.com/unslothai/unsloth/raw/main/images/unsloth%20new%20logo.png\" width=\"115\"></a>\n",
"<a href=\"https://discord.gg/unsloth\"><img src=\"https://github.com/unslothai/unsloth/raw/main/images/Discord button.png\" width=\"145\"></a>\n",
"<a href=\"https://unsloth.ai/docs/\"><img src=\"https://github.com/unslothai/unsloth/blob/main/images/documentation%20green%20button.png?raw=true\" width=\"125\"></a> Join Discord if you need help + ⭐ <i>Star us on <a href=\"https://github.com/unslothai/unsloth\">Github</a> </i> ⭐\n",
"</div>\n",
"\n",
"To install Unsloth Studio on your local device, follow [our guide](https://unsloth.ai/docs/new/unsloth-studio/install). Unsloth Studio is licensed [AGPL-3.0](https://github.com/unslothai/unsloth/blob/main/studio/LICENSE.AGPL-3.0).\n",
"\n",
"### Unsloth Studio\n",
"\n",
"Train and run open models with [**Unsloth Studio**](https://unsloth.ai/docs/new/unsloth-studio/start). NEW! Installation should now only take 2 mins!\n",
"\n",
"\n",
"We are actively working on making Unsloth Studio install on Colab T4 GPUs faster.\n",
"\n",
"[Features](https://unsloth.ai/docs/new/unsloth-studio#features) • [Quickstart](https://unsloth.ai/docs/new/unsloth-studio/start) • [Data Recipes](https://unsloth.ai/docs/new/unsloth-studio/data-recipe) • [Unsloth Chat](https://unsloth.ai/docs/new/unsloth-studio/chat) • [Export](https://unsloth.ai/docs/new/unsloth-studio/export)"
]
},
{
"cell_type": "markdown",
"id": "e4206349",
"metadata": {
"id": "e4206349"
},
"source": [
"<p align=\"left\"><img src=\"https://github.com/unslothai/unsloth/raw/main/studio/frontend/public/studio%20github%20landscape%20colab%20display.png\" width=\"600\"></p>"
]
},
{
"cell_type": "markdown",
"id": "27da2957",
"metadata": {
"id": "27da2957"
},
"source": [
"### Setup: Clone repo and run setup"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "27e68f91",
"metadata": {
"id": "27e68f91"
},
"outputs": [],
"source": "!git clone --depth 1 --branch main https://github.com/unslothai/unsloth.git\n%cd /content/unsloth\n!chmod +x studio/setup.sh && ./studio/setup.sh --local"
},
{
"cell_type": "markdown",
"id": "3e1771a9",
"metadata": {
"id": "3e1771a9"
},
"source": [
"### Start Unsloth Studio"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "277e431e",
"metadata": {
"id": "277e431e"
},
"outputs": [],
"source": "import sys\nsys.path.insert(0, \"/content/unsloth/studio/backend\")\nfrom colab import start\n\n# Default: in-tab iframe only. start() blocks to keep the kernel alive.\nstart()\n\n# For a shareable Cloudflare link, replace start() above with:\n# start(cloudflare=True)"
},
{
"cell_type": "markdown",
"id": "f2b0c6a1",
"metadata": {
"id": "f2b0c6a1"
},
"source": [
"And we're done! If you have any questions on Unsloth, we have a [Discord](https://discord.gg/unsloth) channel! If you find any bugs or want to keep updated with the latest LLM stuff, or need help, join projects etc, feel free to join our Discord!\n",
"\n",
"Some other resources:\n",
"1. Looking to use Unsloth locally? Read our [Installation Guide](https://unsloth.ai/docs/get-started/install) for details on installing Unsloth on Windows, Docker, AMD, Intel GPUs.\n",
"2. Learn how to do Reinforcement Learning with our [RL Guide and notebooks](https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide).\n",
"3. Read our guides and notebooks for [Text-to-speech (TTS)](https://unsloth.ai/docs/basics/text-to-speech-tts-fine-tuning) and [vision](https://unsloth.ai/docs/basics/vision-fine-tuning) model support.\n",
"4. Explore our [LLM Tutorials Directory](https://unsloth.ai/docs/models/tutorials-how-to-fine-tune-and-run-llms) to find dedicated guides for each model.\n",
"5. Need help with Inference? Read our [Inference & Deployment page](https://unsloth.ai/docs/basics/inference-and-deployment) for details on using vLLM, llama.cpp, Ollama etc.\n",
"\n",
"<div class=\"align-center\">\n",
" <a href=\"https://unsloth.ai\"><img src=\"https://github.com/unslothai/unsloth/raw/main/images/unsloth%20new%20logo.png\" width=\"115\"></a>\n",
" <a href=\"https://discord.gg/unsloth\"><img src=\"https://github.com/unslothai/unsloth/raw/main/images/Discord.png\" width=\"145\"></a>\n",
" <a href=\"https://unsloth.ai/docs/\"><img src=\"https://github.com/unslothai/unsloth/blob/main/images/documentation%20green%20button.png?raw=true\" width=\"125\"></a>\n",
"\n",
" Join Discord if you need help + ⭐️ <i>Star us on <a href=\"https://github.com/unslothai/unsloth\">Github</a> </i> ⭐️\n",
"\n",
" <b>This notebook is licensed <a href=\"https://github.com/unslothai/unsloth/blob/main/studio/LICENSE.AGPL-3.0\">AGPL-3.0</a></b>\n",
"</div>"
]
}
],
"metadata": {
"accelerator": "GPU",
"colab": {
"gpuType": "T4",
"provenance": [],
"include_colab_link": true
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
"nbformat": 4,
"nbformat_minor": 5
}

View file

@ -30,6 +30,7 @@ lora:
vision_all_linear: false
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -30,6 +30,7 @@ lora:
vision_all_linear: false
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -33,6 +33,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -30,6 +30,7 @@ lora:
- "query"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -30,6 +30,7 @@ lora:
- "value"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -33,6 +33,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -29,6 +29,7 @@ lora:
- "Wqkv"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -33,6 +33,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -29,6 +29,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -29,6 +29,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -29,6 +29,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -29,6 +29,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -29,6 +29,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -26,6 +26,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -26,6 +26,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -26,6 +26,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -26,6 +26,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -26,6 +26,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -26,6 +26,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -26,6 +26,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -26,6 +26,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -37,6 +37,7 @@ lora:
- "shared_mlp.output_linear"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -37,6 +37,7 @@ lora:
- "shared_mlp.output_linear"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -29,6 +29,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -30,6 +30,7 @@ lora:
- "v_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -29,6 +29,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: false

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -33,6 +33,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -33,6 +33,7 @@ lora:
- "v_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -38,6 +38,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -37,6 +37,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -35,6 +35,7 @@ lora:
- "out_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -29,6 +29,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -37,6 +37,7 @@ lora:
- "out_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -33,6 +33,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -38,6 +38,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -33,6 +33,7 @@ lora:
- "v_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -36,6 +36,7 @@ lora:
- "gate_up_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -29,6 +29,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -34,6 +34,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -29,6 +29,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -36,6 +36,7 @@ lora:
- "gate_up_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -35,6 +35,7 @@ lora:
- "down_proj"
use_rslora: false
use_loftq: false
use_dora: false
logging:
enable_wandb: false

View file

@ -29,6 +29,7 @@ lora:
- "all-linear"
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -30,6 +30,7 @@ lora:
vision_all_linear: true
use_rslora: false
use_loftq: false
use_dora: false
finetune_vision_layers: true
finetune_language_layers: true
finetune_attention_modules: true

View file

@ -1,9 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""
Colab helpers for Unsloth Studio. Uses Colab's built-in proxy.
"""
"""Colab helpers for Unsloth Studio. Uses Colab's built-in proxy."""
from pathlib import Path
import sys
@ -22,11 +20,9 @@ logger = get_logger(__name__)
def get_colab_url(port: int = 8888) -> str:
"""
Get the Colab proxy URL for a port.
"""Get the Colab proxy URL for a port.
Retries up to 3 times, validating the result is a real HTTPS Colab URL.
Falls back to http://localhost:{port} only when all attempts fail.
Retries 3x validating a real HTTPS Colab URL; falls back to localhost on failure.
"""
import time as _time
@ -55,28 +51,244 @@ def get_colab_url(port: int = 8888) -> str:
return fallback
def show_link(port: int = 8888, *, _url: "str | None" = None):
"""Display a styled clickable link to the UI.
*_url* is an optional pre-fetched proxy URL; pass it to avoid a second eval_js round-trip.
"""
from IPython.display import display, HTML
url = _url if _url is not None else get_colab_url(port)
# Truncated display URL; try/except so an odd URL shape still renders the link.
def _short_colab_url(url: str, port: int) -> str:
"""Truncated display form of a Colab proxy URL; falls back to the full URL."""
try:
port_prefix = f"{port}-"
idx = url.index(port_prefix)
next_dash = url.index("-", idx + len(port_prefix))
short_url = url[: next_dash + 1] + "..."
return url[: next_dash + 1] + "..."
except (ValueError, IndexError):
short_url = url
return url
# Plain-text line so the URL shows even if HTML display fails.
logger.info(f"🌐 Unsloth Studio URL: {url}")
html = f"""
def _is_colab_proxy_url(url: str, port: int) -> bool:
"""True when *url* looks like a real Colab kernel proxy, not a localhost fallback."""
return bool(url and isinstance(url, str) and url.startswith("https://") and str(port) in url)
def _is_colab_runtime() -> bool:
"""True on a hosted Colab notebook kernel.
Reuses the backend's main Colab detector (``/content`` + Colab env / ``google.colab``)
instead of a single env var, which is not always present on hosted runtimes.
"""
try:
from main import _IS_COLAB
return bool(_IS_COLAB)
except Exception:
return False
def _colab_login_credentials_path() -> Path:
from auth.storage import DB_PATH
return DB_PATH.parent / ".colab_notebook_login"
def _store_colab_login_credentials(username: str, password: str) -> None:
"""Persist Colab admin credentials for notebook re-runs after interrupt."""
path = _colab_login_credentials_path()
try:
path.parent.mkdir(parents = True, exist_ok = True)
path.write_text(f"{username}\n{password}\n")
try:
import os
os.chmod(path, 0o600)
except OSError:
pass
except OSError as e:
logger.info(f"Could not persist Colab login credentials ({e}).")
def _load_colab_login_credentials() -> "tuple[str, str] | None":
"""Return stored Colab admin credentials from a previous ``start()`` run, if any."""
path = _colab_login_credentials_path()
try:
if not path.is_file():
return None
lines = path.read_text().splitlines()
if len(lines) >= 2 and lines[0] and lines[1]:
return lines[0], lines[1]
except OSError as e:
logger.info(f"Could not load Colab login credentials ({e}).")
return None
def _clear_colab_login_credentials() -> None:
"""Drop the cached Colab credentials once they no longer authenticate."""
path = _colab_login_credentials_path()
try:
path.unlink(missing_ok = True)
except OSError as e:
logger.info(f"Could not clear Colab login credentials ({e}).")
def _colab_credentials_still_valid(username: str, password: str) -> bool:
"""True when *password* still matches the stored admin hash.
Guards against redisplaying a cached first-run password after the user has
changed the admin password through the app, which would print credentials
that no longer authenticate to the current Cloudflare tunnel.
"""
try:
from auth.storage import get_user_and_secret
from auth.hashing import verify_password
except Exception as e:
logger.info(f"Could not load auth to validate cached Colab credentials ({e}).")
return False
try:
row = get_user_and_secret(username)
if not row:
return False
salt, pwd_hash = row[0], row[1]
return bool(verify_password(password, salt, pwd_hash))
except Exception as e:
logger.info(f"Could not validate cached Colab credentials ({e}).")
return False
def _colab_wants_cloudflare(cloudflare: "bool | None") -> bool:
"""Resolve whether to open a Cloudflare tunnel.
``None`` auto-enables on real Colab (the in-cell proxy embed is often blank);
pass ``False`` to opt out.
"""
if cloudflare is not None:
return cloudflare
return _is_colab_runtime()
def _finalize_colab_admin_password() -> "tuple[str, str] | None":
"""Clear the bootstrap-password gate on Colab so Cloudflare tunnels can start.
Returns ``(username, password)`` for display in the notebook. On first run the
random admin password is finalized; on later runs (e.g. after interrupt) the
stored credentials are re-displayed so the Cloudflare link stays usable.
Anyone who can read this cell already controls the runtime.
"""
if not _is_colab_runtime():
return None
try:
from auth.storage import (
DEFAULT_ADMIN_USERNAME,
ensure_default_admin,
generate_bootstrap_password,
get_bootstrap_password,
requires_password_change,
update_password,
)
except Exception as e:
logger.warning(
f"Could not load auth for Colab setup ({e}); Cloudflare link may be blocked."
)
return None
try:
ensure_default_admin()
username = DEFAULT_ADMIN_USERNAME
if not requires_password_change(username):
creds = _load_colab_login_credentials()
if creds is not None and _colab_credentials_still_valid(username, creds[1]):
return creds
# The admin password was changed through the app after the first run,
# so the cached copy is stale; drop it instead of printing dead credentials.
_clear_colab_login_credentials()
return None
password = get_bootstrap_password() or generate_bootstrap_password()
if not update_password(username, password):
logger.warning(
"Could not finalize Colab admin password; Cloudflare link may be blocked."
)
return None
_store_colab_login_credentials(username, password)
return username, password
except Exception as e:
logger.warning(
f"Could not finalize Colab admin password ({e}); Cloudflare link may be blocked."
)
return None
def _colab_login_html(username: str, password: str) -> str:
"""Notebook card with Colab admin credentials (shown once after auto-finalize)."""
return f"""
<div style="display: inline-block; padding: 20px; background: #ffffff; border: 2px solid #000000;
border-radius: 12px; margin: 10px 0; font-family: system-ui, -apple-system, sans-serif;">
<h2 style="color: #000000; margin: 0 0 12px 0; font-size: 22px; font-weight: 800;">
Unsloth Studio Login (Colab)
</h2>
<p style="color: #333333; margin: 0 0 12px 0; font-size: 14px; font-weight: bold;">
Log in to Studio with the Cloudflare link above using these credentials. This cell
is visible only in your notebook session.
</p>
<p style="color: #333333; margin: 0; font-size: 14px; font-family: monospace; font-weight: bold;">
Username: <code>{username}</code><br>
Password: <code>{password}</code>
</p>
</div>
"""
def _show_colab_login_credentials(username: str, password: str) -> None:
"""Display Colab admin credentials in the notebook output."""
from IPython.display import HTML, display
logger.info(f"🔐 Unsloth Studio login — user: {username}")
display(HTML(_colab_login_html(username, password)))
def _ready_card_html(
url: str,
port: int,
*,
has_cloudflare_link: bool = False,
cloudflare_requested: bool = False,
) -> str:
"""Branded ready card for the in-notebook Studio view.
Colab ``*.prod.colab.dev`` proxy URLs are session-scoped and 404 when opened as a
top-level tab or on another device, so never ``window.open`` them. On real Colab the
Cloudflare link is the supported entry point because in-cell proxy embeds often stay blank.
"""
short_url = _short_colab_url(url, port)
if _is_colab_runtime() or _is_colab_proxy_url(url, port):
if has_cloudflare_link:
embed_note = (
"Open Studio with the Cloudflare link above. In-cell proxy previews on "
"current Colab often stay blank, so the tunnel link is the supported path."
)
elif cloudflare_requested:
embed_note = (
"Could not open a Cloudflare tunnel, so Studio may be unreachable on Colab. "
"Check the logs above and re-run this cell. Pass "
'<code style="background:#f3f3f3;padding:2px 6px;border-radius:4px;">'
"cloudflare=True</code> after fixing any tunnel errors."
)
else:
embed_note = (
"Colab proxy links cannot be opened in a new tab (they 404 outside this "
'notebook). Re-run with <code style="background:#f3f3f3;padding:2px 6px;'
'border-radius:4px;">start(cloudflare=True)</code> for a working link.'
)
return f"""
<div style="display: inline-block; padding: 20px; background: #ffffff; border: 2px solid #000000;
border-radius: 12px; margin: 10px 0; font-family: system-ui, -apple-system, sans-serif;">
<h2 style="color: #000000; margin: 0 0 12px 0; font-size: 26px; font-weight: 800;
display: flex; align-items: center; gap: 12px;">
<img src="https://github.com/unslothai/unsloth/raw/main/studio/frontend/public/unsloth-gem.png"
height="48" style="display:block;">
Unsloth Studio is Ready!
</h2>
<p style="color: #333333; margin: 0 0 8px 0; font-size: 15px; font-weight: bold;">
{embed_note}
</p>
<p style="color: #666666; margin: 16px 0 0 0; font-size: 13px; font-family: monospace; font-weight: bold;">
{short_url}
</p>
</div>
"""
return f"""
<div style="display: inline-block; padding: 20px; background: #ffffff; border: 2px solid #000000;
border-radius: 12px; margin: 10px 0; font-family: system-ui, -apple-system, sans-serif;">
<h2 style="color: #000000; margin: 0 0 12px 0; font-size: 26px; font-weight: 800;
@ -100,15 +312,52 @@ def show_link(port: int = 8888, *, _url: "str | None" = None):
</p>
</div>
"""
display(HTML(html))
def show_link(
port: int = 8888,
*,
_url: "str | None" = None,
has_cloudflare_link: bool = False,
cloudflare_requested: bool = False,
):
"""Display a styled ready card for the UI.
Colab proxy URLs are informational only (no new-tab open; they 404 outside the cell);
non-proxy URLs keep a clickable open button. *_url* is an optional pre-fetched proxy
URL to avoid a second eval_js round-trip.
"""
from IPython.display import display, HTML
url = _url if _url is not None else get_colab_url(port)
logger.info(f"🌐 Unsloth Studio URL: {url}")
display(
HTML(
_ready_card_html(
url,
port,
has_cloudflare_link = has_cloudflare_link,
cloudflare_requested = cloudflare_requested,
)
)
)
def _warn_colab_cloudflare_missing(*, use_cloudflare: bool, cloudflare_url: "str | None") -> None:
"""Log a prominent warning when Colab expected a tunnel but none was opened."""
if not use_cloudflare or cloudflare_url or not _is_colab_runtime():
return
logger.warning(
"Colab Cloudflare tunnel unavailable — Studio is unlikely to be reachable in this "
"notebook. Check the logs above for tunnel or auth errors, then re-run start()."
)
def _bootstrap_password_pending() -> bool:
"""True while the default admin still owes a bootstrap-password change.
While pending, main.py injects that password into same-origin GETs, and a public
tunnel GET (no Origin) reads as same-origin, so sharing the link would leak admin
access. Fails safe to pending if the state cannot be read.
While pending, a public tunnel GET (no Origin) reads as same-origin and gets the
injected password, so sharing the link would leak admin access. Fails safe to pending.
"""
try:
from auth.storage import requires_password_change, DEFAULT_ADMIN_USERNAME
@ -121,9 +370,8 @@ def _bootstrap_password_pending() -> bool:
def start_cloudflare_tunnel(port: int) -> "str | None":
"""Open a shareable Cloudflare quick tunnel to localhost:*port*, or None.
run_server suppresses the tunnel on Colab by design, so we start it directly.
Refused while the bootstrap password is pending; any failure collapses to None
and the Colab proxy still works.
run_server suppresses the tunnel on Colab, so we start it directly. Refused while the
bootstrap password is pending; any failure collapses to None (Colab proxy still works).
"""
if _bootstrap_password_pending():
logger.warning(
@ -152,9 +400,9 @@ def start_cloudflare_tunnel(port: int) -> "str | None":
def _publish_cloudflare_url(cloudflare_url: "str | None") -> None:
"""Publish a directly-started tunnel URL onto app.state so /api/health advertises it.
run_server only sets this when it opens the tunnel itself, which it skips on Colab,
so we set it here. Otherwise the frontend's API examples fall back to an
unreachable server_url. Best-effort.
run_server sets this only when it opens the tunnel itself (skipped on Colab), so we
set it here; otherwise the frontend's API examples fall back to an unreachable
server_url. Best-effort.
"""
if not cloudflare_url:
return
@ -183,8 +431,7 @@ def _stop_cloudflare_tunnel() -> None:
def _is_studio_healthy(port: int, timeout: float = 2.0) -> bool:
"""True only if Unsloth Studio (not some other app) answers /api/health on *port*.
The service-marker check stops the reuse path reusing or tunneling a foreign
process that merely serves /api/health.
The service-marker check stops the reuse path reusing or tunneling a foreign process.
"""
import json, urllib.request
try:
@ -222,31 +469,45 @@ def _shareable_link_html(cloudflare_url: str) -> str:
"""
def _show_and_embed(port: int, *, cloudflare_url: "str | None" = None):
"""Render the Unsloth header + iframe for *port*, with a shareable-link card above
when *cloudflare_url* is set. Falls back to serve_kernel_port_as_iframe."""
url = get_colab_url(port)
logger.info(f"🌐 Unsloth Studio URL: {url}")
if cloudflare_url:
logger.info(f"🔗 Shareable Cloudflare link: {cloudflare_url}")
# Height for serve_kernel_port_as_iframe (~82vh on a 1080p screen, clamped).
_COLAB_IFRAME_HEIGHT = 900
def _embed_kernel_port_iframe(port: int) -> bool:
"""Embed Studio via Colab's native kernel-port iframe helper.
Only trusted on a real Colab runtime: colabtools can import ``google.colab`` and
queue browser-side JS without appending an iframe, so callers outside Colab must use
the HTML iframe path instead.
"""
if not _is_colab_runtime():
return False
try:
from google.colab import output as colab_output
except ImportError:
return False
try:
colab_output.serve_kernel_port_as_iframe(
port,
height = _COLAB_IFRAME_HEIGHT,
width = "100%",
)
return True
except Exception as e:
logger.info(f"serve_kernel_port_as_iframe failed ({e}); trying HTML iframe.")
return False
def _embed_html_iframe(url: str, port: int) -> bool:
"""Fallback embed: raw HTML iframe when the Colab helper is unavailable."""
try:
from IPython.display import HTML, display
except ImportError:
return False
iframe_id = f"unsloth-studio-{port}"
# Truncated header URL — best-effort, falls back to full URL.
try:
port_prefix = f"{port}-"
idx = url.index(port_prefix)
next_dash = url.index("-", idx + len(port_prefix))
short_url = url[: next_dash + 1] + "..."
except (ValueError, IndexError):
short_url = url
if cloudflare_url:
display(HTML(_shareable_link_html(cloudflare_url)))
short_url = _short_colab_url(url, port)
iframe_id = f"unsloth-studio-{port}"
try:
display(
HTML(f"""
<div style="font-family:system-ui,-apple-system,sans-serif;margin:8px 0;
@ -266,41 +527,101 @@ def _show_and_embed(port: int, *, cloudflare_url: "str | None" = None):
</div>
""")
)
except Exception:
# Fallback: Colab's built-in helper.
return True
except Exception as e:
logger.info(f"HTML iframe embed failed ({e}).")
return False
def _show_and_embed(
port: int,
*,
cloudflare_url: "str | None" = None,
colab_login: "tuple[str, str] | None" = None,
cloudflare_requested: bool = False,
):
"""Render the Unsloth ready card + iframe for *port*.
Prefer Colab's ``serve_kernel_port_as_iframe`` on real Colab; raw HTML iframe is the
fallback. Cloudflare cards stay clickable.
"""
url = get_colab_url(port)
logger.info(f"🌐 Unsloth Studio URL: {url}")
if cloudflare_url:
logger.info(f"🔗 Shareable Cloudflare link: {cloudflare_url}")
_warn_colab_cloudflare_missing(
use_cloudflare = cloudflare_requested,
cloudflare_url = cloudflare_url,
)
if cloudflare_url:
try:
from google.colab import output as colab_output
colab_output.serve_kernel_port_as_iframe(port, height = 900, width = "100%")
except ImportError:
pass
from IPython.display import HTML, display
display(HTML(_shareable_link_html(cloudflare_url)))
except Exception as e:
logger.info(f"Could not render Cloudflare link card ({e}).")
if colab_login:
try:
_show_colab_login_credentials(*colab_login)
except Exception as e:
logger.info(f"Could not render Colab login card ({e}).")
try:
show_link(
port,
_url = url,
has_cloudflare_link = bool(cloudflare_url),
cloudflare_requested = cloudflare_requested,
)
except Exception as e:
logger.info(f"Could not render Unsloth link card ({e}).")
# On Colab with a working tunnel, skip the in-cell proxy embed (often blank).
if _is_colab_runtime() and cloudflare_url:
return
# Real Colab: kernel helper needs only the port (works when eval_js failed).
if _is_colab_runtime():
if _embed_kernel_port_iframe(port):
return
_embed_html_iframe(url, port)
def start(port: int = 8888, *, cloudflare: bool = False):
def start(port: int = 8888, *, cloudflare: "bool | None" = None):
"""Start Unsloth Studio in Colab and display the URL.
Args:
port: Port to bind/serve on.
cloudflare: Opt in to a shareable Cloudflare HTTPS link reachable from any
device (default OFF). It exposes Unsloth's login page beyond Colab, so it
stays an explicit opt-in; the default shows only the in-tab proxy iframe.
cloudflare: Shareable Cloudflare HTTPS link. ``None`` (default) auto-enables on
real Colab because the in-cell proxy embed is often blank; pass ``False`` to
skip the tunnel or ``True`` to force it on other runtimes.
Usage:
start() # Colab-proxy iframe only (default)
start(cloudflare=True) # also open a shareable Cloudflare link
start() # Cloudflare link on Colab (auto); proxy iframe elsewhere
start(cloudflare=False) # Colab proxy iframe only (often blank on current Colab)
start(cloudflare=True) # force Cloudflare link on any runtime
"""
import time
logger.info("🦥 Starting Unsloth Studio...")
use_cloudflare = _colab_wants_cloudflare(cloudflare)
# Fast path: Unsloth already running (cell re-run). Re-launching would collide on
# the port, so just re-show the link and iframe.
# Fast path: already running (cell re-run); re-show link/iframe instead of rebinding the port.
if _is_studio_healthy(port):
logger.info(f" Unsloth is already running on port {port} — reusing existing server.")
# try/finally: tear the tunnel down even if interrupted mid-start/render.
try:
cf_url = start_cloudflare_tunnel(port) if cloudflare else None
colab_login = _finalize_colab_admin_password() if use_cloudflare else None
cf_url = start_cloudflare_tunnel(port) if use_cloudflare else None
_publish_cloudflare_url(cf_url)
_show_and_embed(port, cloudflare_url = cf_url)
_show_and_embed(
port,
cloudflare_url = cf_url,
colab_login = colab_login,
cloudflare_requested = use_cloudflare,
)
for _ in range(10000):
time.sleep(300)
print("=", end = "", flush = True)
@ -313,7 +634,6 @@ def start(port: int = 8888, *, cloudflare: bool = False):
logger.info(" Loading backend...")
from run import run_server
# Auto-detect frontend path
repo_root = Path(__file__).parent.parent
frontend_path = repo_root / "frontend" / "dist"
@ -323,8 +643,7 @@ def start(port: int = 8888, *, cloudflare: bool = False):
logger.info(" Starting server...")
try:
# cloudflare=False: this helper owns the tunnel (Colab's own
# start(cloudflare=...) drives it), so pin it off explicitly.
# cloudflare=False: this helper owns the tunnel (via start(cloudflare=...)), so pin it off.
app = run_server(
host = "0.0.0.0",
port = port,
@ -339,14 +658,12 @@ def start(port: int = 8888, *, cloudflare: bool = False):
logger.error(f"❌ Unsloth Studio failed to start: {exc}")
return
# run_server auto-increments the port if in use; read back the bound port so the
# proxy URL and iframe point at the right place.
# run_server may auto-increment the port; read back the bound port for the proxy URL/iframe.
actual_port: int = getattr(getattr(app, "state", None), "server_port", None) or port
logger.info(f" Server started on port {actual_port}!")
# Poll health endpoint before showing the link — avoids the race where ready_event
# fires but the process hasn't finished binding.
# Poll health before showing the link: avoids the race where ready_event fires pre-bind.
import urllib.request
server_ready = False
@ -365,12 +682,17 @@ def start(port: int = 8888, *, cloudflare: bool = False):
)
return
# Open the tunnel now the server is healthy, publish its URL for /api/health, and
# tear it down on interrupt (try/finally) rather than orphan the process.
# Server healthy: finalize Colab auth, open the tunnel, publish URL, tear down on interrupt.
try:
cf_url = start_cloudflare_tunnel(actual_port) if cloudflare else None
colab_login = _finalize_colab_admin_password() if use_cloudflare else None
cf_url = start_cloudflare_tunnel(actual_port) if use_cloudflare else None
_publish_cloudflare_url(cf_url)
_show_and_embed(actual_port, cloudflare_url = cf_url)
_show_and_embed(
actual_port,
cloudflare_url = cf_url,
colab_login = colab_login,
cloudflare_requested = use_cloudflare,
)
# Keep kernel alive so the daemon server thread runs.
for _ in range(10000):

View file

@ -1048,6 +1048,21 @@ class ExportBackend:
"Use the safetensors adapter instead.",
None,
)
# llama.cpp's convert_lora_to_gguf.py has no concept of DoRA's
# lora_magnitude_vector tensors: it only reads the standard
# lora_A/lora_B delta, so exporting a DoRA adapter would silently
# drop the magnitude rescaling and produce a GGUF LoRA file that
# loads fine but no longer matches the trained model.
_peft_config = getattr(self.current_model, "peft_config", {}).get("default")
if getattr(_peft_config, "use_dora", False):
return (
False,
"GGUF LoRA export is not supported for DoRA adapters: the GGUF LoRA "
"format has no way to represent DoRA's magnitude vectors, so the "
"exported file would silently lose the DoRA behavior. Use the "
"safetensors adapter instead, or merge to a full GGUF model.",
None,
)
outtype = str(gguf_outtype).lower()
if outtype not in _GGUF_LORA_OUTTYPES:
return (

View file

@ -307,6 +307,26 @@ def _native_linux_system_rocm_lib_dirs(binary_dir: str = "") -> "list[str]":
_DEFAULT_MAX_TOKENS_FLOOR = 32768
_DEFAULT_FIRST_TOKEN_TIMEOUT_S = 1200.0 # 20 min
def _finalize_reasoning_only_cumulative(
cumulative: str, reasoning_text: str, finish_reason: Optional[str], promote_reasoning_only: bool
) -> str:
"""Close a live thinking block and promote it only after a clean stop.
Local inference streams cumulative snapshots. Replacing ``<think>...`` with
bare reasoning at EOF makes the final snapshot shorter, so suffix-based
route consumers drop the intended fallback. Keep the snapshot append-only.
A length-truncated thought is not a final answer, so close it without
promotion and let the client surface the ``length`` terminal state. Raw
consumers that do not split reasoning from visible content can disable the
fallback to avoid returning the same reasoning twice.
"""
visible_fallback = (
reasoning_text if promote_reasoning_only and finish_reason != "length" else ""
)
return cumulative + "</think>" + visible_fallback
# Only large streamed tool payloads get an early provisional card; render_html
# is exempt because it needs immediate artifact feedback.
_PROVISIONAL_ARGS_MIN_CHARS = 256
@ -2781,6 +2801,7 @@ class LlamaCppBackend:
"found": False,
"mtp_token": None,
"supports_mtp": False,
"mtp_probe_inconclusive": True,
"ngram_mod_flavor": None,
"supports_ngram_mod": False,
"spec_draft_n_max_flag": None,
@ -2813,6 +2834,9 @@ class LlamaCppBackend:
supports_no_cache_prompt = False
supports_metrics = False
supports_slot_save = False
saw_spec_type = False
probe_ok = False
help_text = ""
try:
probe_env = cls._llama_server_env_for_binary(bin_path)
result = subprocess.run(
@ -2824,6 +2848,7 @@ class LlamaCppBackend:
check = False,
env = probe_env,
)
probe_ok = result.returncode == 0
help_text = (result.stdout or "") + "\n" + (result.stderr or "")
# Split into per-flag blocks (each --flag line + its indented
# continuation), so the "argument has been removed" description
@ -2868,17 +2893,19 @@ class LlamaCppBackend:
return False
return "argument has been removed" not in desc
# MTP token from the --spec-type line.
spec_line = ""
for line in help_text.splitlines():
if "--spec-type" in line:
spec_line = line
break
# PR #22673 used draft-mtp; later renamed to mtp.
if "draft-mtp" in spec_line:
mtp_token = "draft-mtp"
elif re.search(r"[|,\[]mtp[|,\]]", spec_line):
mtp_token = "mtp"
# MTP token from the full --spec-type help block (decl + indented
# continuation). First-line-only probing missed builds putting the
# enum on the next line (#7302). Prefer draft-mtp (PR #22673) over mtp.
spec_help = blocks.get("--spec-type") or ""
if not spec_help:
# Fallback: join --spec-type lines, avoiding incidental "mtp" in --help.
spec_help = "\n".join(
line for line in help_text.splitlines() if "--spec-type" in line
)
mtp_token = cls._mtp_token_from_spec_help(spec_help)
# Only a resolved --spec-type block confirms missing MTP; empty/crash
# leaves saw_spec_type False so supports_mtp fails open.
saw_spec_type = bool(spec_help.strip()) and "--spec-type" in spec_help
# ngram-mod flag flavor. Post-rename builds advertise both new
# args (real) and legacy ones (stubs); pre-rename builds only
@ -2914,11 +2941,29 @@ class LlamaCppBackend:
supports_slot_save = _is_real("--slot-save-path")
except (OSError, subprocess.SubprocessError) as exc:
logger.debug(f"llama-server --help probe failed: {exc}")
saw_spec_type = False
probe_ok = False
help_text = ""
help_nonempty = bool(help_text.strip())
# Confirmed only when a successful --help lists a --spec-type block with
# mtp/draft-mtp; nonempty --help without it is a definitive pre-spec
# binary; failed/empty probes stay inconclusive (#7302).
if saw_spec_type and probe_ok:
supports_mtp = mtp_token is not None
mtp_probe_inconclusive = False
elif help_nonempty and probe_ok:
supports_mtp = False
mtp_probe_inconclusive = False
else:
supports_mtp = False
mtp_probe_inconclusive = True
info = {
"found": True,
"mtp_token": mtp_token,
"supports_mtp": mtp_token is not None,
"supports_mtp": supports_mtp,
"mtp_probe_inconclusive": mtp_probe_inconclusive,
"ngram_mod_flavor": ngram_mod_flavor,
"supports_ngram_mod": ngram_mod_flavor is not None,
"spec_draft_n_max_flag": spec_draft_n_max_flag,
@ -2934,6 +2979,21 @@ class LlamaCppBackend:
cls._capability_cache[cache_key] = info
return info
@staticmethod
def _mtp_token_from_spec_help(spec_help: str) -> Optional[str]:
"""Extract ``draft-mtp`` / ``mtp`` from a ``--spec-type`` help snippet.
Prefers ``draft-mtp`` (llama.cpp PR #22673) over the later bare ``mtp``
rename. Returns ``None`` when neither token appears as an enum value.
"""
text = spec_help or ""
if "draft-mtp" in text:
return "draft-mtp"
# Bare `mtp` enum token (`|mtp|`, `,mtp,`, ...), not a substring.
if re.search(r"(?<![A-Za-z0-9_-])mtp(?![A-Za-z0-9_-])", text):
return "mtp"
return None
# ── GPU allocation ────────────────────────────────────────────
@staticmethod
@ -6078,6 +6138,24 @@ class LlamaCppBackend:
and ("unknown" in text or "unsupported" in text or "not supported" in text)
)
@staticmethod
def _mmproj_retry_failure_message(*, projector_confirmed: bool, detail: str) -> str:
"""User-facing error when the text-only --mmproj strip retry also fails.
Confirmed projector-format mismatches keep the historical wording.
Bare signal crashes (common on some ROCm/driver paths) must not be
reported as "Vision projector incompatible" that misled #7302.
"""
if projector_confirmed:
return (
"Vision projector incompatible with this llama.cpp "
"build, and the text-only retry also failed: " + detail
)
return (
"Vision model failed to start (llama-server crashed with "
"--mmproj), and the text-only retry also failed: " + detail
)
@staticmethod
def _output_has_nonprojector_diagnostic(output: str) -> bool:
"""True when the output already names a concrete non-projector cause (out
@ -8453,23 +8531,29 @@ class LlamaCppBackend:
self._kill_process()
# The #6415 split-axis abort is latched earlier (first spawn).
# Skip if a cancel/unload is pending (mirrors the MTP guard).
_projector_msg = self._is_projector_incompatibility(out)
_signal_mmproj_guess = self._is_signal_crash(
_crash_rc
) and not self._output_has_nonprojector_diagnostic(out)
if (
launched_with_mmproj
and not self._cancel_event.is_set()
and (
self._is_projector_incompatibility(out)
or (
self._is_signal_crash(_crash_rc)
and not self._output_has_nonprojector_diagnostic(out)
)
)
and (_projector_msg or _signal_mmproj_guess)
):
logger.warning(
"llama-server could not load this model's vision "
"projector (--mmproj). The installed llama.cpp build is "
"likely too old for it. Loading text-only for this "
"session; run 'unsloth studio update' to enable vision."
)
if _projector_msg:
logger.warning(
"llama-server could not load this model's vision "
"projector (--mmproj). The installed llama.cpp build is "
"likely too old for it. Loading text-only for this "
"session; run 'unsloth studio update' to enable vision."
)
else:
logger.warning(
"llama-server crashed while loading this model's vision "
"projector (--mmproj). Retrying text-only for this "
"session; if this persists, run 'unsloth studio update' "
"or check GPU/driver logs."
)
cmd = self._strip_mmproj_args(_last_spawn_cmd)
# This retry bypasses _spawn_and_wait, so refresh the
# launched-argv snapshot itself -- the zero-offload
@ -8497,14 +8581,16 @@ class LlamaCppBackend:
"(e.g. ROCR_VISIBLE_DEVICES=0 exposes only the first "
"GPU) before launching Unsloth Studio."
)
_retry_detail = self._classify_llama_start_failure(
"\n".join(self._stdout_lines[-50:]),
gguf_path,
self._model_identifier,
_retry_rc,
)
raise RuntimeError(
"Vision projector incompatible with this llama.cpp "
"build, and the text-only retry also failed: "
+ self._classify_llama_start_failure(
"\n".join(self._stdout_lines[-50:]),
gguf_path,
self._model_identifier,
_retry_rc,
self._mmproj_retry_failure_message(
projector_confirmed = _projector_msg,
detail = _retry_detail,
)
)
else:
@ -8734,18 +8820,29 @@ class LlamaCppBackend:
caps = self.probe_server_capabilities(binary)
mtp_token = caps.get("mtp_token") if caps else None
if not mtp_token:
logger.warning(
"Requested MTP speculative decoding but "
"llama-server lacks --spec-type mtp/draft-mtp; "
"run `unsloth studio update`. Loading without "
"speculative decoding."
)
inconclusive = bool(caps.get("mtp_probe_inconclusive")) if caps else True
if inconclusive:
logger.info(
"Requested MTP speculative decoding but llama-server MTP "
"capability probe was inconclusive; loading without "
"speculative decoding."
)
else:
logger.warning(
"Requested MTP speculative decoding but "
"llama-server lacks --spec-type mtp/draft-mtp; "
"run `unsloth studio update`. Loading without "
"speculative decoding."
)
# Override an inherited LLAMA_ARG_SPEC_TYPE=draft-mtp (CLI wins
# over env) so the child matches the binary-capability gate and
# the no-MTP budget, like the sibling no-head/non-MTP fallbacks.
flags.append("--spec-default")
self._speculative_type = "default"
self._spec_fallback_reason = "binary_no_mtp"
if inconclusive:
self._spec_fallback_reason = None
else:
self._spec_fallback_reason = "binary_no_mtp"
return False
draft_n_max = _resolved_draft_n_max()
n_max_flag = caps.get("spec_draft_n_max_flag") or "--spec-draft-n-max"
@ -10544,6 +10641,7 @@ class LlamaCppBackend:
reasoning_effort: Optional[str] = None,
preserve_thinking: Optional[bool] = None,
seed: Optional[int] = None,
promote_reasoning_only: bool = True,
_allow_respawn_retry: bool = True,
) -> Generator[Union[str, dict], None, None]:
"""
@ -10626,7 +10724,12 @@ class LlamaCppBackend:
# model put its whole reply in reasoning
# (e.g. Qwen3 always-think). Show it as
# the main response, not a thinking block.
cumulative = reasoning_text
cumulative = _finalize_reasoning_only_cumulative(
cumulative,
reasoning_text,
_metadata_finish_reason,
promote_reasoning_only,
)
yield cumulative
_stream_done = True
break # exit inner while
@ -10723,6 +10826,7 @@ class LlamaCppBackend:
reasoning_effort = reasoning_effort,
preserve_thinking = preserve_thinking,
seed = seed,
promote_reasoning_only = promote_reasoning_only,
_allow_respawn_retry = False,
)
return
@ -10764,6 +10868,7 @@ class LlamaCppBackend:
confirm_tool_calls: bool = False,
bypass_permissions: bool = False,
permission_mode: Optional[str] = None,
promote_reasoning_only: bool = True,
) -> Generator[dict, None, None]:
"""
Agentic loop: let the model call tools, execute them, and continue.
@ -11106,7 +11211,12 @@ class LlamaCppBackend:
),
}
else:
cumulative_display = reasoning_accum
cumulative_display = _finalize_reasoning_only_cumulative(
cumulative_display,
reasoning_accum,
_iter_finish_reason,
promote_reasoning_only,
)
if not _suppress_visible_output:
yield {
"type": "content",
@ -11570,7 +11680,12 @@ class LlamaCppBackend:
if _reasoning_started_at is not None and not _reasoning_summary_emitted:
_reasoning_summary_emitted = True
yield _reasoning_summary_event(_reasoning_started_at)
cumulative_display = reasoning_accum
cumulative_display = _finalize_reasoning_only_cumulative(
cumulative_display,
reasoning_accum,
_iter_finish_reason,
promote_reasoning_only,
)
if not _suppress_visible_output:
yield {
"type": "content",
@ -12134,7 +12249,12 @@ class LlamaCppBackend:
"text": _strip_tool_markup(cumulative, final = True),
}
else:
cumulative = reasoning_text
cumulative = _finalize_reasoning_only_cumulative(
cumulative,
reasoning_text,
_metadata_finish_reason,
promote_reasoning_only,
)
yield {"type": "content", "text": cumulative}
_stream_done = True
break # exit inner while

View file

@ -891,6 +891,7 @@ class UnslothTrainer:
use_gradient_checkpointing: str = "unsloth",
use_rslora: bool = False,
use_loftq: bool = False,
use_dora: bool = False,
modules_to_save: list = None,
) -> bool:
"""
@ -993,6 +994,7 @@ class UnslothTrainer:
use_gradient_checkpointing = use_gradient_checkpointing,
random_state = 3407,
use_rslora = use_rslora,
use_dora = use_dora,
loftq_config = {"loftq_bits": 4, "loftq_iter": 1} if use_loftq else None,
)
# Audio VLM models support VLM-style layer selection
@ -1023,6 +1025,7 @@ class UnslothTrainer:
use_gradient_checkpointing = use_gradient_checkpointing,
random_state = 3407,
use_rslora = use_rslora,
use_dora = use_dora,
loftq_config = {"loftq_bits": 4, "loftq_iter": 1} if use_loftq else None,
task_type = None,
)
@ -1042,6 +1045,7 @@ class UnslothTrainer:
use_gradient_checkpointing = use_gradient_checkpointing,
random_state = 3407,
use_rslora = use_rslora,
use_dora = use_dora,
loftq_config = {"loftq_bits": 4, "loftq_iter": 1} if use_loftq else None,
)
@ -1067,6 +1071,7 @@ class UnslothTrainer:
use_gradient_checkpointing = use_gradient_checkpointing,
random_state = 3407,
use_rslora = use_rslora,
use_dora = use_dora,
loftq_config = {"loftq_bits": 4, "loftq_iter": 1} if use_loftq else None,
modules_to_save = modules_to_save,
)
@ -1087,6 +1092,7 @@ class UnslothTrainer:
use_gradient_checkpointing = use_gradient_checkpointing,
random_state = 3407,
use_rslora = use_rslora,
use_dora = use_dora,
loftq_config = {"loftq_bits": 4, "loftq_iter": 1} if use_loftq else None,
modules_to_save = modules_to_save,
)

View file

@ -196,6 +196,7 @@ def _build_training_worker_config(values: dict[str, Any]) -> dict[str, Any]:
"gradient_checkpointing": values.get("gradient_checkpointing", "unsloth"),
"use_rslora": values.get("use_rslora", False),
"use_loftq": values.get("use_loftq", False),
"use_dora": values.get("use_dora", False),
"train_on_completions": values.get("train_on_completions", False),
"finetune_vision_layers": values.get("finetune_vision_layers", True),
"finetune_language_layers": values.get("finetune_language_layers", True),
@ -455,6 +456,7 @@ class _MLXTrainerAdapter:
use_gradient_checkpointing: Union[str, bool] = "unsloth",
use_rslora: bool = False,
use_loftq: bool = False,
use_dora: bool = False,
) -> bool:
self._peft_config = {
"use_lora": bool(use_lora),
@ -465,6 +467,7 @@ class _MLXTrainerAdapter:
"gradient_checkpointing": use_gradient_checkpointing,
"use_rslora": bool(use_rslora),
"use_loftq": bool(use_loftq),
"use_dora": bool(use_dora),
"finetune_vision_layers": bool(finetune_vision_layers),
"finetune_language_layers": bool(finetune_language_layers),
"finetune_attention_modules": bool(finetune_attention_modules),
@ -572,6 +575,7 @@ class _MLXTrainerAdapter:
"gradient_checkpointing": "unsloth",
"use_rslora": False,
"use_loftq": False,
"use_dora": False,
"finetune_vision_layers": True,
"finetune_language_layers": True,
"finetune_attention_modules": True,

View file

@ -1547,6 +1547,10 @@ def _run_mlx_training(event_queue, stop_queue, config):
message = "LoftQ is not supported for MLX training yet."
_send("error", error = message)
raise NotImplementedError(message)
if config.get("use_dora"):
message = "DoRA is not supported for MLX training yet."
_send("error", error = message)
raise NotImplementedError(message)
if config.get("is_embedding"):
message = "Embedding model training is not supported for MLX training yet."
_send("error", error = message)
@ -3186,6 +3190,7 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
use_gradient_checkpointing = config.get("gradient_checkpointing", "unsloth"),
use_rslora = config.get("use_rslora", False),
use_loftq = config.get("use_loftq", False),
use_dora = config.get("use_dora", False),
)
elif use_lora:
_send_status(event_queue, "Configuring LoRA adapters...")
@ -3202,6 +3207,7 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
use_gradient_checkpointing = config.get("gradient_checkpointing", "unsloth"),
use_rslora = config.get("use_rslora", False),
use_loftq = config.get("use_loftq", False),
use_dora = config.get("use_dora", False),
)
else:
_send_status(event_queue, "Preparing model for full finetuning...")
@ -3630,6 +3636,7 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) ->
use_gradient_checkpointing = gradient_checkpointing,
random_state = config.get("random_seed", 3407),
use_rslora = config.get("use_rslora", False),
use_dora = config.get("use_dora", False),
loftq_config = {"loftq_bits": 4, "loftq_iter": 1}
if config.get("use_loftq")
else None,

View file

@ -445,7 +445,11 @@ def _run_llama_cpp_startup_probes(app: FastAPI) -> None:
import structlog as _structlog
_log = _structlog.get_logger(__name__)
if _caps.get("found") and not _caps.get("supports_mtp"):
if (
_caps.get("found")
and not _caps.get("supports_mtp")
and not _caps.get("mtp_probe_inconclusive")
):
_msg = (
"llama.cpp prebuilt lacks MTP support "
"(--spec-type mtp/draft-mtp). Run `unsloth studio update`. "

View file

@ -70,7 +70,10 @@ class LoadRequest(BaseModel):
cache_type_kv: Optional[str] = Field(
None,
description = "KV cache data type for both K and V (e.g. 'f16', 'bf16', 'q8_0', 'q4_1', 'q5_1')",
description = (
"KV cache data type for both K and V "
"(e.g. 'f16', 'bf16', 'q8_0', 'q4_0', 'q4_1', 'q5_0', 'q5_1', 'iq4_nl', 'f32')"
),
)
gpu_ids: Optional[List[int]] = Field(
None,
@ -442,7 +445,10 @@ class LoadResponse(BaseModel):
)
cache_type_kv: Optional[str] = Field(
None,
description = "KV cache data type for K and V (e.g. 'f16', 'bf16', 'q8_0')",
description = (
"KV cache data type for K and V "
"(e.g. 'f16', 'bf16', 'q8_0', 'q4_0', 'q4_1', 'q5_0', 'q5_1', 'iq4_nl', 'f32')"
),
)
chat_template: Optional[str] = Field(
None,
@ -602,7 +608,11 @@ class InferenceStatusResponse(BaseModel):
)
cache_type_kv: Optional[str] = Field(
None,
description = "KV cache quantization dtype (e.g. 'q8_0'), or None for default",
description = (
"KV cache quantization dtype "
"(e.g. 'f16', 'bf16', 'q8_0', 'q4_0', 'q4_1', 'q5_0', 'q5_1', 'iq4_nl', 'f32'), "
"or None for default"
),
)
chat_template: Optional[str] = Field(
None, description = "Model's default chat template (Jinja2 source), if any"

View file

@ -470,6 +470,7 @@ class TrainingStartRequest(BaseModel):
gradient_checkpointing: str = Field("", description = "Gradient checkpointing setting")
use_rslora: bool = Field(False, description = "Use RSLoRA")
use_loftq: bool = Field(False, description = "Use LoftQ")
use_dora: bool = Field(False, description = "Use DoRA")
train_on_completions: bool = Field(False, description = "Train on completions only")
# Vision-specific LoRA parameters
@ -545,6 +546,37 @@ class TrainingStartRequest(BaseModel):
raise ValueError("Either num_epochs or max_steps must be > 0; both cannot be 0.")
return self
@model_validator(mode = "after")
def _validate_lora_variant_flags(self) -> "TrainingStartRequest":
# The frontend only ever sends one of these and never under Full
# Finetuning, but a direct API/YAML/CLI caller can bypass that. Nothing
# downstream breaks (full finetune ignores them, MLX rejects use_dora/
# use_loftq outright), but reject early here for a clear error instead
# of a silently-ignored flag.
active = [
name
for name, enabled in (
("use_rslora", self.use_rslora),
("use_loftq", self.use_loftq),
("use_dora", self.use_dora),
)
if enabled
]
if len(active) > 1:
raise ValueError(
f"Only one LoRA variant may be enabled at a time; got {active}. "
"use_rslora, use_loftq, and use_dora are mutually exclusive."
)
# getattr, not self.training_type: model_construct() (used by tests that
# validate a single field in isolation) leaves required fields unset, and
# this is a mode="after" validator so it still runs on that partial instance.
if getattr(self, "training_type", None) == "Full Finetuning" and active:
raise ValueError(
f"{active[0]} requires an adapter method (LoRA/QLoRA or "
"Continued Pretraining); it has no effect under Full Finetuning."
)
return self
class TrainingJobResponse(BaseModel):
"""Immediate response when training is initiated"""

View file

@ -160,11 +160,26 @@ class ChatInferenceSettings(BaseModel):
fastMode: Optional[bool] = None
class ChatPresetLoadConfig(BaseModel):
model_config = ConfigDict(extra = "forbid")
customContextLength: Optional[int] = Field(default = None, gt = 0)
maxSeqLength: Optional[float] = None
kvCacheDtype: Optional[str] = None
speculativeType: Optional[str] = None
specDraftNMax: Optional[int] = Field(default = None, ge = 1, le = 16)
tensorParallel: Optional[bool] = None
gpuMemoryMode: Optional[Literal["manual"]] = None
gpuLayers: Optional[int] = None
nCpuMoe: Optional[int] = Field(default = None, ge = 0)
class ChatPreset(BaseModel):
model_config = ConfigDict(extra = "forbid")
name: str
params: ChatInferenceSettings
loadConfig: Optional[ChatPresetLoadConfig] = None
class ChatSettingsPayload(BaseModel):

View file

@ -1794,7 +1794,16 @@ router = APIRouter()
studio_router = APIRouter()
_ARTIFACT_PREVIEW_FRAME_ANCESTORS = "'self' tauri://localhost http://tauri.localhost"
# Packaged desktop runs at tauri://localhost (macOS/Linux) or http://tauri.localhost
# (Windows WebView2); the web build is same-origin ('self'). The `tauri dev` shell,
# however, serves the frontend from the Vite dev origin (http://localhost:5173),
# so the packaged allowlist alone leaves the preview blocked in dev with an
# "ancestor violates frame-ancestors" error. This shell exposes no server resource
# (it only renders postMessage'd HTML in a no-same-origin sandbox), so also allowing
# any localhost/127.0.0.1 dev origin to frame it is safe and unblocks the dev shell.
_ARTIFACT_PREVIEW_FRAME_ANCESTORS = (
"'self' tauri://localhost http://tauri.localhost http://localhost:* http://127.0.0.1:*"
)
_ARTIFACT_PREVIEW_FRAME_STRICT_CSP = (
"default-src 'none'; "
"script-src 'unsafe-inline'; "
@ -5843,10 +5852,15 @@ async def get_status(current_subject: str = Depends(get_current_subject)):
try:
_bin = type(llama_backend)._find_llama_server_binary()
_caps = type(llama_backend).probe_server_capabilities(_bin)
_supports_mtp = bool(_caps.get("supports_mtp", False))
# Fail open on inconclusive probes: False means a definitive
# "binary lacks MTP" to API consumers.
_supports_mtp = bool(
_caps.get("supports_mtp", False)
or (_caps.get("found", False) and _caps.get("mtp_probe_inconclusive", False))
)
except Exception:
_bin = None
_supports_mtp = True # fail open
_supports_mtp = False # no usable binary: MTP genuinely unavailable
try:
from utils.llama_cpp_freshness import check_prebuilt_freshness
_freshness = check_prebuilt_freshness(_bin)
@ -13358,6 +13372,7 @@ async def anthropic_messages(
disable_parallel_tool_use = _disable_parallel,
bypass_permissions = bool(payload.bypass_permissions),
permission_mode = getattr(payload, "permission_mode", None),
promote_reasoning_only = False,
)
if payload.stream:
@ -13397,6 +13412,7 @@ async def anthropic_messages(
max_tokens = payload.max_tokens,
stop = stop,
cancel_event = cancel_event,
promote_reasoning_only = False,
)
if payload.stream:

View file

@ -2692,7 +2692,10 @@ async def get_kv_cache_estimate(
repo_id: str = Query(..., description = "HF repo ID or local path"),
quant: str = Query(..., description = "Quantization label (e.g. Q4_K_M)"),
n_ctx: int = Query(..., ge = 1, description = "Context length to size the KV cache for"),
cache_type_kv: Optional[str] = Query(None, description = "KV cache dtype (e.g. q8_0)"),
cache_type_kv: Optional[str] = Query(
None,
description = "KV cache dtype (e.g. q8_0, q4_0, q5_0, iq4_nl, f32)",
),
current_subject: str = Depends(get_current_subject),
):
"""Estimate KV cache + weight bytes for a downloaded GGUF at n_ctx.

View file

@ -332,6 +332,7 @@ async def start_training(
else "unsloth",
"use_rslora": request.use_rslora,
"use_loftq": request.use_loftq,
"use_dora": request.use_dora,
"train_on_completions": request.train_on_completions,
"finetune_vision_layers": request.finetune_vision_layers,
"finetune_language_layers": request.finetune_language_layers,

View file

@ -68,16 +68,15 @@ def _emitter_client_text(events: list[str]) -> str:
def test_anthropic_emitter_closes_reasoning_only_think_block():
# A reasoning-only reply streams <think>X live then shrinks to bare X at EOF.
# This emitter diffs cumulative snapshots and drops the shrink, so without a
# closing pass the client text would end on an unclosed <think>. finish()
# must balance it.
# Anthropic asks the GGUF generator not to promote reasoning into a duplicate
# visible fallback, so its final cumulative snapshot only balances the block.
emitter = AnthropicStreamEmitter()
events = emitter.start("msg_1", "m")
events += emitter.feed({"type": "content", "text": "<think>The capital"})
events += emitter.feed({"type": "content", "text": "<think>The capital of France is Paris."})
# The generator's final bare-text shrink (dropped by the cumulative diff).
events += emitter.feed({"type": "content", "text": "The capital of France is Paris."})
events += emitter.feed(
{"type": "content", "text": "<think>The capital of France is Paris.</think>"}
)
events += emitter.finish()
assert _emitter_client_text(events) == "<think>The capital of France is Paris.</think>"
@ -1563,6 +1562,44 @@ class TestAnthropicMessagesToolRouting:
assert entry["context_length"] == 2048
assert monitor.active_count() == 0
@pytest.mark.parametrize("stream", [False, True])
@pytest.mark.parametrize("with_tools", [False, True])
def test_reasoning_only_output_is_not_duplicated(self, monkeypatch, stream, with_tools):
reasoning = "The capital of France is Paris."
def _gen_plain(**kwargs):
assert kwargs["promote_reasoning_only"] is False
yield f"<think>{reasoning}"
yield f"<think>{reasoning}</think>"
def _gen_tools(**kwargs):
assert kwargs["promote_reasoning_only"] is False
yield {"type": "content", "text": f"<think>{reasoning}"}
yield {"type": "content", "text": f"<think>{reasoning}</think>"}
_mock_backend(
monkeypatch,
generate_chat_completion = _gen_plain,
generate_chat_completion_with_tools = _gen_tools,
)
payload_fields = {"stream": stream}
if with_tools:
payload_fields.update(
{
"enable_tools": True,
"tools": [{"type": "web_search_20250305", "name": "web_search"}],
}
)
payload = _basic_payload(**payload_fields)
response = _drive(anthropic_messages(payload, request = self._Request(), current_subject = "t"))
if stream:
body = self._sse_blob(self._consume_response(response))
assert body.count(reasoning) == 1
else:
body = json.loads(response.body)
assert body["content"][0]["text"] == f"<think>{reasoning}</think>"
def test_tool_use_non_streaming_records_api_monitor_reply(self, monkeypatch):
import routes.inference as inf_mod

View file

@ -91,6 +91,28 @@ def test_chat_settings_payload_accepts_fast_mode_presets():
assert dumped["customPresets"][0]["params"]["fastMode"] is True
def test_chat_settings_payload_accepts_preset_load_config():
payload = chat_history.ChatSettingsPayload.model_validate(
{
"customPresets": [
{
"name": "GGUF preset",
"params": {"temperature": 0.7, "maxTokens": 512},
"loadConfig": {
"customContextLength": 256,
"kvCacheDtype": "q8_0",
"tensorParallel": False,
},
},
],
}
)
dumped = payload.model_dump(exclude_unset = True)
assert dumped["customPresets"][0]["loadConfig"]["customContextLength"] == 256
assert dumped["customPresets"][0]["loadConfig"]["kvCacheDtype"] == "q8_0"
def test_chat_settings_payload_accepts_nudge_tool_calls():
# extra="forbid" 400s PUT /api/chat/settings on unknown keys, so the
# frontend's persisted nudgeToolCalls needs a payload field (like

View file

@ -0,0 +1,479 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Regression coverage for Colab iframe embedding (#7344)."""
import sys
import types
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import colab
def _mock_google_colab_modules(colab_mod):
"""Mock ``google`` and ``google.colab`` for environments without Google packages."""
google_mod = types.ModuleType("google")
google_mod.colab = colab_mod
return {"google": google_mod, "google.colab": colab_mod}
def test_short_colab_url_truncates_proxy_host():
url = "https://8888-gpu-a100-s-kkb-usc1f0-9hzedjcxrlu8-f.us-central1-0.prod.colab.dev/"
assert colab._short_colab_url(url, 8888) == "https://8888-gpu-..."
def test_short_colab_url_falls_back_on_unexpected_shape():
assert colab._short_colab_url("https://example.com", 8888) == "https://example.com"
def test_is_colab_proxy_url_requires_https_proxy():
assert colab._is_colab_proxy_url("https://8888-test.prod.colab.dev/", 8888) is True
assert colab._is_colab_proxy_url("http://localhost:8888", 8888) is False
assert colab._is_colab_proxy_url("http://127.0.0.1:8888", 8888) is False
def test_ready_card_html_does_not_open_colab_proxy_in_new_tab():
"""Colab proxy hosts 404 as top-level tabs (#7349 reporter); never window.open them."""
html = colab._ready_card_html("https://8888-test.prod.colab.dev/", 8888)
assert "window.open" not in html
assert 'href="https://8888-test.prod.colab.dev/"' not in html
assert "start(cloudflare=True)" in html
def test_ready_card_html_points_to_cloudflare_when_link_ready(monkeypatch):
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: True)
html = colab._ready_card_html(
"https://8888-test.prod.colab.dev/",
8888,
has_cloudflare_link = True,
)
assert "Cloudflare link above" in html
def test_ready_card_html_warns_when_cloudflare_tunnel_missing(monkeypatch):
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: True)
html = colab._ready_card_html(
"https://8888-test.prod.colab.dev/",
8888,
cloudflare_requested = True,
)
assert "Could not open a Cloudflare tunnel" in html
def test_warn_colab_cloudflare_missing_logs_on_colab_without_tunnel(monkeypatch):
warnings: list[str] = []
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: True)
monkeypatch.setattr(colab.logger, "warning", lambda msg, **kwargs: warnings.append(msg))
colab._warn_colab_cloudflare_missing(use_cloudflare = True, cloudflare_url = None)
assert warnings
assert "Cloudflare tunnel unavailable" in warnings[0]
def test_warn_colab_cloudflare_missing_skips_when_tunnel_ready(monkeypatch, caplog):
import logging
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: True)
with caplog.at_level(logging.WARNING):
colab._warn_colab_cloudflare_missing(
use_cloudflare = True,
cloudflare_url = "https://share.trycloudflare.com",
)
assert "Cloudflare tunnel unavailable" not in caplog.text
def test_is_colab_runtime_uses_backend_colab_detector(monkeypatch):
fake_main = types.ModuleType("main")
fake_main._IS_COLAB = True
monkeypatch.setitem(sys.modules, "main", fake_main)
assert colab._is_colab_runtime() is True
fake_main._IS_COLAB = False
assert colab._is_colab_runtime() is False
def test_ready_card_html_uses_cloudflare_hint_on_colab_runtime_localhost(monkeypatch):
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: True)
html = colab._ready_card_html("http://localhost:8888", 8888)
assert "window.open" not in html
assert "start(cloudflare=True)" in html
def test_ready_card_html_keeps_open_button_for_localhost_outside_colab(monkeypatch):
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: False)
html = colab._ready_card_html("http://localhost:8888", 8888)
assert "window.open" in html
assert 'href="http://localhost:8888"' in html
assert "Open Unsloth Studio" in html
def test_embed_kernel_port_iframe_uses_colab_helper(monkeypatch):
colab_output = MagicMock()
google_colab = SimpleNamespace(output = colab_output)
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: True)
with patch.dict("sys.modules", _mock_google_colab_modules(google_colab)):
assert colab._embed_kernel_port_iframe(8888) is True
colab_output.serve_kernel_port_as_iframe.assert_called_once_with(
8888,
height = colab._COLAB_IFRAME_HEIGHT,
width = "100%",
)
def test_embed_kernel_port_iframe_returns_false_without_colab():
with patch.dict("sys.modules", _mock_google_colab_modules(None)):
assert colab._embed_kernel_port_iframe(8888) is False
def test_embed_kernel_port_iframe_skips_colabtools_without_runtime(monkeypatch):
"""colabtools can queue JS without appending an iframe; only trust the helper on Colab."""
colab_output = MagicMock()
google_colab = SimpleNamespace(output = colab_output)
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: False)
with patch.dict("sys.modules", _mock_google_colab_modules(google_colab)):
assert colab._embed_kernel_port_iframe(8888) is False
colab_output.serve_kernel_port_as_iframe.assert_not_called()
def test_show_and_embed_prefers_kernel_port_iframe(monkeypatch):
calls: list[str] = []
monkeypatch.setattr(colab, "get_colab_url", lambda port: f"https://{port}-test.prod.colab.dev/")
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: True)
monkeypatch.setattr(
colab,
"show_link",
lambda port,
*,
_url = None,
has_cloudflare_link = False,
cloudflare_requested = False: calls.append("show_link"),
)
monkeypatch.setattr(
colab,
"_embed_kernel_port_iframe",
lambda port: calls.append("kernel_iframe") or True,
)
monkeypatch.setattr(
colab,
"_embed_html_iframe",
lambda url, port: calls.append("html_iframe") or True,
)
colab._show_and_embed(8888)
assert calls == ["show_link", "kernel_iframe"]
def test_show_and_embed_falls_back_to_html_iframe(monkeypatch):
calls: list[str] = []
monkeypatch.setattr(colab, "get_colab_url", lambda port: f"https://{port}-test.prod.colab.dev/")
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: False)
monkeypatch.setattr(
colab,
"show_link",
lambda port, *, _url = None, has_cloudflare_link = False: None,
)
monkeypatch.setattr(colab, "_embed_kernel_port_iframe", lambda port: False)
monkeypatch.setattr(
colab,
"_embed_html_iframe",
lambda url, port: calls.append((url, port)) or True,
)
colab._show_and_embed(8888)
assert calls == [("https://8888-test.prod.colab.dev/", 8888)]
def test_colab_wants_cloudflare_auto_enables_on_runtime(monkeypatch):
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: True)
assert colab._colab_wants_cloudflare(None) is True
assert colab._colab_wants_cloudflare(True) is True
assert colab._colab_wants_cloudflare(False) is False
def test_colab_wants_cloudflare_defaults_off_outside_runtime(monkeypatch):
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: False)
assert colab._colab_wants_cloudflare(None) is False
assert colab._colab_wants_cloudflare(True) is True
def test_finalize_colab_admin_password_skips_outside_runtime(monkeypatch):
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: False)
assert colab._finalize_colab_admin_password() is None
def test_finalize_colab_admin_password_clears_bootstrap_gate(monkeypatch):
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: True)
monkeypatch.setattr(colab, "_load_colab_login_credentials", lambda: None)
stored: list[tuple[str, str]] = []
monkeypatch.setattr(
colab,
"_store_colab_login_credentials",
lambda username, password: stored.append((username, password)),
)
storage = SimpleNamespace(
DEFAULT_ADMIN_USERNAME = "unsloth",
ensure_default_admin = MagicMock(),
get_bootstrap_password = MagicMock(return_value = "alpha-beta-gamma"),
generate_bootstrap_password = MagicMock(return_value = "alpha-beta-gamma"),
requires_password_change = MagicMock(return_value = True),
update_password = MagicMock(return_value = True),
)
auth_pkg = types.ModuleType("auth")
auth_pkg.storage = storage
with patch.dict("sys.modules", {"auth": auth_pkg, "auth.storage": storage}):
result = colab._finalize_colab_admin_password()
assert result == ("unsloth", "alpha-beta-gamma")
storage.ensure_default_admin.assert_called_once()
storage.update_password.assert_called_once_with("unsloth", "alpha-beta-gamma")
assert stored == [("unsloth", "alpha-beta-gamma")]
def test_start_skips_finalize_when_cloudflare_disabled(monkeypatch):
import time
finalize_calls: list[str] = []
monkeypatch.setattr(colab, "_is_studio_healthy", lambda port: True)
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: True)
monkeypatch.setattr(
colab,
"_finalize_colab_admin_password",
lambda: finalize_calls.append("finalize") or ("unsloth", "secret"),
)
monkeypatch.setattr(
colab, "start_cloudflare_tunnel", lambda port: "https://share.trycloudflare.com"
)
monkeypatch.setattr(colab, "_publish_cloudflare_url", lambda url: None)
monkeypatch.setattr(colab, "_show_and_embed", lambda port, **kwargs: None)
monkeypatch.setattr(colab, "_stop_cloudflare_tunnel", lambda: None)
monkeypatch.setattr(time, "sleep", lambda _: (_ for _ in ()).throw(KeyboardInterrupt))
colab.start(cloudflare = False)
assert finalize_calls == []
def test_finalize_colab_admin_password_redisplay_on_rerun(monkeypatch):
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: True)
monkeypatch.setattr(
colab,
"_load_colab_login_credentials",
lambda: ("unsloth", "saved-pass"),
)
monkeypatch.setattr(colab, "_colab_credentials_still_valid", lambda username, password: True)
storage = SimpleNamespace(
DEFAULT_ADMIN_USERNAME = "unsloth",
ensure_default_admin = MagicMock(),
get_bootstrap_password = MagicMock(),
generate_bootstrap_password = MagicMock(),
requires_password_change = MagicMock(return_value = False),
update_password = MagicMock(),
)
auth_pkg = types.ModuleType("auth")
auth_pkg.storage = storage
with patch.dict("sys.modules", {"auth": auth_pkg, "auth.storage": storage}):
result = colab._finalize_colab_admin_password()
assert result == ("unsloth", "saved-pass")
storage.update_password.assert_not_called()
def test_finalize_colab_admin_password_drops_stale_cached_credentials(monkeypatch):
"""After an in-app password change the cached first-run password no longer
authenticates, so it must not be redisplayed (#7349 Codex review)."""
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: True)
monkeypatch.setattr(
colab,
"_load_colab_login_credentials",
lambda: ("unsloth", "stale-pass"),
)
monkeypatch.setattr(colab, "_colab_credentials_still_valid", lambda username, password: False)
cleared: list[bool] = []
monkeypatch.setattr(colab, "_clear_colab_login_credentials", lambda: cleared.append(True))
storage = SimpleNamespace(
DEFAULT_ADMIN_USERNAME = "unsloth",
ensure_default_admin = MagicMock(),
get_bootstrap_password = MagicMock(),
generate_bootstrap_password = MagicMock(),
requires_password_change = MagicMock(return_value = False),
update_password = MagicMock(),
)
auth_pkg = types.ModuleType("auth")
auth_pkg.storage = storage
with patch.dict("sys.modules", {"auth": auth_pkg, "auth.storage": storage}):
result = colab._finalize_colab_admin_password()
assert result is None
assert cleared == [True]
storage.update_password.assert_not_called()
def test_colab_credentials_still_valid_matches_stored_hash(monkeypatch):
from auth.hashing import hash_password
salt, pwd_hash = hash_password("right-pass")
storage = SimpleNamespace(
get_user_and_secret = MagicMock(return_value = (salt, pwd_hash, "jwt", False)),
)
with patch.dict("sys.modules", {"auth.storage": storage}):
assert colab._colab_credentials_still_valid("unsloth", "right-pass") is True
assert colab._colab_credentials_still_valid("unsloth", "wrong-pass") is False
def test_colab_credentials_still_valid_false_when_user_missing(monkeypatch):
storage = SimpleNamespace(get_user_and_secret = MagicMock(return_value = None))
with patch.dict("sys.modules", {"auth.storage": storage}):
assert colab._colab_credentials_still_valid("unsloth", "any") is False
def test_colab_login_html_includes_credentials():
html = colab._colab_login_html("unsloth", "alpha-beta-gamma-delta")
assert "unsloth" in html
assert "alpha-beta-gamma-delta" in html
def test_show_and_embed_renders_cloudflare_before_colab_login(monkeypatch):
displayed: list[str] = []
ipython_display = SimpleNamespace(
HTML = lambda html: SimpleNamespace(html = html),
display = lambda html: displayed.append(html.html),
)
monkeypatch.setattr(colab, "get_colab_url", lambda port: "https://8888-test.prod.colab.dev/")
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: True)
monkeypatch.setattr(
colab,
"show_link",
lambda port, *, _url = None, has_cloudflare_link = False, cloudflare_requested = False: None,
)
monkeypatch.setattr(colab, "_embed_kernel_port_iframe", lambda port: True)
with patch.dict("sys.modules", {"IPython.display": ipython_display}):
colab._show_and_embed(
8888,
cloudflare_url = "https://share.trycloudflare.com",
colab_login = ("unsloth", "secret-pass"),
)
assert len(displayed) == 2
assert "share.trycloudflare.com" in displayed[0]
assert "secret-pass" in displayed[1]
def test_show_and_embed_skips_iframe_on_colab_when_cloudflare_ready(monkeypatch):
calls: list[str] = []
monkeypatch.setattr(colab, "get_colab_url", lambda port: f"https://{port}-test.prod.colab.dev/")
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: True)
monkeypatch.setattr(
colab,
"show_link",
lambda port, *, _url = None, has_cloudflare_link = False, cloudflare_requested = False: None,
)
monkeypatch.setattr(
colab,
"_embed_kernel_port_iframe",
lambda port: calls.append("kernel_iframe") or True,
)
monkeypatch.setattr(
colab,
"_embed_html_iframe",
lambda url, port: calls.append("html_iframe") or True,
)
colab._show_and_embed(8888, cloudflare_url = "https://share.trycloudflare.com")
assert calls == []
def test_show_and_embed_uses_kernel_helper_on_colab_runtime_despite_localhost(monkeypatch):
calls: list[str] = []
monkeypatch.setattr(colab, "get_colab_url", lambda port: f"http://localhost:{port}")
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: True)
monkeypatch.setattr(
colab,
"show_link",
lambda port,
*,
_url = None,
has_cloudflare_link = False,
cloudflare_requested = False: calls.append("show_link"),
)
monkeypatch.setattr(
colab,
"_embed_kernel_port_iframe",
lambda port: calls.append("kernel_iframe") or True,
)
monkeypatch.setattr(
colab,
"_embed_html_iframe",
lambda url, port: calls.append("html_iframe") or True,
)
colab._show_and_embed(8888)
assert calls == ["show_link", "kernel_iframe"]
def test_show_and_embed_skips_kernel_helper_for_localhost_outside_colab(monkeypatch):
calls: list[str] = []
monkeypatch.setattr(colab, "get_colab_url", lambda port: f"http://localhost:{port}")
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: False)
monkeypatch.setattr(
colab,
"show_link",
lambda port,
*,
_url = None,
has_cloudflare_link = False,
cloudflare_requested = False: calls.append("show_link"),
)
monkeypatch.setattr(
colab,
"_embed_kernel_port_iframe",
lambda port: calls.append("kernel_iframe") or True,
)
monkeypatch.setattr(
colab,
"_embed_html_iframe",
lambda url, port: calls.append("html_iframe") or True,
)
colab._show_and_embed(8888)
assert calls == ["show_link", "html_iframe"]
def test_show_and_embed_still_embeds_when_show_link_fails(monkeypatch):
calls: list[str] = []
monkeypatch.setattr(colab, "get_colab_url", lambda port: f"https://{port}-test.prod.colab.dev/")
monkeypatch.setattr(colab, "_is_colab_runtime", lambda: True)
monkeypatch.setattr(
colab,
"show_link",
lambda port, *, _url = None: (_ for _ in ()).throw(RuntimeError("no display")),
)
monkeypatch.setattr(
colab,
"_embed_kernel_port_iframe",
lambda port: calls.append("kernel_iframe") or True,
)
monkeypatch.setattr(
colab,
"_embed_html_iframe",
lambda url, port: calls.append("html_iframe") or True,
)
colab._show_and_embed(8888)
assert calls == ["kernel_iframe"]

View file

@ -165,6 +165,23 @@ def test_skips_local_path():
assert "local" in d.reason
def test_scans_inactive_hf_cache_snapshot_path(tmp_path):
# An inactive HF cache loads by snapshot path; the gate must recover the repo id +
# commit from models--org--repo/snapshots/<rev> and scan that exact commit, not exempt
# it and not fall back to the default branch (an older commit may hold a dropped pickle).
snapshot = tmp_path / "models--evil--repo" / "snapshots" / "deadbeef"
snapshot.mkdir(parents = True)
status = {
"scansDone": True,
"filesWithIssues": [{"path": "pytorch_model.bin", "level": "unsafe"}],
}
with _patch_status(status) as model_info:
d = evaluate_file_security(str(snapshot))
assert d.blocked is True
assert model_info.call_args.args[0] == "evil/repo"
assert model_info.call_args.kwargs["revision"] == "deadbeef"
def test_remote_gguf_named_repo_is_still_scanned():
# Only LOCAL paths skip the Hub scan, so a remote .gguf repo is still scanned and a
# poisoned pickle smuggled into it is blocked.

View file

@ -789,9 +789,12 @@ class TestLoadHubDownloadExclusion:
# The gguf_load_in_flight marker must be entered before the hub-download
# guard and the unload so a concurrent load can't race the download
# manager. The llama_extra_args inheritance that used to sit between the
# marker and the guard now runs in _guard_chat_load_against_training, ahead
# of the GGUF branch, so it is no longer a landmark inside this slice.
# manager. The llama_extra_args inheritance moved out of the branch into
# _resolve_inherited_extra_args, which must still run BEFORE it: the
# inherited value (e.g. a carried --no-mmproj) shapes the guard's
# require_mmproj. Anchor on the call form so the assertion pins the
# endpoint's call site, not the function definition.
assert source.index("= _resolve_inherited_extra_args(") < source.index("if config.is_gguf:")
assert (
gguf_branch.index("enter_context(gguf_load_in_flight")
< gguf_branch.index("_hub_download_blocks_gguf_load")

Some files were not shown because too many files have changed in this diff Show more