pip: restore the rich, audio and flash-attn dependencies main declares

This commit is contained in:
Daniel Han 2026-07-29 02:43:18 +00:00
commit e14e2d89bf
3 changed files with 80 additions and 2 deletions

View file

@ -59,6 +59,10 @@ dependencies = [
# imports that, so every command needs it. typer supplied it until 0.27
# dropped the dependency, which left this satisfied only by chance.
"click>=8.0",
# unsloth_cli/__init__.py imports commands/chat.py, which imports rich at
# module level, so every command needs it. typer still pulls rich, but it
# pulled click too until 0.26.0 dropped it: same shape, so declare it.
"rich",
]
[project.scripts]
@ -506,16 +510,19 @@ cu126-torch2100 = [
"unsloth[huggingface]",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu126onlytorch2100]",
"unsloth[audio-torch210]",
]
cu128-torch2100 = [
"unsloth[huggingface]",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu128onlytorch2100]",
"unsloth[audio-torch210]",
]
cu130-torch2100 = [
"unsloth[huggingface]",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu130onlytorch2100]",
"unsloth[audio-torch210]",
]
kaggle = [
"unsloth[huggingface]",
@ -537,6 +544,17 @@ flashattention = [
"ninja ; ('linux' in sys_platform)",
"flash-attn>=2.6.3 ; ('linux' in sys_platform)",
]
# torchcodec is the audio decode path; its releases track torch minor versions,
# so each torch minor gets its own window (#7225).
audio-torch280 = [
"torchcodec>=0.6.0,<0.8.0 ; python_version >= '3.9'",
]
audio-torch290 = [
"torchcodec>=0.8.0,<0.10.0 ; python_version >= '3.10'",
]
audio-torch210 = [
"torchcodec>=0.10.0,<0.11.0 ; python_version >= '3.10'",
]
colab-ampere-torch211 = [
"unsloth[huggingface]",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
@ -769,6 +787,7 @@ cu130-ampere-torch280 = [
"unsloth[huggingface]",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu130onlytorch280]",
"unsloth[flashattention]",
]
cu126-ampere-torch290 = [
"unsloth[huggingface]",
@ -804,16 +823,19 @@ cu126-ampere-torch2100 = [
"unsloth[huggingface]",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu126onlytorch2100]",
"unsloth[audio-torch210]",
]
cu128-ampere-torch2100 = [
"unsloth[huggingface]",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu128onlytorch2100]",
"unsloth[audio-torch210]",
]
cu130-ampere-torch2100 = [
"unsloth[huggingface]",
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
"unsloth[cu130onlytorch2100]",
"unsloth[audio-torch210]",
]
[project.urls]

View file

@ -101,3 +101,52 @@ class TestAmdExtraIsInstallableFromPyPI:
extras = _load()["project"].get("optional-dependencies", {})
assert any("huggingfacenotorch" in d for d in extras["amd"])
assert "huggingfacenotorch" in extras
class TestRuntimeImportsAreDeclared:
"""This branch's base install has to satisfy every module-scope import on the CLI
entry path. typer supplied click until 0.26.0 dropped it (#7504); it still supplies
rich, so rich is satisfied only by chance unless we declare it ourselves."""
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:
"""Each accelerator extra composes a stack: `-ampere-` variants add flash-attn and
torch 2.10 variants add the torchcodec audio path. A variant that silently drops one
installs a quietly weaker environment 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,22 @@ def _stub_torch(monkeypatch, version: str):
def test_torch210_extras_bundle_audio_torch210():
"""The ROCm extras pin torch from the AMD wheel index, which PyPI rejects as a
direct reference, so this branch does not carry them. Check whichever torch 2.10
extras it does define, 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():