amd: require bitsandbytes>=0.50.0 in the amd extra (fixes ROCm 4-bit NaNs) (#7535)

* amd: require bitsandbytes>=0.50.0 in the amd extra

bnb <= 0.49.2 NaNs at decode shape on every AMD GPU. The ROCm 4-bit GEMV
fix (bnb PR #1887) first ships in 0.50.0, on PyPI since 2026-07-24, so the
old >=0.49.1 floor could still resolve the broken range.

Mirrors the same change made on the pip release branch in #7278.

* amd: cite the 0.50.0 ROCm work accurately in the bnb floor comment

The comment credited bnb PR #1887 as "the ROCm 4-bit GEMV fix" for every
AMD GPU. #1887 decouples blocksize from warp size and fixes a hardcoded
warp size of 32 in kgemm_4bit_inference_naive, which is a CDNA problem by
construction. The RDNA-side work is #1979 (fused 4-bit SIMT GEMM) and
#2012 (RDNA3/4 workgroup resonance). All three first ship in 0.50.0, so
the >=0.50.0 floor is unchanged; only the justification was wrong.

* amd: raise the installer bitsandbytes fallback floors to 0.50.0

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

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

* amd: stop reporting the bitsandbytes PyPI fallback as broken

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

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

* Tighten AMD bnb floor comments

* Keep the amd extra citation and the AMD install guide reference

* amd: do not promise aarch64 a ROCm 4-bit backend it never gets

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

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

* amd: fall back to the PyPI bitsandbytes floor on Windows ROCm too

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

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

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Leo Borcherding 2026-07-28 20:12:26 -05:00 committed by GitHub
commit 411cb86d62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 206 additions and 46 deletions

View file

@ -862,3 +862,76 @@ class TestNoTorchPersistenceParity:
manifest = (REPO_ROOT / "studio" / "install_manifest.py").read_text(encoding = "utf-8")
assert 'NO_TORCH_TRUTHY: Tuple[str, ...] = ("1", "true", "yes", "on")' in manifest
assert "install_manifest.NO_TORCH_TRUTHY" in STACK_PY.read_text(encoding = "utf-8")
class TestAmdBnbFloorParity:
"""bitsandbytes <= 0.49.2 NaNs at 4-bit decode shape on every AMD GPU; the ROCm
4-bit GEMV fix (bnb #1887) first ships on PyPI in 0.50.0. The `amd` extra,
install.sh and the Studio stack resolve bitsandbytes independently, so all three
must carry the same floor or an unreachable pre-release wheel silently reinstates
the broken range."""
FLOOR = "0.50.0"
PYPROJECT = REPO_ROOT / "pyproject.toml"
def test_amd_extra_floor(self):
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"
specs = re.findall(r'"(bitsandbytes[^"]*)"', amd.group(1))
assert specs, "the amd extra must pin bitsandbytes"
for spec in specs:
assert spec.startswith(
f"bitsandbytes>={self.FLOOR}"
), f"amd extra bitsandbytes floor must be >={self.FLOOR}, got {spec!r}"
def test_install_sh_pypi_fallback_floor(self):
text = INSTALL_SH.read_text(encoding = "utf-8")
assert (
f'_BNB_ROCM_PYPI_FALLBACK="bitsandbytes>={self.FLOOR}"' in text
), f"install.sh _install_bnb_rocm PyPI fallback must floor at {self.FLOOR}"
def test_stack_py_pypi_fallback_floor(self):
text = STACK_PY.read_text(encoding = "utf-8")
assert (
f'_BNB_ROCM_PYPI_FALLBACK = "bitsandbytes>={self.FLOOR}"' in text
), f"install_python_stack.py PyPI fallback must floor at {self.FLOOR}"
def test_no_installer_still_allows_the_broken_range(self):
for path in (INSTALL_SH, INSTALL_PS1, SETUP_PS1, STACK_PY, self.PYPROJECT):
text = path.read_text(encoding = "utf-8")
for line in text.splitlines():
if "bitsandbytes>=0.49" in line and not line.lstrip().startswith(("#", "//")):
raise AssertionError(
f"{path.name} still floors bitsandbytes in the broken ROCm range: {line.strip()!r}"
)
def test_fallback_is_not_reported_as_broken(self):
"""The fallback now installs the first fixed release, so neither installer
may still call 4-bit decode broken on ROCm."""
for path in (INSTALL_SH, STACK_PY):
text = path.read_text(encoding = "utf-8")
assert (
"4-bit decode broken on ROCm" not in text
), f"{path.name} still reports the repaired PyPI fallback as broken"
assert (
"4-bit decode will be broken on ROCm" not in text
), f"{path.name} still reports the repaired PyPI fallback as broken"
def test_aarch64_is_not_told_it_has_a_rocm_backend(self):
"""bitsandbytes ships no ROCm kernels in its aarch64 wheel at any version, so
neither installer may hand aarch64 the x86_64 "carries the ROCm 4-bit fix"
message, and both must warn that 4-bit needs a source build there."""
sh = INSTALL_SH.read_text(encoding = "utf-8")
assert "_bnb_rocm_arch_has_binary()" in sh
assert "_warn_bnb_no_rocm_binary()" in sh
assert (
sh.count("_warn_bnb_no_rocm_binary\n") >= 2
), "install.sh must warn on aarch64 after both the pre-release and the fallback install"
py = STACK_PY.read_text(encoding = "utf-8")
assert "def _bnb_rocm_arch_has_binary(" in py
assert "_bnb_rocm_arch_has_binary()" in py
for text, name in ((sh, "install.sh"), (py, "install_python_stack.py")):
assert (
"4-bit QLoRA needs a source build" in text
), f"{name} must tell aarch64 users 4-bit needs a source build"

View file

@ -3157,12 +3157,35 @@ class TestInstallBnbWindowsRocm:
assert result is False
assert "BNB_ROCM_VERSION" not in os.environ
def test_no_op_when_win_amd64_url_missing(self):
"""Should be silent no-op if win_amd64 key absent from _BNB_ROCM_PRERELEASE_URLS."""
def test_falls_back_to_pypi_when_win_amd64_url_missing(self):
"""No win_amd64 pre-release wheel must not mean no bitsandbytes: PyPI
>=0.50.0 ships libbitsandbytes_rocm{714,72}.dll, so it is a real ROCm build."""
with patch.object(stack_mod, "_BNB_ROCM_PRERELEASE_URLS", {}):
with patch.object(stack_mod, "pip_install_try") as mock_pip:
with patch.object(stack_mod, "pip_install_try", return_value = True) as mock_pip:
stack_mod._install_bnb_windows_rocm()
mock_pip.assert_not_called()
assert mock_pip.call_count == 1
assert stack_mod._BNB_ROCM_PYPI_FALLBACK in mock_pip.call_args.args
def test_falls_back_to_pypi_when_prerelease_install_fails(self):
"""A blocked GitHub pre-release URL must fall through to the PyPI floor rather
than leaving Windows ROCm with no working bitsandbytes."""
with patch.object(stack_mod, "pip_install_try", side_effect = [False, True]) as mock_pip:
with patch.object(stack_mod, "_detect_bnb_rocm_dll_ver", return_value = "72"):
result = stack_mod._install_bnb_windows_rocm()
assert result is True
assert mock_pip.call_count == 2
assert "win_amd64" in str(mock_pip.call_args_list[0])
assert stack_mod._BNB_ROCM_PYPI_FALLBACK in mock_pip.call_args_list[1].args
def test_returns_false_only_when_both_paths_fail(self):
"""Both the pre-release wheel and the PyPI fallback must fail before the
helper reports failure."""
with patch.dict(os.environ, {}, clear = False):
os.environ.pop("BNB_ROCM_VERSION", None)
with patch.object(stack_mod, "pip_install_try", return_value = False) as mock_pip:
result = stack_mod._install_bnb_windows_rocm()
assert result is False
assert mock_pip.call_count == 2
def test_sets_bnb_rocm_version_from_detected_dll(self):
"""BNB_ROCM_VERSION is set from the DLL detected after install."""