unsloth/tests/python/test_prebuilt_cuda_wheels_windows.py
Daniel Han 4b8447f63a
Prebuilt wheels: build causal-conv1d and mamba-ssm for Linux torch 2.10 to 2.12 (#14336)
Linux users on torch 2.10, 2.11 or 2.12 still get upstream's
causal-conv1d and mamba-ssm wheels: 205 MB and 351 MB downloads
(torch2.10 builds, reused on 2.11 and 2.12), with `-lineinfo` and every
arch. This lets the prebuilt wheel workflow build our trimmed versions
for those cells, on both linux_x86_64 and linux_aarch64.

## Changes

- `linux_x86_64` and `linux_aarch64` gain `extra_torches` 2.10.0 /
2.11.0 / 2.12.0, restricted to causal-conv1d and mamba-ssm by a new
`extra_torch_packages` key. flash-attn is not built there: upstream's
2.8.1 for torch 2.10 is already smaller (125 MB, no PTX) than ours.
- `_platform_packages(platform, torch)` is the one place both
`build_matrix` and the `wheel-name` CLI read, so the CLI now also
refuses a package a platform does not build (it used to print a name for
flash-attn on win_arm64).
- Release notes describe packages with different coverage separately, so
Linux does not read as having flash-attn for torch 2.10 to 2.12.
- A default dispatch builds exactly what it did before (12 cells); the
older torches stay opt-in.

## Rollout

The Studio resolver does not ask for these cells yet
(`_UNSLOTH_PREBUILT_EXTRA_CELLS` is unchanged, and the existing test
keeps it a subset of what the workflow builds). After this merges:
dispatch `platforms=linux_x86_64,linux_aarch64`,
`packages=causal-conv1d,mamba-ssm`,
`torch_versions=2.10.0,2.11.0,2.12.0`, `python_versions=3.11,3.12,3.13`
(36 cells), then a Studio change adds the Linux cells to the resolver
for those two packages only.

Expected sizes from the torch 2.13 / 2.14 Linux builds just published
from the same recipe: causal-conv1d about 29 MB (x86_64) / 42 MB
(aarch64) download, mamba-ssm about 54 / 79 MB.

## Tests

`tests/python/test_prebuilt_cuda_wheels*.py`: 256 passed. The updated
matrix and CLI tests fail on main (Linux refused every torch below
2.13); a new test covers the release notes split.
2026-10-20 06:11:17 -07:00

837 lines
37 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""The win_amd64 half of .github/workflows/prebuilt-cuda-wheels.yml.
Nobody publishes flash-attn, causal-conv1d or mamba-ssm for Windows: not one Windows asset exists
across every Dao-AILab and state-spaces release. These pin the parts of the Windows legs that can
be checked without a Windows runner:
* the Linux cells are byte-for-byte what they were, only gaining keys;
* Windows names follow each upstream setup.py's own naming (`cxx11abiTRUE`, as torch reports, and `win_amd64`);
* the shard selector understands Windows ninja targets, and can never run a targetless ninja;
* the deadline wrapper ends the whole process tree;
* SHA256SUMS is rebuilt over every wheel on the release, so a one-platform publish keeps the other;
* the workflow's Windows steps exist in warm and build alike, are gated on the OS, and keep the
supply-chain checks (Authenticode on the CUDA installer, a pinned sha256 on ccache).
"""
from __future__ import annotations
import importlib.util
import json
import subprocess
import sys
import time
import types
from pathlib import Path
import pytest
import yaml
REPO = Path(__file__).resolve().parents[2]
WORKFLOW = REPO / ".github" / "workflows" / "prebuilt-cuda-wheels.yml"
SCRIPTS = REPO / ".github" / "scripts"
def _load(name: str, path: Path):
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
prebuilt_wheels = _load("prebuilt_wheels_win", SCRIPTS / "prebuilt_wheels.py")
shard = _load("prebuilt_wheels_shard_win", SCRIPTS / "prebuilt_wheels_shard.py")
deadline = _load("prebuilt_wheels_timeout_win", SCRIPTS / "prebuilt_wheels_timeout.py")
def triggers(doc: dict) -> dict:
return doc.get(True) if True in doc else doc.get("on")
@pytest.fixture(scope = "module")
def workflow():
return yaml.safe_load(WORKFLOW.read_text(encoding = "utf-8"))
def _steps(workflow, job):
return workflow["jobs"][job]["steps"]
def _step(workflow, job, name):
return next(s for s in _steps(workflow, job) if s.get("name") == name)
# Linux cells as published before Windows existed; changing them changes released wheels.
LINUX_BEFORE = {
"flash-attn": {
"build_env": "FLASH_ATTENTION_FORCE_BUILD=TRUE FLASH_ATTENTION_FORCE_CXX11_ABI=TRUE "
"FLASH_ATTN_CUDA_ARCHS=80;90;100;120",
"abi": "TRUE",
"shards": 8,
"label": "flash-attn / torch 2.13 / cp313",
"wheel_name": "flash_attn-2.8.4+cu13torch2.13cxx11abiTRUE-cp313-cp313-linux_x86_64.whl",
},
"causal-conv1d": {
"build_env": "CAUSAL_CONV1D_FORCE_BUILD=TRUE",
"abi": "TRUE",
"shards": 0,
"label": "causal-conv1d / torch 2.13 / cp313",
"wheel_name": "causal_conv1d-1.7.0+cu13torch2.13cxx11abiTRUE-cp313-cp313-linux_x86_64.whl",
},
"mamba-ssm": {
"build_env": "MAMBA_FORCE_BUILD=TRUE MAMBA_KEEP_CUDA_BUILD=TRUE",
"abi": "TRUE",
"shards": 0,
"label": "mamba-ssm / torch 2.13 / cp313",
"wheel_name": "mamba_ssm-2.3.2.post1+cu13torch2.13cxx11abiTRUE-cp313-cp313-linux_x86_64.whl",
},
}
class TestMatrix:
def test_linux_cells_are_unchanged(self):
cells = prebuilt_wheels.build_matrix(torches = "2.13.0", platforms = "linux_x86_64")
assert [c["package"] for c in cells] == list(prebuilt_wheels.SPECS)
for cell in cells:
for key, value in LINUX_BEFORE[cell["package"]].items():
assert cell[key] == value, (cell["package"], key)
assert cell["runner"] == "ubuntu-22.04"
assert cell["pytorch_nvcc"] == "ccache /usr/local/cuda-13.0/bin/nvcc"
assert cell["smoke_exclude"] == cell["smoke_extra"] == ""
def test_linux_comes_first_so_its_cells_keep_their_order(self):
cells = prebuilt_wheels.build_matrix()
platforms = [c["platform"] for c in cells]
assert platforms == sorted(platforms, key = list(prebuilt_wheels.PLATFORMS).index)
def test_windows_cells(self):
cells = prebuilt_wheels.build_matrix(platforms = "win_amd64")
assert len(cells) == 6
for cell in cells:
assert cell["os"] == "windows"
assert cell["runner"] == "windows-2022"
assert cell["abi"] == "TRUE"
assert cell["wheel_name"].endswith("cxx11abiTRUE-cp313-cp313-win_amd64.whl")
assert cell["label"].endswith(" / win_amd64")
assert cell["pytorch_nvcc"] == "C:/ccache/ccache.exe C:/cuda/bin/nvcc.exe"
def test_windows_flash_attn_names_the_abi_torch_reports(self):
"""The Windows cu130 torch reports _GLIBCXX_USE_CXX11_ABI=True (measured on windows-2022),
and FORCE_CXX11_ABI follows the platform's `abi`, so the name and the assert agree."""
(cell,) = prebuilt_wheels.build_matrix(
packages = "flash-attn", torches = "2.14.0", platforms = "win_amd64"
)
assert "FLASH_ATTENTION_FORCE_CXX11_ABI=TRUE" in cell["build_env"].split()
assert "FLASH_ATTN_CUDA_ARCHS=80;90;100;120" in cell["build_env"].split()
assert cell["shards"] == 8
def test_only_windows_mamba_swaps_its_smoke_dependencies(self):
for cell in prebuilt_wheels.build_matrix():
if cell["os"] == "windows" and cell["package"] == "mamba-ssm":
pin = prebuilt_wheels.TRITON_WINDOWS[cell["torch_mm"]]
assert cell["smoke_extra"] == f"triton-windows=={pin}"
assert set(cell["smoke_exclude"].split()) == {
"triton",
"tilelang",
"apache-tvm-ffi",
"quack-kernels",
}
else:
assert cell["smoke_extra"] == cell["smoke_exclude"] == ""
def test_every_torch_minor_has_a_triton_windows_pin(self):
"""torch 2.10 / 2.11 pin triton 3.6.0, 2.12 3.7.0, 2.13 3.7.1, 2.14 3.8.x; triton-windows follows."""
minors = {
prebuilt_wheels.torch_minor(v)
for platform, target in prebuilt_wheels.PLATFORMS.items()
if target["os"] == "windows"
for v in prebuilt_wheels._platform_torches(platform)
}
assert set(prebuilt_wheels.TRITON_WINDOWS) == minors
assert prebuilt_wheels.TRITON_WINDOWS["2.10"].startswith("3.6.0.post")
assert prebuilt_wheels.TRITON_WINDOWS["2.11"].startswith("3.6.0.post")
assert prebuilt_wheels.TRITON_WINDOWS["2.12"].startswith("3.7.0.post")
assert prebuilt_wheels.TRITON_WINDOWS["2.13"].startswith("3.7.1.post")
assert prebuilt_wheels.TRITON_WINDOWS["2.14"].startswith("3.8.0.post")
@pytest.mark.parametrize("torch_version", ["2.10.0", "2.11.0", "2.12.0"])
def test_older_torches_are_opt_in(self, torch_version):
torch_mm = prebuilt_wheels.torch_minor(torch_version)
cells = prebuilt_wheels.build_matrix(
torches = torch_version, platforms = "win_amd64", pythons = "3.11,3.12,3.13"
)
assert len(cells) == 9
assert {cell["python_tag"] for cell in cells} == {"cp311", "cp312", "cp313"}
assert {cell["torch_spec"] for cell in cells} == {f"torch=={torch_version}"}
for cell in cells:
assert f"+cu13torch{torch_mm}cxx11abiTRUE-" in cell["wheel_name"]
if cell["package"] == "mamba-ssm":
triton = prebuilt_wheels.TRITON_WINDOWS[torch_mm]
assert cell["smoke_extra"] == f"triton-windows=={triton}"
with pytest.raises(SystemExit):
prebuilt_wheels.build_matrix(torches = torch_version, platforms = "win_arm64")
# Linux builds causal-conv1d and mamba-ssm only: upstream flash-attn 2.8.1 is smaller there.
for platform in ("linux_x86_64", "linux_aarch64"):
with pytest.raises(SystemExit):
prebuilt_wheels.build_matrix(
torches = torch_version, platforms = platform, packages = "flash-attn"
)
linux = prebuilt_wheels.build_matrix(
torches = torch_version, platforms = platform, pythons = "3.11,3.12,3.13"
)
assert len(linux) == 6
assert {c["package"] for c in linux} == {"causal-conv1d", "mamba-ssm"}
assert {c["python_tag"] for c in linux} == {"cp311", "cp312", "cp313"}
for cell in linux:
assert cell["wheel_name"].endswith(f"-{platform}.whl")
assert f"+cu13torch{torch_mm}cxx11abiTRUE-" in cell["wheel_name"]
# A default dispatch builds what it did before.
assert torch_version not in {cell["torch"] for cell in prebuilt_wheels.build_matrix()}
both = prebuilt_wheels.build_matrix(torches = f"{torch_version},2.13.0")
assert {(c["platform"], c["torch"], c["package"]) for c in both} == {
*(("win_amd64", torch_version, p) for p in prebuilt_wheels.SPECS),
*(("win_amd64", "2.13.0", p) for p in prebuilt_wheels.SPECS),
*(("linux_x86_64", "2.13.0", p) for p in prebuilt_wheels.SPECS),
("linux_x86_64", torch_version, "causal-conv1d"),
("linux_x86_64", torch_version, "mamba-ssm"),
}
def test_win_amd64_inherits_a_new_global_torch(self, monkeypatch):
monkeypatch.setattr(prebuilt_wheels, "TORCH_VERSIONS", ("2.13.0", "2.14.0", "2.15.0"))
assert prebuilt_wheels._platform_torches("win_amd64") == (
"2.10.0",
"2.11.0",
"2.12.0",
"2.13.0",
"2.14.0",
"2.15.0",
)
assert prebuilt_wheels._platform_torches("linux_x86_64")[-1] == "2.15.0"
def test_pytorch_nvcc_paths_have_no_spaces(self):
"""torch writes PYTORCH_NVCC into build.ninja verbatim; a space would split a path."""
for target in prebuilt_wheels.PLATFORMS.values():
words = target["pytorch_nvcc"].split(" ")
assert len(words) == 2 and all(words), target
@pytest.mark.parametrize(
"platforms", ["win_arm64ec", "macosx_arm64", "win_amd64; whoami", "$(id)"]
)
def test_unknown_platforms_are_refused(self, platforms):
with pytest.raises(SystemExit):
prebuilt_wheels.build_matrix(platforms = platforms)
def test_the_gpu_runner_only_gets_linux_cells(self):
cells = prebuilt_wheels.build_matrix()
gpu = prebuilt_wheels.gpu_matrix(cells)
assert gpu and all(c["os"] == "linux" for c in gpu)
assert len(gpu) == len([c for c in cells if c["platform"] == "linux_x86_64"])
assert prebuilt_wheels.gpu_matrix(prebuilt_wheels.build_matrix(platforms = "win_amd64")) == []
def test_a_windows_only_plan_writes_an_empty_gpu_matrix(self, tmp_path):
output = tmp_path / "output"
subprocess.run(
[sys.executable, str(SCRIPTS / "prebuilt_wheels.py"), "matrix", "--github"],
env = {"GITHUB_OUTPUT": str(output), "UW_PLATFORMS": "win_amd64"},
check = True,
capture_output = True,
)
values = dict(line.split("=", 1) for line in output.read_text().splitlines())
assert values["count"] == "6"
assert values["gpu_count"] == "0"
assert values["warm_count"] == "16"
warm = json.loads(values["warm_matrix"])["include"]
assert {w["platform"] for w in warm} == {"win_amd64"}
class TestNames:
def test_windows_names_are_what_upstream_setup_py_would_write(self):
"""flash-attn, causal-conv1d and mamba's get_wheel_url() all format
`+cu{cuda}torch{mm}cxx11abi{torch._C._GLIBCXX_USE_CXX11_ABI}` and `win_amd64`."""
assert (
prebuilt_wheels.wheel_name("flash-attn", "2.13.0", "3.13", "win_amd64")
== "flash_attn-2.8.4+cu13torch2.13cxx11abiTRUE-cp313-cp313-win_amd64.whl"
)
assert (
prebuilt_wheels.wheel_name("causal-conv1d", "2.14.0", "3.13", "win_amd64")
== "causal_conv1d-1.7.0+cu13torch2.14cxx11abiTRUE-cp313-cp313-win_amd64.whl"
)
assert (
prebuilt_wheels.wheel_name("mamba-ssm", "2.14.0", "3.12", "win_amd64")
== "mamba_ssm-2.3.2.post1+cu13torch2.14cxx11abiTRUE-cp312-cp312-win_amd64.whl"
)
def test_the_default_platform_is_still_linux(self):
assert prebuilt_wheels.wheel_name("flash-attn", "2.13.0", "3.13") == (
prebuilt_wheels.wheel_name("flash-attn", "2.13.0", "3.13", "linux_x86_64")
)
def test_wheel_name_cli_takes_a_platform(self):
out = subprocess.run(
[
sys.executable,
str(SCRIPTS / "prebuilt_wheels.py"),
"wheel-name",
"--package",
"causal-conv1d",
"--torch",
"2.13.0",
"--python",
"3.13",
"--platform",
"win_amd64",
],
check = True,
capture_output = True,
text = True,
).stdout.strip()
assert out == "causal_conv1d-1.7.0+cu13torch2.13cxx11abiTRUE-cp313-cp313-win_amd64.whl"
def test_wheel_name_cli_refuses_a_cell_nothing_builds(self):
def name(package, *platform):
return subprocess.run(
[
sys.executable,
str(SCRIPTS / "prebuilt_wheels.py"),
"wheel-name",
"--package",
package,
"--torch",
"2.11.0",
"--python",
"3.12",
*platform,
],
capture_output = True,
text = True,
)
# win_arm64 builds torch 2.14 only; Linux builds no flash-attn for torch 2.10 to 2.12.
for package, platform in (
("mamba-ssm", ["--platform", "win_arm64"]),
("flash-attn", []),
("flash-attn", ["--platform", "linux_x86_64"]),
("flash-attn", ["--platform", "linux_aarch64"]),
):
result = name(package, *platform)
assert result.returncode != 0, (package, platform)
assert "not built for" in result.stderr
for package, platform in (
("flash-attn", "win_amd64"),
("mamba-ssm", "win_amd64"),
("mamba-ssm", "linux_x86_64"),
("causal-conv1d", "linux_aarch64"),
):
result = name(package, "--platform", platform)
assert result.returncode == 0, (package, platform)
assert result.stdout.strip().endswith(
f"torch2.11cxx11abiTRUE-cp312-cp312-{platform}.whl"
)
def test_round_trip_on_both_platforms(self):
for platform in prebuilt_wheels.PLATFORMS:
for package in prebuilt_wheels.SPECS:
name = prebuilt_wheels.wheel_name(package, "2.14.0", "3.13", platform)
parsed = prebuilt_wheels.parse_wheel_name(name)
assert parsed["package"] == package
assert parsed["platform"] == platform
assert parsed["abi"] == prebuilt_wheels.PLATFORMS[platform]["abi"]
assert parsed["version"] == prebuilt_wheels.SPECS[package]["version"]
@pytest.mark.parametrize(
"name",
[
"flash_attn-2.8.4+cu13torch2.13cxx11abiFALSE-cp313-cp313-win_amd64.whl",
"flash_attn-2.8.4+cu13torch2.13cxx11abiFALSE-cp313-cp313-linux_x86_64.whl",
"flash_attn-2.8.4+cu13torch2.14-cp313-cp313-win_arm64.whl",
],
)
def test_an_abi_the_platform_does_not_have_is_not_ours(self, name):
assert prebuilt_wheels.parse_wheel_name(name) is None
def test_the_version_comes_from_the_file(self):
parsed = prebuilt_wheels.parse_wheel_name(
"flash_attn-9.9.9+cu13torch2.13cxx11abiTRUE-cp313-cp313-linux_x86_64.whl"
)
assert parsed["version"] == "9.9.9"
def _entries(platforms, torches = ("2.13.0", "2.14.0")):
return [
(f"{index:064x}", prebuilt_wheels.wheel_name(package, torch, "3.13", platform))
for index, (platform, torch, package) in enumerate(
(platform, torch, package)
for platform in platforms
for torch in torches
for package in prebuilt_wheels.SPECS
)
]
class TestNotes:
def test_both_platforms(self):
notes = prebuilt_wheels.render_notes(
_entries(["linux_x86_64", "win_amd64"]), tag = "t", repo = "o/r"
)
assert notes == (
"Prebuilt CUDA 13 wheels. Linux x86_64: flash-attn 2.8.4, causal-conv1d 1.7.0 and "
"mamba-ssm 2.3.2.post1, built for PyTorch 2.13 and 2.14 on Python 3.13. "
"Windows x86_64: flash-attn 2.8.4, causal-conv1d 1.7.0 and mamba-ssm 2.3.2.post1, "
"built for PyTorch 2.13 and 2.14 on Python 3.13.\n"
)
def test_packages_with_different_coverage_are_described_apart(self):
"""Linux torch 2.10 to 2.12 carries causal-conv1d and mamba-ssm but no flash-attn."""
entries = [
(f"{index:064x}", prebuilt_wheels.wheel_name(package, torch, "3.13", "linux_x86_64"))
for index, (package, torch) in enumerate(
[(pkg, "2.10.0") for pkg in ("causal-conv1d", "mamba-ssm")]
+ [(pkg, "2.13.0") for pkg in prebuilt_wheels.SPECS]
)
]
notes = prebuilt_wheels.render_notes(entries, tag = "t", repo = "r")
assert notes == (
"Prebuilt Linux x86_64 CUDA 13 wheels for flash-attn 2.8.4, built for PyTorch 2.13 on "
"Python 3.13; causal-conv1d 1.7.0 and mamba-ssm 2.3.2.post1, built for PyTorch 2.10 "
"and 2.13 on Python 3.13.\n"
)
def test_torch_minors_with_different_interpreters_are_not_merged(self):
entries = [
(f"{index:064x}", prebuilt_wheels.wheel_name(package, torch, python, "win_amd64"))
for index, (package, torch, python) in enumerate(
[
(pkg, t, py)
for t in ("2.10.0", "2.11.0", "2.12.0")
for py in ("3.11", "3.12", "3.13")
for pkg in prebuilt_wheels.SPECS
]
+ [(pkg, t, "3.13") for t in ("2.13.0", "2.14.0") for pkg in prebuilt_wheels.SPECS]
)
]
notes = prebuilt_wheels.render_notes(entries, tag = "t", repo = "r")
assert (
"built for PyTorch 2.10, 2.11 and 2.12 on Python 3.11, 3.12 and 3.13; 2.13 and 2.14 "
"on Python 3.13." in notes
)
def test_platforms_with_different_coverage_are_not_merged(self):
entries = _entries(["linux_x86_64"]) + _entries(["win_amd64"], torches = ("2.14.0",))
notes = prebuilt_wheels.render_notes(entries, tag = "t", repo = "o/r")
assert "built for PyTorch 2.13 and 2.14 on Python 3.13. Windows" in notes
assert notes.endswith("built for PyTorch 2.14 on Python 3.13.\n")
def test_packages_are_listed_per_platform(self):
entries = _entries(["linux_x86_64"]) + [
row for row in _entries(["win_amd64"]) if row[1].startswith("mamba_ssm-")
]
notes = prebuilt_wheels.render_notes(entries, tag = "t", repo = "o/r")
windows = notes.split("Windows x86_64: ", 1)[1]
assert windows == "mamba-ssm 2.3.2.post1, built for PyTorch 2.13 and 2.14 on Python 3.13.\n"
def test_windows_only(self):
notes = prebuilt_wheels.render_notes(_entries(["win_amd64"]), tag = "t", repo = "o/r")
assert notes.startswith("Prebuilt Windows x86_64 CUDA 13 wheels for flash-attn 2.8.4")
def test_linux_only_reads_as_before(self):
notes = prebuilt_wheels.render_notes(_entries(["linux_x86_64"]), tag = "t", repo = "o/r")
assert notes == (
"Prebuilt Linux x86_64 CUDA 13 wheels for flash-attn 2.8.4, causal-conv1d 1.7.0 and "
"mamba-ssm 2.3.2.post1, built for PyTorch 2.13 and 2.14 on Python 3.13.\n"
)
class TestSums:
def test_every_wheel_sorted_by_name(self):
entries = _entries(["win_amd64", "linux_x86_64"])
entries.append(("sha256:" + "f" * 64, "SHA256SUMS"))
entries.append(("a" * 64, "x.whl.sigstore.json"))
text = prebuilt_wheels.render_sums(entries)
lines = text.splitlines()
assert len(lines) == 12
names = [line.split(" ", 1)[1] for line in lines]
assert names == sorted(names)
assert all(name.endswith(".whl") for name in names)
assert {n.rsplit("-", 1)[1] for n in names} == {"linux_x86_64.whl", "win_amd64.whl"}
def test_the_sha256_prefix_is_dropped(self):
text = prebuilt_wheels.render_sums([("sha256:" + "AB" * 32, "a.whl")])
assert text == "ab" * 32 + " a.whl\n"
@pytest.mark.parametrize("digest", ["unknown", "sha256:unknown", "-", "", "z" * 64, "a" * 63])
def test_a_missing_digest_is_refused(self, digest):
with pytest.raises(SystemExit):
prebuilt_wheels.render_sums([(digest, "a.whl")])
def test_nothing_to_list_is_refused(self):
with pytest.raises(SystemExit):
prebuilt_wheels.render_sums([("a" * 64, "SHA256SUMS")])
def test_cli(self):
stdin = f"{'b' * 64} z.whl\n{'a' * 64} a.whl\n"
out = subprocess.run(
[sys.executable, str(SCRIPTS / "prebuilt_wheels.py"), "sums"],
input = stdin,
check = True,
capture_output = True,
text = True,
).stdout
assert out == f"{'a' * 64} a.whl\n{'b' * 64} z.whl\n"
WINDOWS_LISTING = "\n".join(
[
rf"D:\a\unsloth\unsloth\src\build\temp.win-amd64-cpython-313\Release\csrc\flash_attn\src\k_{i:03d}.obj: cuda_compile"
for i in range(20)
]
+ [
r"D:\a\unsloth\unsloth\src\build\temp.win-amd64-cpython-313\Release\csrc\flash_attn\flash_api.obj: compile",
r"D:\a\unsloth\unsloth\src\build\lib.win-amd64-cpython-313\flash_attn_2_cuda.cp313-win_amd64.pyd: link",
"all: phony",
]
)
class TestShards:
def test_windows_targets_are_whole_paths(self):
objects = shard.list_objects(WINDOWS_LISTING)
assert len(objects) == 21
assert all(o.startswith("D:\\a\\") and o.endswith(".obj") for o in objects)
def test_windows_slices_cover_everything_once(self):
slices = [shard.slice_objects(WINDOWS_LISTING, k, 8) for k in range(8)]
assert sorted(o for s in slices for o in s) == shard.list_objects(WINDOWS_LISTING)
def test_linux_targets_are_unchanged(self):
listing = "/src/build/temp/a.o: cuda_compile\n/src/build/temp/b.o: compile\nall: phony"
assert shard.list_objects(listing) == ["/src/build/temp/a.o", "/src/build/temp/b.o"]
def test_no_objects_is_an_error_not_a_full_build(self):
with pytest.raises(SystemExit) as raised:
shard.slice_objects("all: phony\nfoo.pyd: link", 0, 8)
assert raised.value.code != 0
def test_an_empty_slice_never_runs_ninja(self, tmp_path, monkeypatch):
"""Two objects across eight shards leaves six empty slices. `ninja -v -j 1` with no
target would build the default target, the whole extension, in every one of them."""
calls = []
def fake_run(command, **kwargs):
calls.append(command)
return types.SimpleNamespace(stdout = "a.obj: cuda_compile\nb.obj: cuda_compile\n")
cpp_extension = types.ModuleType("torch.utils.cpp_extension")
cpp_extension._run_ninja_build = None
torch = types.ModuleType("torch")
utils = types.ModuleType("torch.utils")
torch.utils = utils
utils.cpp_extension = cpp_extension
monkeypatch.setitem(sys.modules, "torch", torch)
monkeypatch.setitem(sys.modules, "torch.utils", utils)
monkeypatch.setitem(sys.modules, "torch.utils.cpp_extension", cpp_extension)
monkeypatch.setattr(shard.subprocess, "run", fake_run)
(tmp_path / "setup.py").write_text(
"import torch.utils.cpp_extension as c\nc._run_ninja_build('.', False, '')\n"
)
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(sys, "argv", ["prebuilt_wheels_shard.py", "5", "8"])
with pytest.raises(SystemExit) as raised:
shard.main()
assert raised.value.code == 0
assert calls == [["ninja", "-t", "targets", "all"]]
def test_a_non_empty_slice_names_its_targets(self, tmp_path, monkeypatch):
calls = []
def fake_run(command, **kwargs):
calls.append(command)
return types.SimpleNamespace(stdout = "a.obj: cuda_compile\nb.obj: cuda_compile\n")
cpp_extension = types.ModuleType("torch.utils.cpp_extension")
torch = types.ModuleType("torch")
utils = types.ModuleType("torch.utils")
torch.utils = utils
utils.cpp_extension = cpp_extension
monkeypatch.setitem(sys.modules, "torch", torch)
monkeypatch.setitem(sys.modules, "torch.utils", utils)
monkeypatch.setitem(sys.modules, "torch.utils.cpp_extension", cpp_extension)
monkeypatch.setattr(shard.subprocess, "run", fake_run)
monkeypatch.setenv("MAX_JOBS", "1")
(tmp_path / "setup.py").write_text(
"import torch.utils.cpp_extension as c\nc._run_ninja_build('.', False, '')\n"
)
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(sys, "argv", ["prebuilt_wheels_shard.py", "1", "8"])
with pytest.raises(SystemExit):
shard.main()
assert calls[-1] == ["ninja", "-v", "-j", "1", "b.obj"]
class TestDeadline:
@pytest.mark.parametrize(
"text, seconds", [("300m", 18000), ("90s", 90), ("2h", 7200), ("45", 45)]
)
def test_durations(self, text, seconds):
assert deadline.parse_duration(text) == seconds
@pytest.mark.parametrize("text", ["", "m", "-5m", "0", "five"])
def test_bad_durations(self, text):
with pytest.raises(SystemExit):
deadline.parse_duration(text)
def test_the_exit_status_passes_through(self):
assert deadline.run(30, [sys.executable, "-c", "raise SystemExit(3)"]) == 0 + 3
@pytest.mark.skipif(sys.platform == "win32", reason = "POSIX process groups")
def test_the_whole_tree_ends_at_the_deadline(self, tmp_path):
marker = tmp_path / "grandchild.pid"
script = (
"import subprocess, sys, time\n"
"child = subprocess.Popen([sys.executable, '-c', 'import time; time.sleep(120)'])\n"
f"open({str(marker)!r}, 'w').write(str(child.pid))\n"
"time.sleep(120)\n"
)
started = time.monotonic()
assert deadline.run(2, [sys.executable, "-c", script]) == 124
assert time.monotonic() - started < 60
grandchild = int(marker.read_text())
for _ in range(50):
try:
import os
os.kill(grandchild, 0)
except ProcessLookupError:
break
time.sleep(0.1)
else:
pytest.fail("the grandchild outlived the deadline")
def test_usage(self):
assert deadline.main(["x", "5m"]) == 2
assert deadline.main(["x", "5m", "echo"]) == 2
MSVC_STEP = "Activate the MSVC ${{ matrix.platform == 'win_arm64' && 'arm64' || 'x64' }} environment (Windows)"
# Step name -> its `if`: the two toolkits are per Windows platform, the rest per OS.
WINDOWS_SETUP = {
"Enable long paths (Windows)": "runner.os == 'Windows'",
"Install the CUDA ${{ matrix.cuda_tag }} toolkit (Windows)": "matrix.platform == 'win_amd64'",
"Install the CUDA 13.4 toolkit (Windows ARM64)": "matrix.platform == 'win_arm64'",
MSVC_STEP: "runner.os == 'Windows'",
"Install ccache (Windows)": "runner.os == 'Windows'",
"Report memory, page file and disk (Windows)": "runner.os == 'Windows'",
}
LINUX_ONLY = (
"Harden runner (audit)",
"Free up disk space",
"Install the CUDA ${{ matrix.cuda_tag }} toolkit",
"Install ccache",
"Set up swap space",
)
class TestWorkflow:
def test_platforms_input_defaults_to_both(self, workflow):
inputs = triggers(workflow)["workflow_dispatch"]["inputs"]
assert inputs["platforms"]["default"] == "linux_x86_64,win_amd64"
plan = _step(workflow, "plan", "Expand the dispatch inputs into a matrix")
assert plan["env"]["UW_PLATFORMS"] == "${{ inputs.platforms }}"
@pytest.mark.parametrize("job", ["warm", "build"])
def test_windows_setup_exists_in_both_jobs_and_is_gated(self, workflow, job):
names = [s.get("name") for s in _steps(workflow, job)]
for name, gate in WINDOWS_SETUP.items():
step = _step(workflow, job, name)
assert step["if"] == gate, (job, name)
assert step["shell"] == "pwsh", (job, name)
for name in LINUX_ONLY:
assert _step(workflow, job, name)["if"] == "runner.os == 'Linux'", (job, name)
first_checkout = next(i for i, n in enumerate(names) if n and n.startswith("Checkout"))
assert names.index("Enable long paths (Windows)") < first_checkout
assert workflow["jobs"][job]["defaults"]["run"]["shell"] == "bash"
def test_warm_and_build_run_the_same_windows_setup(self, workflow):
for name in WINDOWS_SETUP:
assert _step(workflow, "warm", name) == _step(workflow, "build", name), name
def test_the_cuda_installer_is_nvidias_and_signature_checked(self, workflow):
run = _step(workflow, "build", "Install the CUDA ${{ matrix.cuda_tag }} toolkit (Windows)")[
"run"
]
assert "https://developer.download.nvidia.com/compute/cuda/" in run
assert "Get-AuthenticodeSignature" in run and "O=NVIDIA Corporation" in run
assert "'-s'" in run and "'-n'" in run
assert "New-Item -ItemType Junction -Path 'C:\\cuda'" in run
for package in ("nvcc", "cudart", "cublas_dev", "cusparse_dev", "cusolver_dev"):
assert f"'{package}'" in run, package
assert "visual_studio_integration" not in run and "Display.Driver" not in run
def test_ccache_is_pinned_by_sha256(self, workflow):
run = _step(workflow, "build", "Install ccache (Windows)")["run"]
assert "ccache-4.14.1-windows-x86_64" in run
assert "6219f3865ca59aec41ee4b678df171d5d35855ecb2b6dbbbd20690b3a68af7b4" in run
assert "CCACHE_COMPILERCHECK=content" in run
def test_msvc_environment_is_exported_with_distutils_use_sdk(self, workflow):
step = _step(workflow, "build", MSVC_STEP)
run = step["run"]
assert "vswhere.exe" in run and "$env:VCVARS" in run
assert (
"'vcvars64.bat'" in step["env"]["VCVARS"]
and "'vcvarsarm64.bat'" in step["env"]["VCVARS"]
)
assert "DISTUTILS_USE_SDK=1" in run
assert "GITHUB_ENV" in run and "GITHUB_PATH" in run
def test_artifacts_and_caches_are_per_platform(self, workflow):
body = json.dumps(workflow["jobs"])
for prefix in ("ccache-", "wheel-", "prebuilt-ccache-"):
for value in (
v
for v in body.split('"')
if v.startswith(prefix) and "${{ matrix" in v and "*" not in v
):
assert value.startswith(prefix + "${{ matrix.platform }}"), value
def test_the_windows_build_ends_the_whole_tree_at_the_deadline(self, workflow):
for job, name in (
("warm", "Compile slice ${{ matrix.shard }} of ${{ matrix.shards }}"),
("build", "Build the wheel"),
):
run = _step(workflow, job, name)["run"]
assert "prebuilt_wheels_timeout.py" in run
assert 'timeout --signal=INT --kill-after=120 "$BUILD_TIMEOUT"' in run
def test_the_rename_expects_the_cells_platform(self, workflow):
step = _step(workflow, "build", "Name the wheel the way upstream names it")
assert step["env"]["WHEEL_PLATFORM"] == "${{ matrix.platform }}"
assert (
"PLATFORM" not in step["env"]
) # vcvars exports Platform=x64; Windows env is case-insensitive
assert "-${WHEEL_PLATFORM}.whl" in step["run"]
def test_the_torch_abi_check_follows_the_cell(self, workflow):
for job in ("warm", "build"):
step = next(
s
for s in _steps(workflow, job)
if s.get("name", "") == "Install ${{ matrix.torch_spec }}"
)
assert step["env"]["MATRIX_ABI"] == "${{ matrix.abi }}"
assert 'os.environ["MATRIX_ABI"]' in step["run"]
def test_the_windows_smoke_hides_the_toolkit(self, workflow):
"""A user has no CUDA toolkit; the import must resolve against torch's own DLLs."""
smoke = _step(workflow, "build", "Smoke test the wheel in a fresh venv")
run = smoke["run"]
assert "Scripts/python.exe" in run
assert "unset CUDA_HOME CUDA_PATH" in run
assert smoke["env"]["SMOKE_EXCLUDE"] == "${{ matrix.smoke_exclude }}"
assert smoke["env"]["SMOKE_EXTRA"] == "${{ matrix.smoke_extra }}"
assert 'os.environ.get("SMOKE_EXCLUDE"' in run and 'os.environ.get("SMOKE_EXTRA"' in run
def test_the_gpu_job_only_gets_linux_cells(self, workflow):
job = workflow["jobs"]["gpu-smoke"]
assert job["strategy"]["matrix"] == "${{ fromJSON(needs.plan.outputs.gpu_matrix) }}"
assert "needs.plan.outputs.gpu_count != '0'" in job["if"]
assert workflow["jobs"]["plan"]["outputs"]["gpu_count"]
def test_publish_rebuilds_sha256sums_over_the_whole_release(self, workflow):
steps = workflow["jobs"]["publish"]["steps"]
create = next(s for s in steps if s.get("name") == "Create or update the release")
# This run's SHA256SUMS lists this run only; it must not become the release's.
assert "wheels/SHA256SUMS" not in create["run"]
regen = next(s for s in steps if "SHA256SUMS" in s.get("name", ""))
run = regen["run"]
assert "prebuilt_wheels.py sums" in run
assert "--clobber SHA256SUMS" in run
assert "releases/assets/$id" in run
assert "sha256:unknown" not in run
assert run.index("prebuilt_wheels.py sums") < run.index("prebuilt_wheels.py notes")
# The text the patch keys on, from the pinned upstream files.
_MAMBA_KERNEL = """#include <c10/cuda/CUDAException.h>
#ifndef USE_ROCM
#include <cub/block/block_load.cuh>
#else
#include <hipcub/hipcub.hpp>
#endif
constexpr float kLog2e = M_LOG2E;
void launch() {
BOOL_SWITCH(x, kX, [&] {
BOOL_SWITCH(y, kY, [&] {
if (kSmemSize >= 48 * 1024) {
#ifndef USE_ROCM
C10_CUDA_CHECK(cudaFuncSetAttribute(
kernel, cuda_branch));
#else
C10_CUDA_CHECK(cudaFuncSetAttribute(
(void *) kernel, rocm_branch));
#endif
}
});
});
}
void dispatch() {
#ifndef USE_ROCM
cuda_dispatch();
#else
rocm_dispatch();
#endif
}
"""
class TestMambaMsvcPatch:
def _tree(
self,
tmp_path,
text = _MAMBA_KERNEL,
):
kernels = tmp_path / "csrc" / "selective_scan"
kernels.mkdir(parents = True)
for name in ("selective_scan_fwd_kernel.cuh", "selective_scan_bwd_kernel.cuh"):
(kernels / name).write_text(text, encoding = "utf-8")
return kernels
def _run(self, root):
return subprocess.run(
[sys.executable, str(SCRIPTS / "patch_mamba_msvc.py"), str(root)],
capture_output = True,
text = True,
)
def test_keeps_the_cuda_branch_and_is_idempotent(self, tmp_path):
kernels = self._tree(tmp_path)
assert self._run(tmp_path).returncode == 0
first = (kernels / "selective_scan_fwd_kernel.cuh").read_text(encoding = "utf-8")
assert "cuda_branch" in first and "rocm_branch" not in first
# File-scope USE_ROCM blocks are legal under MSVC and stay.
assert "hipcub" in first
# Function-scope USE_ROCM blocks are legal under MSVC and stay.
assert "rocm_dispatch()" in first
again = self._run(tmp_path)
assert again.returncode == 0 and "already patched" in again.stdout
assert (kernels / "selective_scan_fwd_kernel.cuh").read_text(encoding = "utf-8") == first
def test_a_second_in_lambda_block_fails_loudly(self, tmp_path):
body = _MAMBA_KERNEL.split("void launch()")[1].split("void dispatch()")[0]
self._tree(tmp_path, _MAMBA_KERNEL + "void launch2()" + body)
result = self._run(tmp_path)
assert result.returncode == 1
assert "found 2" in result.stderr
def test_runs_on_windows_mamba_cells_only(self, workflow):
for job in ("warm", "build"):
step = next(
s
for s in workflow["jobs"][job]["steps"]
if "patch_mamba_msvc.py" in s.get("run", "")
)
assert step["if"] == "runner.os == 'Windows' && matrix.package == 'mamba-ssm'"