install: make torch 2.11 the default supported line on wheel-backed platforms

Fresh installs now resolve the torch 2.11 trio (torch 2.11.0,
torchvision 0.26.0, torchaudio 2.11.0) everywhere the wheels exist,
verified by live index resolution for py3.11-3.13: Linux x86_64
(cpu, cu126, cu128, cu130, rocm7.1, rocm7.2), Linux aarch64 (cpu,
cu130), Windows x86_64 (cpu, cu126, cu128, cu130; triton-windows<3.7
resolves 3.6.0.post26, the 2.11 pairing) and macOS arm64. Existing
installs are unaffected: the release preservation on both installers
keeps the installed torch on re-runs (verified end to end against the
live cpu index: a seeded 2.10.0 venv stays 2.10.0 with paired 0.25/2.10
companions while a fresh venv resolves the 2.11 trio).

- install.sh: the supported range is centralized in _TORCH_CEILING /
  _TORCHVISION_CEILING / _TORCHAUDIO_CEILING and composed into the
  default constraints, so the next bump (torch 2.12) is a three-line
  change. The now-redundant cu* widen arm and custom-pin companion
  block collapse into the default; the curated ROCm >=2.11 floors stay
  literal.
- install.ps1: the hoisted default trio, the CUDA flavor repair and the
  ROCm CPU fallback all use the <2.12 trio (the leaf gate collapsed
  since cu and non-cu now share the range).
- setup.ps1: the unknown-leaf pinned CUDA trio widens to <2.12,
  matching the pinned cpu path.

Deliberately NOT widened: rocm6.4 (tops at torch 2.9.1) and rocm7.0
(2.10.0) by index content; macOS x86_64 (no >=2.4 wheels, stays
no-torch); the AMD per-arch repo.amd.com curated bounds for arches
outside the 2.11 allowlist.

Constraint suites updated to the ceiling scheme; parity suites updated
to the widened trio. All sh, ps1 and pytest installer suites pass (the
host-defaults suite and the tokenizers negative-control are known
pre-existing failures).
This commit is contained in:
Daniel Han 2026-07-20 10:44:42 +00:00
commit 3f79b5e53d
6 changed files with 100 additions and 132 deletions

View file

@ -2156,29 +2156,21 @@ exit 0
}
}
} elseif ($TorchIndexUrl -or $ROCmIndexUrl) {
# Leaf-gated bounded trio, HOISTED so the release-preservation decision below can use it as the
# default route window: torchaudio 2.11 dropped its torch pin, so a bare companion can drift from
# a capped torch. cu<digits> families ship torch 2.11.x (paired triton-windows 3.6) so the ceiling
# widens to <2.12; other leaves keep the 2.10 line. Mirrors install.sh and _CUDA_TORCH_PKG_SPEC.
$_idxLeaf = (($TorchIndexUrl -split '[?#]', 2)[0].TrimEnd('/') -split '/')[-1].ToLowerInvariant()
if ($_idxLeaf -match '^cu[0-9]+$') {
$_pinTorchSpec = "torch>=2.4,<2.12.0"
$_pinVisionSpec = "torchvision>=0.19,<0.27.0"
$_pinAudioSpec = "torchaudio>=2.4,<2.12.0"
} else {
$_pinTorchSpec = "torch>=2.4,<2.11.0"
$_pinVisionSpec = "torchvision>=0.19,<0.26.0"
$_pinAudioSpec = "torchaudio>=2.4,<2.11.0"
}
# Bounded default trio (torch 2.11 line; wheels verified on cpu + cu126/cu128/cu130 for
# win_amd64 with paired triton-windows 3.6), HOISTED so the release-preservation decision
# below can use it as the default route window. torchaudio 2.11 dropped its torch pin, so a
# bare companion can drift from a capped torch. Bump the three ceilings together with
# install.sh's _TORCH_CEILING trio and the repair/fallback sites below (2 more literals).
$_pinTorchSpec = "torch>=2.4,<2.12.0"
$_pinVisionSpec = "torchvision>=0.19,<0.27.0"
$_pinAudioSpec = "torchaudio>=2.4,<2.12.0"
# Release preservation (twin of install.sh's _PREV_TORCH_PIN decision): evaluated after every
# index/floor choice, incl. the ROCm reroute, so a raised floor rejects an older release. The
# route window is the leaf-gated CUDA trio by default; the ROCm path uses its floor (or the
# torch>=2.4,<2.11.0 range the ROCm->CPU fallback installs). The kept release is exported for
# setup.ps1 (UNSLOTH_KEPT_TORCH) and cleared after setup runs.
# kept release is exported for setup.ps1 (UNSLOTH_KEPT_TORCH) and cleared after setup runs.
$script:PrevTorchPin = $null
if (-not $SkipTorch -and $script:PrevTorchVer) {
$_routeWindow = $_pinTorchSpec
if ($ROCmIndexUrl) { if ($ROCmTorchFloor) { $_routeWindow = $ROCmTorchFloor } else { $_routeWindow = "torch>=2.4,<2.11.0" } }
if ($ROCmIndexUrl -and $ROCmTorchFloor) { $_routeWindow = $ROCmTorchFloor }
$script:PrevTorchPin = Get-PreviousTorchPin -TorchVersion $script:PrevTorchVer -Constraint $_routeWindow
if ($script:PrevTorchPin) {
$env:UNSLOTH_KEPT_TORCH = $script:PrevTorchPin.Release.PublicBase
@ -2212,7 +2204,7 @@ exit 0
$CpuFallbackIndexUrl = if ($env:UNSLOTH_PYTORCH_MIRROR) { "$($env:UNSLOTH_PYTORCH_MIRROR.TrimEnd('/'))/cpu" } else { "https://download.pytorch.org/whl/cpu" }
substep "ROCm PyTorch install failed (exit $torchInstallExit); using a CPU base, Unsloth setup retries ROCm." "Yellow"
# --force-reinstall: a failed ROCm install can leave an unpinned ROCm torch that still satisfies the CPU torch>= range, so without it uv would keep the ROCm build and only swap companions -- a mismatched venv the flavor-repair block won't fix. (No kept-release attempt: the ROCm attempts above always resolve or clear $script:PrevTorchPin first, so a pin never reaches this CPU base.)
$torchInstallExit = Invoke-InstallCommandRetry -Label "install PyTorch (CPU fallback)" { uv pip install --python $VenvPython --force-reinstall "torch>=2.4,<2.11.0" "torchvision>=0.19,<0.26.0" "torchaudio>=2.4,<2.11.0" --default-index $CpuFallbackIndexUrl }
$torchInstallExit = Invoke-InstallCommandRetry -Label "install PyTorch (CPU fallback)" { uv pip install --python $VenvPython --force-reinstall "torch>=2.4,<2.12.0" "torchvision>=0.19,<0.27.0" "torchaudio>=2.4,<2.12.0" --default-index $CpuFallbackIndexUrl }
if ($torchInstallExit -ne 0) {
Write-Host "[ERROR] Failed to install PyTorch (ROCm and CPU base both failed, exit code $torchInstallExit)" -ForegroundColor Red
return (Exit-InstallFailure "Failed to install PyTorch (exit code $torchInstallExit)" $torchInstallExit)
@ -2363,13 +2355,8 @@ exit 0
}
$installedTorchTag = Get-InstalledTorchTag -PythonExe $VenvPython
} elseif ($expectedTorchTag -ne 'rocm') {
# CUDA: stale +cpu (or wrong cuXXX) against a CUDA index -> reinstall triplet. Same leaf-gated ceiling as the install above: cu* serves 2.11.
$_fixLeaf = (($TorchIndexUrl -split '[?#]', 2)[0].TrimEnd('/') -split '/')[-1].ToLowerInvariant()
if ($_fixLeaf -match '^cu[0-9]+$') {
$_fixTorchSpec = "torch>=2.4,<2.12.0"; $_fixVisionSpec = "torchvision>=0.19,<0.27.0"; $_fixAudioSpec = "torchaudio>=2.4,<2.12.0"
} else {
$_fixTorchSpec = "torch>=2.4,<2.11.0"; $_fixVisionSpec = "torchvision>=0.19,<0.26.0"; $_fixAudioSpec = "torchaudio>=2.4,<2.11.0"
}
# CUDA: stale +cpu (or wrong cuXXX) against a CUDA index -> reinstall triplet with the default 2.11-line trio (ceiling bump site, see the hoisted trio above).
$_fixTorchSpec = "torch>=2.4,<2.12.0"; $_fixVisionSpec = "torchvision>=0.19,<0.27.0"; $_fixAudioSpec = "torchaudio>=2.4,<2.12.0"
# Kept-release substitution (twin of install.sh's _install_torch_default_index honoring _PREV_TORCH_PIN): honor the preserved torch when the pin survived the E-decision; restore the range specs and retry if it isn't installable here. The --reinstall-package triplet stays on both attempts.
$_cudaKept = $false
if ($script:PrevTorchPin) {

View file

@ -1888,18 +1888,24 @@ if [ -x "$VENV_DIR/bin/python" ]; then
substep "${VENV_DIR}"
fi
# Supported torch line: the default range admits torch 2.11 (wheels verified on
# cpu/cu126/cu128/cu130/rocm7.1+/mac arm64). Bump the three ceilings together
# when the next torch minor is validated; curated ROCm floors below stay literal.
_TORCH_CEILING="2.12.0"
_TORCHVISION_CEILING="0.27.0"
_TORCHAUDIO_CEILING="2.12.0"
# Default torch constraint; tightened for Python 3.13+ on arm64 macOS (torch <2.6 has no cp313 macOS arm64 wheels).
TORCH_CONSTRAINT="torch>=2.4,<2.11.0"
TORCH_CONSTRAINT="torch>=2.4,<${_TORCH_CEILING}"
if [ "$SKIP_TORCH" = false ] && [ "$OS" = "macos" ] && [ "$_ARCH" = "arm64" ]; then
_PY_MINOR=$("$VENV_DIR/bin/python" -c \
"import sys; print(sys.version_info.minor)" 2>/dev/null || echo "0")
if [ "$_PY_MINOR" -ge 13 ] 2>/dev/null; then
TORCH_CONSTRAINT="torch>=2.6,<2.11.0"
TORCH_CONSTRAINT="torch>=2.6,<${_TORCH_CEILING}"
fi
fi
# Companion constraints bounded to torch's window: torchaudio 2.11 dropped its torch pin, so a bare companion beside a <2.11 torch resolves 2.11.
TORCHVISION_CONSTRAINT="torchvision>=0.19,<0.26.0"
TORCHAUDIO_CONSTRAINT="torchaudio>=2.4,<2.11.0"
# Companion constraints bounded to torch's window: torchaudio 2.11 dropped its torch pin, so a bare companion can drift from a capped torch.
TORCHVISION_CONSTRAINT="torchvision>=0.19,<${_TORCHVISION_CEILING}"
TORCHAUDIO_CONSTRAINT="torchaudio>=2.4,<${_TORCHAUDIO_CEILING}"
# ── Resolve repo root (for --local installs) ──
_REPO_ROOT="$(cd "$(dirname "$0" 2>/dev/null || echo ".")" && pwd)"
@ -2480,27 +2486,15 @@ else
fi
# rocm7.2 and the per-gfx indexes (Strix _grouped_mm fix) ship torch 2.11.0: raise the floor and pin companions; match the FINAL leaf only.
# (cu*/cpu/custom leaves all use the default <2.12 trio above.)
case "$_torch_index_leaf" in
rocm7.2|gfx120x-all|gfx1151|gfx1150)
TORCH_CONSTRAINT="torch>=2.11.0,<2.12.0"
TORCHVISION_CONSTRAINT="torchvision>=0.26.0,<0.27.0"
TORCHAUDIO_CONSTRAINT="torchaudio>=2.11.0,<2.12.0"
;;
# CUDA cu12x/cu13x indexes ship torch 2.11.x: widen the trio ceiling to <2.12.0.
cu[0-9]*)
TORCH_CONSTRAINT="torch>=2.4,<2.12.0"
TORCHVISION_CONSTRAINT="torchvision>=0.19,<0.27.0"
TORCHAUDIO_CONSTRAINT="torchaudio>=2.4,<2.12.0"
;;
esac
# A pinned custom/unknown-leaf index has no curated companion set: bound the companions to the same <2.11 range the Python path pins.
if [ "$_torch_index_pinned" = true ] && \
[ -z "$(_expected_torch_flavor_tag "$TORCH_INDEX_URL")" ]; then
TORCHVISION_CONSTRAINT="torchvision>=0.19,<0.26.0"
TORCHAUDIO_CONSTRAINT="torchaudio>=2.4,<2.11.0"
fi
# Detect a Radeon card (*/rocm* index + rocminfo "Marketing Name:.*Radeon"); skipped when the index is pinned.
_amd_gpu_radeon=false
if [ "$_torch_index_pinned" = false ]; then

View file

@ -2999,9 +2999,9 @@ if (-not $ROCmIndexUrl -and ($CuTag -eq "cpu" -or $ROCmCpuFallback)) {
$cudaVisionSpec = "torchvision"
$cudaAudioSpec = "torchaudio"
if ($TorchIndexPinned -and -not (Test-CudaFamilyLeaf $CuTag)) {
$cudaTorchSpec = "torch>=2.4,<2.11.0"
$cudaVisionSpec = "torchvision>=0.19,<0.26.0"
$cudaAudioSpec = "torchaudio>=2.4,<2.11.0"
$cudaTorchSpec = "torch>=2.4,<2.12.0"
$cudaVisionSpec = "torchvision>=0.19,<0.27.0"
$cudaAudioSpec = "torchaudio>=2.4,<2.12.0"
}
# Release preservation: keep install.ps1's exported torch RELEASE (UNSLOTH_KEPT_TORCH) so a re-run
# reinstalls the same CUDA build (+cuXXX follows this index). On a failed install, restore the computed

View file

@ -405,30 +405,25 @@ class TestKnown211SetParity:
), f"{label} floor gate must not use the unanchored ^rocm(\\d+)\\.(\\d+) prefix"
def test_install_ps1_bounds_unknown_leaf_pinned_torch(self):
"""install.ps1's pinned-torch install must bound BOTH companions on EVERY
index, cu<digits> families included: torchaudio 2.11 dropped its exact torch
pin from the wheel metadata, so a bare companion beside torch<2.11 can
resolve a mismatched 2.11.0 build (Codex P2, then unconditional per the
torchaudio 2.11 unpinning)."""
"""install.ps1's pinned-torch install must bound the whole trio on EVERY
index with the default torch 2.11 line (<2.12 trio, matching install.sh's
ceiling-composed default and _CUDA_TORCH_PKG_SPEC): torchaudio 2.11
dropped its exact torch pin from the wheel metadata, so a bare companion
beside a capped torch can resolve a mismatched build."""
text = INSTALL_PS1.read_text(encoding = "utf-8")
assert (
'$_pinVisionSpec = "torchvision>=0.19,<0.26.0"' in text
), "install.ps1 custom-pin install must bound torchvision (>=0.19,<0.26.0)"
assert (
'$_pinAudioSpec = "torchaudio>=2.4,<2.11.0"' in text
), "install.ps1 custom-pin install must bound torchaudio (>=2.4,<2.11.0)"
# cu<digits> leaves widen the whole trio to the torch 2.11 line (<2.12),
# matching install.sh's cu[0-9]* widen and _CUDA_TORCH_PKG_SPEC; other
# leaves keep the 2.10 line. The trio stays bounded on every index.
assert '$_pinTorchSpec = "torch>=2.4,<2.12.0"' in text, (
"install.ps1 must widen torch to <2.12.0 on cu<digits> index leaves"
"install.ps1 default install must use the torch 2.11 line (<2.12.0)"
)
assert (
'$_pinVisionSpec = "torchvision>=0.19,<0.27.0"' in text
), "install.ps1 cu-leaf install must pair torchvision <0.27.0 with torch <2.12"
), "install.ps1 must pair torchvision <0.27.0 with torch <2.12"
assert (
'$_pinAudioSpec = "torchaudio>=2.4,<2.12.0"' in text
), "install.ps1 cu-leaf install must pair torchaudio <2.12.0 with torch <2.12"
), "install.ps1 must pair torchaudio <2.12.0 with torch <2.12"
# No stale 2.10-line default remains anywhere in the Windows installer.
assert '"torch>=2.4,<2.11.0"' not in text, (
"install.ps1 must not retain a <2.11.0 default torch range"
)
# The bounded trio must actually be passed to the install command.
assert re.search(
r'\$_pinTorchSpec \$_pinVisionSpec \$_pinAudioSpec --default-index \$TorchIndexUrl',
@ -672,9 +667,9 @@ class TestPinnedIndexClearsUvEnvParity:
# The custom-leaf branch bounds torch AND both companions (parity with the
# other installers' custom-pin trio bounds), gated on a non-cu-family leaf.
for spec in (
'$cudaTorchSpec = "torch>=2.4,<2.11.0"',
'$cudaVisionSpec = "torchvision>=0.19,<0.26.0"',
'$cudaAudioSpec = "torchaudio>=2.4,<2.11.0"',
'$cudaTorchSpec = "torch>=2.4,<2.12.0"',
'$cudaVisionSpec = "torchvision>=0.19,<0.27.0"',
'$cudaAudioSpec = "torchaudio>=2.4,<2.12.0"',
):
assert spec in text, f"setup.ps1 must bound the custom-leaf trio: {spec}"
assert (

View file

@ -64,35 +64,30 @@ class TestStructuralTorchConstraint:
_sh = _read(_INSTALL_SH)
def test_default_assignment_exists(self):
assert 'TORCH_CONSTRAINT="torch>=2.4,<2.11.0"' in self._sh
"""The default range composes the per-file ceiling variable, so the
supported line (torch 2.11 today) is bumped in one place."""
assert '_TORCH_CEILING="2.12.0"' in self._sh
assert 'TORCH_CONSTRAINT="torch>=2.4,<${_TORCH_CEILING}"' in self._sh
def test_tightened_assignment_exists(self):
assert 'TORCH_CONSTRAINT="torch>=2.6,<2.11.0"' in self._sh
assert 'TORCH_CONSTRAINT="torch>=2.6,<${_TORCH_CEILING}"' in self._sh
def test_cuda_constraint_widened_to_2_12(self):
"""A fresh CUDA install widens the ceiling to <2.12.0 so cu12x/cu13x
land torch 2.11.x (matches the base image and _CUDA_TORCH_PKG_SPEC);
without it cu128/cu130 resolves torch 2.10.x."""
assert 'TORCH_CONSTRAINT="torch>=2.4,<2.12.0"' in self._sh
def test_cuda_case_widens_via_index_leaf(self):
"""The cu* branch of the _torch_index_leaf case sets the widened
constraint (parallel to rocm7.2), anchored on the leaf."""
m = re.search(
r'cu\[0-9\]\*\)\s*TORCH_CONSTRAINT="torch>=2\.4,<2\.12\.0"',
self._sh,
)
assert m is not None, "CUDA (cu*) TORCH_CONSTRAINT widening case not found"
def test_companion_ceilings_composed(self):
"""Companions bound to the same window via their own ceiling vars."""
assert '_TORCHVISION_CEILING="0.27.0"' in self._sh
assert '_TORCHAUDIO_CEILING="2.12.0"' in self._sh
assert 'TORCHVISION_CONSTRAINT="torchvision>=0.19,<${_TORCHVISION_CEILING}"' in self._sh
assert 'TORCHAUDIO_CONSTRAINT="torchaudio>=2.4,<${_TORCHAUDIO_CEILING}"' in self._sh
def test_variable_used_in_pip_install(self):
"""$TORCH_CONSTRAINT must appear in a uv pip install line."""
assert '"$TORCH_CONSTRAINT"' in self._sh
def test_hardcoded_torch_constraint_only_once(self):
"""The hard-coded torch>=2.4,<2.11.0 string should appear exactly once
in install.sh (the default assignment), not in pip install lines."""
count = self._sh.count('"torch>=2.4,<2.11.0"')
assert count == 1, f"Expected 1, found {count}"
def test_hardcoded_torch_constraint_gone(self):
"""No hard-coded default ranges remain outside the ceiling-composed
assignments (curated ROCm >=2.11 floors stay literal)."""
assert self._sh.count('"torch>=2.4,<2.11.0"') == 0
assert self._sh.count('"torch>=2.4,<2.12.0"') == 0
def test_tightening_guarded_by_skip_torch(self):
"""The block must check SKIP_TORCH=false."""
@ -122,7 +117,7 @@ class TestStructuralInstallPs1Unchanged:
assert "$TorchConstraint" not in self._ps1
def test_hardcoded_torch_constraint_present(self):
assert '"torch>=2.4,<2.11.0"' in self._ps1
assert '"torch>=2.4,<2.12.0"' in self._ps1
class TestInstallPs1UvDefaultIndex:

View file

@ -76,11 +76,12 @@ run_constraint_snippet() {
OS=\"$_os\"
_ARCH=\"$_arch\"
VENV_DIR=\"$_venv_dir\"
TORCH_CONSTRAINT=\"torch>=2.4,<2.11.0\"
_TORCH_CEILING=\"2.12.0\"
TORCH_CONSTRAINT=\"torch>=2.4,<\${_TORCH_CEILING}\"
if [ \"\$SKIP_TORCH\" = false ] && [ \"\$OS\" = \"macos\" ] && [ \"\$_ARCH\" = \"arm64\" ]; then
_PY_MINOR=\$(\"\$VENV_DIR/bin/python\" -c \"import sys; print(sys.version_info.minor)\" 2>/dev/null || echo \"0\")
if [ \"\$_PY_MINOR\" -ge 13 ] 2>/dev/null; then
TORCH_CONSTRAINT=\"torch>=2.6,<2.11.0\"
TORCH_CONSTRAINT=\"torch>=2.6,<\${_TORCH_CEILING}\"
fi
fi
echo \"\$TORCH_CONSTRAINT\"
@ -94,43 +95,39 @@ echo "=== Structural: TORCH_CONSTRAINT in install.sh ==="
_SH_CONTENT=$(cat "$INSTALL_SH")
_count=$(grep -c 'TORCH_CONSTRAINT="torch>=2.4,<2.11.0"' "$INSTALL_SH" || true)
assert_eq "default TORCH_CONSTRAINT assignment exists" "1" "$_count"
# The supported line is centralized in per-file ceiling variables so a future
# torch 2.12 bump is a three-line change; the default range admits torch 2.11.
_count=$(grep -c '_TORCH_CEILING="2.12.0"' "$INSTALL_SH" || true)
assert_eq "torch ceiling variable defined once" "1" "$_count"
_count=$(grep -c '_TORCHVISION_CEILING="0.27.0"' "$INSTALL_SH" || true)
assert_eq "torchvision ceiling variable defined once" "1" "$_count"
_count=$(grep -c '_TORCHAUDIO_CEILING="2.12.0"' "$INSTALL_SH" || true)
assert_eq "torchaudio ceiling variable defined once" "1" "$_count"
_count=$(grep -c 'TORCH_CONSTRAINT="torch>=2.6,<2.11.0"' "$INSTALL_SH" || true)
assert_eq "tightened TORCH_CONSTRAINT assignment exists" "1" "$_count"
_count=$(grep -c 'TORCH_CONSTRAINT="torch>=2.4,<${_TORCH_CEILING}"' "$INSTALL_SH" || true)
assert_eq "default TORCH_CONSTRAINT composes the ceiling" "1" "$_count"
_count=$(grep -c 'TORCH_CONSTRAINT="torch>=2.6,<${_TORCH_CEILING}"' "$INSTALL_SH" || true)
assert_eq "tightened TORCH_CONSTRAINT composes the ceiling" "1" "$_count"
_count=$(grep -c '"\$TORCH_CONSTRAINT"' "$INSTALL_SH" || true)
_has_var=$([ "$_count" -ge 1 ] && echo "yes" || echo "no")
assert_eq "\$TORCH_CONSTRAINT used in pip install" "yes" "$_has_var"
# Hardcoded torch>=2.4,<2.11.0 should only appear once (the default assignment)
_hardcoded=$(grep -c '"torch>=2.4,<2.11.0"' "$INSTALL_SH" || true)
assert_eq "hardcoded torch>=2.4 appears exactly once" "1" "$_hardcoded"
# No stray hardcoded default ranges outside the ceiling-composed assignments
# (the curated ROCm >=2.11 floors are deliberately literal).
_hardcoded=$(grep -c '"torch>=2.4,<2.11.0"\|"torch>=2.4,<2.12.0"' "$INSTALL_SH" || true)
assert_eq "no hardcoded default torch range remains" "0" "$_hardcoded"
# Companions must be bounded to torch's window everywhere: the <2.11 bound appears
# twice (default assignments + the pinned custom-leaf block), never bare. torchaudio
# 2.11 dropped its exact torch pin, so a bare companion next to a <2.11-capped torch
# resolves a mismatched 2.11 build.
_count=$(grep -c 'TORCHVISION_CONSTRAINT="torchvision>=0.19,<0.26.0"' "$INSTALL_SH" || true)
assert_eq "torchvision bounded (<0.26) at default + custom-leaf" "2" "$_count"
_count=$(grep -c 'TORCHAUDIO_CONSTRAINT="torchaudio>=2.4,<2.11.0"' "$INSTALL_SH" || true)
assert_eq "torchaudio bounded (<2.11) at default + custom-leaf" "2" "$_count"
# Companions must be bounded to torch's window everywhere, never bare: torchaudio
# 2.11 dropped its exact torch pin, so a bare companion can drift from a capped torch.
_count=$(grep -c 'TORCHVISION_CONSTRAINT="torchvision>=0.19,<${_TORCHVISION_CEILING}"' "$INSTALL_SH" || true)
assert_eq "torchvision default composes the ceiling" "1" "$_count"
_count=$(grep -c 'TORCHAUDIO_CONSTRAINT="torchaudio>=2.4,<${_TORCHAUDIO_CEILING}"' "$INSTALL_SH" || true)
assert_eq "torchaudio default composes the ceiling" "1" "$_count"
_count=$(grep -c 'TORCHVISION_CONSTRAINT="torchvision"$' "$INSTALL_SH" || true)
assert_eq "no bare torchvision companion remains" "0" "$_count"
_count=$(grep -c 'TORCHAUDIO_CONSTRAINT="torchaudio"$' "$INSTALL_SH" || true)
assert_eq "no bare torchaudio companion remains" "0" "$_count"
# The cu* widen must carry the companions with it (torch <2.12 with torchaudio <2.11
# would cap a mismatched pair the other way).
assert_eq "cu widen pairs torchaudio (<2.12)" "1" "$(grep -c 'TORCHAUDIO_CONSTRAINT="torchaudio>=2.4,<2.12.0"' "$INSTALL_SH" || true)"
_gated=$(grep -c '_expected_torch_flavor_tag "$TORCH_INDEX_URL"' "$INSTALL_SH" || true)
_has_gate=$([ "$_gated" -ge 1 ] && echo "yes" || echo "no")
assert_eq "custom-companion bound gated on empty flavor tag" "yes" "$_has_gate"
# A fresh CUDA install widens the ceiling to <2.12.0 so cu12x/cu13x land torch
# 2.11.x (matches the base image and _CUDA_TORCH_PKG_SPEC).
_cuda_widen=$(grep -c 'TORCH_CONSTRAINT="torch>=2.4,<2.12.0"' "$INSTALL_SH" || true)
assert_eq "CUDA TORCH_CONSTRAINT widened to <2.12.0" "1" "$_cuda_widen"
# Widening keys off the final leaf (_torch_index_leaf), not the full URL, so a
# mirror base path with cu*/rocm7.2 but a cpu/older-rocm leaf is not mis-widened.
@ -181,7 +178,7 @@ _PS1_CONTENT=$(cat "$INSTALL_PS1")
_ps1_has_var=$(echo "$_PS1_CONTENT" | grep -c 'TORCH_CONSTRAINT\|TorchConstraint' || true)
assert_eq "install.ps1 has no TORCH_CONSTRAINT variable" "0" "$_ps1_has_var"
_ps1_hardcoded=$(echo "$_PS1_CONTENT" | grep -c '"torch>=2.4,<2.11.0"' || true)
_ps1_hardcoded=$(echo "$_PS1_CONTENT" | grep -c '"torch>=2.4,<2.12.0"' || true)
_ps1_has_hc=$([ "$_ps1_hardcoded" -ge 1 ] && echo "yes" || echo "no")
assert_eq "install.ps1 has hardcoded torch constraint" "yes" "$_ps1_has_hc"
@ -196,55 +193,55 @@ trap 'rm -rf "$TMPDIR_BASE"' EXIT
# 1. arm64 macOS py3.13 -> tightened
_result=$(run_constraint_snippet false macos arm64 13 "$TMPDIR_BASE/v1")
assert_eq "arm64+macos+py313 -> tightened" "torch>=2.6,<2.11.0" "$_result"
assert_eq "arm64+macos+py313 -> tightened" "torch>=2.6,<2.12.0" "$_result"
# 2. arm64 macOS py3.14 -> tightened (future-proofed)
_result=$(run_constraint_snippet false macos arm64 14 "$TMPDIR_BASE/v2")
assert_eq "arm64+macos+py314 -> tightened" "torch>=2.6,<2.11.0" "$_result"
assert_eq "arm64+macos+py314 -> tightened" "torch>=2.6,<2.12.0" "$_result"
# 3. arm64 macOS py3.12 -> default
_result=$(run_constraint_snippet false macos arm64 12 "$TMPDIR_BASE/v3")
assert_eq "arm64+macos+py312 -> default" "torch>=2.4,<2.11.0" "$_result"
assert_eq "arm64+macos+py312 -> default" "torch>=2.4,<2.12.0" "$_result"
# 4. arm64 macOS py3.11 -> default
_result=$(run_constraint_snippet false macos arm64 11 "$TMPDIR_BASE/v4")
assert_eq "arm64+macos+py311 -> default" "torch>=2.4,<2.11.0" "$_result"
assert_eq "arm64+macos+py311 -> default" "torch>=2.4,<2.12.0" "$_result"
# 5. Linux x86_64 py3.13 -> default (Linux unaffected)
_result=$(run_constraint_snippet false linux x86_64 13 "$TMPDIR_BASE/v5")
assert_eq "linux+x86_64+py313 -> default" "torch>=2.4,<2.11.0" "$_result"
assert_eq "linux+x86_64+py313 -> default" "torch>=2.4,<2.12.0" "$_result"
# 6. Linux aarch64 py3.13 -> default (guard checks OS=macos)
_result=$(run_constraint_snippet false linux aarch64 13 "$TMPDIR_BASE/v6")
assert_eq "linux+aarch64+py313 -> default" "torch>=2.4,<2.11.0" "$_result"
assert_eq "linux+aarch64+py313 -> default" "torch>=2.4,<2.12.0" "$_result"
# 7. Intel Mac x86_64 py3.12 -> default (arch mismatch)
_result=$(run_constraint_snippet false macos x86_64 12 "$TMPDIR_BASE/v7")
assert_eq "macos+x86_64+py312 -> default" "torch>=2.4,<2.11.0" "$_result"
assert_eq "macos+x86_64+py312 -> default" "torch>=2.4,<2.12.0" "$_result"
# 8. SKIP_TORCH=true arm64 macOS py3.13 -> block skipped, default
_result=$(run_constraint_snippet true macos arm64 13 "$TMPDIR_BASE/v8")
assert_eq "SKIP_TORCH=true -> default" "torch>=2.4,<2.11.0" "$_result"
assert_eq "SKIP_TORCH=true -> default" "torch>=2.4,<2.12.0" "$_result"
# 9. WSL py3.13 -> default
_result=$(run_constraint_snippet false wsl x86_64 13 "$TMPDIR_BASE/v9")
assert_eq "wsl+py313 -> default" "torch>=2.4,<2.11.0" "$_result"
assert_eq "wsl+py313 -> default" "torch>=2.4,<2.12.0" "$_result"
# 10. py_minor=0 (failed query fallback) -> default
_result=$(run_constraint_snippet false macos arm64 0 "$TMPDIR_BASE/v10")
assert_eq "py_minor=0 fallback -> default" "torch>=2.4,<2.11.0" "$_result"
assert_eq "py_minor=0 fallback -> default" "torch>=2.4,<2.12.0" "$_result"
# 11. Boundary: py_minor=12 -> NOT tightened
_result=$(run_constraint_snippet false macos arm64 12 "$TMPDIR_BASE/v11")
assert_eq "boundary py_minor=12 -> default" "torch>=2.4,<2.11.0" "$_result"
assert_eq "boundary py_minor=12 -> default" "torch>=2.4,<2.12.0" "$_result"
# 12. Boundary: py_minor=13 -> tightened
_result=$(run_constraint_snippet false macos arm64 13 "$TMPDIR_BASE/v12")
assert_eq "boundary py_minor=13 -> tightened" "torch>=2.6,<2.11.0" "$_result"
assert_eq "boundary py_minor=13 -> tightened" "torch>=2.6,<2.12.0" "$_result"
# 13. Intel Mac py3.13 -> default (arch=x86_64, not arm64)
_result=$(run_constraint_snippet false macos x86_64 13 "$TMPDIR_BASE/v13")
assert_eq "macos+x86_64+py313 -> default" "torch>=2.4,<2.11.0" "$_result"
assert_eq "macos+x86_64+py313 -> default" "torch>=2.4,<2.12.0" "$_result"
# ======================================================================
# Mock uv integration