unsloth/tests/sh/test_previous_torch_pin.sh
Daniel Han 4e1cb4affa
install.sh: route Strix to the AMD arch index on rocm7.2 (#7264) + PCI detection hint (#7293)
* install.sh: route Strix (gfx1151/gfx1150) to the AMD arch index on rocm7.2, add PCI hint

Two Linux install fixes for AMD Strix Halo / Strix Point:

1. #7264: Strix reverts to rocm7.2. Modern ROCm (7.3+) caps to the generic
   rocm7.2 index and the Radeon repo can be unavailable, so gfx1151/gfx1150
   landed on a non-arch-specific build (torch 2.11+rocm7.2) instead of
   repo.amd.com/rocm/whl/gfx<arch> (torch 2.11+rocm7.13, AMD's real Strix
   fixes). The reroute to that arch index only fired on rocm7.1; broaden it to
   rocm7.2 too. Only acts when a gfx1151/gfx1150 is actually detected, so other
   arches on rocm7.2 pass through unchanged.

2. Rows about Strix not detected -> CPU-only: when no GPU is detected but an AMD
   display GPU is on the PCI bus, print a targeted hint (ROCm kernel stack /
   /dev/kfd missing) instead of only the generic docs pointer. Purely
   additive diagnostic; does not change the torch index decision.

* Address review on PR #7293: gate PCI hint on ROCm-detection failure, fix test marker, use read builtin

- Only show the 'ROCm cannot see the GPU' hint when _has_amd_rocm_gpu fails;
  a detected-but-too-old ROCm (rocminfo works, wheels need 6.0+) has its own path.
- Update test_previous_torch_pin.sh to the stable 'Strix Halo / Strix Point:'
  marker after the heading reworded (the old grep broke the ordering assert).
- _amd_gpu_present_via_pci: read builtin instead of spawning cat twice per
  device, and guard /sys/bus/pci/devices existence.

* install.sh: reroute Strix on any generic index older than the arch build

Generalize the Strix reroute from the hardcoded rocm7.1/rocm7.2 match to a
version compare against the arch index's own build (rocm7.13):

- backwards: rocm6.0-6.4 and rocm7.0 now reroute (were silently missed)
- forwards: any future intermediate rocm7.x below 7.13 reroutes; rocm7.13+
  is left alone so a generic index that already carries the fix is not
  downgraded to the arch build

_rocm_index_below does an integer major.minor compare (so rocm7.2 < rocm7.13);
non-rocm, arch (gfx), and unparseable URLs return false, so NVIDIA/CPU and the
arch index itself are untouched. Reroute still fires only for gfx1150/gfx1151.

* install.sh: tighten _amd_gpu_present_via_pci comment (no code change)

* install.sh: match the index leaf in the Strix version reroute (#7293 review)

Address two review points on the rocm-version reroute:

- Parse the final path segment (_torch_index_leaf) instead of grepping the whole
  URL. A custom mirror whose base path holds its own rocm token (e.g.
  .../rocm7.13/cache/rocm7.2) previously matched the base and skipped the reroute;
  now it compares the leaf (rocm7.2) like the nearby index-family logic. Renamed
  the helper to _rocm_leaf_below and switched the case selector to $_torch_index_leaf.
- Replace the stale test_strix_override_only_fires_on_rocm71 (which passed by
  matching the new rocm7.13 comment) with an executed test that runs _rocm_leaf_below
  and asserts rocm6.0-7.12 reroute while rocm7.13+/gfx/cu leaves do not.

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

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

* install.sh: keep gfx probes non-fatal under set -e (#7293 review)

The Strix reroute now matches every rocm* index, not just rocm7.1, so its gfx
detection runs on all AMD installs. Each `_gfx_all=$(rocminfo|amd-smi | grep -oE
gfx...)` returns 1 when grep finds no match, which under set -euo pipefail aborts
the installer before the next fallback runs (e.g. rocminfo present but emitting no
gfx token). Append `|| true` to the three probes, matching the display block that
already guards this. Add an executed regression test (shimmed rocminfo/amd-smi)
that fails if any probe becomes fatal again.

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-21 19:40:59 -07:00

137 lines
9.8 KiB
Bash

#!/bin/bash
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
# Unit tests for install.sh's _previous_torch_pin, which keeps the previous
# venv's torch RELEASE on a re-run instead of moving the user to a newer one.
# The release is kept regardless of the old build's flavor tag (PyPI bare,
# +cuXXX, +rocm, +cpu): the pin installs from the freshly chosen index, so the
# flavor follows the machine while the release follows the user. Per-leaf
# windows still win (rocm7.2 / Strix floors, out-of-window manual installs).
# Helpers are extracted from install.sh and sourced.
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INSTALL_SH="$SCRIPT_DIR/../../install.sh"
PASS=0
FAIL=0
# Extract _previous_torch_pin and its dependency _torch_release_in_window.
_FUNC_FILE=$(mktemp)
{
sed -n '/^_torch_release_in_window()/,/^}/p' "$INSTALL_SH"
echo ""
sed -n '/^_previous_torch_pin()/,/^}/p' "$INSTALL_SH"
} > "$_FUNC_FILE"
# shellcheck disable=SC1090
. "$_FUNC_FILE"
rm -f "$_FUNC_FILE"
assert_eq() {
_label="$1"; _expected="$2"; _actual="$3"
if [ "$_actual" = "$_expected" ]; then
echo " PASS: $_label"; PASS=$((PASS + 1))
else
echo " FAIL: $_label (expected '$_expected', got '$_actual')"; FAIL=$((FAIL + 1))
fi
}
unset UNSLOTH_TORCH_UPGRADE
echo "=== _previous_torch_pin: in-window releases are kept, any flavor ==="
assert_eq "cu126 wheel" "torch==2.10.0" "$(_previous_torch_pin '2.10.0+cu126' 'torch>=2.4,<2.12.0')"
assert_eq "cu130 wheel" "torch==2.10.0" "$(_previous_torch_pin '2.10.0+cu130' 'torch>=2.4,<2.12.0')"
assert_eq "cpu wheel" "torch==2.10.0" "$(_previous_torch_pin '2.10.0+cpu' 'torch>=2.4,<2.12.0')"
assert_eq "PyPI bare version (CUDA build on Linux)" "torch==2.10.0" "$(_previous_torch_pin '2.10.0' 'torch>=2.4,<2.12.0')"
assert_eq "rocm wheel" "torch==2.10.0" "$(_previous_torch_pin '2.10.0+rocm6.4' 'torch>=2.4,<2.11.0')"
assert_eq "rocm three-component tag" "torch==2.9.1" "$(_previous_torch_pin '2.9.1+rocm7.2.1' 'torch>=2.4,<2.12.0')"
assert_eq "Intel xpu wheel" "torch==2.9.0" "$(_previous_torch_pin '2.9.0+xpu' 'torch>=2.4,<2.12.0')"
assert_eq "local suffix stripped" "torch==2.9.1" "$(_previous_torch_pin '2.9.1+cu128' 'torch>=2.4,<2.12.0')"
echo "=== _previous_torch_pin: raised floors reject older releases ==="
# rocm7.2 / Strix gfx leaves raise TORCH_CONSTRAINT to >=2.11.0 BEFORE the pin
# is evaluated, so an old 2.10 is out of window there and the floor wins.
assert_eq "old 2.10 vs rocm7.2 floor" "" "$(_previous_torch_pin '2.10.0+rocm7.1' 'torch>=2.11.0,<2.12.0')"
assert_eq "2.11 passes the rocm7.2 floor" "torch==2.11.0" "$(_previous_torch_pin '2.11.0+rocm7.2' 'torch>=2.11.0,<2.12.0')"
echo "=== _previous_torch_pin: probe noise never becomes a pin ==="
assert_eq "empty version" "" "$(_previous_torch_pin '' 'torch>=2.4,<2.12.0')"
assert_eq "garbage version" "" "$(_previous_torch_pin 'not-a-version' 'torch>=2.4,<2.12.0')"
assert_eq "traceback fragment" "" "$(_previous_torch_pin "ModuleNotFoundError: No module named 'torch'" 'torch>=2.4,<2.12.0')"
echo "=== _previous_torch_pin: nightly / dev / source builds never pin ==="
# No stable index carries these, so pinning would print "keeping it" and then
# burn a doomed resolve before the range fallback rescues the install.
assert_eq "nightly dev build" "" "$(_previous_torch_pin '2.11.0.dev20250704+cu128' 'torch>=2.4,<2.12.0')"
assert_eq "source build a0 tag" "" "$(_previous_torch_pin '2.9.0a0+gitabc1234' 'torch>=2.4,<2.12.0')"
assert_eq "release candidate" "" "$(_previous_torch_pin '2.11.0rc1+cu130' 'torch>=2.4,<2.12.0')"
echo "=== _previous_torch_pin: out-of-window releases never pin ==="
assert_eq "2.3.x below the cu floor" "" "$(_previous_torch_pin '2.3.1+cu118' 'torch>=2.4,<2.12.0')"
assert_eq "2.12.x above the cu ceiling" "" "$(_previous_torch_pin '2.12.0+cu130' 'torch>=2.4,<2.12.0')"
assert_eq "floor boundary 2.4.0 kept" "torch==2.4.0" "$(_previous_torch_pin '2.4.0+cu126' 'torch>=2.4,<2.12.0')"
assert_eq "ceiling-adjacent 2.11.x kept" "torch==2.11.1" "$(_previous_torch_pin '2.11.1+cu130' 'torch>=2.4,<2.12.0')"
assert_eq "cpu window excludes 2.11.x" "" "$(_previous_torch_pin '2.11.0+cpu' 'torch>=2.4,<2.11.0')"
assert_eq "mac floor excludes 2.5.x" "" "$(_previous_torch_pin '2.5.1' 'torch>=2.6,<2.11.0')"
assert_eq "malformed window never pins" "" "$(_previous_torch_pin '2.10.0+cu126' 'torch')"
assert_eq "empty window never pins" "" "$(_previous_torch_pin '2.10.0+cu126' '')"
echo "=== _torch_release_in_window ==="
assert_eq "in window" "yes" "$(_torch_release_in_window '2.10.0' 'torch>=2.4,<2.12.0')"
assert_eq "at floor" "yes" "$(_torch_release_in_window '2.4.0' 'torch>=2.4,<2.12.0')"
assert_eq "below floor" "no" "$(_torch_release_in_window '2.3.1' 'torch>=2.4,<2.12.0')"
assert_eq "at ceiling" "no" "$(_torch_release_in_window '2.12.0' 'torch>=2.4,<2.12.0')"
assert_eq "next major" "no" "$(_torch_release_in_window '3.0.0' 'torch>=2.4,<2.12.0')"
assert_eq "patch-level floor" "yes" "$(_torch_release_in_window '2.11.5' 'torch>=2.11.0,<2.12.0')"
assert_eq "no ceiling -> no" "no" "$(_torch_release_in_window '2.10.0' 'torch>=2.4')"
assert_eq "garbage minor -> no" "no" "$(_torch_release_in_window '2.x' 'torch>=2.4,<2.12.0')"
echo "=== _previous_torch_pin: UNSLOTH_TORCH_UPGRADE=1 opts out ==="
assert_eq "upgrade env set" "" "$(UNSLOTH_TORCH_UPGRADE=1 _previous_torch_pin '2.10.0+cu126' 'torch>=2.4,<2.12.0')"
assert_eq "upgrade env 0" "torch==2.10.0" "$(UNSLOTH_TORCH_UPGRADE=0 _previous_torch_pin '2.10.0+cu126' 'torch>=2.4,<2.12.0')"
echo "=== install.sh wiring ==="
# The probe must run against the OLD venv, before it is moved aside for rollback.
_probe_line=$(grep -n '_PREV_TORCH_VER=\$(' "$INSTALL_SH" | head -1 | cut -d: -f1)
_move_line=$(grep -n '_start_studio_venv_replacement "\$VENV_DIR"' "$INSTALL_SH" | head -1 | cut -d: -f1)
assert_eq "probe exists" "yes" "$([ -n "$_probe_line" ] && echo yes)"
assert_eq "probe before venv replacement" "yes" "$([ -n "$_probe_line" ] && [ -n "$_move_line" ] && [ "$_probe_line" -lt "$_move_line" ] && echo yes)"
# The pin must be evaluated AFTER the last index/constraint decision (the Strix
# reroute raises the floor), so a raised floor rejects an older kept release.
_pin_line=$(grep -n '_prev_pin=\$(_previous_torch_pin' "$INSTALL_SH" | head -1 | cut -d: -f1)
_strix_line=$(grep -n 'Strix Halo / Strix Point:' "$INSTALL_SH" | head -1 | cut -d: -f1)
assert_eq "pin evaluated after the Strix reroute" "yes" "$([ -n "$_pin_line" ] && [ -n "$_strix_line" ] && [ "$_pin_line" -gt "$_strix_line" ] && echo yes)"
# A kept release that vanished from the index must fall back to the supported range.
assert_eq "resolve-failure fallback wired" "yes" "$(grep -q 'TORCH_CONSTRAINT="\$_PREV_FALLBACK_CONSTRAINT"' "$INSTALL_SH" && echo yes)"
assert_eq "pin gated on SKIP_TORCH" "yes" "$(grep -q 'if \[ "\$SKIP_TORCH" = false \]; then' "$INSTALL_SH" && echo yes)"
# Every --default-index torch install path must go through the kept-release
# helper (definition + default path + three ROCm-index fallbacks + two ROCm
# repairs + the flavor repair), so a pinned release missing from the index
# never aborts a rerun.
_helper_uses=$(grep -c '_install_torch_default_index' "$INSTALL_SH")
assert_eq "kept-release helper used by all default-index paths" "yes" "$([ "$_helper_uses" -ge 8 ] && echo yes)"
_repair_uses=$(grep -c '_install_torch_default_index --force-reinstall' "$INSTALL_SH")
assert_eq "ROCm repairs routed through the kept-release helper" "yes" "$([ "$_repair_uses" -ge 2 ] && echo yes)"
# The wrong-flavor repair must use the helper too (it runs under set -e, so a
# direct uv call with an unresolvable pin would abort the whole installer).
assert_eq "flavor repair routed through the kept-release helper" "yes" "$(grep -q '_install_torch_default_index \\' "$INSTALL_SH" && grep -q -- '--reinstall-package torch --reinstall-package torchvision --reinstall-package torchaudio' "$INSTALL_SH" && echo yes)"
# The kept-release install must pair the companions with the kept minor:
# torchaudio no longer exact-pins torch, so unconstrained it resolves a newer
# mismatched build (verified: torch==2.9.0 pulled torchaudio 2.11.0 on cu130).
assert_eq "kept-release install pairs torchvision/torchaudio to the kept minor" "yes" "$(grep -q 'torchaudio==2.\${_itdi_minor}.\*' "$INSTALL_SH" && grep -q 'torchvision==0.\$((_itdi_minor + 15)).\*' "$INSTALL_SH" && echo yes)"
# The Radeon direct-wheel path must also honor the pin: an exact-first kept-trio
# attempt (exact patch, else the kept minor's newest patch, with paired
# vision/audio) runs BEFORE the newest-trio search, and the newest-trio search
# only runs when that attempt did not produce a match, so a kept release can
# neither drift to another patch/minor nor be undercut by the gap search.
_radeon_kept_line=$(grep -n '_kept_torch=\$(_pick_radeon_wheel "torch" *"\${_prev_kept_base}"' "$INSTALL_SH" | head -1 | cut -d: -f1)
_radeon_loop_line=$(grep -n 'Loop downwards to find the first complete matching trio' "$INSTALL_SH" | head -1 | cut -d: -f1)
assert_eq "Radeon kept-trio attempt before the newest-trio search" "yes" "$([ -n "$_radeon_kept_line" ] && [ -n "$_radeon_loop_line" ] && [ "$_radeon_kept_line" -lt "$_radeon_loop_line" ] && echo yes)"
assert_eq "Radeon newest-trio search gated on no kept match" "yes" "$(grep -q 'if \[ "\$_radeon_versions_match" != true \] &&' "$INSTALL_SH" && echo yes)"
assert_eq "Radeon kept-trio gap falls back with a warning" "yes" "$(grep -q 'lacks a complete wheel set for kept' "$INSTALL_SH" && echo yes)"
echo ""
if [ "$FAIL" -gt 0 ]; then
echo "$FAIL check(s) FAILED"
exit 1
fi
echo "All $PASS checks passed"