torchcodec: warn when torch outruns the last lockstep row

The ABI-stable branch clears torch >= 2.11 paired with torchcodec >= 0.12,
but everything else fell through to the lockstep matrix, which has no row
past torch 2.11. So torch 2.12 or 2.13 with torchcodec 0.10 or 0.11 hit
`allowed is None` and returned no hint at all, even though upstream pins
0.11 to torch 2.11 exactly and 0.10 to torch 2.10 exactly. torchcodec
ships no `Requires-Dist: torch`, so pip cannot catch it either and the
user only sees a raw "Could not load libtorchcodec" at first use.

Split the lookup into three cases: a torch minor below the table stays
silent as before, a torch minor with a row keeps the existing per-minor
pin and audio-torch2xx extra hint, and a torch minor at or past the
ABI-stable floor with no row now points at `torchcodec>=0.12.0`.

Mirror the same three-way split in scripts/notebook_validator.py's
R-INST-004 so the two stay in step, and cover both with tests plus a
regression test that torch below the table still produces no finding.
This commit is contained in:
Daniel Han 2026-07-27 13:28:15 +00:00
commit 6df6f82557
3 changed files with 81 additions and 9 deletions

View file

@ -630,7 +630,22 @@ def rule_inst_004_torchcodec_torch(
c_minor = version_minor(codec_v)
allowed = TORCH_TORCHCODEC.get(t_minor)
if allowed is None:
return findings # unknown torch minor — don't flag
if cmp_versions(torch_v, TORCHCODEC_ABI_STABLE_TORCH) < 0:
return findings # torch older than the table — don't flag
# Torch at or past the ABI-stable floor with a pre-0.12 torchcodec: the
# ABI-stable branch above already returned for 0.12+, so this pin is a
# legacy build locked to an older torch minor and cannot load.
findings.append(
Finding(
rule = "R-INST-004",
file = file,
cell = cell_idx,
severity = "error",
message = f"torch=={torch_v} (minor {t_minor}) is incompatible with torchcodec=={codec_v} (minor {c_minor}); torchcodec <{TORCHCODEC_ABI_STABLE_CODEC} is built against a single older torch minor",
hint = f"pin `torchcodec>={TORCHCODEC_ABI_STABLE_CODEC}.0` (the ABI-stable line, which targets torch >={TORCHCODEC_ABI_STABLE_TORCH})",
)
)
return findings
if c_minor not in allowed:
findings.append(
Finding(

View file

@ -228,6 +228,53 @@ def test_torch210_still_rejects_abi_stable_torchcodec(monkeypatch):
assert "audio-torch210" in hint
def test_torch_past_last_lockstep_row_rejects_legacy_torchcodec(monkeypatch):
"""torch newer than the last lockstep row only pairs with the 0.12+ line.
torchcodec 0.11 is pinned to torch 2.11 exactly (upstream compatibility
table), so torch 2.12/2.13 with a pre-0.12 codec must still be reported even
though the matrix has no row for those torch minors.
"""
import importlib.metadata
fixes = _load_import_fixes_module()
for torch_version in ("2.12.1+cu130", "2.13.0"):
for codec_version in ("0.11.1", "0.10.0"):
_stub_torch(monkeypatch, torch_version)
monkeypatch.setattr(importlib.metadata, "version", lambda _name, _v = codec_version: _v)
hint = fixes._torchcodec_version_mismatch_hint()
assert hint is not None, f"{torch_version} + torchcodec {codec_version} must warn"
assert "torchcodec>=0.12.0" in hint
# No audio-torch2xx extra exists for these minors, so none is offered.
assert "unsloth[audio-torch" not in hint
def test_torch_below_the_table_stays_silent(monkeypatch):
"""A torch minor older than the matrix keeps the original no-opinion behaviour."""
import importlib.metadata
fixes = _load_import_fixes_module()
_stub_torch(monkeypatch, "2.4.0")
monkeypatch.setattr(importlib.metadata, "version", lambda _name: "0.0.3")
assert fixes._torchcodec_version_mismatch_hint() is None
def test_notebook_validator_rejects_legacy_codec_past_last_lockstep_row():
"""The mirrored notebook rule must flag the same pairing as import_fixes."""
nv = _load_notebook_validator_module()
cell = '!pip install --no-deps "torch==2.12.1" "torchcodec==0.11.1"'
findings = nv.rule_inst_004_torchcodec_torch(cell, {}, "nb.ipynb", 0)
assert len(findings) == 1
assert findings[0].rule == "R-INST-004"
assert findings[0].severity == "error"
assert "torchcodec>=0.12.0" in findings[0].hint
# Torch older than the table is still out of scope.
old = '!pip install --no-deps "torch==2.4.0" "torchcodec==0.0.3"'
assert nv.rule_inst_004_torchcodec_torch(old, {}, "nb.ipynb", 0) == []
def test_notebook_validator_allows_abi_stable_pairing():
"""R-INST-004 is an error-severity rule: it must not fire on torch 2.11 + 0.12+."""
nv = _load_notebook_validator_module()

View file

@ -1601,15 +1601,25 @@ def _torchcodec_version_mismatch_hint() -> str | None:
torch_minor = ".".join(str(p) for p in torch_release)
codec_minor = ".".join(str(p) for p in codec_release)
allowed = _TORCH_TORCHCODEC_MINORS.get(torch_minor)
if allowed is None or codec_minor in allowed:
if allowed is None:
# No lockstep row for this torch minor. Torch older than the table is
# out of scope, so stay silent. Torch at or past the ABI-stable floor
# only pairs with the open-ended line (>= 0.12), and the early return
# above already cleared those, so whatever reaches here is a legacy
# single-minor codec built for an older torch: point at the 0.12+ line.
if torch_release < _TORCHCODEC_ABI_STABLE_TORCH:
return None
abi_pin = ".".join(str(p) for p in _TORCHCODEC_ABI_STABLE_CODEC)
install_hint = f"`pip install 'torchcodec>={abi_pin}.0'`"
elif codec_minor in allowed:
return None
pin = sorted(allowed)[-1]
upper = _torchcodec_exclusive_upper(pin)
install_hint = f"`pip install 'torchcodec>={pin},{upper}'`"
extra = _TORCH_TORCHCODEC_EXTRAS.get(torch_minor)
if extra is not None:
install_hint += f" or `pip install 'unsloth[{extra}]'`"
else:
pin = sorted(allowed)[-1]
upper = _torchcodec_exclusive_upper(pin)
install_hint = f"`pip install 'torchcodec>={pin},{upper}'`"
extra = _TORCH_TORCHCODEC_EXTRAS.get(torch_minor)
if extra is not None:
install_hint += f" or `pip install 'unsloth[{extra}]'`"
return (
f"torchcodec {torchcodec_version} is incompatible with torch {torch.__version__}; "
f"install a matching build with {install_hint}."