mirror of
https://github.com/unslothai/unsloth.git
synced 2026-07-09 15:58:41 +00:00
Some checks are pending
Core / Core (HF=default + TRL=default) (push) Waiting to run
Core / Core (HF=4.57.6 + TRL<1) (push) Waiting to run
Core / Core (HF=latest + TRL=latest) (push) Waiting to run
Core / llama.cpp build + smoke (push) Waiting to run
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Waiting to run
MLX CI on Mac M1 / dispatch (push) Waiting to run
Security audit / advisory audit (pip + npm + cargo) (push) Waiting to run
Security audit / pip scan-packages :: extras (push) Waiting to run
Security audit / pip scan-packages :: studio (push) Waiting to run
Security audit / pip scan-packages :: hf-stack (push) Waiting to run
Security audit / npm scan-packages (Studio frontend tarballs) (push) Waiting to run
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Waiting to run
Security audit / pytest tests/security (push) Waiting to run
Security audit / npm provenance + new install-script diff (push) Waiting to run
Studio API CI / Studio API & Auth Tests (push) Waiting to run
Backend CI / (Python 3.10) (push) Waiting to run
Backend CI / (Python 3.11) (push) Waiting to run
Backend CI / (Python 3.12) (push) Waiting to run
Backend CI / (Python 3.13) (push) Waiting to run
Backend CI / Repo tests (CPU) (push) Waiting to run
Frontend CI / Frontend build + bundle sanity (push) Waiting to run
Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Studio GGUF CI / Tool calling Tests (push) Waiting to run
Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio API CI / Studio API & Auth Tests (push) Waiting to run
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Mac Studio GGUF CI / Tool calling Tests (push) Waiting to run
Mac Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Waiting to run
Mac Studio UI CI / Chat UI Tests (push) Waiting to run
Mac Studio Update CI / Studio Updating Tests (push) Waiting to run
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Studio UI CI / Chat UI Tests (push) Waiting to run
Studio Update CI / Studio Updating Tests (push) Waiting to run
Windows Studio API CI / Studio API & Auth Tests (push) Waiting to run
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Windows Studio GGUF CI / Tool calling Tests (push) Waiting to run
Windows Studio GGUF CI / JSON, images (push) Waiting to run
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Waiting to run
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Waiting to run
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Waiting to run
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Waiting to run
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Waiting to run
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Waiting to run
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Waiting to run
Windows Studio UI CI / Chat UI Tests (push) Waiting to run
Windows Studio Update CI / Studio Updating Tests (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
* Fix gpt-oss offload_embedding and generate() logits_to_keep on fused models
offload_embedding=True moved embed_tokens to CPU but left the input/output device-shuffling forward hooks commented out ('[TODO] Doesn't seem to work!'), so an eager forward/generate with CUDA input_ids hit the CPU embedding and raised a device-mismatch RuntimeError. Re-implement them in a testable helper _install_offload_embedding_hooks that saves the origin device on the module (the pre-hook returns a new tensor, so a device stashed on the original input is lost) and runs the lookup on the embedding weight's CURRENT device. Reading the weight device at call time (not a hard-coded cpu) also handles a non-quantized (bf16) embedding that a later model.to(...) pulls back onto the GPU, which the hard-coded version broke in the opposite direction.
unsloth_base_fast_generate injected logits_to_keep/num_logits_to_keep whenever an inner submodule forward accepted it, but transformers validates generate kwargs against the top-level prepare_inputs_for_generation (plus forward when it takes kwargs). On fused/PEFT-wrapped gpt-oss this raised 'model_kwargs are not used by the model: [logits_to_keep]'. Only inject when the top level would accept it, mirroring transformers _validate_model_kwargs. Behavior is unchanged for every model that works today.
Adds tests/test_offload_embedding_hooks.py and tests/test_generate_kwarg_gate.py.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* gpt-oss offload hooks: store origin device on the tensor, not the shared module
The pre-hook stashed the input device on embed_tokens itself, which races when
concurrent forwards share the module (serving). Ride it on the moved tensor and
read it from the post-hook args instead: stateless and thread-safe.
* Also strip mm_token_type_ids that generate() rejects (Qwen3-VL vision GRPO)
The vision processor (Transformers 5.x path) emits mm_token_type_ids, which
Qwen3-VL's generate() then rejects in _validate_model_kwargs on transformers
4.x, so vision GRPO fails at the first rollout:
ValueError: The following `model_kwargs` are not used by the model:
['mm_token_type_ids']
Unlike logits_to_keep this is an incoming kwarg rather than one we inject, so
drop it in unsloth_base_fast_generate when the top level generate does not
accept it, reusing the same _unsloth_generate_accepts_kwarg gate. Extends the
GPU-free gate test with the accept/reject mm_token_type_ids cases (7/7 pass).
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Trim mm_token_type_ids comment
* Trim comments in gpt-oss offload/logits fix (comment-only)
* gpt-oss offload: return embedding output to the decoder device, not the input's
When offload_embedding moves the embedding to CPU, model.device can become CPU and
inputs then arrive on CPU, so returning the output to the input device left it on CPU
and the CUDA decoder hit a device mismatch. Capture the decoder device before offload
and always return there. This also drops the per-request tensor state (stateless, so
concurrent forwards stay correct).
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* gpt-oss offload: refuse offload_embedding for tied word embeddings
Tied models share embed_tokens.weight with lm_head, so offloading the weight
to CPU strands the output projection there (device mismatch at generate) and
saves no VRAM since lm_head still needs it on GPU. Detect the shared weight via
get_output_embeddings and raise NotImplementedError instead of loading into a
crash. Untied models (gpt-oss, Llama-3.1-8B) offload unchanged.
Adds tests/test_offload_tied_guard.py.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* gpt-oss offload: skip embedding offload on fast_inference (vLLM)
vLLM manages its own weights, so offload_embedding cannot apply on the
fast_inference path (previously it was silently ignored). Disable it with a
notice, mirroring the WSL and Windows skips.
* Trim offload embedding comments (comment-only)
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* gpt-oss offload: track decoder device live so it survives model.to()
The post-hook returned the embedding output to a device captured at load time.
If a model is loaded on CPU then moved with model.to(cuda), that device is
stale and the output lands on the wrong device. Read the decoder device live
from the (untied) output embeddings, keeping the captured device as a fallback.
Adds a stale-fallback regression test.
* Make generate-kwarg-gate cases pytest-collectable
Cases lived in run(), which pytest does not collect, so CI never exercised the
gate. Expose them as test_generate_kwarg_gate; still runnable via __main__.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* gpt-oss offload: skip a meta (disk-offloaded) lm_head as the return device
A device_map that disk-offloads an untied lm_head leaves its weight on the meta
device until that module's own hook runs, so reading it as the decoder device
would move real hidden states to meta. Skip meta (and a missing weight) and fall
back to the captured device. Adds a regression test.
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
129 lines
4.6 KiB
Python
129 lines
4.6 KiB
Python
"""Tests _install_offload_embedding_hooks in vision.py: the offloaded lookup must work and
|
|
its output must land on the decoder device, read live from the output embeddings (lm_head)
|
|
so it tracks model.to() moves. CUDA cases skip without a GPU."""
|
|
|
|
import ast, os
|
|
import torch
|
|
import torch.nn as nn
|
|
|
|
HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
VISION = os.path.join(HERE, "unsloth", "models", "vision.py")
|
|
|
|
|
|
def _load_installer():
|
|
src = open(VISION).read()
|
|
mod = ast.parse(src)
|
|
for node in mod.body:
|
|
if isinstance(node, ast.FunctionDef) and node.name == "_install_offload_embedding_hooks":
|
|
ns = {"torch": torch}
|
|
exec(ast.get_source_segment(src, node), ns)
|
|
return ns["_install_offload_embedding_hooks"]
|
|
raise AssertionError("_install_offload_embedding_hooks not found in vision.py")
|
|
|
|
|
|
install = _load_installer()
|
|
CPU = torch.device("cpu")
|
|
|
|
|
|
def _emb():
|
|
return nn.Embedding(32, 8)
|
|
|
|
|
|
def _lm_head(device):
|
|
# Stand-in decoder reference (untied lm_head) whose weight device is the target.
|
|
return nn.Linear(8, 32, bias = False).to(device)
|
|
|
|
|
|
def test_install_and_idempotent():
|
|
emb = _emb()
|
|
lm = _lm_head(CPU)
|
|
assert install(emb, lm, CPU) is True
|
|
assert emb._unsloth_offload_hooks_installed is True
|
|
n_pre = len(emb._forward_pre_hooks)
|
|
n_post = len(emb._forward_hooks)
|
|
assert install(emb, lm, CPU) is True
|
|
assert len(emb._forward_pre_hooks) == n_pre and len(emb._forward_hooks) == n_post
|
|
assert install(None, lm, CPU) is False
|
|
|
|
|
|
def test_cpu_noop_forward():
|
|
# cpu weight + cpu decoder + cpu input -> output stays cpu.
|
|
emb = _emb()
|
|
install(emb, _lm_head(CPU), CPU)
|
|
out = emb(torch.randint(0, 32, (2, 5)))
|
|
assert out.shape == (2, 5, 8)
|
|
assert out.device.type == "cpu"
|
|
|
|
|
|
def test_cuda_input_roundtrip():
|
|
if not torch.cuda.is_available():
|
|
print("[SKIP] CUDA not available")
|
|
return
|
|
# CPU weight, CUDA decoder + input -> lookup on cpu, output back on cuda.
|
|
emb = _emb().to("cpu")
|
|
install(emb, _lm_head("cuda"), torch.device("cuda"))
|
|
out = emb(torch.randint(0, 32, (2, 5), device = "cuda"))
|
|
assert out.device.type == "cuda", out.device
|
|
|
|
|
|
def test_cpu_input_still_returns_to_decoder():
|
|
if not torch.cuda.is_available():
|
|
print("[SKIP] CUDA not available")
|
|
return
|
|
# P1: offload makes the input arrive on cpu; the output must still reach the cuda decoder.
|
|
emb = _emb().to("cpu")
|
|
install(emb, _lm_head("cuda"), torch.device("cuda"))
|
|
out = emb(torch.randint(0, 32, (2, 5), device = "cpu"))
|
|
assert out.device.type == "cuda", out.device
|
|
|
|
|
|
def test_live_decoder_over_stale_fallback():
|
|
if not torch.cuda.is_available():
|
|
print("[SKIP] CUDA not available")
|
|
return
|
|
# P2: fallback captured as cpu (model loaded on cpu), but the decoder later lives on cuda.
|
|
# The output must follow the live lm_head device, not the stale cpu fallback.
|
|
emb = _emb().to("cpu")
|
|
install(emb, _lm_head("cuda"), CPU)
|
|
out = emb(torch.randint(0, 32, (2, 5), device = "cuda"))
|
|
assert out.device.type == "cuda", out.device
|
|
|
|
|
|
def test_meta_lm_head_falls_back():
|
|
# A disk-offloaded (meta) lm_head must not be used as the return device: moving hidden
|
|
# states to meta is unrecoverable, so fall back to the captured device. No GPU needed.
|
|
emb = _emb().to("cpu")
|
|
lm = _lm_head(CPU)
|
|
lm.weight = nn.Parameter(lm.weight.to("meta"))
|
|
install(emb, lm, CPU)
|
|
out = emb(torch.randint(0, 32, (2, 5)))
|
|
assert out.device.type == "cpu", out.device
|
|
|
|
|
|
def test_cuda_weight_pulled_back_to_gpu():
|
|
if not torch.cuda.is_available():
|
|
print("[SKIP] CUDA not available")
|
|
return
|
|
# bf16 weight later pulled back to gpu + cuda input -> no-op, stays on cuda.
|
|
emb = _emb().to("cuda")
|
|
install(emb, _lm_head("cuda"), torch.device("cuda"))
|
|
out = emb(torch.randint(0, 32, (2, 5), device = "cuda"))
|
|
assert out.device.type == "cuda", out.device
|
|
|
|
|
|
if __name__ == "__main__":
|
|
test_install_and_idempotent()
|
|
print("[PASS] install + idempotent")
|
|
test_cpu_noop_forward()
|
|
print("[PASS] cpu no-op forward")
|
|
test_cuda_input_roundtrip()
|
|
print("[PASS] cuda input roundtrip")
|
|
test_cpu_input_still_returns_to_decoder()
|
|
print("[PASS] cpu input still returns to cuda decoder (P1)")
|
|
test_live_decoder_over_stale_fallback()
|
|
print("[PASS] live decoder device beats stale fallback (P2)")
|
|
test_meta_lm_head_falls_back()
|
|
print("[PASS] meta lm_head falls back to captured device (P2)")
|
|
test_cuda_weight_pulled_back_to_gpu()
|
|
print("[PASS] cuda weight-on-gpu no-op")
|
|
print("OK: offloaded embedding output always lands on the live decoder device")
|