# SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. # # Cross-version compat canary for the four upstream packages whose # release cadence regularly breaks unsloth + unsloth-zoo: # # 1. vLLM (LoRA worker manager, BnB loader, cumem allocator) # 2. TRL / GRPO (trainer source rewriters in unsloth.models.rl*) # 3. PEFT (LoraConfig, get_peft_model, LoraLayer, bnb integration) # 4. sentence-transformers (Transformer/Pooling/Normalize, Trainer) # 5. bitsandbytes (Linear4bit, dequantize_4bit) # # Strategy: GitHub raw-fetch + symbol grep against every tracked # version (no pip install, CPU-only). When upstream renames a symbol # we depend on, the matching test fails BEFORE a user hits it. The # `main` branch entries give us a few-day lead on PyPI releases. # # Cross-references: # tests/vllm_compat/test_vllm_pinned_symbols.py (vLLM symbols) # tests/version_compat/test_trl_grpo_pinned_symbols.py # tests/version_compat/test_peft_pinned_symbols.py # tests/version_compat/test_sentence_transformers_pinned_symbols.py # tests/version_compat/test_bitsandbytes_pinned_symbols.py name: Version Compat CI on: pull_request: # Trigger on any unsloth source change, not just the three previously # named files. The symbol-existence tests verify that EVERY pinned # upstream reference in unsloth still resolves; a new # `from peft.foo import Bar` added in unsloth/kernels/whatever.py # is just as much a compat regression risk as one added in # unsloth/models/rl.py. paths: - 'unsloth/**' - 'tests/vllm_compat/**' - 'tests/version_compat/**' - 'pyproject.toml' - '.github/workflows/version-compat-ci.yml' schedule: # Daily 06:43 UTC. Catches upstream PyPI releases roughly within # 24 h. Off the :00 / :30 fleet-collision spots. - cron: '43 6 * * *' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: vllm-pinned-symbols: name: vLLM pinned-symbol matrix (≥ 0.9.0 + main) runs-on: ubuntu-latest timeout-minutes: 12 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.12' cache: 'pip' - name: Install pytest only # The test fetches from raw.githubusercontent.com and greps # source. No pip install of vllm / torch / transformers is # needed — that's the whole point of this canary. run: | python -m pip install --upgrade pip pip install 'pytest>=8' - name: Run vllm-compat suite env: # Authenticated requests get a 5000-req/h quota on raw # fetches; unauthenticated is 60/h and trips on the matrix. GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | python -m pytest tests/vllm_compat/test_vllm_pinned_symbols.py -v --tb=short trl-grpo-pinned-symbols: name: TRL / GRPO pinned-symbol matrix runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.12' cache: 'pip' - name: Install pytest only run: | python -m pip install --upgrade pip pip install 'pytest>=8' - name: Run trl-compat suite env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # PYTHONPATH=. so `from tests.version_compat._fetch import …` # works without an editable install of unsloth itself. PYTHONPATH=. python -m pytest \ tests/version_compat/test_trl_grpo_pinned_symbols.py \ -v --tb=short peft-pinned-symbols: name: PEFT pinned-symbol matrix (pyproject window + main) runs-on: ubuntu-latest timeout-minutes: 8 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.12' cache: 'pip' - name: Install pytest only run: | python -m pip install --upgrade pip pip install 'pytest>=8' - name: Run peft-compat suite env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PYTHONPATH=. python -m pytest \ tests/version_compat/test_peft_pinned_symbols.py \ tests/version_compat/test_unsloth_zoo_save_merged_pinned_symbols.py \ -v --tb=short st-pinned-symbols: name: sentence-transformers pinned-symbol matrix runs-on: ubuntu-latest timeout-minutes: 8 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.12' cache: 'pip' - name: Install pytest only run: | python -m pip install --upgrade pip pip install 'pytest>=8' - name: Run sentence-transformers compat suite env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PYTHONPATH=. python -m pytest \ tests/version_compat/test_sentence_transformers_pinned_symbols.py \ -v --tb=short bitsandbytes-pinned-symbols: name: bitsandbytes pinned-symbol matrix runs-on: ubuntu-latest timeout-minutes: 8 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.12' cache: 'pip' - name: Install pytest only run: | python -m pip install --upgrade pip pip install 'pytest>=8' - name: Run bitsandbytes compat suite env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PYTHONPATH=. python -m pytest \ tests/version_compat/test_bitsandbytes_pinned_symbols.py \ -v --tb=short transformers-pinned-symbols: name: transformers pinned-symbol matrix (4.57.6 + 5.x + main) runs-on: ubuntu-latest timeout-minutes: 12 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.12' cache: 'pip' - name: Install pytest only run: | python -m pip install --upgrade pip pip install 'pytest>=8' - name: Run transformers compat suite env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PYTHONPATH=. python -m pytest \ tests/version_compat/test_transformers_pinned_symbols.py \ -v --tb=short # Optional second layer: actually `pip install` ONE representative # version of each package and verify unsloth + unsloth-zoo modules # import on it under the existing CUDA spoof. CPU-only, runs on # ubuntu-latest. Catches the small set of breakages that the static # symbol check misses (e.g. import-time side effects). zoo-imports-under-spoof: name: unsloth_zoo vllm/grpo/peft/st modules import under CUDA spoof runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false path: unsloth - name: Clone unsloth-zoo @ main run: | # github.com occasionally 500s on the git fetch; retry so a # single upstream blip does not fail CI. for attempt in 1 2 3; do rm -rf "$RUNNER_TEMP/unsloth-zoo" if git clone --depth=1 https://github.com/unslothai/unsloth-zoo \ "$RUNNER_TEMP/unsloth-zoo"; then break fi if [ "$attempt" -eq 3 ]; then echo "::error::git clone unsloth-zoo failed after 3 attempts" exit 1 fi delay=$((5 * attempt)) echo "::warning::clone failed (attempt $attempt/3), retrying in ${delay}s..." sleep "$delay" done - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.12' cache: 'pip' - name: Install CPU torch + supported pkg pins run: | python -m pip install --upgrade pip # CPU torch (vllm/peft/st all depend on it). pip install --index-url https://download.pytorch.org/whl/cpu --extra-index-url https://pypi.org/simple \ 'torch>=2.4,<2.11' 'torchvision<0.26' 'torchcodec<0.10' # torchcodec is a hard requirement on transformers 5.x: # transformers/audio_utils.py:55 does # `importlib.metadata.version("torchcodec")` UNCONDITIONALLY, # which raises PackageNotFoundError on a CPU runner that # otherwise has no audio path -- and that error trickles up # through every `import unsloth_zoo.` because # unsloth-zoo's vision_utils transitively pulls # transformers.processing_utils (-> audio_utils). The 0.10 # cap mirrors the torch 2.10 / torchvision 0.26 ABI window # we already pin above. # Ladder of supported floor versions per pyproject.toml. pip install \ 'transformers>=4.56,<5.6' 'trl>=0.22,<0.26' \ 'peft>=0.18.0' 'sentence-transformers>=5.0' \ 'accelerate>=1.0' 'datasets>=3.4,<5' \ 'bitsandbytes>=0.45.5' \ sentencepiece protobuf safetensors numpy 'pytest>=8' \ 'huggingface_hub>=0.34' tqdm packaging psutil triton Pillow # Editable-install both repos so the test imports the # checkouts (not whatever stale PyPI version pip resolved). pip install --no-deps -e "$RUNNER_TEMP/unsloth-zoo" pip install --no-deps -e ./unsloth - name: Run vllm_compat zoo-imports tests under spoof env: UNSLOTH_IS_PRESENT: '1' UNSLOTH_COMPILE_DISABLE: '1' PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION: python run: | cd unsloth # tests/vllm_compat/test_unsloth_zoo_imports.py: narrow vllm/grpo # import gates (5 tests). # tests/vllm_compat/test_extended_module_imports.py: full sweep # of unsloth_zoo + unsloth.models.* modules + RL dispatch # table population + FastModel API surface under spoof # (~30 tests). Catches transformers / peft / bnb symbol pin # drift at module-top BEFORE any runtime call. PYTHONPATH=. python -m pytest \ tests/vllm_compat/test_unsloth_zoo_imports.py \ tests/vllm_compat/test_extended_module_imports.py \ -v --tb=short # Fake-CUDA GRPO/SFT/DPO patch run against REAL TRL (latest + main). Unlike # the static symbol/source greps above, this drives unsloth's actual # source-transform patchers (models/rl.py + rl_replacements.py) on a CPU-only # runner under the tests/conftest.py spoof harness -- no GPU, no training. # Catches structural TRL drift the greps miss (e.g. TRL 1.7.0's 2->3-tuple # per-token-logps return, restructured PEFT ref-adapter block) by asserting # the generated Unsloth trainer still satisfies the transform contracts. grpo-fake-run: name: GRPO fake-run (latest + main TRL, CPU spoof) runs-on: ubuntu-latest timeout-minutes: 18 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false path: unsloth - name: Clone unsloth-zoo @ main run: | for attempt in 1 2 3; do rm -rf "$RUNNER_TEMP/unsloth-zoo" if git clone --depth=1 https://github.com/unslothai/unsloth-zoo \ "$RUNNER_TEMP/unsloth-zoo"; then break fi if [ "$attempt" -eq 3 ]; then echo "::error::git clone unsloth-zoo failed after 3 attempts" exit 1 fi delay=$((5 * attempt)) echo "::warning::clone failed (attempt $attempt/3), retrying in ${delay}s..." sleep "$delay" done - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.12' cache: 'pip' - name: Install CPU torch + ecosystem + TRL latest run: | python -m pip install --upgrade pip pip install --index-url https://download.pytorch.org/whl/cpu --extra-index-url https://pypi.org/simple \ 'torch>=2.4,<2.11' 'torchvision<0.26' 'torchcodec<0.10' # Ecosystem floors unsloth needs; TRL itself is installed last so it # can pull the transformers/peft it requires. pip install \ 'transformers>=4.57' 'peft>=0.18.0' 'accelerate>=1.0' 'datasets>=3.4,<5' \ 'bitsandbytes>=0.45.5' sentencepiece protobuf safetensors numpy 'pytest>=8' \ 'huggingface_hub>=0.34' tqdm packaging psutil triton Pillow pip install --upgrade trl pip install --no-deps -e "$RUNNER_TEMP/unsloth-zoo" pip install --no-deps -e ./unsloth - name: Fake-run vs TRL latest env: UNSLOTH_IS_PRESENT: '1' UNSLOTH_COMPILE_DISABLE: '1' # Disable dynamo/inductor at the process level, before conftest.py's early # `import unsloth`, so the GRPO hot path never compiles on the GPU-less runner # (defense in depth; the CPU fake-train also flips this at runtime). TORCHDYNAMO_DISABLE: '1' TORCH_COMPILE_DISABLE: '1' PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION: python run: | cd unsloth python -c "import trl; print('Resolved TRL', trl.__version__)" PYTHONPATH=. python -m pytest \ tests/version_compat/test_trl_grpo_fake_run.py \ tests/version_compat/test_trl_fake_train_cpu.py \ -v --tb=short # `main` is scheduled/dispatch-only so PR jobs stay fast and a bleeding-edge # TRL break does not red every PR. github.event_name is valid in a step if. - name: Fake-run vs TRL main (scheduled / dispatch only) if: ${{ github.event_name != 'pull_request' }} env: UNSLOTH_IS_PRESENT: '1' UNSLOTH_COMPILE_DISABLE: '1' TORCHDYNAMO_DISABLE: '1' TORCH_COMPILE_DISABLE: '1' PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION: python run: | pip install --upgrade "git+https://github.com/huggingface/trl" cd unsloth python -c "import trl; print('Resolved TRL', trl.__version__)" PYTHONPATH=. python -m pytest \ tests/version_compat/test_trl_grpo_fake_run.py \ tests/version_compat/test_trl_fake_train_cpu.py \ -v --tb=short # Daily-only: same suites but with --strict on importable upstream # tags. Schedule-only so PR jobs stay fast; cron tolerates a flake. daily-fresh-fetch: name: daily fresh-fetch sweep (cron only) if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.12' cache: 'pip' - name: Install pytest run: pip install 'pytest>=8' - name: Run all version-compat suites in one process (no cache) env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PYTHONPATH=. python -m pytest \ tests/vllm_compat/test_vllm_pinned_symbols.py \ tests/version_compat/ \ -v --tb=short