Skip to content

fix(studio): load run.py by path for editable installs#5909

Merged
danielhanchen merged 8 commits into
unslothai:mainfrom
jimdawdy-hub:fix/studio-editable-run-import
Jun 12, 2026
Merged

fix(studio): load run.py by path for editable installs#5909
danielhanchen merged 8 commits into
unslothai:mainfrom
jimdawdy-hub:fix/studio-editable-run-import

Conversation

@jimdawdy-hub

Copy link
Copy Markdown
Contributor

Summary

  • Add _load_run_module() in unsloth_cli/commands/studio.py to import studio/backend/run.py by file path via the existing _find_run_py() helper, instead of from studio.backend.run import ….
  • Use _load_run_module() in both studio run and plain unsloth studio startup paths (server launch, shutdown hooks, external-IP display).

This fixes ModuleNotFoundError: No module named 'studio.backend.run' when developing with pip install -e after unsloth studio update --local.

Root cause

studio update / plugin build steps can leave a partial package tree under the Studio venv:

~/.unsloth/studio/unsloth_studio/lib/python3.13/site-packages/studio/backend/
├── plugins/          # build artefacts only
└── (no run.py)

Python’s import machinery can resolve studio / studio.backend to that shadow tree instead of the editable checkout at …/Projects/unsloth/studio/backend/run.py. The CLI then crashes at:

from studio.backend.run import run_server, _resolve_external_ip

because run.py never existed in the shadow directory.

_find_run_py() already locates the real run.py without relying on CWD; this PR uses it for the actual import as well.

Related PRs

Independent of the chat race / mmproj fixes:

No file overlap with those PRs (this change is CLI-only).

Reproduction (before fix)

Environment: Arch Linux, dual RTX 5060 Ti, editable install (pip install -e /home/jim/Projects/unsloth), Studio venv at ~/.unsloth/studio/unsloth_studio.

Command:

/home/jim/.unsloth/studio/unsloth_studio/bin/unsloth studio run \
  -m unsloth/Qwen3.6-27B-MTP-GGUF \
  --variant UD-IQ2_XXS \
  --no-mmproj

Result: immediate crash — CLI never reaches model load:

╭───────────────────── Traceback (most recent call last) ──────────────────────╮
│ /home/jim/Projects/unsloth/unsloth_cli/commands/studio.py:1020 in run        │
│                                                                              │
│   1017 │   │   │   os.execvp(str(studio_bin), args)                          │
│   1018 │                                                                     │
│   1019 │   # ── 2. Start server (always suppress built-in banner)            │
│        ─────────────                                                         │
│ ❱ 1020 │   from studio.backend.run import run_server, _resolve_external_ip   │
│   1021 │                                                                     │
│   1022 │   run_kwargs = dict(host = host, port = port, silent = True,        │
│        llama_parallel_slots = parallel)                                      │
│   1023 │   if frontend is not None:                                          │
╰──────────────────────────────────────────────────────────────────────────────╯
ModuleNotFoundError: No module named 'studio.backend.run'

Note: --variant is a llama-server pass-through flag (invalid for this repo’s CLI). Use --gguf-variant for the Studio quant selector. The failure above occurs before llama-server starts, on the import line.

Workaround without this patch:

rm -rf ~/.unsloth/studio/unsloth_studio/lib/python3.13/site-packages/studio

(Must be repeated after studio update recreates the shadow tree.)

Verification (after fix)

Same machine, same editable install, with partial shadow tree present at
site-packages/studio/backend/plugins/ (no run.py):

editable run.py exists: True
shadow site-packages/studio exists: True
shadow backend/run.py exists: False
_load_run_module: OK - /home/jim/Projects/unsloth/studio/backend/run.py

Successful studio run (correct Studio flag):

/home/jim/.unsloth/studio/unsloth_studio/bin/unsloth studio run \
  -m unsloth/Qwen3.6-27B-MTP-GGUF \
  --gguf-variant UD-IQ2_XXS \
  --no-mmproj
INFO:     Started server process [2335602]
INFO:     Waiting for application startup.
Hardware detected: CUDA -- NVIDIA GeForce RTX 5060 Ti
INFO:     Application startup complete.
INFO:     Unsloth Studio running on http://127.0.0.1:8890
Starting Unsloth Studio...
Loading model: unsloth/Qwen3.6-27B-MTP-GGUF...
{"timestamp": "2026-05-31T23:38:36.245652Z", "level": "info", "event": "Detected remote GGUF repo 'unsloth/Qwen3.6-27B-MTP-GGUF', variant=UD-IQ2_XXS, vision=True"}
{"timestamp": "2026-05-31T23:38:36.705110Z", "level": "info", "event": "Appending user extra args to llama-server: ['--no-mmproj']"}
{"timestamp": "2026-05-31T23:38:36.705165Z", "level": "info", "event": "Starting llama-server: .../Qwen3.6-27B-UD-IQ2_XXS.gguf ... --no-mmproj"}
{"timestamp": "2026-05-31T23:39:04.985285Z", "level": "info", "event": "llama-server ready on port 37507 for model 'unsloth/Qwen3.6-27B-MTP-GGUF'"}
{"timestamp": "2026-05-31T23:39:05.052035Z", "level": "info", "event": "Loaded GGUF model via llama-server: unsloth/Qwen3.6-27B-MTP-GGUF"}
========================================================
  Unsloth Studio running at http://127.0.0.1:8890
  Model loaded: unsloth/Qwen3.6-27B-MTP-GGUF (UD-IQ2_XXS)
  API Key:      sk-unsloth-…
========================================================

No ModuleNotFoundError. Model loads with --no-mmproj honored (no mmproj download; llama-server launched with --no-mmproj only).

Test plan

  • pip install -e . into Studio venv, run unsloth studio update --local, confirm partial site-packages/studio/backend/ exists
  • unsloth studio run -m <gguf-repo> --gguf-variant <variant> --no-mmproj starts without import error
  • Plain unsloth studio -p 8888 still starts and shuts down cleanly (Ctrl+C)
  • Non-editable (pip install .) installs unaffected — _find_run_py() still resolves run.py under site-packages when present

Made with Cursor

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a dynamic module loader, _load_run_module, in unsloth_cli/commands/studio.py to load studio.backend.run directly by its file path. This change prevents import conflicts where partial site-packages installations shadow editable installs. The command functions have been refactored to use this dynamic loader instead of static imports. Feedback was provided to optimize the loader by avoiding unnecessary path resolution when the module is not already loaded, and to defensively clean up sys.modules if the module execution fails.

Comment thread unsloth_cli/commands/studio.py
@jimdawdy-hub

Copy link
Copy Markdown
Contributor Author

The one Codex item (null-check before resolving loaded_path, and rolling back sys.modules on exec_module failure) was addressed in a prior commit.

Pushed f39dee5 to resolve the merge conflict. The dirty state was caused by commit 6f27ecc6 on main (a CI merge that reset studio.py to a version without the PR's changes). f39dee5 rebases the functional _load_run_module() changes onto the current upstream studio.py, dropping only the cosmetic line-wrapping changes that had no functional significance.

No requested reviewers are set on this PR — could a maintainer assign reviewers and approve the action_required CI workflows?

`studio update` can leave a partial site-packages/studio/backend/ tree
(plugin build artefacts only). That shadowed tree wins over an editable
install and breaks `from studio.backend.run import ...`. Loading run.py
by file path via importlib sidesteps the conflict.

The module is cached in _RUN_MODULE so repeated calls are cheap.
If exec_module fails, the module is removed from sys.modules before
re-raising so a subsequent retry starts clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jimdawdy-hub jimdawdy-hub force-pushed the fix/studio-editable-run-import branch from f39dee5 to 74b7805 Compare June 9, 2026 04:56
@danielhanchen danielhanchen self-assigned this Jun 11, 2026
@danielhanchen

Copy link
Copy Markdown
Member

Pushed 58bb422 which handles one edge case: if a partial shadow tree leaves studio.backend.run in sys.modules as a namespace package, __file__ is None and the cached-module path check would raise TypeError. Now coerced to an empty string so we fall through to the path-based load. Rest of the PR looks good - thanks for the detailed root cause writeup.

@jimdawdy-hub

Copy link
Copy Markdown
Contributor Author

Synced with latest main and cleared open review threads.

  • Merged origin/main into this branch; PR is mergeable again.
  • Resolved remaining Codex/Gemini review threads (including items already addressed in @danielhanchen's follow-up commits).

Waiting on maintainer review/approval. pre-commit.ci is the only automated gate visible from fork PRs; GitHub Actions still require maintainer approval for first-time contributors.

@danielhanchen

Copy link
Copy Markdown
Member

Ran cross-platform simulations (isolated uv venv + real-filesystem reproduction) and found two issues in the editable-install module loaders, now fixed:

  1. _load_backend_auth_storage (which arrived from the unsloth chat work on main) had the same __file__ = None crash that _load_run_module was hardened against: a namespace-package entry in sys.modules makes getattr(loaded, "__file__", "") return None, and Path(None) raises TypeError. Reproduced it on a real FS, then fixed by coercing None -> "" and only computing the path inside the is not None guard. While there I made the cache-key comparison resolve-symmetric (storage_py.resolve()), which also lets the cache actually hit on repeat calls.

  2. The new in-venv tests in test_studio_run_parallel_flag.py and test_studio_cloudflare_flag.py (added on main after this PR opened) mock the backend by pre-seeding sys.modules['studio.backend.run']. That worked with the old from studio.backend.run import ..., but _load_run_module() deliberately ignores a sys.modules entry whose __file__ does not match the real run.py, so the mock was bypassed and 7 tests failed. Adapted those tests to inject the mock as the cached run module so they exercise the new loader. All 41 tests in those two files pass.

Filesystem sim also confirms the core fix: the original ModuleNotFoundError reproduces without the PR and is resolved by _load_run_module, across both Linux (lib/python*/site-packages) and Windows (Lib/site-packages) layouts.

@danielhanchen

Copy link
Copy Markdown
Member

@jimdawdy-hub Appreciate the PR - this works!

@danielhanchen danielhanchen merged commit 3e69206 into unslothai:main Jun 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants