From ae319b11b0ff2df20b2d4f8dd80461c9a413adfa Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 8 Jul 2026 01:45:18 +0000 Subject: [PATCH 01/21] Add cu128/cu126/cu130 torch 2.11.0 extras (torch2110) with xformers 0.0.35 The cu*-ampere-torch2100 / cu*-torch2100 extras cap the CUDA training stack at torch 2.10.0 because their xformers 0.0.34 wheel hard-requires torch==2.10.0. There was no torch 2.11.0 counterpart for the CUDA indexes, so anything that wants torch 2.11.0 (the Docker image, torchao 0.17.0, the rocm7.2 studio path) could not select a matching unsloth extra: torch==2.11.0 plus unsloth[cu128-ampere-torch2100] is unsatisfiable (xformers 0.0.34 -> torch 2.10.0). Add the torch2110 family mirroring the torch2100 layout for cu126/cu128/cu130: - cu{126,128,130}onlytorch2110: xformers 0.0.35 (linux + win32) plus an explicit torch>=2.11.0,<2.12.0 trio. xformers 0.0.35 does not hard-pin torch the way 0.0.34 pinned 2.10.0, so without the trio a bare resolve floats torch to 2.12.x; the pin keeps the "torch2110" name faithful and matches studio's torch>=2.11.0,<2.12.0 convention and the existing xpu torch2110 extra. - cu{126,128,130}-torch2110 and cu{126,128,130}-ampere-torch2110: huggingface + bitsandbytes + the matching onlytorch2110, same shape as the torch2100 wrappers. Resolves cleanly: unsloth[cu128-ampere-torch2110] -> torch 2.11.0+cu128 / torchvision 0.26.0 / torchaudio 2.11.0 / xformers 0.0.35. --- pyproject.toml | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 80b3d757e3..c26176ddd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -337,6 +337,31 @@ cu130onlytorch2100 = [ "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)", "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')", ] +cu126onlytorch2110 = [ + # xformers 0.0.35 does not hard-pin torch the way 0.0.34 pinned torch==2.10.0, + # so pin the torch 2.11.x trio explicitly to keep this "torch2110" extra faithful + # (a bare resolve otherwise floats torch up to 2.12.x). Mirrors studio's + # torch>=2.11.0,<2.12.0 convention and the xpu torch2110 extra. + "torch>=2.11.0,<2.12.0", + "torchvision>=0.26.0,<0.27.0", + "torchaudio>=2.11.0,<2.12.0", + "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)", + "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32')", +] +cu128onlytorch2110 = [ + "torch>=2.11.0,<2.12.0", + "torchvision>=0.26.0,<0.27.0", + "torchaudio>=2.11.0,<2.12.0", + "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)", + "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32')", +] +cu130onlytorch2110 = [ + "torch>=2.11.0,<2.12.0", + "torchvision>=0.26.0,<0.27.0", + "torchaudio>=2.11.0,<2.12.0", + "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)", + "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32')", +] cu118 = [ "unsloth[huggingface]", "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", @@ -542,6 +567,21 @@ cu130-torch2100 = [ "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", "unsloth[cu130onlytorch2100]", ] +cu126-torch2110 = [ + "unsloth[huggingface]", + "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", + "unsloth[cu126onlytorch2110]", +] +cu128-torch2110 = [ + "unsloth[huggingface]", + "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", + "unsloth[cu128onlytorch2110]", +] +cu130-torch2110 = [ + "unsloth[huggingface]", + "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", + "unsloth[cu130onlytorch2110]", +] kaggle = [ "unsloth[huggingface]", ] @@ -841,6 +881,21 @@ cu130-ampere-torch2100 = [ "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", "unsloth[cu130onlytorch2100]", ] +cu126-ampere-torch2110 = [ + "unsloth[huggingface]", + "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", + "unsloth[cu126onlytorch2110]", +] +cu128-ampere-torch2110 = [ + "unsloth[huggingface]", + "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", + "unsloth[cu128onlytorch2110]", +] +cu130-ampere-torch2110 = [ + "unsloth[huggingface]", + "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", + "unsloth[cu130onlytorch2110]", +] flashattentiontorch260abiFALSEcu12x = [ "flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp39-cp39-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.9'", "flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp310-cp310-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.10'", From bf9bca08e065893104426c2b35d6a17eac173b3f Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 8 Jul 2026 06:31:18 +0000 Subject: [PATCH 02/21] Route torch 2.11.x to the torch2110 extra family in auto-install selector unsloth/_auto_install.py fell through to RuntimeError("Torch = {v} too new!") for any installed torch >= 2.10.1, so an environment running torch 2.11.x could not resolve the new cu{cuda}-torch2110 extras. Add a 2.11.x branch right after the 2.10.1 case. The existing CUDA guard already permits 12.6/12.8/13.0 for torch >= 2.10, matching the cu126/cu128/cu130 torch2110 extras. --- unsloth/_auto_install.py | 1 + 1 file changed, 1 insertion(+) diff --git a/unsloth/_auto_install.py b/unsloth/_auto_install.py index d34e09e89c..13f5167856 100644 --- a/unsloth/_auto_install.py +++ b/unsloth/_auto_install.py @@ -36,6 +36,7 @@ elif v < V('2.8.9'): x = 'cu{}{}-torch280' elif v < V('2.9.1'): x = 'cu{}{}-torch290' elif v < V('2.9.2'): x = 'cu{}{}-torch291' elif v < V('2.10.1'): x = 'cu{}{}-torch2100' +elif v < V('2.11.1'): x = 'cu{}{}-torch2110' else: raise RuntimeError(f"Torch = {v} too new!") if v > V('2.6.9') and cuda not in ("11.8", "12.6", "12.8", "13.0"): raise RuntimeError(f"CUDA = {cuda} not supported!") if v >= V('2.10.0') and cuda not in ("12.6", "12.8", "13.0"): raise RuntimeError(f"Torch 2.10 requires CUDA 12.6, 12.8, or 13.0! Got CUDA = {cuda}") From 39af326e1a7bd552e22194ae59fec96d85858556 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 8 Jul 2026 08:28:17 +0000 Subject: [PATCH 03/21] Pin CUDA-12 torch2110 extras to the matching +cuNNN torch build torch 2.11's default PyPI wheel is CUDA 13.0 (torch 2.10 defaulted to CUDA 12.x), so the bare torch>=2.11 constraints in cu126onlytorch2110 / cu128onlytorch2110 would resolve a cu130 torch from PyPI alongside the cu126/ cu128 xformers wheel and break at import. Pin the torch/torchvision/torchaudio trio to the +cu126 / +cu128 local build so it only resolves from the matching download.pytorch.org index the installer configures (or fails loudly if that index is absent) instead of silently mismatching. cu130onlytorch2110 stays bare because torch 2.11's PyPI default already lands on cu130. Adds tests/test_torch2110_cuda_extras.py to guard the pins. --- pyproject.toml | 28 ++++++++----- tests/test_torch2110_cuda_extras.py | 61 +++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 tests/test_torch2110_cuda_extras.py diff --git a/pyproject.toml b/pyproject.toml index c26176ddd3..506f1e4c75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -339,23 +339,33 @@ cu130onlytorch2100 = [ ] cu126onlytorch2110 = [ # xformers 0.0.35 does not hard-pin torch the way 0.0.34 pinned torch==2.10.0, - # so pin the torch 2.11.x trio explicitly to keep this "torch2110" extra faithful - # (a bare resolve otherwise floats torch up to 2.12.x). Mirrors studio's - # torch>=2.11.0,<2.12.0 convention and the xpu torch2110 extra. - "torch>=2.11.0,<2.12.0", - "torchvision>=0.26.0,<0.27.0", - "torchaudio>=2.11.0,<2.12.0", + # so the torch 2.11 trio is pinned explicitly. Use the +cu126 LOCAL build, not + # a bare torch>=2.11: torch 2.11's default PyPI wheel is CUDA 13.0 (2.10 was + # CUDA 12.x), so a bare resolve would pair a cu130 torch with the cu126 + # xformers below and fail at import. The +cu126 label only resolves from the + # matching download.pytorch.org/whl/cu126 index the installer configures, so + # torch stays on the same CUDA as xformers (or fails loudly if that index is + # absent) instead of silently mismatching. + "torch==2.11.0+cu126", + "torchvision==0.26.0+cu126", + "torchaudio==2.11.0+cu126", "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)", "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32')", ] cu128onlytorch2110 = [ - "torch>=2.11.0,<2.12.0", - "torchvision>=0.26.0,<0.27.0", - "torchaudio>=2.11.0,<2.12.0", + # +cu128 local build for the same reason as cu126onlytorch2110 above: torch + # 2.11's default PyPI wheel is CUDA 13.0, so pin to the cu128 index to keep the + # torch trio on the same CUDA as the cu128 xformers wheel. + "torch==2.11.0+cu128", + "torchvision==0.26.0+cu128", + "torchaudio==2.11.0+cu128", "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)", "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32')", ] cu130onlytorch2110 = [ + # No +cu130 local pin needed: torch 2.11's DEFAULT PyPI wheel is already CUDA + # 13.0, so a bare resolve lands on a cu130 torch that matches the cu130 + # xformers below even without a dedicated index. "torch>=2.11.0,<2.12.0", "torchvision>=0.26.0,<0.27.0", "torchaudio>=2.11.0,<2.12.0", diff --git a/tests/test_torch2110_cuda_extras.py b/tests/test_torch2110_cuda_extras.py new file mode 100644 index 0000000000..4a632e1407 --- /dev/null +++ b/tests/test_torch2110_cuda_extras.py @@ -0,0 +1,61 @@ +"""Regression guard for the CUDA torch2110 optional-dependency extras. + +torch 2.11's DEFAULT PyPI wheel is CUDA 13.0 (torch 2.10 defaulted to CUDA 12.x). +So the CUDA-12 `cuXXXonlytorch2110` extras must pin the torch trio to the matching +`+cuXXX` local build; a bare `torch>=2.11` there would resolve a cu130 torch from +PyPI alongside the cu126/cu128 xformers wheel and fail at import. The cu130 extra +needs no local pin because the bare default already lands on cu130. + +Hermetic: only parses pyproject.toml, no network or install. +""" + +from __future__ import annotations + +import tomllib +from pathlib import Path + +import pytest +from packaging.requirements import Requirement + +PYPROJECT = Path(__file__).resolve().parents[1] / "pyproject.toml" +_TORCH_TRIO = ("torch", "torchvision", "torchaudio") + + +def _extra(name: str) -> list[str]: + with open(PYPROJECT, "rb") as f: + data = tomllib.load(f) + return data["project"]["optional-dependencies"][name] + + +def _reqs(specs: list[str]) -> dict[str, Requirement]: + out = {} + for spec in specs: + r = Requirement(spec) + out[r.name.lower()] = r + return out + + +@pytest.mark.parametrize("cuda", ["cu126", "cu128"]) +def test_cuda12_torch2110_pins_matching_local_build(cuda: str): + # Each of torch/torchvision/torchaudio must pin the exact +cuXXX local build + # so it can only resolve from the matching PyTorch CUDA index, never the + # CUDA-13 default on PyPI. + reqs = _reqs(_extra(f"{cuda}onlytorch2110")) + for pkg in _TORCH_TRIO: + spec = str(reqs[pkg].specifier) + assert spec == f"=={('2.11.0' if pkg != 'torchvision' else '0.26.0')}+{cuda}", ( + f"{cuda}onlytorch2110: {pkg} pinned as '{spec}', expected the +{cuda} local build" + ) + # xformers must come from the same CUDA index. + xf = reqs["xformers"] + assert xf.url and f"/whl/{cuda}/" in xf.url, f"xformers not on the {cuda} index: {xf.url}" + + +def test_cu130_torch2110_stays_bare(): + # cu130 matches torch 2.11's PyPI default, so no local pin is needed (and a + # +cu130 pin could fail to resolve from PyPI's unlabelled default wheel). + reqs = _reqs(_extra("cu130onlytorch2110")) + for pkg in _TORCH_TRIO: + spec = str(reqs[pkg].specifier) + assert "+cu" not in spec, f"cu130onlytorch2110: {pkg} should stay bare, got '{spec}'" + assert ">=2.11" in spec or ">=0.26" in spec, f"cu130: {pkg} lost its 2.11 floor: '{spec}'" From f757e68dacd8a1e6084d78527e77849819cfdcd1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:31:19 +0000 Subject: [PATCH 04/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_torch2110_cuda_extras.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_torch2110_cuda_extras.py b/tests/test_torch2110_cuda_extras.py index 4a632e1407..c110bad682 100644 --- a/tests/test_torch2110_cuda_extras.py +++ b/tests/test_torch2110_cuda_extras.py @@ -43,9 +43,9 @@ def test_cuda12_torch2110_pins_matching_local_build(cuda: str): reqs = _reqs(_extra(f"{cuda}onlytorch2110")) for pkg in _TORCH_TRIO: spec = str(reqs[pkg].specifier) - assert spec == f"=={('2.11.0' if pkg != 'torchvision' else '0.26.0')}+{cuda}", ( - f"{cuda}onlytorch2110: {pkg} pinned as '{spec}', expected the +{cuda} local build" - ) + assert ( + spec == f"=={('2.11.0' if pkg != 'torchvision' else '0.26.0')}+{cuda}" + ), f"{cuda}onlytorch2110: {pkg} pinned as '{spec}', expected the +{cuda} local build" # xformers must come from the same CUDA index. xf = reqs["xformers"] assert xf.url and f"/whl/{cuda}/" in xf.url, f"xformers not on the {cuda} index: {xf.url}" From c216d27ac4e404216cd835af96a64bfa9238ba40 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 8 Jul 2026 08:45:42 +0000 Subject: [PATCH 05/21] Add CUDA index to torch2110 auto-install and a tomli test fallback The cu126/cu128 torch2110 extras pin torch==2.11.0+cuNNN, which only resolves from the matching PyTorch CUDA index (torch 2.11's default PyPI wheel is CUDA 13.0). _auto_install.py now appends --extra-index-url download.pytorch.org/whl/ cuNNN for exactly the CUDA-12 torch 2.11 selections so the printed install command resolves; cu130 and non-torch2110 selections are unchanged. tests/test_torch2110_cuda_extras.py falls back to the tomli backport when tomllib is unavailable (Python 3.9 / 3.10). --- tests/test_torch2110_cuda_extras.py | 6 +++++- unsloth/_auto_install.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_torch2110_cuda_extras.py b/tests/test_torch2110_cuda_extras.py index c110bad682..2ec0bb701c 100644 --- a/tests/test_torch2110_cuda_extras.py +++ b/tests/test_torch2110_cuda_extras.py @@ -11,12 +11,16 @@ Hermetic: only parses pyproject.toml, no network or install. from __future__ import annotations -import tomllib from pathlib import Path import pytest from packaging.requirements import Requirement +try: # tomllib is stdlib on Python 3.11+; older interpreters need the tomli backport. + import tomllib +except ModuleNotFoundError: # pragma: no cover - Python 3.9 / 3.10 + tomllib = pytest.importorskip("tomli") + PYPROJECT = Path(__file__).resolve().parents[1] / "pyproject.toml" _TORCH_TRIO = ("torch", "torchvision", "torchaudio") diff --git a/unsloth/_auto_install.py b/unsloth/_auto_install.py index 13f5167856..396fa34d12 100644 --- a/unsloth/_auto_install.py +++ b/unsloth/_auto_install.py @@ -41,4 +41,8 @@ else: raise RuntimeError(f"Torch = {v} too new!") if v > V('2.6.9') and cuda not in ("11.8", "12.6", "12.8", "13.0"): raise RuntimeError(f"CUDA = {cuda} not supported!") if v >= V('2.10.0') and cuda not in ("12.6", "12.8", "13.0"): raise RuntimeError(f"Torch 2.10 requires CUDA 12.6, 12.8, or 13.0! Got CUDA = {cuda}") x = x.format(cuda.replace(".", ""), "-ampere" if False else "") # is_ampere is broken due to flash-attn -print(f'pip install --upgrade pip setuptools wheel && pip install --no-deps git+https://github.com/unslothai/unsloth-zoo.git && pip install "unsloth[{x}] @ git+https://github.com/unslothai/unsloth.git" --no-build-isolation') \ No newline at end of file +# The cu126/cu128 torch2110 extras pin torch==2.11.0+cuNNN, a local build that only +# resolves from the matching PyTorch CUDA index (torch 2.11's default PyPI wheel is +# CUDA 13.0), so add that index for exactly those CUDA-12 torch 2.11 environments. +extra_index = f' --extra-index-url https://download.pytorch.org/whl/cu{cuda.replace(".", "")}' if (x.endswith('-torch2110') and cuda in ("12.6", "12.8")) else '' +print(f'pip install --upgrade pip setuptools wheel && pip install --no-deps git+https://github.com/unslothai/unsloth-zoo.git && pip install "unsloth[{x}] @ git+https://github.com/unslothai/unsloth.git" --no-build-isolation{extra_index}') \ No newline at end of file From dacbf893ba8cea1e41dec9dc69c703cc2c493887 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 12 Jul 2026 10:25:36 +0000 Subject: [PATCH 06/21] scripts: re-baseline sentencepiece's stdout-redirect dup2 after release drift The security audit's hf-stack shard fails on one CRITICAL finding: the reverse-shell regex matches os.dup2 in sentencepiece/__init__.py. This is the package's well-known _redirect_wrapper stdout/stderr suppression helper, verified verbatim against google/sentencepiece's official python wrapper (python/src/sentencepiece/__init__.py L772/L777). The baseline already carries this same benign finding for an earlier release; the new release's matched-code window differs, producing a new evidence_hash, so the old entry no longer suppresses it. Add the refreshed entry alongside the old one (older resolves can still occur from cache). One entry added; no scanner or code changes. --- scripts/scan_packages_baseline.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/scan_packages_baseline.json b/scripts/scan_packages_baseline.json index 27fa801a2a..929cc37bda 100644 --- a/scripts/scan_packages_baseline.json +++ b/scripts/scan_packages_baseline.json @@ -634,6 +634,14 @@ "evidence": "L1221: os.dup2(self.ostream.fileno(), self.orig_stream_fileno) | L1226: os.dup2(self.orig_stream_dup, self.orig_stream_fileno)", "evidence_hash": "bba233b67f8ea4f0723b2fecaabf56528531bccd77ace836165bf38b47246bcc" }, + { + "package": "sentencepiece", + "file": "sentencepiece/__init__.py", + "check": "Reverse shell / bind shell pattern", + "severity": "CRITICAL", + "evidence": "L772: os.dup2(self.ostream.fileno(), self.orig_stream_fileno) | L777: os.dup2(self.orig_stream_dup, self.orig_stream_fileno)", + "evidence_hash": "65b5a11cce128fe09b3f238c01bed7c883d1740d7d46d659118f67940f6c17dc" + }, { "package": "setuptools", "file": "distutils-precedence.pth", From ac830337c18cc99f839197e74e9e26f93f974d93 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 12 Jul 2026 10:53:22 +0000 Subject: [PATCH 07/21] Pin the cu130 torch2110 trio with arbitrary equality to exclude CUDA-12 locals PEP 440 ranks a local build above the unlabelled release of the same version, so cu130onlytorch2110's range specs let a configured CUDA-12 extra index win: pip verified that torch>=2.11.0,<2.12.0 with --extra-index-url .../cu126 installs torch 2.11.0+cu126, silently pairing a CUDA-12 trio with the cu130 xformers wheel. Arbitrary equality (torch===2.11.0) matches only the unlabelled PyPI default, which is the CUDA 13.0 build: pip resolves it correctly next to a CUDA-12 extra index, and uv's first-index strategy fails loudly instead of mismatching. Regression test updated to lock in the === pins. --- pyproject.toml | 13 +++++++++---- tests/test_torch2110_cuda_extras.py | 18 +++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c6d5d8c6d3..6f5cb83d13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -365,10 +365,15 @@ cu128onlytorch2110 = [ cu130onlytorch2110 = [ # No +cu130 local pin needed: torch 2.11's DEFAULT PyPI wheel is already CUDA # 13.0, so a bare resolve lands on a cu130 torch that matches the cu130 - # xformers below even without a dedicated index. - "torch>=2.11.0,<2.12.0", - "torchvision>=0.26.0,<0.27.0", - "torchaudio>=2.11.0,<2.12.0", + # xformers below even without a dedicated index. Arbitrary equality (===) + # rather than a range: PEP 440 ranks a local build (2.11.0+cu126/+cu128) + # ABOVE the unlabelled 2.11.0, so with any CUDA-12 extra index configured a + # range would silently pair a CUDA-12 torch trio with the cu130 xformers + # (pip verified: torch>=2.11,<2.12 + --extra-index-url .../cu126 installs + # 2.11.0+cu126; torch===2.11.0 installs the unlabelled cu130 default). + "torch===2.11.0", + "torchvision===0.26.0", + "torchaudio===2.11.0", "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)", "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32')", ] diff --git a/tests/test_torch2110_cuda_extras.py b/tests/test_torch2110_cuda_extras.py index 2ec0bb701c..cc76f82704 100644 --- a/tests/test_torch2110_cuda_extras.py +++ b/tests/test_torch2110_cuda_extras.py @@ -55,11 +55,19 @@ def test_cuda12_torch2110_pins_matching_local_build(cuda: str): assert xf.url and f"/whl/{cuda}/" in xf.url, f"xformers not on the {cuda} index: {xf.url}" -def test_cu130_torch2110_stays_bare(): - # cu130 matches torch 2.11's PyPI default, so no local pin is needed (and a - # +cu130 pin could fail to resolve from PyPI's unlabelled default wheel). +def test_cu130_torch2110_uses_arbitrary_equality(): + # cu130 matches torch 2.11's PyPI default, so no +cu130 local pin is needed + # (it could fail to resolve from PyPI's unlabelled default wheel). But a + # RANGE is not enough either: PEP 440 ranks a local build (2.11.0+cu126) + # above the unlabelled 2.11.0, so with any CUDA-12 extra index configured a + # range silently pairs a CUDA-12 trio with the cu130 xformers. Arbitrary + # equality (===) matches only the unlabelled release, excluding every + # +cuXXX local candidate. + expected = {"torch": "2.11.0", "torchvision": "0.26.0", "torchaudio": "2.11.0"} reqs = _reqs(_extra("cu130onlytorch2110")) for pkg in _TORCH_TRIO: spec = str(reqs[pkg].specifier) - assert "+cu" not in spec, f"cu130onlytorch2110: {pkg} should stay bare, got '{spec}'" - assert ">=2.11" in spec or ">=0.26" in spec, f"cu130: {pkg} lost its 2.11 floor: '{spec}'" + assert spec == f"==={expected[pkg]}", ( + f"cu130onlytorch2110: {pkg} must pin ==={expected[pkg]} " + f"(excludes +cuXXX local builds), got '{spec}'" + ) From ed275cbd25f5dc69a9ef952862c3cc10cc17fdac Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 12 Jul 2026 11:36:04 +0000 Subject: [PATCH 08/21] cu130onlytorch2110: pin the trio to +cu130 like the cu126/cu128 extras A bare torch==2.11.0 range lets a configured CUDA-12 extra index win the resolve: PEP 440 ranks the local build 2.11.0+cu126 above the unlabelled 2.11.0, so the cu130 extra could silently install cu126 wheels. An ===2.11.0 arbitrary-equality pin has the opposite problem: it rejects the +cu130 local builds that an install from the official cu130 index produces. Pinning ==2.11.0+cu130 matches the cu126/cu128 sibling extras exactly: it accepts only the cu130 builds (all three wheels exist on download.pytorch.org/whl/cu130) and fails the resolve loudly when no cu130 index is configured instead of installing the wrong flavor. _auto_install now appends the cu130 extra index for CUDA 13.0 the same way it does for 12.6/12.8, and the extras test parametrizes cu130 alongside its siblings. --- pyproject.toml | 21 ++++++++++----------- tests/test_torch2110_cuda_extras.py | 24 ++++-------------------- unsloth/_auto_install.py | 10 ++++++---- 3 files changed, 20 insertions(+), 35 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6f5cb83d13..c6a8ff6b99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -363,17 +363,16 @@ cu128onlytorch2110 = [ "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32')", ] cu130onlytorch2110 = [ - # No +cu130 local pin needed: torch 2.11's DEFAULT PyPI wheel is already CUDA - # 13.0, so a bare resolve lands on a cu130 torch that matches the cu130 - # xformers below even without a dedicated index. Arbitrary equality (===) - # rather than a range: PEP 440 ranks a local build (2.11.0+cu126/+cu128) - # ABOVE the unlabelled 2.11.0, so with any CUDA-12 extra index configured a - # range would silently pair a CUDA-12 torch trio with the cu130 xformers - # (pip verified: torch>=2.11,<2.12 + --extra-index-url .../cu126 installs - # 2.11.0+cu126; torch===2.11.0 installs the unlabelled cu130 default). - "torch===2.11.0", - "torchvision===0.26.0", - "torchaudio===2.11.0", + # Pinned to the +cu130 local build like the cu126/cu128 extras: a bare range + # lets a configured CUDA-12 extra index win (PEP 440 ranks 2.11.0+cu126 above + # the unlabelled 2.11.0), and a ===2.11.0 pin would force-replace the trio on + # machines installed from the official cu130 index (whose wheels carry the + # +cu130 local tag). The exact +cu130 pins accept the official cu130-index + # install as-is and fail loudly anywhere the cu130 index is missing; + # _auto_install.py appends the matching index for every torch2110 CUDA extra. + "torch==2.11.0+cu130", + "torchvision==0.26.0+cu130", + "torchaudio==2.11.0+cu130", "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)", "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32')", ] diff --git a/tests/test_torch2110_cuda_extras.py b/tests/test_torch2110_cuda_extras.py index cc76f82704..4865db39c4 100644 --- a/tests/test_torch2110_cuda_extras.py +++ b/tests/test_torch2110_cuda_extras.py @@ -3,8 +3,9 @@ torch 2.11's DEFAULT PyPI wheel is CUDA 13.0 (torch 2.10 defaulted to CUDA 12.x). So the CUDA-12 `cuXXXonlytorch2110` extras must pin the torch trio to the matching `+cuXXX` local build; a bare `torch>=2.11` there would resolve a cu130 torch from -PyPI alongside the cu126/cu128 xformers wheel and fail at import. The cu130 extra -needs no local pin because the bare default already lands on cu130. +PyPI alongside the cu126/cu128 xformers wheel and fail at import. The cu130 extra is +pinned to +cu130 too: a bare or ===-pinned spec either lets a foreign CUDA index +outrank the intended wheel or force-replaces official cu130-index installs. Hermetic: only parses pyproject.toml, no network or install. """ @@ -39,7 +40,7 @@ def _reqs(specs: list[str]) -> dict[str, Requirement]: return out -@pytest.mark.parametrize("cuda", ["cu126", "cu128"]) +@pytest.mark.parametrize("cuda", ["cu126", "cu128", "cu130"]) def test_cuda12_torch2110_pins_matching_local_build(cuda: str): # Each of torch/torchvision/torchaudio must pin the exact +cuXXX local build # so it can only resolve from the matching PyTorch CUDA index, never the @@ -54,20 +55,3 @@ def test_cuda12_torch2110_pins_matching_local_build(cuda: str): xf = reqs["xformers"] assert xf.url and f"/whl/{cuda}/" in xf.url, f"xformers not on the {cuda} index: {xf.url}" - -def test_cu130_torch2110_uses_arbitrary_equality(): - # cu130 matches torch 2.11's PyPI default, so no +cu130 local pin is needed - # (it could fail to resolve from PyPI's unlabelled default wheel). But a - # RANGE is not enough either: PEP 440 ranks a local build (2.11.0+cu126) - # above the unlabelled 2.11.0, so with any CUDA-12 extra index configured a - # range silently pairs a CUDA-12 trio with the cu130 xformers. Arbitrary - # equality (===) matches only the unlabelled release, excluding every - # +cuXXX local candidate. - expected = {"torch": "2.11.0", "torchvision": "0.26.0", "torchaudio": "2.11.0"} - reqs = _reqs(_extra("cu130onlytorch2110")) - for pkg in _TORCH_TRIO: - spec = str(reqs[pkg].specifier) - assert spec == f"==={expected[pkg]}", ( - f"cu130onlytorch2110: {pkg} must pin ==={expected[pkg]} " - f"(excludes +cuXXX local builds), got '{spec}'" - ) diff --git a/unsloth/_auto_install.py b/unsloth/_auto_install.py index 396fa34d12..0518d75a74 100644 --- a/unsloth/_auto_install.py +++ b/unsloth/_auto_install.py @@ -41,8 +41,10 @@ else: raise RuntimeError(f"Torch = {v} too new!") if v > V('2.6.9') and cuda not in ("11.8", "12.6", "12.8", "13.0"): raise RuntimeError(f"CUDA = {cuda} not supported!") if v >= V('2.10.0') and cuda not in ("12.6", "12.8", "13.0"): raise RuntimeError(f"Torch 2.10 requires CUDA 12.6, 12.8, or 13.0! Got CUDA = {cuda}") x = x.format(cuda.replace(".", ""), "-ampere" if False else "") # is_ampere is broken due to flash-attn -# The cu126/cu128 torch2110 extras pin torch==2.11.0+cuNNN, a local build that only -# resolves from the matching PyTorch CUDA index (torch 2.11's default PyPI wheel is -# CUDA 13.0), so add that index for exactly those CUDA-12 torch 2.11 environments. -extra_index = f' --extra-index-url https://download.pytorch.org/whl/cu{cuda.replace(".", "")}' if (x.endswith('-torch2110') and cuda in ("12.6", "12.8")) else '' +# The CUDA torch2110 extras pin the torch trio to the exact +cuNNN local build, +# which only resolves from the matching PyTorch CUDA index (a bare spec would let +# a configured foreign CUDA index outrank the intended wheel, since PEP 440 ranks +# any local build above the unlabelled release), so add that index for every +# CUDA torch 2.11 environment. +extra_index = f' --extra-index-url https://download.pytorch.org/whl/cu{cuda.replace(".", "")}' if (x.endswith('-torch2110') and cuda in ("12.6", "12.8", "13.0")) else '' print(f'pip install --upgrade pip setuptools wheel && pip install --no-deps git+https://github.com/unslothai/unsloth-zoo.git && pip install "unsloth[{x}] @ git+https://github.com/unslothai/unsloth.git" --no-build-isolation{extra_index}') \ No newline at end of file From c446f8edac8f3f735488deca0256b5bd69103258 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 12 Jul 2026 11:37:50 +0000 Subject: [PATCH 09/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_torch2110_cuda_extras.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_torch2110_cuda_extras.py b/tests/test_torch2110_cuda_extras.py index 4865db39c4..08976e2f50 100644 --- a/tests/test_torch2110_cuda_extras.py +++ b/tests/test_torch2110_cuda_extras.py @@ -54,4 +54,3 @@ def test_cuda12_torch2110_pins_matching_local_build(cuda: str): # xformers must come from the same CUDA index. xf = reqs["xformers"] assert xf.url and f"/whl/{cuda}/" in xf.url, f"xformers not on the {cuda} index: {xf.url}" - From 0167b5d72b632c0e276d7551deee2d2d678215c2 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 13 Jul 2026 02:39:54 +0000 Subject: [PATCH 10/21] torch2110 extras: gate x86-64 xformers wheels on platform_machine The cuNNN torch2110/torch2100 extras pinned direct xformers wheel URLs that are x86_64/win_amd64-only while their markers checked only the operating system, so on Linux AArch64 (DGX Spark/GB10) or Windows ARM64 the marker matched and pip aborted on an unsupported wheel even though the torch 2.11/2.10 aarch64 CUDA wheels themselves exist. Append the same platform_machine guard the file already uses for its other direct-URL wheels (bitsandbytes, triton), so ARM64 installs resolve the trio and simply skip xformers. Also in _auto_install.py: torch 2.10.1 fell into the new <2.11.1 branch and emitted the torch2110 extra, whose exact 2.11.0 pins would replace the detected runtime; restore the pre-PR loud RuntimeError for the 2.10.1..<2.11.0 gap, and report the actual torch version in the CUDA requirement error instead of a hard-coded 2.10. Tests: _reqs() now keeps both platform wheel entries per extra (the Windows entry previously shadowed Linux), asserts the ARM64 exclusions, and covers all six public wrapper extras. --- pyproject.toml | 24 ++++++++-------- tests/test_torch2110_cuda_extras.py | 44 ++++++++++++++++++++++++----- unsloth/_auto_install.py | 3 +- 3 files changed, 51 insertions(+), 20 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c6a8ff6b99..927cf7612c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -326,16 +326,16 @@ cu130onlytorch291 = [ "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.33.post2-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')", ] cu126onlytorch2100 = [ - "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)", - "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')", + "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", + "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] cu128onlytorch2100 = [ - "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)", - "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')", + "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", + "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] cu130onlytorch2100 = [ - "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)", - "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')", + "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", + "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] cu126onlytorch2110 = [ # xformers 0.0.35 does not hard-pin torch the way 0.0.34 pinned torch==2.10.0, @@ -349,8 +349,8 @@ cu126onlytorch2110 = [ "torch==2.11.0+cu126", "torchvision==0.26.0+cu126", "torchaudio==2.11.0+cu126", - "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)", - "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32')", + "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", + "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] cu128onlytorch2110 = [ # +cu128 local build for the same reason as cu126onlytorch2110 above: torch @@ -359,8 +359,8 @@ cu128onlytorch2110 = [ "torch==2.11.0+cu128", "torchvision==0.26.0+cu128", "torchaudio==2.11.0+cu128", - "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)", - "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32')", + "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", + "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] cu130onlytorch2110 = [ # Pinned to the +cu130 local build like the cu126/cu128 extras: a bare range @@ -373,8 +373,8 @@ cu130onlytorch2110 = [ "torch==2.11.0+cu130", "torchvision==0.26.0+cu130", "torchaudio==2.11.0+cu130", - "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)", - "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32')", + "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", + "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] cu118 = [ "unsloth[huggingface]", diff --git a/tests/test_torch2110_cuda_extras.py b/tests/test_torch2110_cuda_extras.py index 08976e2f50..5dbf506378 100644 --- a/tests/test_torch2110_cuda_extras.py +++ b/tests/test_torch2110_cuda_extras.py @@ -32,11 +32,14 @@ def _extra(name: str) -> list[str]: return data["project"]["optional-dependencies"][name] -def _reqs(specs: list[str]) -> dict[str, Requirement]: - out = {} +def _reqs(specs: list[str]) -> dict[str, list[Requirement]]: + # Keyed by name -> list: each extra carries one Linux and one Windows + # xformers requirement, so a plain name -> Requirement dict would silently + # drop the Linux entry. + out: dict[str, list[Requirement]] = {} for spec in specs: r = Requirement(spec) - out[r.name.lower()] = r + out.setdefault(r.name.lower(), []).append(r) return out @@ -47,10 +50,37 @@ def test_cuda12_torch2110_pins_matching_local_build(cuda: str): # CUDA-13 default on PyPI. reqs = _reqs(_extra(f"{cuda}onlytorch2110")) for pkg in _TORCH_TRIO: - spec = str(reqs[pkg].specifier) + (req,) = reqs[pkg] + spec = str(req.specifier) assert ( spec == f"=={('2.11.0' if pkg != 'torchvision' else '0.26.0')}+{cuda}" ), f"{cuda}onlytorch2110: {pkg} pinned as '{spec}', expected the +{cuda} local build" - # xformers must come from the same CUDA index. - xf = reqs["xformers"] - assert xf.url and f"/whl/{cuda}/" in xf.url, f"xformers not on the {cuda} index: {xf.url}" + # Both xformers wheels (Linux and Windows) must come from the same CUDA index. + xformers = reqs["xformers"] + assert len(xformers) == 2, f"expected Linux + Windows xformers wheels, got {xformers}" + linux = [r for r in xformers if r.url and r.url.endswith("manylinux_2_28_x86_64.whl")] + windows = [r for r in xformers if r.url and r.url.endswith("win_amd64.whl")] + assert len(linux) == 1 and len(windows) == 1, f"unexpected xformers wheels: {xformers}" + for r in linux + windows: + assert f"/whl/{cuda}/xformers-0.0.35-" in r.url, f"xformers not on the {cuda} index: {r.url}" + # The wheels are x86-64 only, so the markers must exclude other machines + # (e.g. Linux aarch64 such as GB200/DGX Spark, Windows ARM64) where the + # torch trio resolves fine but these wheels would abort the install. + assert r.marker is not None + assert not r.marker.evaluate({"sys_platform": "linux", "platform_machine": "aarch64"}) + assert not r.marker.evaluate({"sys_platform": "win32", "platform_machine": "ARM64"}) + assert linux[0].marker.evaluate({"sys_platform": "linux", "platform_machine": "x86_64"}) + assert windows[0].marker.evaluate({"sys_platform": "win32", "platform_machine": "AMD64"}) + + +@pytest.mark.parametrize("cuda", ["cu126", "cu128", "cu130"]) +@pytest.mark.parametrize("variant", ["", "ampere-"]) +def test_torch2110_wrapper_references_matching_leaf(cuda: str, variant: str): + # The six public wrappers must pull in the usual huggingface + bitsandbytes + # pair and reference the internal leaf of the SAME CUDA version. + specs = _extra(f"{cuda}-{variant}torch2110") + assert specs == [ + "unsloth[huggingface]", + "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", + f"unsloth[{cuda}onlytorch2110]", + ] diff --git a/unsloth/_auto_install.py b/unsloth/_auto_install.py index 0518d75a74..a7b29b4616 100644 --- a/unsloth/_auto_install.py +++ b/unsloth/_auto_install.py @@ -36,10 +36,11 @@ elif v < V('2.8.9'): x = 'cu{}{}-torch280' elif v < V('2.9.1'): x = 'cu{}{}-torch290' elif v < V('2.9.2'): x = 'cu{}{}-torch291' elif v < V('2.10.1'): x = 'cu{}{}-torch2100' +elif v < V('2.11.0'): raise RuntimeError(f"Torch = {v} not supported!") elif v < V('2.11.1'): x = 'cu{}{}-torch2110' else: raise RuntimeError(f"Torch = {v} too new!") if v > V('2.6.9') and cuda not in ("11.8", "12.6", "12.8", "13.0"): raise RuntimeError(f"CUDA = {cuda} not supported!") -if v >= V('2.10.0') and cuda not in ("12.6", "12.8", "13.0"): raise RuntimeError(f"Torch 2.10 requires CUDA 12.6, 12.8, or 13.0! Got CUDA = {cuda}") +if v >= V('2.10.0') and cuda not in ("12.6", "12.8", "13.0"): raise RuntimeError(f"Torch = {v} requires CUDA 12.6, 12.8, or 13.0! Got CUDA = {cuda}") x = x.format(cuda.replace(".", ""), "-ampere" if False else "") # is_ampere is broken due to flash-attn # The CUDA torch2110 extras pin the torch trio to the exact +cuNNN local build, # which only resolves from the matching PyTorch CUDA index (a bare spec would let From 7e9ab42d305d57c00258af02d54a5ef20bb05ce4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:40:31 +0000 Subject: [PATCH 11/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_torch2110_cuda_extras.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_torch2110_cuda_extras.py b/tests/test_torch2110_cuda_extras.py index 5dbf506378..b9524d6e4b 100644 --- a/tests/test_torch2110_cuda_extras.py +++ b/tests/test_torch2110_cuda_extras.py @@ -62,7 +62,9 @@ def test_cuda12_torch2110_pins_matching_local_build(cuda: str): windows = [r for r in xformers if r.url and r.url.endswith("win_amd64.whl")] assert len(linux) == 1 and len(windows) == 1, f"unexpected xformers wheels: {xformers}" for r in linux + windows: - assert f"/whl/{cuda}/xformers-0.0.35-" in r.url, f"xformers not on the {cuda} index: {r.url}" + assert ( + f"/whl/{cuda}/xformers-0.0.35-" in r.url + ), f"xformers not on the {cuda} index: {r.url}" # The wheels are x86-64 only, so the markers must exclude other machines # (e.g. Linux aarch64 such as GB200/DGX Spark, Windows ARM64) where the # torch trio resolves fine but these wheels would abort the install. From 8997bf020ac2b8d2f9df56a980f471cbc170341d Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 13 Jul 2026 02:52:05 +0000 Subject: [PATCH 12/21] torch2100 extras: pin torch explicitly now that xformers can be skipped The torch2100 leaves relied on the xformers 0.0.34 wheel's transitive torch==2.10.0 pin as their only torch constraint. With the new ARM64 platform_machine markers those leaves contributed no requirement at all off x86-64, so a cu-torch2100 wrapper install on Linux aarch64 or Windows ARM64 proceeded unpinned and resolved a newer torch. Pin torch==2.10.0 explicitly (identical to the transitive pin, so x86-64 resolution is unchanged): Linux aarch64 installs the real 2.10.0 aarch64 wheels and Windows ARM64 fails loudly instead of silently drifting. --- pyproject.toml | 10 ++++++++++ tests/test_torch2110_cuda_extras.py | 16 ++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 927cf7612c..28ed380266 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -326,14 +326,24 @@ cu130onlytorch291 = [ "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.33.post2-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')", ] cu126onlytorch2100 = [ + # The explicit torch pin replicates the xformers 0.0.34 wheel's transitive + # torch==2.10.0 pin on machines where the x86-64-only wheel is skipped + # (Linux aarch64 has real 2.10.0 wheels; Windows ARM64 fails loudly). + # Without it an ARM64 install of the cu126-torch2100 wrapper would proceed + # unpinned and resolve a newer torch. + "torch==2.10.0", "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] cu128onlytorch2100 = [ + # Same explicit torch pin as cu126onlytorch2100 above. + "torch==2.10.0", "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] cu130onlytorch2100 = [ + # Same explicit torch pin as cu126onlytorch2100 above. + "torch==2.10.0", "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] diff --git a/tests/test_torch2110_cuda_extras.py b/tests/test_torch2110_cuda_extras.py index b9524d6e4b..19d40560f4 100644 --- a/tests/test_torch2110_cuda_extras.py +++ b/tests/test_torch2110_cuda_extras.py @@ -86,3 +86,19 @@ def test_torch2110_wrapper_references_matching_leaf(cuda: str, variant: str): "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", f"unsloth[{cuda}onlytorch2110]", ] + + +@pytest.mark.parametrize("cuda", ["cu126", "cu128", "cu130"]) +def test_cuda12_torch2100_keeps_torch_pinned_off_x86(cuda: str): + # The torch2100 leaves used to rely on the xformers 0.0.34 wheel's transitive + # torch==2.10.0 pin. Now that the x86-64-only wheels carry platform_machine + # markers, the leaf must pin torch explicitly so an ARM64 install stays on + # torch 2.10 (Linux aarch64 wheels exist) or fails loudly (Windows ARM64) + # instead of resolving an unpinned newer torch. + reqs = _reqs(_extra(f"{cuda}onlytorch2100")) + (torch_req,) = reqs["torch"] + assert str(torch_req.specifier) == "==2.10.0", ( + f"{cuda}onlytorch2100 must pin torch==2.10.0 for machines where the " + f"x86-64-only xformers wheel (and its transitive pin) is skipped" + ) + assert torch_req.marker is None, "the torch pin must apply on every machine" From 8a2389c5dc640752d22e9943d021162af8301636 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 14 Jul 2026 13:43:44 +0000 Subject: [PATCH 13/21] install: tighten comments --- pyproject.toml | 33 ++++++------------- tests/test_torch2110_cuda_extras.py | 51 +++++++++++++++-------------- unsloth/_auto_install.py | 7 ++-- 3 files changed, 38 insertions(+), 53 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 28ed380266..49bd9d74d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -326,11 +326,8 @@ cu130onlytorch291 = [ "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.33.post2-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')", ] cu126onlytorch2100 = [ - # The explicit torch pin replicates the xformers 0.0.34 wheel's transitive - # torch==2.10.0 pin on machines where the x86-64-only wheel is skipped - # (Linux aarch64 has real 2.10.0 wheels; Windows ARM64 fails loudly). - # Without it an ARM64 install of the cu126-torch2100 wrapper would proceed - # unpinned and resolve a newer torch. + # Pin torch so ARM64 installs (x86-64-only xformers wheel and its transitive + # torch pin skipped) stay on 2.10 instead of resolving newer. "torch==2.10.0", "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", @@ -348,14 +345,9 @@ cu130onlytorch2100 = [ "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] cu126onlytorch2110 = [ - # xformers 0.0.35 does not hard-pin torch the way 0.0.34 pinned torch==2.10.0, - # so the torch 2.11 trio is pinned explicitly. Use the +cu126 LOCAL build, not - # a bare torch>=2.11: torch 2.11's default PyPI wheel is CUDA 13.0 (2.10 was - # CUDA 12.x), so a bare resolve would pair a cu130 torch with the cu126 - # xformers below and fail at import. The +cu126 label only resolves from the - # matching download.pytorch.org/whl/cu126 index the installer configures, so - # torch stays on the same CUDA as xformers (or fails loudly if that index is - # absent) instead of silently mismatching. + # xformers 0.0.35 does not pin torch, so pin the trio to the +cu126 local + # build: torch 2.11 defaults to a CUDA-13 PyPI wheel, and only the +cu126 tag + # resolves from the cu126 index alongside the cu126 xformers below. "torch==2.11.0+cu126", "torchvision==0.26.0+cu126", "torchaudio==2.11.0+cu126", @@ -363,9 +355,8 @@ cu126onlytorch2110 = [ "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] cu128onlytorch2110 = [ - # +cu128 local build for the same reason as cu126onlytorch2110 above: torch - # 2.11's default PyPI wheel is CUDA 13.0, so pin to the cu128 index to keep the - # torch trio on the same CUDA as the cu128 xformers wheel. + # Same +cuNNN pin as cu126onlytorch2110: keeps the trio on the cu128 index + # instead of torch 2.11's CUDA-13 PyPI default. "torch==2.11.0+cu128", "torchvision==0.26.0+cu128", "torchaudio==2.11.0+cu128", @@ -373,13 +364,9 @@ cu128onlytorch2110 = [ "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] cu130onlytorch2110 = [ - # Pinned to the +cu130 local build like the cu126/cu128 extras: a bare range - # lets a configured CUDA-12 extra index win (PEP 440 ranks 2.11.0+cu126 above - # the unlabelled 2.11.0), and a ===2.11.0 pin would force-replace the trio on - # machines installed from the official cu130 index (whose wheels carry the - # +cu130 local tag). The exact +cu130 pins accept the official cu130-index - # install as-is and fail loudly anywhere the cu130 index is missing; - # _auto_install.py appends the matching index for every torch2110 CUDA extra. + # Same +cuNNN pin as the cu126/cu128 extras: a bare range lets a CUDA-12 index + # win (PEP 440 ranks +cu126 above unlabelled 2.11.0) and ===2.11.0 would + # force-replace official cu130-index installs. _auto_install.py adds the index. "torch==2.11.0+cu130", "torchvision==0.26.0+cu130", "torchaudio==2.11.0+cu130", diff --git a/tests/test_torch2110_cuda_extras.py b/tests/test_torch2110_cuda_extras.py index 19d40560f4..8e7ff04e09 100644 --- a/tests/test_torch2110_cuda_extras.py +++ b/tests/test_torch2110_cuda_extras.py @@ -1,13 +1,24 @@ +# Unsloth Zoo - Utilities for Unsloth +# Copyright 2023-present Daniel Han-Chen, Michael Han-Chen & the Unsloth team. All rights reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + """Regression guard for the CUDA torch2110 optional-dependency extras. -torch 2.11's DEFAULT PyPI wheel is CUDA 13.0 (torch 2.10 defaulted to CUDA 12.x). -So the CUDA-12 `cuXXXonlytorch2110` extras must pin the torch trio to the matching -`+cuXXX` local build; a bare `torch>=2.11` there would resolve a cu130 torch from -PyPI alongside the cu126/cu128 xformers wheel and fail at import. The cu130 extra is -pinned to +cu130 too: a bare or ===-pinned spec either lets a foreign CUDA index -outrank the intended wheel or force-replaces official cu130-index installs. - -Hermetic: only parses pyproject.toml, no network or install. +The cuXXXonlytorch2110 extras must pin the torch trio to the matching +cuXXX local +build (torch 2.11 defaults to a CUDA-13 PyPI wheel), or resolution mismatches the +xformers wheel. Hermetic: only parses pyproject.toml, no network or install. """ from __future__ import annotations @@ -17,7 +28,7 @@ from pathlib import Path import pytest from packaging.requirements import Requirement -try: # tomllib is stdlib on Python 3.11+; older interpreters need the tomli backport. +try: # tomllib is stdlib on 3.11+; older interpreters need the tomli backport. import tomllib except ModuleNotFoundError: # pragma: no cover - Python 3.9 / 3.10 tomllib = pytest.importorskip("tomli") @@ -33,9 +44,7 @@ def _extra(name: str) -> list[str]: def _reqs(specs: list[str]) -> dict[str, list[Requirement]]: - # Keyed by name -> list: each extra carries one Linux and one Windows - # xformers requirement, so a plain name -> Requirement dict would silently - # drop the Linux entry. + # name -> list: each extra has one Linux and one Windows xformers requirement. out: dict[str, list[Requirement]] = {} for spec in specs: r = Requirement(spec) @@ -45,9 +54,7 @@ def _reqs(specs: list[str]) -> dict[str, list[Requirement]]: @pytest.mark.parametrize("cuda", ["cu126", "cu128", "cu130"]) def test_cuda12_torch2110_pins_matching_local_build(cuda: str): - # Each of torch/torchvision/torchaudio must pin the exact +cuXXX local build - # so it can only resolve from the matching PyTorch CUDA index, never the - # CUDA-13 default on PyPI. + # Each trio member must pin the exact +cuXXX local build. reqs = _reqs(_extra(f"{cuda}onlytorch2110")) for pkg in _TORCH_TRIO: (req,) = reqs[pkg] @@ -65,9 +72,7 @@ def test_cuda12_torch2110_pins_matching_local_build(cuda: str): assert ( f"/whl/{cuda}/xformers-0.0.35-" in r.url ), f"xformers not on the {cuda} index: {r.url}" - # The wheels are x86-64 only, so the markers must exclude other machines - # (e.g. Linux aarch64 such as GB200/DGX Spark, Windows ARM64) where the - # torch trio resolves fine but these wheels would abort the install. + # x86-64-only wheels: markers must exclude aarch64 / ARM64. assert r.marker is not None assert not r.marker.evaluate({"sys_platform": "linux", "platform_machine": "aarch64"}) assert not r.marker.evaluate({"sys_platform": "win32", "platform_machine": "ARM64"}) @@ -78,8 +83,7 @@ def test_cuda12_torch2110_pins_matching_local_build(cuda: str): @pytest.mark.parametrize("cuda", ["cu126", "cu128", "cu130"]) @pytest.mark.parametrize("variant", ["", "ampere-"]) def test_torch2110_wrapper_references_matching_leaf(cuda: str, variant: str): - # The six public wrappers must pull in the usual huggingface + bitsandbytes - # pair and reference the internal leaf of the SAME CUDA version. + # Wrappers pull huggingface + bitsandbytes and the leaf of the same CUDA version. specs = _extra(f"{cuda}-{variant}torch2110") assert specs == [ "unsloth[huggingface]", @@ -90,11 +94,8 @@ def test_torch2110_wrapper_references_matching_leaf(cuda: str, variant: str): @pytest.mark.parametrize("cuda", ["cu126", "cu128", "cu130"]) def test_cuda12_torch2100_keeps_torch_pinned_off_x86(cuda: str): - # The torch2100 leaves used to rely on the xformers 0.0.34 wheel's transitive - # torch==2.10.0 pin. Now that the x86-64-only wheels carry platform_machine - # markers, the leaf must pin torch explicitly so an ARM64 install stays on - # torch 2.10 (Linux aarch64 wheels exist) or fails loudly (Windows ARM64) - # instead of resolving an unpinned newer torch. + # Now the xformers wheels carry x86-64 markers, the leaf must pin torch + # explicitly so ARM64 installs stay on 2.10 instead of resolving newer. reqs = _reqs(_extra(f"{cuda}onlytorch2100")) (torch_req,) = reqs["torch"] assert str(torch_req.specifier) == "==2.10.0", ( diff --git a/unsloth/_auto_install.py b/unsloth/_auto_install.py index a7b29b4616..2b6ae43eb8 100644 --- a/unsloth/_auto_install.py +++ b/unsloth/_auto_install.py @@ -42,10 +42,7 @@ else: raise RuntimeError(f"Torch = {v} too new!") if v > V('2.6.9') and cuda not in ("11.8", "12.6", "12.8", "13.0"): raise RuntimeError(f"CUDA = {cuda} not supported!") if v >= V('2.10.0') and cuda not in ("12.6", "12.8", "13.0"): raise RuntimeError(f"Torch = {v} requires CUDA 12.6, 12.8, or 13.0! Got CUDA = {cuda}") x = x.format(cuda.replace(".", ""), "-ampere" if False else "") # is_ampere is broken due to flash-attn -# The CUDA torch2110 extras pin the torch trio to the exact +cuNNN local build, -# which only resolves from the matching PyTorch CUDA index (a bare spec would let -# a configured foreign CUDA index outrank the intended wheel, since PEP 440 ranks -# any local build above the unlabelled release), so add that index for every -# CUDA torch 2.11 environment. +# The torch2110 extras pin the trio to +cuNNN local builds, which only resolve +# from the matching PyTorch CUDA index, so add that index for torch 2.11. extra_index = f' --extra-index-url https://download.pytorch.org/whl/cu{cuda.replace(".", "")}' if (x.endswith('-torch2110') and cuda in ("12.6", "12.8", "13.0")) else '' print(f'pip install --upgrade pip setuptools wheel && pip install --no-deps git+https://github.com/unslothai/unsloth-zoo.git && pip install "unsloth[{x}] @ git+https://github.com/unslothai/unsloth.git" --no-build-isolation{extra_index}') \ No newline at end of file From 1086bab371fdb5246a2c0aad210dd0db1070e851 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sat, 18 Jul 2026 08:17:15 +0000 Subject: [PATCH 14/21] Tighten comments in torch2110 CUDA extras --- pyproject.toml | 20 ++++++++------------ tests/test_torch2110_cuda_extras.py | 10 +++------- unsloth/_auto_install.py | 3 +-- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 49bd9d74d0..15cd7496fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -326,28 +326,26 @@ cu130onlytorch291 = [ "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.33.post2-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')", ] cu126onlytorch2100 = [ - # Pin torch so ARM64 installs (x86-64-only xformers wheel and its transitive - # torch pin skipped) stay on 2.10 instead of resolving newer. + # Pin torch so ARM64 (x86-64-only xformers wheel skipped) stays on 2.10. "torch==2.10.0", "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] cu128onlytorch2100 = [ - # Same explicit torch pin as cu126onlytorch2100 above. + # Same torch pin as cu126onlytorch2100. "torch==2.10.0", "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] cu130onlytorch2100 = [ - # Same explicit torch pin as cu126onlytorch2100 above. + # Same torch pin as cu126onlytorch2100. "torch==2.10.0", "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] cu126onlytorch2110 = [ - # xformers 0.0.35 does not pin torch, so pin the trio to the +cu126 local - # build: torch 2.11 defaults to a CUDA-13 PyPI wheel, and only the +cu126 tag - # resolves from the cu126 index alongside the cu126 xformers below. + # xformers 0.0.35 does not pin torch; pin the trio to +cu126 so it resolves + # from the cu126 index (torch 2.11 defaults to a CUDA-13 PyPI wheel). "torch==2.11.0+cu126", "torchvision==0.26.0+cu126", "torchaudio==2.11.0+cu126", @@ -355,8 +353,7 @@ cu126onlytorch2110 = [ "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] cu128onlytorch2110 = [ - # Same +cuNNN pin as cu126onlytorch2110: keeps the trio on the cu128 index - # instead of torch 2.11's CUDA-13 PyPI default. + # Same +cuNNN pin as cu126onlytorch2110, on the cu128 index. "torch==2.11.0+cu128", "torchvision==0.26.0+cu128", "torchaudio==2.11.0+cu128", @@ -364,9 +361,8 @@ cu128onlytorch2110 = [ "xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] cu130onlytorch2110 = [ - # Same +cuNNN pin as the cu126/cu128 extras: a bare range lets a CUDA-12 index - # win (PEP 440 ranks +cu126 above unlabelled 2.11.0) and ===2.11.0 would - # force-replace official cu130-index installs. _auto_install.py adds the index. + # Same +cuNNN pin as cu126/cu128: a bare range lets +cu126 win (PEP 440) and + # ===2.11.0 would force-replace cu130-index installs. _auto_install.py adds the index. "torch==2.11.0+cu130", "torchvision==0.26.0+cu130", "torchaudio==2.11.0+cu130", diff --git a/tests/test_torch2110_cuda_extras.py b/tests/test_torch2110_cuda_extras.py index 8e7ff04e09..83b1a4079a 100644 --- a/tests/test_torch2110_cuda_extras.py +++ b/tests/test_torch2110_cuda_extras.py @@ -44,7 +44,7 @@ def _extra(name: str) -> list[str]: def _reqs(specs: list[str]) -> dict[str, list[Requirement]]: - # name -> list: each extra has one Linux and one Windows xformers requirement. + # name -> reqs (one Linux + one Windows xformers per extra) out: dict[str, list[Requirement]] = {} for spec in specs: r = Requirement(spec) @@ -54,7 +54,6 @@ def _reqs(specs: list[str]) -> dict[str, list[Requirement]]: @pytest.mark.parametrize("cuda", ["cu126", "cu128", "cu130"]) def test_cuda12_torch2110_pins_matching_local_build(cuda: str): - # Each trio member must pin the exact +cuXXX local build. reqs = _reqs(_extra(f"{cuda}onlytorch2110")) for pkg in _TORCH_TRIO: (req,) = reqs[pkg] @@ -62,7 +61,6 @@ def test_cuda12_torch2110_pins_matching_local_build(cuda: str): assert ( spec == f"=={('2.11.0' if pkg != 'torchvision' else '0.26.0')}+{cuda}" ), f"{cuda}onlytorch2110: {pkg} pinned as '{spec}', expected the +{cuda} local build" - # Both xformers wheels (Linux and Windows) must come from the same CUDA index. xformers = reqs["xformers"] assert len(xformers) == 2, f"expected Linux + Windows xformers wheels, got {xformers}" linux = [r for r in xformers if r.url and r.url.endswith("manylinux_2_28_x86_64.whl")] @@ -72,7 +70,7 @@ def test_cuda12_torch2110_pins_matching_local_build(cuda: str): assert ( f"/whl/{cuda}/xformers-0.0.35-" in r.url ), f"xformers not on the {cuda} index: {r.url}" - # x86-64-only wheels: markers must exclude aarch64 / ARM64. + # markers must exclude aarch64 / ARM64 assert r.marker is not None assert not r.marker.evaluate({"sys_platform": "linux", "platform_machine": "aarch64"}) assert not r.marker.evaluate({"sys_platform": "win32", "platform_machine": "ARM64"}) @@ -83,7 +81,6 @@ def test_cuda12_torch2110_pins_matching_local_build(cuda: str): @pytest.mark.parametrize("cuda", ["cu126", "cu128", "cu130"]) @pytest.mark.parametrize("variant", ["", "ampere-"]) def test_torch2110_wrapper_references_matching_leaf(cuda: str, variant: str): - # Wrappers pull huggingface + bitsandbytes and the leaf of the same CUDA version. specs = _extra(f"{cuda}-{variant}torch2110") assert specs == [ "unsloth[huggingface]", @@ -94,8 +91,7 @@ def test_torch2110_wrapper_references_matching_leaf(cuda: str, variant: str): @pytest.mark.parametrize("cuda", ["cu126", "cu128", "cu130"]) def test_cuda12_torch2100_keeps_torch_pinned_off_x86(cuda: str): - # Now the xformers wheels carry x86-64 markers, the leaf must pin torch - # explicitly so ARM64 installs stay on 2.10 instead of resolving newer. + # xformers wheels now carry x86-64 markers, so the leaf must pin torch for ARM64. reqs = _reqs(_extra(f"{cuda}onlytorch2100")) (torch_req,) = reqs["torch"] assert str(torch_req.specifier) == "==2.10.0", ( diff --git a/unsloth/_auto_install.py b/unsloth/_auto_install.py index 2b6ae43eb8..f283071a99 100644 --- a/unsloth/_auto_install.py +++ b/unsloth/_auto_install.py @@ -42,7 +42,6 @@ else: raise RuntimeError(f"Torch = {v} too new!") if v > V('2.6.9') and cuda not in ("11.8", "12.6", "12.8", "13.0"): raise RuntimeError(f"CUDA = {cuda} not supported!") if v >= V('2.10.0') and cuda not in ("12.6", "12.8", "13.0"): raise RuntimeError(f"Torch = {v} requires CUDA 12.6, 12.8, or 13.0! Got CUDA = {cuda}") x = x.format(cuda.replace(".", ""), "-ampere" if False else "") # is_ampere is broken due to flash-attn -# The torch2110 extras pin the trio to +cuNNN local builds, which only resolve -# from the matching PyTorch CUDA index, so add that index for torch 2.11. +# torch2110 extras pin +cuNNN local builds that only resolve from the matching index. extra_index = f' --extra-index-url https://download.pytorch.org/whl/cu{cuda.replace(".", "")}' if (x.endswith('-torch2110') and cuda in ("12.6", "12.8", "13.0")) else '' print(f'pip install --upgrade pip setuptools wheel && pip install --no-deps git+https://github.com/unslothai/unsloth-zoo.git && pip install "unsloth[{x}] @ git+https://github.com/unslothai/unsloth.git" --no-build-isolation{extra_index}') \ No newline at end of file From fd306b03623a74d565329520da955238bf869d2b Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 19 Jul 2026 15:34:44 +0000 Subject: [PATCH 15/21] tests: track the moved pass-through inheritance in the gguf order check Main moved the llama_extra_args pass-through inheritance out of the GGUF branch into _resolve_inherited_extra_args, which runs before it, so the source-order assertion's "if request.llama_extra_args is None" anchor no longer exists inside the branch and the check failed after the main merge. The test now asserts the same property in the current shape: inheritance before the GGUF branch (a carried --no-mmproj still shapes the hub guard's companion requirement), and marker, hub guard, unload in order within the branch. Full file passes (32 tests). --- studio/backend/tests/test_gguf_load_cache_reuse.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/studio/backend/tests/test_gguf_load_cache_reuse.py b/studio/backend/tests/test_gguf_load_cache_reuse.py index 15d91cd324..6e707f6c76 100644 --- a/studio/backend/tests/test_gguf_load_cache_reuse.py +++ b/studio/backend/tests/test_gguf_load_cache_reuse.py @@ -728,9 +728,11 @@ class TestLoadHubDownloadExclusion: source = (Path(__file__).resolve().parent.parent / "routes" / "inference.py").read_text() gguf_branch = source[source.index("if config.is_gguf:") :] + # Pass-through inheritance runs before the GGUF branch, so a carried + # --no-mmproj shapes the hub guard's companion requirement. + 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("if request.llama_extra_args is None") < gguf_branch.index("_hub_download_blocks_gguf_load") < gguf_branch.index("unsloth_backend.unload_model") ) From 6a3db3b9e0723522a40cd5334580e361011d98f7 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 19 Jul 2026 16:21:19 +0000 Subject: [PATCH 16/21] tests: anchor the inheritance order check on the call, not the definition source.index("_resolve_inherited_extra_args(") matched the function definition, which always precedes the endpoint, so the ordering assertion was vacuously true. Anchoring on "= _resolve_inherited_ extra_args(" pins the first call site inside the load endpoint (line 4505), which is the statement whose position relative to the GGUF branch the test is meant to guard. 32 tests pass. --- studio/backend/tests/test_gguf_load_cache_reuse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/backend/tests/test_gguf_load_cache_reuse.py b/studio/backend/tests/test_gguf_load_cache_reuse.py index 6e707f6c76..6c39f813b1 100644 --- a/studio/backend/tests/test_gguf_load_cache_reuse.py +++ b/studio/backend/tests/test_gguf_load_cache_reuse.py @@ -730,7 +730,7 @@ class TestLoadHubDownloadExclusion: # Pass-through inheritance runs before the GGUF branch, so a carried # --no-mmproj shapes the hub guard's companion requirement. - assert source.index("_resolve_inherited_extra_args(") < source.index("if config.is_gguf:") + 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") From 96fe03011db5f286fcfcc74b28fd730c53527330 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 20 Jul 2026 00:21:23 +0000 Subject: [PATCH 17/21] tests: align the gguf order test with main Main fixed the stale ordering assertion in PR 7252; adopting its version verbatim removes this file from the branch diff entirely and avoids a conflict on the next main merge. 32 tests pass. --- studio/backend/tests/test_gguf_load_cache_reuse.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/studio/backend/tests/test_gguf_load_cache_reuse.py b/studio/backend/tests/test_gguf_load_cache_reuse.py index 6c39f813b1..62596fcc8a 100644 --- a/studio/backend/tests/test_gguf_load_cache_reuse.py +++ b/studio/backend/tests/test_gguf_load_cache_reuse.py @@ -728,9 +728,11 @@ class TestLoadHubDownloadExclusion: source = (Path(__file__).resolve().parent.parent / "routes" / "inference.py").read_text() gguf_branch = source[source.index("if config.is_gguf:") :] - # Pass-through inheritance runs before the GGUF branch, so a carried - # --no-mmproj shapes the hub guard's companion requirement. - assert source.index("= _resolve_inherited_extra_args(") < source.index("if config.is_gguf:") + # 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. assert ( gguf_branch.index("enter_context(gguf_load_in_flight") < gguf_branch.index("_hub_download_blocks_gguf_load") From 5be994aa1ae822ab2805e51b9b3af3bc2dd3dd0c Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 20 Jul 2026 05:11:18 +0000 Subject: [PATCH 18/21] pyproject: tighten extras comments --- pyproject.toml | 6 +++--- studio/backend/tests/test_gguf_load_cache_reuse.py | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f68a194c83..9e489fc320 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -345,8 +345,8 @@ cu130onlytorch2100 = [ "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.34-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] cu126onlytorch2110 = [ - # xformers 0.0.35 does not pin torch; pin the trio to +cu126 so it resolves - # from the cu126 index (torch 2.11 defaults to a CUDA-13 PyPI wheel). + # Pin trio to +cu126 so it resolves from the cu126 index (torch 2.11 defaults + # to a CUDA-13 wheel; xformers 0.0.35 does not pin torch). "torch==2.11.0+cu126", "torchvision==0.26.0+cu126", "torchaudio==2.11.0+cu126", @@ -363,7 +363,7 @@ cu128onlytorch2110 = [ ] cu130onlytorch2110 = [ # Same +cuNNN pin as cu126/cu128: a bare range lets +cu126 win (PEP 440) and - # ===2.11.0 would force-replace cu130-index installs. _auto_install.py adds the index. + # === would force-replace cu130-index installs. _auto_install.py adds the index. "torch==2.11.0+cu130", "torchvision==0.26.0+cu130", "torchaudio==2.11.0+cu130", diff --git a/studio/backend/tests/test_gguf_load_cache_reuse.py b/studio/backend/tests/test_gguf_load_cache_reuse.py index 62596fcc8a..c70e579092 100644 --- a/studio/backend/tests/test_gguf_load_cache_reuse.py +++ b/studio/backend/tests/test_gguf_load_cache_reuse.py @@ -728,11 +728,9 @@ class TestLoadHubDownloadExclusion: source = (Path(__file__).resolve().parent.parent / "routes" / "inference.py").read_text() gguf_branch = source[source.index("if config.is_gguf:") :] - # 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. + # gguf_load_in_flight must be entered before the hub-download guard and + # unload so a concurrent load can't race the download manager. The old + # llama_extra_args landmark moved to _guard_chat_load_against_training. assert ( gguf_branch.index("enter_context(gguf_load_in_flight") < gguf_branch.index("_hub_download_blocks_gguf_load") From fb177fa867af1e67fc35f1474e841c1adabfe71c Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 26 Jul 2026 12:53:41 +0000 Subject: [PATCH 19/21] Add the CUDA torch 2.12 extras alongside the 2.11 ones torch 2.12.0 and 2.12.1 are published on the cu126 and cu130 indexes, so _auto_install.py raised "too new" for anyone already on them. The new cuNNNonlytorch2120 and cuNNNonlytorch2121 leaves follow the 2110 shape: the trio is pinned to the matching +cuNNN local build because xformers 0.0.35 depends on torch without pinning it, so an unpinned trio walks torch up to the newest release the index serves. torchvision exact-pins torch, so 2.12.0 takes 0.27.0 and 2.12.1 takes 0.27.1. torchaudio has no 2.12 release at all; 2.11.0 dropped its exact torch pin, so it stays as the audio member of both trios. There is no cu128 leaf: that index tops out at torch 2.11.0, so a cu128 2.12 extra would be unresolvable. _auto_install.py now rejects CUDA 12.8 on torch 2.12 with a message naming the two flavors that exist instead of printing a command for an extra that does not. Verified in throwaway uv venvs on a real GPU: each candidate trio plus the pinned xformers wheel and bitsandbytes installed, then torch bf16 matmul, sdpa, torchvision nms, torchaudio resample, xformers memory_efficient_attention with its registered op set, the swiglu symbol and a bitsandbytes 4bit forward all ran. cu130 2.12.0 and 2.12.1 pass end to end, with the shipping 2.11.0 trio as the control. The cu126 wheels build sm_50 through sm_90 so they cannot launch kernels on the sm_100 test box; those two cases were checked for extension loading and CPU execution instead. Every new leaf also resolves cleanly through uv pip compile for both linux and windows targets. --- pyproject.toml | 75 ++++++++++++++++++++++ tests/test_torch2110_cuda_extras.py | 96 ++++++++++++++++++++++++++--- unsloth/_auto_install.py | 9 ++- 3 files changed, 170 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b0c23b3056..7739f10a29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -381,6 +381,41 @@ cu130onlytorch2110 = [ "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", ] +# torch 2.12 ships on the cu126 and cu130 indexes only, so there is no cu128 leaf. +# torchaudio has no 2.12 release; 2.11.0 carries no torch pin and pairs with 2.12. +cu126onlytorch2120 = [ + # Same +cuNNN pin as cu126onlytorch2110: xformers 0.0.35 depends on torch without + # pinning it, so an unpinned trio walks up to the newest release on the index. + "torch==2.12.0+cu126", + "torchvision==0.27.0+cu126", + "torchaudio==2.11.0+cu126", + "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", + "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", +] +cu130onlytorch2120 = [ + # Same +cuNNN pin as cu126onlytorch2120, on the cu130 index. + "torch==2.12.0+cu130", + "torchvision==0.27.0+cu130", + "torchaudio==2.11.0+cu130", + "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", + "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", +] +cu126onlytorch2121 = [ + # torchvision exact-pins torch, so the 2.12.1 patch takes 0.27.1. + "torch==2.12.1+cu126", + "torchvision==0.27.1+cu126", + "torchaudio==2.11.0+cu126", + "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", + "xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", +] +cu130onlytorch2121 = [ + # Same +cuNNN pin as cu126onlytorch2121, on the cu130 index. + "torch==2.12.1+cu130", + "torchvision==0.27.1+cu130", + "torchaudio==2.11.0+cu130", + "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform) and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", + "xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.35-py39-none-win_amd64.whl ; (sys_platform == 'win32') and (platform_machine == 'AMD64' or platform_machine == 'x86_64')", +] cu118 = [ "unsloth[huggingface]", "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", @@ -604,6 +639,26 @@ cu130-torch2110 = [ "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", "unsloth[cu130onlytorch2110]", ] +cu126-torch2120 = [ + "unsloth[huggingface]", + "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", + "unsloth[cu126onlytorch2120]", +] +cu130-torch2120 = [ + "unsloth[huggingface]", + "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", + "unsloth[cu130onlytorch2120]", +] +cu126-torch2121 = [ + "unsloth[huggingface]", + "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", + "unsloth[cu126onlytorch2121]", +] +cu130-torch2121 = [ + "unsloth[huggingface]", + "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", + "unsloth[cu130onlytorch2121]", +] kaggle = [ "unsloth[huggingface]", ] @@ -921,6 +976,26 @@ cu130-ampere-torch2110 = [ "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", "unsloth[cu130onlytorch2110]", ] +cu126-ampere-torch2120 = [ + "unsloth[huggingface]", + "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", + "unsloth[cu126onlytorch2120]", +] +cu130-ampere-torch2120 = [ + "unsloth[huggingface]", + "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", + "unsloth[cu130onlytorch2120]", +] +cu126-ampere-torch2121 = [ + "unsloth[huggingface]", + "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", + "unsloth[cu126onlytorch2121]", +] +cu130-ampere-torch2121 = [ + "unsloth[huggingface]", + "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", + "unsloth[cu130onlytorch2121]", +] flashattentiontorch260abiFALSEcu12x = [ "flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp39-cp39-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.9'", "flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp310-cp310-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.10'", diff --git a/tests/test_torch2110_cuda_extras.py b/tests/test_torch2110_cuda_extras.py index 83b1a4079a..6662431d8b 100644 --- a/tests/test_torch2110_cuda_extras.py +++ b/tests/test_torch2110_cuda_extras.py @@ -14,11 +14,13 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -"""Regression guard for the CUDA torch2110 optional-dependency extras. +"""Regression guard for the CUDA torch2110 and torch212x optional-dependency extras. -The cuXXXonlytorch2110 extras must pin the torch trio to the matching +cuXXX local -build (torch 2.11 defaults to a CUDA-13 PyPI wheel), or resolution mismatches the -xformers wheel. Hermetic: only parses pyproject.toml, no network or install. +The cuXXXonlytorch2110 / cuXXXonlytorch212X extras must pin the torch trio to the +matching +cuXXX local build (these releases default to a CUDA-13 PyPI wheel, and +xformers 0.0.35 depends on torch without pinning it), or resolution walks torch up +to the newest release on the index and mismatches the xformers wheel. Hermetic: +only parses pyproject.toml and _auto_install.py, no network or install. """ from __future__ import annotations @@ -33,14 +35,27 @@ try: # tomllib is stdlib on 3.11+; older interpreters need the tomli backport. except ModuleNotFoundError: # pragma: no cover - Python 3.9 / 3.10 tomllib = pytest.importorskip("tomli") -PYPROJECT = Path(__file__).resolve().parents[1] / "pyproject.toml" +REPO = Path(__file__).resolve().parents[1] +PYPROJECT = REPO / "pyproject.toml" +AUTO_INSTALL = REPO / "unsloth" / "_auto_install.py" _TORCH_TRIO = ("torch", "torchvision", "torchaudio") +# torchaudio has no 2.12 release, so the 2.12 leaves keep the unpinned 2.11.0 audio wheel. +_TORCH212_TRIO = { + "torch2120": {"torch": "2.12.0", "torchvision": "0.27.0", "torchaudio": "2.11.0"}, + "torch2121": {"torch": "2.12.1", "torchvision": "0.27.1", "torchaudio": "2.11.0"}, +} +# torch 2.12 is absent from the cu128 index, so only these two flavors get 2.12 extras. +_TORCH212_CUDA = ("cu126", "cu130") + + +def _extras() -> dict[str, list[str]]: + with open(PYPROJECT, "rb") as f: + data = tomllib.load(f) + return data["project"]["optional-dependencies"] def _extra(name: str) -> list[str]: - with open(PYPROJECT, "rb") as f: - data = tomllib.load(f) - return data["project"]["optional-dependencies"][name] + return _extras()[name] def _reqs(specs: list[str]) -> dict[str, list[Requirement]]: @@ -89,6 +104,71 @@ def test_torch2110_wrapper_references_matching_leaf(cuda: str, variant: str): ] +@pytest.mark.parametrize("cuda", _TORCH212_CUDA) +@pytest.mark.parametrize("series", sorted(_TORCH212_TRIO)) +def test_cuda12_torch212_pins_matching_local_build(cuda: str, series: str): + reqs = _reqs(_extra(f"{cuda}only{series}")) + for pkg, want in _TORCH212_TRIO[series].items(): + (req,) = reqs[pkg] + spec = str(req.specifier) + assert spec == f"=={want}+{cuda}", ( + f"{cuda}only{series}: {pkg} pinned as '{spec}', " + f"expected the =={want}+{cuda} local build" + ) + assert req.marker is None, f"the {pkg} pin must apply on every machine" + xformers = reqs["xformers"] + linux = [r for r in xformers if r.url and r.url.endswith("manylinux_2_28_x86_64.whl")] + windows = [r for r in xformers if r.url and r.url.endswith("win_amd64.whl")] + assert len(linux) == 1 and len(windows) == 1, f"unexpected xformers wheels: {xformers}" + for r in linux + windows: + assert f"/whl/{cuda}/xformers-0.0.35-" in r.url, f"xformers not on the {cuda} index: {r.url}" + assert r.marker is not None + assert not r.marker.evaluate({"sys_platform": "linux", "platform_machine": "aarch64"}) + assert not r.marker.evaluate({"sys_platform": "win32", "platform_machine": "ARM64"}) + assert linux[0].marker.evaluate({"sys_platform": "linux", "platform_machine": "x86_64"}) + assert windows[0].marker.evaluate({"sys_platform": "win32", "platform_machine": "AMD64"}) + + +@pytest.mark.parametrize("cuda", _TORCH212_CUDA) +@pytest.mark.parametrize("series", sorted(_TORCH212_TRIO)) +@pytest.mark.parametrize("variant", ["", "ampere-"]) +def test_torch212_wrapper_references_matching_leaf(cuda: str, series: str, variant: str): + specs = _extra(f"{cuda}-{variant}{series}") + assert specs == [ + "unsloth[huggingface]", + "bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0", + f"unsloth[{cuda}only{series}]", + ] + + +@pytest.mark.parametrize("series", sorted(_TORCH212_TRIO)) +def test_no_cu128_torch212_extras(series: str): + # torch 2.12 is not published on the cu128 index; a cu128 leaf would be unresolvable. + names = _extras() + for name in (f"cu128only{series}", f"cu128-{series}", f"cu128-ampere-{series}"): + assert name not in names, f"{name} cannot resolve: no torch 2.12 on the cu128 index" + + +@pytest.mark.parametrize("series", sorted(_TORCH212_TRIO)) +def test_auto_install_maps_torch212_to_defined_extras(series: str): + # The printed command must name extras that exist, and must add the index that + # serves the +cuNNN local builds those extras pin. + source = AUTO_INSTALL.read_text() + assert f"'cu{{}}{{}}-{series}'" in source, f"_auto_install.py never selects {series}" + assert f"'-{series}'" in source, f"{series} missing from the extra-index-url gate" + names = _extras() + for cuda in _TORCH212_CUDA: + for variant in ("", "-ampere"): + assert f"cu{cuda[2:]}{variant}-{series}" in names + + +def test_auto_install_rejects_cuda128_on_torch212(): + # cu128 tops out at torch 2.11, so 2.12 on that flavor must fail loudly rather + # than print an install command for an extra that does not exist. + source = AUTO_INSTALL.read_text() + assert 'if v >= V(\'2.12.0\') and cuda not in ("12.6", "13.0")' in source + + @pytest.mark.parametrize("cuda", ["cu126", "cu128", "cu130"]) def test_cuda12_torch2100_keeps_torch_pinned_off_x86(cuda: str): # xformers wheels now carry x86-64 markers, so the leaf must pin torch for ARM64. diff --git a/unsloth/_auto_install.py b/unsloth/_auto_install.py index f283071a99..7973788c90 100644 --- a/unsloth/_auto_install.py +++ b/unsloth/_auto_install.py @@ -38,10 +38,15 @@ elif v < V('2.9.2'): x = 'cu{}{}-torch291' elif v < V('2.10.1'): x = 'cu{}{}-torch2100' elif v < V('2.11.0'): raise RuntimeError(f"Torch = {v} not supported!") elif v < V('2.11.1'): x = 'cu{}{}-torch2110' +elif v < V('2.12.0'): raise RuntimeError(f"Torch = {v} not supported!") +elif v < V('2.12.1'): x = 'cu{}{}-torch2120' +elif v < V('2.12.2'): x = 'cu{}{}-torch2121' else: raise RuntimeError(f"Torch = {v} too new!") if v > V('2.6.9') and cuda not in ("11.8", "12.6", "12.8", "13.0"): raise RuntimeError(f"CUDA = {cuda} not supported!") if v >= V('2.10.0') and cuda not in ("12.6", "12.8", "13.0"): raise RuntimeError(f"Torch = {v} requires CUDA 12.6, 12.8, or 13.0! Got CUDA = {cuda}") +# torch 2.12 is published on the cu126 and cu130 indexes only, so there is no cu128 extra. +if v >= V('2.12.0') and cuda not in ("12.6", "13.0"): raise RuntimeError(f"Torch = {v} requires CUDA 12.6 or 13.0! Got CUDA = {cuda}") x = x.format(cuda.replace(".", ""), "-ampere" if False else "") # is_ampere is broken due to flash-attn -# torch2110 extras pin +cuNNN local builds that only resolve from the matching index. -extra_index = f' --extra-index-url https://download.pytorch.org/whl/cu{cuda.replace(".", "")}' if (x.endswith('-torch2110') and cuda in ("12.6", "12.8", "13.0")) else '' +# torch2110 and later extras pin +cuNNN local builds that only resolve from the matching index. +extra_index = f' --extra-index-url https://download.pytorch.org/whl/cu{cuda.replace(".", "")}' if (x.endswith(('-torch2110', '-torch2120', '-torch2121')) and cuda in ("12.6", "12.8", "13.0")) else '' print(f'pip install --upgrade pip setuptools wheel && pip install --no-deps git+https://github.com/unslothai/unsloth-zoo.git && pip install "unsloth[{x}] @ git+https://github.com/unslothai/unsloth.git" --no-build-isolation{extra_index}') \ No newline at end of file From d7e137ee79387bd3e8e1e40f206816ec887d0cad Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 26 Jul 2026 12:54:47 +0000 Subject: [PATCH 20/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_torch2110_cuda_extras.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_torch2110_cuda_extras.py b/tests/test_torch2110_cuda_extras.py index 6662431d8b..0b80920abc 100644 --- a/tests/test_torch2110_cuda_extras.py +++ b/tests/test_torch2110_cuda_extras.py @@ -121,7 +121,9 @@ def test_cuda12_torch212_pins_matching_local_build(cuda: str, series: str): windows = [r for r in xformers if r.url and r.url.endswith("win_amd64.whl")] assert len(linux) == 1 and len(windows) == 1, f"unexpected xformers wheels: {xformers}" for r in linux + windows: - assert f"/whl/{cuda}/xformers-0.0.35-" in r.url, f"xformers not on the {cuda} index: {r.url}" + assert ( + f"/whl/{cuda}/xformers-0.0.35-" in r.url + ), f"xformers not on the {cuda} index: {r.url}" assert r.marker is not None assert not r.marker.evaluate({"sys_platform": "linux", "platform_machine": "aarch64"}) assert not r.marker.evaluate({"sys_platform": "win32", "platform_machine": "ARM64"}) From c433254893b9d0cea33c9fa5f8375bb7e2e080cb Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 27 Jul 2026 10:37:56 +0000 Subject: [PATCH 21/21] Record why torch 2.12 on Blackwell needs the cu130 leaf torch 2.12 ships on the cu126 and cu130 indexes only, and the gate added here already restricts it to those two. What the gate cannot express is that the two are not interchangeable on current hardware. Measured on torch 2.12.1: cu126 arch list: sm_50 sm_60 sm_70 sm_75 sm_80 sm_86 sm_90 cu130 arch list: sm_75 sm_80 sm_86 sm_90 sm_100 sm_120 A B200 is sm_100, so a cu126 build of 2.12 fails a plain matmul there with "CUDA error: no kernel image is available for execution on the device". Nothing in this repo causes that and nothing here can repair it; the wheel simply carries no code for the arch. The gate keys off the detected CUDA version rather than the GPU, which stays correct: cu126 is the right leaf for a pre-Blackwell host on CUDA 12.6, and a Blackwell host needs CUDA 13 regardless. The comment records the measurement so the cu126 leaf is not later mistaken for a Blackwell-capable option. --- unsloth/_auto_install.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/unsloth/_auto_install.py b/unsloth/_auto_install.py index 7973788c90..d8a758f75d 100644 --- a/unsloth/_auto_install.py +++ b/unsloth/_auto_install.py @@ -45,6 +45,11 @@ else: raise RuntimeError(f"Torch = {v} too new!") if v > V('2.6.9') and cuda not in ("11.8", "12.6", "12.8", "13.0"): raise RuntimeError(f"CUDA = {cuda} not supported!") if v >= V('2.10.0') and cuda not in ("12.6", "12.8", "13.0"): raise RuntimeError(f"Torch = {v} requires CUDA 12.6, 12.8, or 13.0! Got CUDA = {cuda}") # torch 2.12 is published on the cu126 and cu130 indexes only, so there is no cu128 extra. +# Of those two, only cu130 covers Blackwell: measured on 2.12.1, the cu126 build's +# arch list ends at sm_90 while cu130 carries sm_100 and sm_120, so on a B200 a cu126 +# 2.12 fails even a plain matmul with "no kernel image is available for execution on +# the device". This gate keys off the detected CUDA, not the GPU, so cu126 stays valid +# for pre-Blackwell; a Blackwell host needs CUDA 13. if v >= V('2.12.0') and cuda not in ("12.6", "13.0"): raise RuntimeError(f"Torch = {v} requires CUDA 12.6 or 13.0! Got CUDA = {cuda}") x = x.format(cuda.replace(".", ""), "-ampere" if False else "") # is_ampere is broken due to flash-attn # torch2110 and later extras pin +cuNNN local builds that only resolve from the matching index.