Compare commits

...
Sign in to create a new pull request.

111 commits

Author SHA1 Message Date
unslothai
d838353e9f Tighten the comments on the PyPI metadata guards 2026-07-29 03:38:40 +00:00
Daniel Han
148de9f970 pip: gate the torchcodec extras to platforms with a wheel
torchcodec publishes no sdist and only manylinux_2_28_x86_64, macosx_*_arm64 and
win_amd64 wheels, so on Linux aarch64, Windows ARM64 and Intel Mac there is
nothing for pip to resolve and the whole install fails before the user gets an
environment. Wiring audio-torch210 into the cu*-torch2100 extras made that
reachable from those extras too. Gate on the platforms that have a wheel,
matching PLATFORM_LACKS_TORCHCODEC_WHEEL in studio/install_python_stack.py.

Also record why the amd and huggingfacenotorch extras cannot be torch-free on
this branch: extras are additive to the base dependencies, which deliberately
carry the full runtime here so a bare pip install works.
2026-07-29 03:23:43 +00:00
pre-commit-ci[bot]
b6ecab8654 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-29 03:22:42 +00:00
unslothai
adb846ab51 Parse the guard tests' requirements instead of pattern matching them
The direct-URL check matched the substring " @ https://", but PEP 508 allows any
whitespace around the @ and the scheme is case insensitive, so flash-attn@https://...
and flash-attn @ HTTPS://... are both direct references the check waved through -
the exact upload failure it exists to catch. Ask packaging: Requirement.url is set
for every spelling.

The extras cross-reference used a lowercase regex, but project names and extra names
are both case and separator insensitive (PEP 503, PEP 685), so pip honours
Unsloth[Rocm72_Torch2100] while the regex never looked at it. Parse and canonicalize
both sides instead.
2026-07-29 03:21:43 +00:00
pre-commit-ci[bot]
c391e158e3 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-29 02:46:12 +00:00
Daniel Han
e14e2d89bf pip: restore the rich, audio and flash-attn dependencies main declares 2026-07-29 02:45:15 +00:00
pre-commit-ci[bot]
b6e7ffcd76 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-29 01:52:24 +00:00
Daniel Han
fa5bb096cc pip: add the amd and huggingfacenotorch extras, and guard against unpublishable metadata 2026-07-29 01:50:57 +00:00
Daniel Han
0d320c5b56 Skip the amd extra floor assertion while this branch has no amd extra 2026-07-29 01:38:17 +00:00
Daniel Han
0756f8f645 Merge main into pip
Keeps pip's pyproject.toml verbatim: main's extras carry direct URL
requirements, which PyPI rejects in Requires-Dist, so they cannot ship in
the uploaded metadata.
2026-07-29 01:36:44 +00:00
Daniel Han
2bea7862ef
pip: declare click, which typer no longer pulls in (#7504)
unsloth_cli/commands/start.py imports click at module scope, and
unsloth_cli/__init__.py imports that module, so every unsloth command needs
it. typer carried click through 0.19 and dropped it in 0.27, and the declared
floor is typer>=0.12.0, so a fresh resolve gets none. The wheel still works
today only because huggingface_hub requires click<9,>=8.4.2, which is luck
rather than a declaration.

Verified on a wheel built from a dependency list without that transitive
provider: every command, including `unsloth --help`, died with
ModuleNotFoundError for click. Same class as the structlog gap in #7493, so
the drift test covers both now.
2026-07-27 06:50:10 -07:00
Daniel Han
735fcde44c
pip: declare the Studio dependencies the wheel's own modules import (#7493)
* pip: declare the Studio dependencies the wheel's own modules import

The wheel packages studio/ and studio.backend*, so pip install unsloth puts
five commands on PATH -- train, export, chat, inference, studio -- and every one
of them imports studio.backend.*. None of those imports were declared, so all
five ended in a rich traceback at ModuleNotFoundError: No module named
'structlog' (#4701, #5260, #7147). --help rendered fine for all of them because
typer defers the import, which is why this went unnoticed.

Walking module-level, non-try-guarded imports from each entry point shows
structlog is the only hard requirement they share, once starlette's
annotation-only import in loggers/handlers.py moves under TYPE_CHECKING. So
structlog becomes a core dependency and the rest of the server stack (fastapi,
uvicorn, matplotlib, pandas, pymupdf, ...) becomes a [studio] extra mirroring
studio/backend/requirements/studio.txt, with a test that fails if the two drift.

pip install unsloth           -> train / export work
pip install "unsloth[studio]" -> the server works

* Apply ruff-format quote normalisation

* Trim the comments added in this PR
2026-07-27 03:44:07 -07:00
Daniel Han
8d38165771 Merge branch 'main' into pip 2026-07-27 10:34:51 +00:00
Daniel Han
3455b45977 pip: track main's packaging and tooling config
The merge keeps this branch's own dependencies and extras, which are
deliberately reduced, but the rest of pyproject.toml should not diverge.
Missing here and present on main:

- unsloth_cli package-data, node_prebuilt_pins.json and the backend/assets
  jinja/html globs, so the wheel was not shipping files the code loads
- ruff line-length and pytest pythonpath

test_pins_manifest_is_declared_in_package_data now passes on this branch.
2026-07-27 10:25:55 +00:00
Daniel Han
b1b499c743 Merge branch 'main' into pip 2026-07-27 10:17:31 +00:00
Daniel Han
b22b243e1b Update pyproject.toml 2026-07-23 03:07:15 -07:00
Daniel Han
ef4894c559 Merge branch 'main' into pip 2026-07-23 03:05:55 -07:00
Daniel Han
6c3c01c0c9 Update pyproject.toml 2026-07-20 07:08:29 -07:00
Daniel Han
5dec1284eb Merge branch 'main' into pip 2026-07-20 07:08:24 -07:00
Daniel Han
7beecd279e Update pyproject.toml 2026-07-15 11:43:23 -07:00
Daniel Han
75f831616e Merge branch 'main' into pip 2026-07-15 11:42:44 -07:00
Daniel Han
4373fdbaee Merge branch 'main' into pip 2026-07-08 07:38:08 -07:00
Daniel Han
3f55e7a98c Update pyproject.toml 2026-07-08 07:20:22 -07:00
Daniel Han
a9b3321915 Merge branch 'main' into pip 2026-07-08 07:20:07 -07:00
Daniel Han
a42720636b Merge branch 'main' into pip 2026-07-07 07:44:05 -07:00
Daniel Han
2f111da0f6 Update pyproject.toml 2026-07-07 06:20:52 -07:00
Daniel Han
2c9587d9fc Merge branch 'main' into pip 2026-07-07 06:20:44 -07:00
Daniel Han
c436f3ca67 Merge branch 'main' into pip 2026-06-22 09:06:48 -07:00
Daniel Han
12f34025dc Update pyproject.toml 2026-06-22 09:01:36 -07:00
Daniel Han
5e5c6c8fed Merge branch 'main' into pip 2026-06-22 09:00:34 -07:00
Daniel Han
287243a8fd Update pyproject.toml 2026-06-18 10:37:48 -07:00
Daniel Han
4aa7dd5b18 Merge branch 'main' into pip 2026-06-18 10:37:40 -07:00
Daniel Han
c9aa094c91 Update pyproject.toml 2026-06-13 04:43:58 -07:00
Daniel Han
1b1807ef9a Merge branch 'main' into pip 2026-06-13 04:43:50 -07:00
Daniel Han
a370521879 Merge branch 'main' into pip 2026-06-12 11:20:56 -07:00
Daniel Han
b712f9f557 Merge branch 'main' into pip 2026-06-12 07:37:43 -07:00
Daniel Han
667467a315 Merge branch 'main' into pip 2026-06-12 07:36:20 -07:00
Daniel Han
55075f691e Merge branch 'main' into pip 2026-06-12 06:55:57 -07:00
Daniel Han
0e929cddac Update pyproject.toml 2026-06-12 06:35:51 -07:00
Daniel Han
ecaf3dde2a Merge branch 'main' into pip 2026-06-12 06:35:37 -07:00
Daniel Han
fc9d82f6bf Update pyproject.toml 2026-06-11 09:22:35 -07:00
Daniel Han
4d2afc62a0 Merge branch 'main' into pip 2026-06-11 09:22:24 -07:00
Daniel Han
43c669e6ec Update pyproject.toml 2026-06-10 11:11:36 -07:00
Daniel Han
11b69c1051 Merge branch 'main' into pip 2026-06-10 11:11:29 -07:00
Daniel Han
39c7a4d290 Merge branch 'main' into pip 2026-06-03 07:30:11 -07:00
Daniel Han
ff1088af40 Update pyproject.toml 2026-06-03 06:36:36 -07:00
Daniel Han
6d3849b821 Merge branch 'main' into pip 2026-06-03 06:36:15 -07:00
Daniel Han
8e26a368e1 Merge branch 'main' into pip 2026-06-01 08:22:08 -07:00
Daniel Han
e31d4c6aea Update pyproject.toml 2026-05-31 07:11:09 -07:00
Daniel Han
5688072af6 Merge branch 'main' into pip 2026-05-31 07:10:55 -07:00
Daniel Han
d3ac7447eb Merge branch 'main' into pip 2026-05-26 07:26:37 -07:00
Daniel Han
2fdfe24fb1 Merge branch 'main' into pip 2026-05-24 07:11:32 -07:00
Daniel Han
e2215c9d11 Update pyproject.toml 2026-05-22 09:24:40 -07:00
Daniel Han
b11f13a710 Merge branch 'main' into pip 2026-05-22 09:24:32 -07:00
Daniel Han
66ae2d416c Merge branch 'main' into pip 2026-05-19 07:27:07 -07:00
Daniel Han
b0a35ddeab Merge branch 'main' into pip 2026-05-19 07:24:07 -07:00
Daniel Han
405add94a1 Update pyproject.toml 2026-05-19 07:00:38 -07:00
Daniel Han
64dc11faa2 Merge branch 'main' into pip 2026-05-19 07:00:31 -07:00
Daniel Han
07e2fccf38 Merge branch 'main' into pip 2026-05-18 08:43:14 -07:00
Daniel Han
d482382a92
Sync pyproject.toml fixes from main into pip (#5555)
- Add missing cu124onlytorch240 extras (xformers==0.0.28.post1)
- Add sentence-transformers to huggingface and colab-new extras
- Add triton extras group (split out from main deps)
- Add windows extras group
- Drop xformers<0.0.27 cap in colab-no-deps; gate on linux/win + x86_64
- Add [tool.ruff] config (target py311, lint select/ignore, extend-exclude)
- Add [tool.pytest.ini_options] testpaths = ["tests/security"]

Only PyPI-compatible (version-pin) entries copied; URL-based intel/amd/rocm/
flashattentiontorch* extras from main are intentionally skipped.
2026-05-18 06:39:41 -07:00
Daniel Han
342bbc2333 Merge branch 'main' into pip 2026-05-18 06:31:17 -07:00
Daniel Han
e1fe3be939 Update pyproject.toml 2026-05-18 06:30:03 -07:00
Daniel Han
d459f60458 Merge branch 'main' into pip 2026-05-18 06:26:54 -07:00
Daniel Han
2c4bf9ae35 Route CPU-only Linux x86_64 to ggml-org/llama.cpp prebuilts (#5302)
* Route CPU-only Linux x86_64 to ggml-org/llama.cpp prebuilts

setup.sh hard-coded _HELPER_RELEASE_REPO=unslothai/llama.cpp for every
non-Darwin host. unslothai/llama.cpp only publishes Linux CUDA bundles
(app-*-linux-x64-cuda*.tar.gz), so a CPU-only Linux host walked ~30
releases looking for a non-existent app-*-linux-x64-cpu asset, exited
the prebuilt planner with "no compatible Linux prebuilt asset was
found", and fell through to a source build. Free CI runners
(ubuntu-latest with no GPU) hit this on every install, and anyone
running Studio on a Linux laptop without an NVIDIA GPU paid the
~3 minute cmake+make cost on first install.

ggml-org publishes llama-<tag>-bin-ubuntu-x64.tar.gz on every release
and install_llama_prebuilt.py already knows how to fetch it: when
called with --published-repo ggml-org/llama.cpp, the Linux x86_64 +
not has_usable_nvidia branch in direct_upstream_release_plan picks up
that asset directly. The fix is purely on the routing side.

Tighten the gate so a Linux host routes to ggml-org only when it is
x86_64 and has no GPU detection tool installed (nvidia-smi, rocminfo,
amd-smi, hipconfig, hipinfo). Everything else stays on the current
path:

  - macOS: already on ggml-org, unchanged
  - Windows: already on ggml-org via setup.ps1, unchanged
  - Linux CUDA: nvidia-smi present -> unslothai/llama.cpp, unchanged
  - Linux ROCm: rocminfo / amd-smi / hipconfig / hipinfo present
                -> unslothai/llama.cpp -> source build with HIP,
                unchanged
  - Linux Intel / Vulkan / SYCL: no NVIDIA / AMD tools, hits the new
                ggml-org route, gets upstream CPU asset (same as
                today's source-build CPU output, ~3 min faster)
  - Linux arm64 / s390x: not x86_64 -> unslothai/llama.cpp ->
                source build, unchanged

* Tighten routing comment in studio/setup.sh
2026-05-06 06:22:56 +00:00
Daniel Han
a87a08610e Merge branch 'main' into pip 2026-05-06 00:27:15 +00:00
danielhanchen
41abe89041 Bump version to 2026.5.2
Cuts a new PyPI release that ships PR #5296: Studio chat history and
attachments work again with newer @assistant-ui/react, plus the pinned
assistant-ui surface and frontend package-lock.json so future installs
cannot drift back onto a broken bundle.
2026-05-06 00:25:04 +00:00
Daniel Han
d638d1bd6f Merge branch 'main' into pip 2026-05-06 00:24:48 +00:00
Daniel Han
2e3b2bdc27 Update pyproject.toml 2026-05-05 05:27:54 -07:00
Daniel Han
9cc539c1b4 Merge branch 'main' into pip 2026-05-05 05:27:25 -07:00
Daniel Han
973c7d80c2 Merge branch 'main' into pip 2026-04-23 06:58:47 -07:00
Daniel Han
c27f9b99e9 Merge branch 'main' into pip 2026-04-22 09:18:09 -07:00
Daniel Han
0c24d61708 Update pyproject.toml 2026-04-16 12:06:55 -07:00
Daniel Han
e9a2b5c010 Merge branch 'main' into pip 2026-04-16 12:06:45 -07:00
Daniel Han
e8355451ea Update pyproject.toml 2026-04-15 08:07:33 -07:00
Daniel Han
a7b4ae19ee Merge branch 'main' into pip 2026-04-15 08:07:11 -07:00
Daniel Han
9066946615 Merge branch 'main' into pip 2026-04-06 09:39:11 -07:00
Daniel Han
b2580ae32b Update pyproject.toml 2026-04-06 09:21:23 -07:00
Daniel Han
7ae9580ce6 Merge branch 'main' into pip 2026-04-06 09:21:07 -07:00
Daniel Han
9ad3b761ee Merge branch 'main' into pip 2026-04-03 15:02:41 -07:00
Daniel Han
8f721d28d6 Merge branch 'main' into pip 2026-04-02 12:28:40 -07:00
Daniel Han
396aa05ead Update pyproject.toml 2026-04-02 12:03:10 -07:00
Daniel Han
2297f73cad Merge branch 'main' into pip 2026-04-02 12:02:48 -07:00
Daniel Han
a728f7c308 Update pyproject.toml 2026-03-31 06:51:58 -07:00
Daniel Han
884152daee Merge branch 'main' into pip 2026-03-31 06:51:48 -07:00
Daniel Han
7437af2e44 Merge branch 'main' into pip 2026-03-27 08:42:05 -07:00
Daniel Han
67678d2c29 Merge branch 'main' into pip 2026-03-27 07:24:42 -07:00
Daniel Han
e568000a92 Update pyproject.toml 2026-03-27 07:23:42 -07:00
Daniel Han
6700dd60f0 Merge branch 'main' into pip 2026-03-27 07:23:27 -07:00
Daniel Han
abf578327c Merge branch 'main' into pip 2026-03-27 03:35:13 -07:00
Daniel Han
a4ae80cd6a Merge branch 'main' into pip 2026-03-25 09:40:21 -07:00
Daniel Han
ec47b2984d Merge branch 'main' into pip 2026-03-25 09:38:41 -07:00
Daniel Han
d9d1a63397 Fix Colab huggingface-hub conflict, ensurepip fallback, bump to 2026.3.14
- Strip version constraints on Colab dep install
- Upgrade huggingface-hub>=1.0 if is_offline_mode is missing
- ensurepip fallback for uv venvs without pip
- Bump installer pins to 2026.3.14
2026-03-25 16:36:25 +00:00
Daniel Han
0bb6379aad Fix Colab huggingface-hub conflict and pip bootstrap on uv venvs
- colab.py / setup.sh: relax == pins to >= when installing studio.txt
  on Colab so huggingface-hub 0.36.2 does not clobber Colab's bundled
  version (which breaks transformers is_offline_mode import)
- install_python_stack.py: when uv is unavailable and pip is missing
  (uv-created venvs), bootstrap via ensurepip before attempting upgrade
- Bump version to 2026.3.14
2026-03-25 16:15:29 +00:00
Daniel Han
1c608e8ff7 Fix Colab dep install: relax == pins to >= to avoid breaking transformers
studio.txt pins huggingface-hub==0.36.2 and datasets==4.3.0 which
overwrite Colab's pre-installed versions and break its bundled
transformers (is_offline_mode was removed in newer huggingface-hub).

Relax == to >= in both colab.py and setup.sh Colab paths so pip keeps
existing compatible versions instead of force-upgrading.
2026-03-25 16:13:30 +00:00
Daniel Han
f9adf6834d Merge branch 'main' into pip 2026-03-25 09:04:26 -07:00
Daniel Han
1fd5853741 Merge branch 'main' into pip 2026-03-25 09:01:49 -07:00
Daniel Han
93a70fbe4e Merge branch 'main' into pip 2026-03-25 08:34:48 -07:00
Daniel Han
8b77451e75 Update pyproject.toml 2026-03-25 07:31:31 -07:00
Daniel Han
481f0618ff Merge branch 'main' into pip 2026-03-25 07:30:50 -07:00
Daniel Han
ddf6f6d1f9 fix(studio): remove litellm dep (quarantined on PyPI) (#4553)
litellm has been quarantined on PyPI due to a supply chain attack
in version 1.82.8 (malicious credential-stealing .pth file).
No versions are currently installable, which blocks
`unsloth studio setup` at step 8/11 (data-designer deps).

Remove litellm from the single-env data-designer requirements
so setup completes. litellm can be re-added once PyPI lifts the
quarantine.

Ref: https://github.com/BerriAI/litellm/issues/24512
2026-03-24 14:21:57 +00:00
Daniel Han
dddc9eac92 Merge branch 'main' into pip 2026-03-24 06:51:20 -07:00
Daniel Han
da81c94510 Merge branch 'main' into pip 2026-03-22 08:23:44 -07:00
Daniel Han
4c139503c1 Merge branch 'main' into pip 2026-03-22 06:14:40 -07:00
Daniel Han
9d1e3c38bc Merge branch 'main' into pip 2026-03-19 02:31:50 -07:00
Daniel Han
8bc26f4e1d Merge branch 'main' into pip 2026-03-18 11:13:18 -07:00
Daniel Han
044e67f5aa Merge branch 'main' into pip 2026-03-18 10:40:39 -07:00
Daniel Han
ba36c12240 Merge branch 'main' into pip 2026-03-18 09:10:55 -07:00
Daniel Han
0a5652281f Merge branch 'main' into pip 2026-03-18 08:33:28 -07:00
Daniel Han
bf63f79414 Merge branch 'main' into pip 2026-03-18 08:31:20 -07:00
Daniel Han
2973bea3d0 Merge branch 'main' into pip 2026-03-17 07:58:59 -07:00
Daniel Han
ddbbfe52cf Update pyproject.toml 2026-03-17 07:58:28 -07:00
4 changed files with 323 additions and 661 deletions

File diff suppressed because it is too large Load diff

View file

@ -874,10 +874,15 @@ class TestAmdBnbFloorParity:
FLOOR = "0.50.0"
PYPROJECT = REPO_ROOT / "pyproject.toml"
def test_amd_extra_floor(self):
def test_amd_extra_floor_when_present(self):
"""This branch ships no `amd` extra: main's version carries direct URL
requirements, which PyPI rejects in Requires-Dist, so the extras block is not
merged here. Assert the floor once #7278 adds the extra in floor form, so it
cannot drift from the two installer fallbacks."""
text = self.PYPROJECT.read_text(encoding = "utf-8")
amd = re.search(r"^amd = \[(.*?)^\]", text, re.S | re.M)
assert amd, "pyproject.toml must define an `amd` extra"
if amd is None:
pytest.skip("pyproject.toml has no amd extra on this branch")
specs = re.findall(r'"(bitsandbytes[^"]*)"', amd.group(1))
assert specs, "the amd extra must pin bitsandbytes"
for spec in specs:

View file

@ -0,0 +1,162 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""This branch is what gets uploaded to PyPI, so its metadata has to satisfy PyPI's rules.
PyPI rejects any PEP 508 direct reference in ``Requires-Dist`` (pypi/warehouse#7136) and
``twine check`` does not catch it (pypa/twine#726): the upload just 400s. ``main`` carries
hundreds of direct URL requirements for the wheel indexes, so every merge from it can
break publishing, and nothing else guards that. Structural checks only, no network.
"""
from __future__ import annotations
import pathlib
import sys
import pytest
REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
PYPROJECT = REPO_ROOT / "pyproject.toml"
def _load() -> dict:
if sys.version_info >= (3, 11):
import tomllib
else:
tomllib = pytest.importorskip("tomli")
return tomllib.loads(PYPROJECT.read_text(encoding = "utf-8"))
def _all_requirements() -> list[tuple[str, str]]:
"""(extra name, requirement string) for the base deps and every extra."""
project = _load()["project"]
items = [("<base>", r) for r in project.get("dependencies", [])]
for extra, deps in project.get("optional-dependencies", {}).items():
items += [(extra, r) for r in deps]
return items
class TestNoDirectUrlRequirements:
def test_no_requirement_uses_a_direct_url(self):
"""Parse rather than substring-match: PEP 508 allows any whitespace around the
`@` and a case-insensitive scheme, so `flash-attn@https://...` and
`flash-attn @ HTTPS://...` both slip past a " @ https://" match.
`Requirement.url` is set for every form.
"""
packaging_requirements = pytest.importorskip("packaging.requirements")
offenders = []
for extra, req in _all_requirements():
try:
parsed = packaging_requirements.Requirement(req)
except Exception: # noqa: BLE001 - test_every_requirement_parses reports it
continue
if parsed.url:
offenders.append((extra, req))
assert offenders == [], (
"PyPI rejects direct references in Requires-Dist, so these would fail the "
f"upload (only found at publish time): {offenders[:5]}"
)
def test_every_requirement_parses(self):
packaging_requirements = pytest.importorskip("packaging.requirements")
bad = []
for extra, req in _all_requirements():
try:
packaging_requirements.Requirement(req)
except Exception as exc: # noqa: BLE001 - report every malformed spec at once
bad.append((extra, req, str(exc)))
assert bad == [], f"unparseable requirements: {bad}"
class TestExtraReferencesResolve:
def test_every_unsloth_extra_reference_exists(self):
"""A `unsloth[foo]` naming an undefined extra installs nothing and fails silently.
Canonicalized on both sides: project and extra names are case and separator
insensitive (PEP 503, PEP 685), so pip honours `Unsloth[Rocm72_Torch2100]`
while a lowercase regex would never look at it.
"""
packaging_requirements = pytest.importorskip("packaging.requirements")
packaging_utils = pytest.importorskip("packaging.utils")
canonicalize = packaging_utils.canonicalize_name
extras = _load()["project"].get("optional-dependencies", {})
defined = {canonicalize(name) for name in extras}
dangling = []
for name, deps in extras.items():
for dep in deps:
try:
parsed = packaging_requirements.Requirement(dep)
except Exception: # noqa: BLE001 - test_every_requirement_parses reports it
continue
if canonicalize(parsed.name) != "unsloth":
continue
for ref in parsed.extras:
if canonicalize(ref) not in defined:
dangling.append((name, ref))
assert dangling == [], f"extras referencing undefined extras: {dangling}"
class TestAmdExtraIsInstallableFromPyPI:
"""`pip install unsloth[amd]` is the supported AMD entry point, so the extra has to
exist here and stay a version floor: a URL pin would be unpublishable."""
def test_amd_extra_exists_and_floors_bitsandbytes(self):
extras = _load()["project"].get("optional-dependencies", {})
assert "amd" in extras, "pyproject.toml must define an `amd` extra"
specs = [d for d in extras["amd"] if d.lower().startswith("bitsandbytes")]
assert specs, "the amd extra must constrain bitsandbytes"
for spec in specs:
assert spec.startswith(
"bitsandbytes>=0.50.0"
), f"bitsandbytes <= 0.49.2 NaNs at 4-bit decode on ROCm; got {spec!r}"
def test_amd_extra_pulls_the_torch_free_runtime(self):
extras = _load()["project"].get("optional-dependencies", {})
assert any("huggingfacenotorch" in d for d in extras["amd"])
assert "huggingfacenotorch" in extras
class TestRuntimeImportsAreDeclared:
"""The base install must cover every module-scope import on the CLI entry path:
typer supplied click until 0.26.0 dropped it (#7504), and supplies rich by chance."""
ENTRY_PATH_IMPORTS = ("click", "rich", "structlog", "typer")
def test_cli_entry_path_imports_are_base_dependencies(self):
packaging_requirements = pytest.importorskip("packaging.requirements")
base = _load()["project"].get("dependencies", [])
declared = {
packaging_requirements.Requirement(r).name.lower().replace("_", "-") for r in base
}
missing = [p for p in self.ENTRY_PATH_IMPORTS if p not in declared]
assert missing == [], (
"imported at module scope by unsloth_cli/__init__.py's import chain but not "
f"declared in base dependencies: {missing}"
)
class TestAcceleratorExtrasCarryTheirCompanions:
"""`-ampere-` variants add flash-attn and torch 2.10 variants add the torchcodec
audio path; dropping one installs a quietly weaker stack than its siblings."""
def test_torch2100_extras_pull_the_audio_path(self):
extras = _load()["project"].get("optional-dependencies", {})
targets = [
n for n in extras if n.endswith("torch2100") and "only" not in n and n.startswith("cu")
]
assert targets, "expected cu*-torch2100 extras to exist"
missing = [n for n in targets if not any("audio-torch" in d for d in extras[n])]
assert missing == [], f"torch 2.10 extras missing the audio extra: {missing}"
def test_ampere_torch280_extras_pull_flash_attention(self):
extras = _load()["project"].get("optional-dependencies", {})
targets = [n for n in extras if n.endswith("-ampere-torch280")]
assert targets, "expected *-ampere-torch280 extras to exist"
missing = [
n
for n in targets
if not any("flashattention" in d or "flash-attn" in d for d in extras[n])
]
assert missing == [], f"ampere torch 2.8 extras missing flash-attn: {missing}"

View file

@ -45,15 +45,21 @@ def _stub_torch(monkeypatch, version: str):
def test_torch210_extras_bundle_audio_torch210():
"""ROCm extras pin torch by direct URL, which PyPI rejects, so this branch omits
them: check whichever torch 2.10 extras it defines, and require at least one."""
text = PYPROJECT.read_text(encoding = "utf-8")
checked = 0
for extra in (
"cu128-torch2100",
"cu126-ampere-torch2100",
"rocm72-torch2100",
):
match = re.search(rf"^{extra} = \[(.*?)^\]", text, re.MULTILINE | re.DOTALL)
assert match is not None, extra
assert "unsloth[audio-torch210]" in match.group(1)
if match is None:
continue
assert "unsloth[audio-torch210]" in match.group(1), extra
checked += 1
assert checked, "no torch 2.10 extra found to check"
def test_torchcodec_matrix_matches_notebook_validator():