Skip to content

Commit 8c975fc

Browse files
Souravrajvi0pre-commit-ci[bot]danielhanchen
authored
fix: pin torchcodec for torch 2.10 and warn on ABI mismatch (#7299)
* fix: pin torchcodec for torch 2.10 and warn on ABI mismatch Add unsloth[audio] extra with torchcodec>=0.10.0,<0.11.0 and emit a clear warning when installed torchcodec minors disagree with torch (#7225). * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(packaging): address Codex review on torchcodec/torch 2.10 compat (#7299) - Postpone annotations so import_fixes loads on Python 3.9 - Align TORCH_TORCHCODEC matrix with upstream (2.9: 0.8/0.9, 2.8: 0.6/0.7) - Fix mismatch hint upper bound (<0.11.0) and gate audio-torch210 suggestion - Split audio extra per torch minor; gate torch210 pin behind python>=3.10 - Bundle audio-torch210 only in *-torch2100 install extras * fix(security): refresh openai CRITICAL scan baseline hashes (#7299) openai package code drift reopened five CRITICAL findings in the extras pip-scan-packages shard (C2 loop body hashes + IMDS/network evidence). Update the reviewed allowlist evidence/hashes so CI gates on new findings only, not benign SDK churn. * chore: retrigger CI after baseline refresh (#7299) * chore: touch scan baseline comment to retrigger security audit (#7299) * Guard torchcodec version parsing so bad version strings cannot break import * Bundle audio pin into intel-gpu-torch210 and guard the mismatch warning * Tighten comments --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel Han <danielhanchen@gmail.com>
1 parent b448fb5 commit 8c975fc

5 files changed

Lines changed: 227 additions & 14 deletions

File tree

pyproject.toml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@ huggingfacenotorch = [
9393
"trl>=0.18.2,!=0.19.0,<=0.24.0",
9494
"sentence-transformers",
9595
]
96+
# torchcodec backend for Gemma audio / datasets>=4 (#7225).
97+
# Pick the audio-torch* pin matching your torch minor (see TORCH_TORCHCODEC).
98+
audio-torch210 = [
99+
"torchcodec>=0.10.0,<0.11.0 ; python_version >= '3.10'",
100+
]
101+
audio-torch290 = [
102+
"torchcodec>=0.8.0,<0.10.0 ; python_version >= '3.10'",
103+
]
104+
audio-torch280 = [
105+
"torchcodec>=0.6.0,<0.8.0 ; python_version >= '3.9'",
106+
]
96107
huggingface = [
97108
"unsloth[huggingfacenotorch]",
98109
"unsloth_zoo>=2026.7.6",
@@ -532,16 +543,19 @@ cu126-torch2100 = [
532543
"unsloth[huggingface]",
533544
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
534545
"unsloth[cu126onlytorch2100]",
546+
"unsloth[audio-torch210]",
535547
]
536548
cu128-torch2100 = [
537549
"unsloth[huggingface]",
538550
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
539551
"unsloth[cu128onlytorch2100]",
552+
"unsloth[audio-torch210]",
540553
]
541554
cu130-torch2100 = [
542555
"unsloth[huggingface]",
543556
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
544557
"unsloth[cu130onlytorch2100]",
558+
"unsloth[audio-torch210]",
545559
]
546560
kaggle = [
547561
"unsloth[huggingface]",
@@ -831,16 +845,19 @@ cu126-ampere-torch2100 = [
831845
"unsloth[huggingface]",
832846
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
833847
"unsloth[cu126onlytorch2100]",
848+
"unsloth[audio-torch210]",
834849
]
835850
cu128-ampere-torch2100 = [
836851
"unsloth[huggingface]",
837852
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
838853
"unsloth[cu128onlytorch2100]",
854+
"unsloth[audio-torch210]",
839855
]
840856
cu130-ampere-torch2100 = [
841857
"unsloth[huggingface]",
842858
"bitsandbytes>=0.45.5,!=0.46.0,!=0.48.0",
843859
"unsloth[cu130onlytorch2100]",
860+
"unsloth[audio-torch210]",
844861
]
845862
flashattentiontorch260abiFALSEcu12x = [
846863
"flash-attn @ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp39-cp39-linux_x86_64.whl ; ('linux' in sys_platform) and python_version == '3.9'",
@@ -1125,7 +1142,8 @@ intelgputorch210 = [
11251142
"torchvision @ https://download.pytorch.org/whl/xpu/torchvision-0.25.0%2Bxpu-cp313-cp313-win_amd64.whl#sha256=1c4b44b36a557f7381e3076fb8843366742238648441d607c8d049c6da0f8886 ; sys_platform == 'win32' and python_version == '3.13' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",
11261143
]
11271144
intel-gpu-torch210 = [
1128-
"unsloth[intelgputorch210]"
1145+
"unsloth[intelgputorch210]",
1146+
"unsloth[audio-torch210]",
11291147
]
11301148
intelgputorch2110 = [
11311149
"unsloth_zoo[intelgpu]",
@@ -1279,6 +1297,7 @@ rocm72-torch2100 = [
12791297
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torchvision-0.25.0%2Brocm7.2.0.git82df5f59-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
12801298
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torchvision-0.25.0%2Brocm7.2.0.git82df5f59-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
12811299
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torchvision-0.25.0%2Brocm7.2.0.git82df5f59-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
1300+
"unsloth[audio-torch210]",
12821301
]
12831302
rocm711-torch2100 = [
12841303
"unsloth[amd]",
@@ -1297,6 +1316,7 @@ rocm711-torch2100 = [
12971316
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torchvision-0.25.0%2Brocm7.1.1.git82df5f59-cp311-cp311-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.11' and platform_machine == 'x86_64'",
12981317
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torchvision-0.25.0%2Brocm7.1.1.git82df5f59-cp312-cp312-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.12' and platform_machine == 'x86_64'",
12991318
"torchvision @ https://repo.radeon.com/rocm/manylinux/rocm-rel-7.1.1/torchvision-0.25.0%2Brocm7.1.1.git82df5f59-cp313-cp313-linux_x86_64.whl ; platform_system == 'Linux' and python_version == '3.13' and platform_machine == 'x86_64'",
1319+
"unsloth[audio-torch210]",
13001320
]
13011321

13021322
[project.urls]

scripts/notebook_validator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ def _atomic_write_bytes(path: pathlib.Path, data: bytes) -> None:
9595
# Source: pytorch/torchcodec compatibility matrix on its README.
9696
TORCH_TORCHCODEC: dict[str, set[str]] = {
9797
"2.10": {"0.10"},
98-
"2.9": {"0.7", "0.8", "0.9"},
99-
"2.8": {"0.6"},
98+
"2.9": {"0.8", "0.9"},
99+
"2.8": {"0.6", "0.7"},
100100
"2.7": {"0.3", "0.4", "0.5"},
101101
"2.6": {"0.2", "0.3"},
102102
"2.5": {"0.1", "0.2"},

scripts/scan_packages_baseline.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"_comment": "scan_packages.py allowlist. Each entry is a CRITICAL/HIGH finding manually judged benign. Matched on (package, package-relative file, check, evidence_hash); evidence_hash is over the matched code with L<NN>: markers stripped, so version bumps and line shifts do not reopen an entry but changed code does. severity and evidence are for review only. Regenerate with --write-baseline AFTER reviewing every line.",
2+
"_comment": "scan_packages.py allowlist (reviewed). Each entry is a CRITICAL/HIGH finding manually judged benign. Matched on (package, package-relative file, check, evidence_hash); evidence_hash is over the matched code with L<NN>: markers stripped, so version bumps and line shifts do not reopen an entry but changed code does. severity and evidence are for review only. Regenerate with --write-baseline AFTER reviewing every line.",
33
"version": 1,
44
"entries": [
55
{
@@ -303,8 +303,8 @@
303303
"file": "openai/_base_client.py",
304304
"check": "C2 polling/beaconing loop detected",
305305
"severity": "CRITICAL",
306-
"evidence": "L264: while True: sha256:95ca67e46d42354ae650abbdc5b0d97df8b0ed43187800bf40f5690c3901b94b",
307-
"evidence_hash": "a57d8d15fed0bf04f9967dcc18a18b80bb19f4095675bccbb78ac0450d7fce14"
306+
"evidence": "L274: while True: sha256:90a38e5c1e26893c7c273354143612640e9a9c0f079d3e2b60612d79f24e80a6",
307+
"evidence_hash": "1022e8e8649436ec64a98a9d9141d085452c49549fd2157b0278fc369a83ac66"
308308
},
309309
{
310310
"package": "openai",
@@ -319,8 +319,8 @@
319319
"file": "openai/auth/_workload.py",
320320
"check": "Accesses cloud metadata/IMDS AND makes network calls",
321321
"severity": "CRITICAL",
322-
"evidence": "IMDS: L96: url = \"http://169.254.169.254/metadata/identity/oauth2/token\" | L149: url = \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity\"\nNetwork: L77: http_client: httpx.Client | None = None, | L108: with httpx.Client() as client: | L133: http_client: httpx.Client | None = None, | L155: with httpx.Client() as client: | L248: with httpx.Client() as client:",
323-
"evidence_hash": "1581d9f4a23393e9af23fbe5ef9f66807b22c5b5a3f1fe167254c9ebee108567"
322+
"evidence": "IMDS: L97: url = \"http://169.254.169.254/metadata/identity/oauth2/token\" | L150: url = \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity\"\nNetwork: L78: http_client: httpx.Client | None = None, | L109: with httpx.Client() as client: | L134: http_client: httpx.Client | None = None, | L156: with httpx.Client() as client: | L251: exchange_client = DefaultHttpx2Client(follow_redirects=False) if self._use_httpx2 else httpx.Client()",
323+
"evidence_hash": "9717e51cb961dc14c458955d91a1e48e3753997346ecea0106bded3a8d64bfe0"
324324
},
325325
{
326326
"package": "openai",
@@ -343,8 +343,8 @@
343343
"file": "openai/resources/beta/responses/responses.py",
344344
"check": "C2 polling/beaconing loop detected",
345345
"severity": "CRITICAL",
346-
"evidence": "L3999: while True: sha256:df298b6eaf3416589b79f4ef283f8fb76e54d505bfda8840673f8e6419117e2e",
347-
"evidence_hash": "10ce5cb5a7097fcff4042ddcfb4802edda60aa4b7b113c8b926a52ddb76f78c2"
346+
"evidence": "L4000: while True: sha256:f8ab538118daba9ec06e27399dbdc90a4521c3390e6a47a6348a1f180a83effd",
347+
"evidence_hash": "31481ea83c687acc27144d72d3832d4fb98dd1c79fb5e0ddd85080de95997b9f"
348348
},
349349
{
350350
"package": "openai",
@@ -359,16 +359,16 @@
359359
"file": "openai/resources/realtime/realtime.py",
360360
"check": "C2 polling/beaconing loop detected",
361361
"severity": "CRITICAL",
362-
"evidence": "L310: while True: sha256:458198ff3d3f05870bf98c9564cbfd68c739e57b9bbe4120ed81e3eb6af74a05",
363-
"evidence_hash": "a3165d21e46b3ce553795daeae53e8f80e8e89c5cb228e68e6dcaff54bca5a89"
362+
"evidence": "L311: while True: sha256:5b63313072aae9ca28677e03426513ccf12221e4f4e0ea6c31efbe09790633b5",
363+
"evidence_hash": "05e1af469d651b51673763a7c4cdf759af9472fb627b7b470adc28cc237bd650"
364364
},
365365
{
366366
"package": "openai",
367367
"file": "openai/resources/responses/responses.py",
368368
"check": "C2 polling/beaconing loop detected",
369369
"severity": "CRITICAL",
370-
"evidence": "L3950: while True: sha256:1ce0b5a388c747945cdfda1a71b77afdfd03ae840d7aa9fa62f02eb00aa5e29f",
371-
"evidence_hash": "6de300ebb5e6e17cb51c89cbcdf08515a44655182f0776f0908a9d1043ebbcd7"
370+
"evidence": "L3951: while True: sha256:d68ef896bf0743ca430cfacb9a3353da1f3b9c51c3a21b6450a07a32b55aa2ac",
371+
"evidence_hash": "160eecdd79b521bffbe8476f782b69a0724c35d1b19376a7600807165fd54f9f"
372372
},
373373
{
374374
"package": "openai",
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# SPDX-License-Identifier: AGPL-3.0-only
2+
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
3+
4+
"""torch / torchcodec ABI guardrails (unslothai/unsloth#7225)."""
5+
6+
from __future__ import annotations
7+
8+
import importlib.util
9+
import re
10+
import sys
11+
import types
12+
from pathlib import Path
13+
14+
15+
REPO_ROOT = Path(__file__).resolve().parents[2]
16+
PYPROJECT = REPO_ROOT / "pyproject.toml"
17+
IMPORT_FIXES_PATH = REPO_ROOT / "unsloth" / "import_fixes.py"
18+
19+
20+
def _load_import_fixes_module():
21+
spec = importlib.util.spec_from_file_location(
22+
"unsloth_import_fixes_under_test",
23+
IMPORT_FIXES_PATH,
24+
)
25+
assert spec and spec.loader
26+
mod = importlib.util.module_from_spec(spec)
27+
spec.loader.exec_module(mod)
28+
return mod
29+
30+
31+
def test_pyproject_declares_torch210_audio_extra_with_python_gate():
32+
text = PYPROJECT.read_text(encoding = "utf-8")
33+
assert "audio-torch210 = [" in text
34+
assert "torchcodec>=0.10.0,<0.11.0" in text
35+
assert "python_version >= '3.10'" in text
36+
assert "audio-torch290 = [" in text
37+
assert "audio-torch280 = [" in text
38+
assert "\naudio = [" not in text
39+
40+
41+
def _stub_torch(monkeypatch, version: str):
42+
torch_mod = types.ModuleType("torch")
43+
torch_mod.__version__ = version
44+
monkeypatch.setitem(sys.modules, "torch", torch_mod)
45+
46+
47+
def test_torch210_extras_bundle_audio_torch210():
48+
text = PYPROJECT.read_text(encoding = "utf-8")
49+
for extra in (
50+
"cu128-torch2100",
51+
"cu126-ampere-torch2100",
52+
"rocm72-torch2100",
53+
):
54+
match = re.search(rf"^{extra} = \[(.*?)^\]", text, re.MULTILINE | re.DOTALL)
55+
assert match is not None, extra
56+
assert "unsloth[audio-torch210]" in match.group(1)
57+
58+
59+
def test_torchcodec_matrix_matches_notebook_validator():
60+
from scripts import notebook_validator as nv
61+
fixes = _load_import_fixes_module()
62+
assert fixes._TORCH_TORCHCODEC_MINORS == nv.TORCH_TORCHCODEC
63+
64+
65+
def test_torchcodec_exclusive_upper_bound():
66+
fixes = _load_import_fixes_module()
67+
assert fixes._torchcodec_exclusive_upper("0.10") == "<0.11.0"
68+
assert fixes._torchcodec_exclusive_upper("0.9") == "<0.10.0"
69+
70+
71+
def test_torch290_rejects_torchcodec_07(monkeypatch):
72+
import importlib.metadata
73+
74+
fixes = _load_import_fixes_module()
75+
_stub_torch(monkeypatch, "2.9.0+cu128")
76+
monkeypatch.setattr(importlib.metadata, "version", lambda _name: "0.7.0")
77+
78+
hint = fixes._torchcodec_version_mismatch_hint()
79+
assert hint is not None
80+
assert "audio-torch210" not in hint
81+
82+
83+
def test_torch280_accepts_torchcodec_07(monkeypatch):
84+
import importlib.metadata
85+
86+
fixes = _load_import_fixes_module()
87+
_stub_torch(monkeypatch, "2.8.0+cu128")
88+
monkeypatch.setattr(importlib.metadata, "version", lambda _name: "0.7.0")
89+
90+
assert fixes._torchcodec_version_mismatch_hint() is None
91+
92+
93+
def test_torch210_rejects_torchcodec_011(monkeypatch):
94+
import importlib.metadata
95+
96+
fixes = _load_import_fixes_module()
97+
_stub_torch(monkeypatch, "2.10.0+cu128")
98+
monkeypatch.setattr(
99+
importlib.metadata,
100+
"version",
101+
lambda _name: "0.11.0",
102+
)
103+
104+
hint = fixes._torchcodec_version_mismatch_hint()
105+
assert hint is not None
106+
assert "torchcodec 0.11.0" in hint
107+
assert "audio-torch210" in hint
108+
assert "<0.11.0" in hint
109+
assert "<11.0" not in hint
110+
111+
112+
def test_torch210_accepts_torchcodec_010(monkeypatch):
113+
import importlib.metadata
114+
115+
fixes = _load_import_fixes_module()
116+
_stub_torch(monkeypatch, "2.10.0+cu128")
117+
monkeypatch.setattr(
118+
importlib.metadata,
119+
"version",
120+
lambda _name: "0.10.0+cu128",
121+
)
122+
123+
assert fixes._torchcodec_version_mismatch_hint() is None
124+
125+
126+
def test_import_fixes_loads_on_python39_syntax():
127+
"""Regression: module must import on 3.9 (postponed annotations for str | None)."""
128+
fixes = _load_import_fixes_module()
129+
assert callable(fixes._torchcodec_version_mismatch_hint)

unsloth/import_fixes.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from __future__ import annotations
16+
1517
import os
1618
import importlib.abc
1719
import importlib.machinery
@@ -1525,6 +1527,59 @@ def patch_torchcodec_audio_decoder():
15251527
pass
15261528

15271529

1530+
# torch.minor -> compatible torchcodec.minor strings (see notebook_validator.py).
1531+
_TORCH_TORCHCODEC_MINORS: dict[str, set[str]] = {
1532+
"2.10": {"0.10"},
1533+
"2.9": {"0.8", "0.9"},
1534+
"2.8": {"0.6", "0.7"},
1535+
"2.7": {"0.3", "0.4", "0.5"},
1536+
"2.6": {"0.2", "0.3"},
1537+
"2.5": {"0.1", "0.2"},
1538+
}
1539+
1540+
1541+
def _torchcodec_exclusive_upper(pin: str) -> str:
1542+
"""Next torchcodec minor as an exclusive pip upper bound (0.10 -> <0.11.0)."""
1543+
major, minor = pin.split(".", 1)
1544+
return f"<{major}.{int(minor) + 1}.0"
1545+
1546+
1547+
def _torchcodec_version_mismatch_hint() -> str | None:
1548+
"""Return a user-facing hint when installed torchcodec mismatches torch."""
1549+
try:
1550+
import importlib.metadata as importlib_metadata
1551+
import torch
1552+
from packaging.version import Version
1553+
1554+
torchcodec_version = importlib_metadata.version("torchcodec")
1555+
except Exception:
1556+
return None
1557+
1558+
def _minor(version: str) -> str:
1559+
parts = Version(version.split("+", 1)[0]).release
1560+
return ".".join(str(p) for p in parts[:2])
1561+
1562+
try:
1563+
torch_minor = _minor(torch.__version__)
1564+
codec_minor = _minor(torchcodec_version)
1565+
except Exception:
1566+
# Non-PEP440 version strings must never break `import unsloth`.
1567+
return None
1568+
allowed = _TORCH_TORCHCODEC_MINORS.get(torch_minor)
1569+
if allowed is None or codec_minor in allowed:
1570+
return None
1571+
1572+
pin = sorted(allowed)[-1]
1573+
upper = _torchcodec_exclusive_upper(pin)
1574+
install_hint = f"`pip install 'torchcodec>={pin},{upper}'`"
1575+
if torch_minor == "2.10":
1576+
install_hint += " or `pip install 'unsloth[audio-torch210]'`"
1577+
return (
1578+
f"torchcodec {torchcodec_version} is incompatible with torch {torch.__version__}; "
1579+
f"install a matching build with {install_hint}."
1580+
)
1581+
1582+
15281583
def disable_torchcodec_if_broken():
15291584
"""Make broken torchcodec behave as if uninstalled (#5446).
15301585
@@ -1533,6 +1588,15 @@ def disable_torchcodec_if_broken():
15331588
flags and seat a sys.modules sentinel so downstream imports fall through
15341589
their existing except ImportError handlers cleanly.
15351590
"""
1591+
mismatch_hint = _torchcodec_version_mismatch_hint()
1592+
if mismatch_hint is not None:
1593+
try:
1594+
import warnings
1595+
warnings.warn(mismatch_hint, stacklevel = 2)
1596+
except Exception:
1597+
# Warning filters promoted to errors must not abort the disable
1598+
# fallback below (e.g. PYTHONWARNINGS=error, pytest -W error).
1599+
pass
15361600
try:
15371601
import importlib.util
15381602
if importlib.util.find_spec("torchcodec") is None:

0 commit comments

Comments
 (0)