* tests: pinned-symbol canary for unsloth-zoo save_pretrained_merged guards (#5410) unsloth#5410 was a class of silent-write bug in the save_pretrained_merged path that the existing CI matrix could not detect because the merge-helper tests were not wired through the upstream-drift suite. The full fix lives in unslothai/unsloth-zoo#647 (layout-aware MoE merge helpers, authoritative num_experts resolver, loud-fail counter, generation_config.json save). This PR adds the unsloth-side canary that watches for the four guards staying in place in unsloth-zoo so a future refactor cannot silently regress them. tests/version_compat/test_unsloth_zoo_save_merged_pinned_symbols.py fetches unsloth_zoo/saving_utils.py + tests/test_unsloth_zoo_lora_merge.py from unslothai/unsloth-zoo:main and asserts: - _MOE_MERGE_STATE / _reset_moe_merge_state / _record_moe_merge_fallback are still defined and a `raise RuntimeError(...MoE...)` still fires when fallback > 0. - _detect_moe_lora_layout exists and both "swapped" / "standard" branch labels are reachable in the source. - _resolve_num_experts_from_lora_stats is present AND its base_layer walk is bounded by `for _ in range(N):` (a cyclic ParamWrapper chain must not hang the merge). - merge_and_overwrite_lora still calls model.generation_config.save_pretrained(...). - tests/test_unsloth_zoo_lora_merge.py keeps the six PEFT 0.19+ standard-layout regression tests added in #647. - Local unsloth/save.py still names save_pretrained_merged and routes through merge_and_overwrite_lora (i.e. the entry point still reaches the upstream fix). While #647 is still open, the four symbol tests SKIP cleanly with a message naming #647. When #647 merges into unsloth-zoo main, the same tests automatically become hard gates and catch any future regression. The sixth test (local entry-point grep) passes today. CPU-only static fetch, ~0.1s. Wired into the existing peft-pinned-symbols job in .github/workflows/version-compat-ci.yml so it runs on every PR that touches unsloth/** and on the daily schedule. Local run: 1 passed, 5 skipped (expected; #647 open). * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * tests/version_compat: relax MoE/generation_config regex to fit zoo#647 zoo#647 landed two layout changes that broke the pinned-symbol canary's exact-string regex matches but kept the underlying guarantees intact: - The post-loop MoE LoRA fallback `raise RuntimeError(...)` wraps the "MoE" wording onto a second line; the old `[^\n]*` did not cross newlines. Switch to `.*?` + re.DOTALL. - The generation_config save now binds the attr to a local var `gen_cfg = getattr(model, "generation_config", ...)` and calls `gen_cfg.save_pretrained(save_directory)`, so a literal `generation_config.save_pretrained(` substring no longer matches. Anchor on the conceptual operation: a `generation_config` mention followed (within a small char window) by a `.save_pretrained(` call. That is what the canary actually cares about. Verified locally: pytest tests/version_compat/test_unsloth_zoo_save_merged_pinned_symbols.py -> 2 passed (4 deselected) --------- Co-authored-by: Daniel Han-Chen <info@unsloth.ai> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
312 lines
12 KiB
YAML
312 lines
12 KiB
YAML
# 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 \
|
|
'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.<module>` 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
|
|
|
|
# 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
|