Fix Windows no-torch setup (#7511)

* Fix Windows no-torch setup

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

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

* Fix no-torch env normalization on Windows

* Accept on for Windows no-torch mode

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

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

* Keep no-torch mode across studio update on Windows

Guarding the direct torch/Triton install made `install.ps1 --no-torch`
actually produce a torch-free venv, which then broke the next
`unsloth studio update`. That path exports no UNSLOTH_NO_TORCH, so
$NoTorchMode was false, the stale-venv check read the missing torch as a
broken venv, and setup tried to delete the venv it was running out of:

  [ERROR] Could not remove stale venv: Access to the path 'python.exe' is denied.

That teardown can never succeed there, because setup.ps1 runs via
unsloth.exe out of that same venv. The same gap also let the shared
dependency pass reinstall torch from PyPI, unpinned, into a GGUF-only
environment.

install_python_stack.py now records the mode in the install manifest and
setup.ps1 reads it back when no env var is exported, then re-exports a
canonical value for the dependency pass (setup.ps1 drops the manifest
before invoking it, so the child cannot repeat the lookup). The key is
additive and MANIFEST_SCHEMA is unchanged, so existing manifests stay
valid and a missing key keeps today's behaviour.

Also:
- read_manifest() caught only OSError, but UnicodeDecodeError is a
  ValueError. That is now on the installer's import path, so a manifest
  re-saved as ANSI or truncated mid-write would abort every install.
- The env predicate now trims surrounding whitespace, matching the
  Python side.
- The Windows update smoke workflow asserts the update leaves the venv
  GGUF-only, which is what would have caught this.

Known follow-up, pre-existing: an install killed between the manifest
drop and the dependency pass leaves no recorded mode, so a later update
still walks the stale-venv path. Closing that needs a marker the
installer never drops.

* Persist no-torch mode in a marker the dependency pass cannot drop

The install manifest alone was not enough. Both setup.ps1 and
install_python_stack.py remove it before every dependency pass, and it is
only rewritten on success, so a no-torch install interrupted in between
left nothing recording the mode. The next update then resolved no-torch
as false, read the expected missing torch as a stale venv, and tried to
delete the environment whose python.exe was running it, which leaves the
install unrepairable from the CLI.

Add .unsloth-no-torch next to the existing .unsloth-studio-owned marker,
written before the pass and cleared when torch is wanted. setup.ps1
writes it as soon as the mode resolves, so the window between the
manifest drop and its own torch install is covered too.

Read order stays manifest key first, then marker, so migrating out of
no-torch is never blocked by a marker an earlier run left behind. Neither
present still reads as "install torch", so nothing changes for installs
made before either existed.

Also adds the AGPL-3.0 header the new test file was missing.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <unslothai@gmail.com>
This commit is contained in:
Lee Jackson 2026-07-28 13:54:25 +01:00 committed by GitHub
parent 6818318867
commit d7594ec10f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 540 additions and 9 deletions

View file

@ -2215,13 +2215,28 @@ def _windows_hidden_subprocess_kwargs() -> dict[str, object]:
def _infer_no_torch() -> bool:
"""Determine whether to run in no-torch (GGUF-only) mode.
Checks UNSLOTH_NO_TORCH first. When unset, falls back to platform
detection so Intel Macs use GGUF-only mode even when invoked from
``unsloth studio update`` (which does not inject the env var).
Precedence: UNSLOTH_NO_TORCH (install.sh / install.ps1 export it, "false"
included, so an explicit value always wins) -> the mode recorded in this
venv's install manifest -> platform detection, so Intel Macs use GGUF-only
mode even when invoked from ``unsloth studio update``.
The manifest tier is what keeps ``unsloth studio update`` in no-torch mode:
it injects no env var, so without it every update reinstalls torch into a
GGUF-only venv. Note setup.ps1 resolves the mode itself and re-exports
UNSLOTH_NO_TORCH, because it drops the manifest before invoking this script.
An empty value counts as unset: PowerShell cannot represent a set-but-empty
variable (assigning "" deletes it), so the two must mean the same thing here.
Evaluated at import, which is before install_python_stack() drops the
manifest. Do not defer this call into main().
"""
env = os.environ.get("UNSLOTH_NO_TORCH")
if env is not None:
return env.strip().lower() in ("1", "true")
if env is not None and env.strip():
return env.strip().lower() in install_manifest.NO_TORCH_TRUTHY
recorded = install_manifest.recorded_no_torch()
if recorded is not None:
return recorded
return IS_MAC_INTEL
@ -2871,6 +2886,11 @@ def install_python_stack() -> int:
)
return 1
# The manifest just went away, so record the mode in a marker that survives a
# pass killed part-way. Otherwise the next update sees neither, reads the
# absent torch as a stale venv, and tries to delete the running environment.
install_manifest.set_no_torch_marker(NO_TORCH)
# 1. Try uv for faster installs (before pip upgrade -- uv venvs don't
# include pip by default).
USE_UV = _bootstrap_uv()
@ -3256,6 +3276,7 @@ def install_python_stack() -> int:
req_root = REQ_ROOT,
steps_total = _TOTAL,
package_name = package_name,
no_torch = NO_TORCH,
)
is None
):