From 6df6f82557304d3efd47f27a2ef753ec8a657d6b Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 27 Jul 2026 13:28:15 +0000 Subject: [PATCH] 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. --- scripts/notebook_validator.py | 17 ++++++- tests/python/test_torchcodec_torch_compat.py | 47 ++++++++++++++++++++ unsloth/import_fixes.py | 26 +++++++---- 3 files changed, 81 insertions(+), 9 deletions(-) diff --git a/scripts/notebook_validator.py b/scripts/notebook_validator.py index 5f5a3dd227..b564fe718e 100644 --- a/scripts/notebook_validator.py +++ b/scripts/notebook_validator.py @@ -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( diff --git a/tests/python/test_torchcodec_torch_compat.py b/tests/python/test_torchcodec_torch_compat.py index 02dd9182b8..84c4860fd8 100644 --- a/tests/python/test_torchcodec_torch_compat.py +++ b/tests/python/test_torchcodec_torch_compat.py @@ -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() diff --git a/unsloth/import_fixes.py b/unsloth/import_fixes.py index 531235a577..917f9a05d3 100644 --- a/unsloth/import_fixes.py +++ b/unsloth/import_fixes.py @@ -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}."