Merge remote-tracking branch 'origin/main' into cuda-torch-index-override
This commit is contained in:
commit
c1e956c80a
203 changed files with 15913 additions and 1923 deletions
71
tests/python/test_change_system_message.py
Normal file
71
tests/python/test_change_system_message.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import ast
|
||||
import re
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _load_change_system_message():
|
||||
# Extract _change_system_message without importing unsloth (needs unsloth_zoo / a GPU).
|
||||
source = Path(__file__).parents[2] / "unsloth" / "chat_templates.py"
|
||||
tree = ast.parse(source.read_text(encoding = "utf-8"))
|
||||
funcs = [
|
||||
node
|
||||
for node in tree.body
|
||||
if isinstance(node, ast.FunctionDef) and node.name == "_change_system_message"
|
||||
]
|
||||
namespace = {
|
||||
"re": re,
|
||||
"logger": types.SimpleNamespace(warning_once = lambda *a, **k: None),
|
||||
"DEFAULT_SYSTEM_MESSAGE": {"unsloth": "You are a helpful assistant to the user"},
|
||||
}
|
||||
module = ast.Module(body = funcs, type_ignores = [])
|
||||
ast.fix_missing_locations(module)
|
||||
exec(compile(module, str(source), "exec"), namespace)
|
||||
return namespace["_change_system_message"]
|
||||
|
||||
|
||||
CUSTOM = "mycustom" # no predefined default
|
||||
|
||||
|
||||
def test_custom_template_fills_placeholder():
|
||||
# A {system_message} placeholder must be filled, not left literal.
|
||||
fn = _load_change_system_message()
|
||||
template, used = fn("System: {system_message}\nUser:", CUSTOM, "You are a pirate")
|
||||
assert template == "System: You are a pirate\nUser:"
|
||||
assert "{system_message}" not in template
|
||||
assert used == "You are a pirate"
|
||||
|
||||
|
||||
def test_custom_template_preserves_backslashes():
|
||||
# str.replace not re.sub: re.sub treats backslashes specially (r"C:\Users"
|
||||
# bad-escape, r"\1" group ref), so messages must be inserted verbatim.
|
||||
fn = _load_change_system_message()
|
||||
for msg in (r"C:\Users\me", r"\frac{a}{b}", r"see \1 here"):
|
||||
template, used = fn("System: {system_message}", CUSTOM, msg)
|
||||
assert template == f"System: {msg}"
|
||||
assert used == msg
|
||||
|
||||
|
||||
def test_custom_template_requires_system_message():
|
||||
# A placeholder with no system message must raise, not stay literal.
|
||||
fn = _load_change_system_message()
|
||||
with pytest.raises(ValueError):
|
||||
fn("System: {system_message}", CUSTOM, None)
|
||||
|
||||
|
||||
def test_custom_template_without_placeholder_unchanged():
|
||||
fn = _load_change_system_message()
|
||||
template, used = fn("System: fixed", CUSTOM, "ignored")
|
||||
assert template == "System: fixed"
|
||||
|
||||
|
||||
def test_predefined_template_uses_default_then_override():
|
||||
# Predefined templates with a default are unaffected.
|
||||
fn = _load_change_system_message()
|
||||
t1, u1 = fn("System: {system_message}", "unsloth", None)
|
||||
assert t1 == "System: You are a helpful assistant to the user"
|
||||
t2, u2 = fn("System: {system_message}", "unsloth", "Custom override")
|
||||
assert t2 == "System: Custom override"
|
||||
assert u2 == "Custom override"
|
||||
193
tests/python/test_v100_fullft_precision.py
Normal file
193
tests/python/test_v100_fullft_precision.py
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
||||
|
||||
"""Regression tests for full finetuning precision on no-bf16 GPUs (V100/T4).
|
||||
|
||||
Full finetuning upcasts trainable weights to float32, so the model dtype is
|
||||
float32 (not bfloat16). The SFTTrainer mixed-precision template in
|
||||
unsloth/models/rl.py must then:
|
||||
- run the forward pass under float16 autocast for normal models,
|
||||
- keep FORCE_FLOAT32 models (Gemma3, gpt_oss, ...) in pure float32,
|
||||
- never select bf16 on hardware without bf16.
|
||||
|
||||
We execute the REAL template block extracted from rl.py source (no heavy unsloth
|
||||
import) against mocked inputs. See issue #4082.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
torch = pytest.importorskip("torch")
|
||||
|
||||
RL_PY = Path(__file__).resolve().parents[2] / "unsloth" / "models" / "rl.py"
|
||||
|
||||
|
||||
def _extract_mixed_precision_code() -> str:
|
||||
lines = RL_PY.read_text().split("\n")
|
||||
try:
|
||||
start = next(i for i, l in enumerate(lines) if "mixed_precision = (" in l)
|
||||
except StopIteration:
|
||||
pytest.skip("mixed_precision template not found in rl.py")
|
||||
body, k = [], start + 1
|
||||
while lines[k].strip() != ")":
|
||||
body.append(lines[k])
|
||||
k += 1
|
||||
return eval("(\n" + "\n".join(body) + "\n)") # only string literals + comments
|
||||
|
||||
|
||||
CODE = _extract_mixed_precision_code()
|
||||
|
||||
|
||||
def _restore(mapping, saved):
|
||||
"""Restore a dict-like to its saved snapshot: pop keys that were absent."""
|
||||
for k, v in saved.items():
|
||||
if v is None:
|
||||
mapping.pop(k, None)
|
||||
else:
|
||||
mapping[k] = v
|
||||
|
||||
|
||||
def _decide(dtype, *, bf16_supported, force_float32, full_finetuning, mixed_precision, fp16, bf16):
|
||||
"""Run the template block; return (args.fp16, args.bf16, ACCELERATE_MP, raised).
|
||||
|
||||
Stubs (sys.modules, env vars, torch.cuda.is_bf16_supported) are restored on
|
||||
exit so a decision can't leak into later tests in the same process.
|
||||
"""
|
||||
uzu = types.ModuleType("unsloth_zoo.utils")
|
||||
uzu._get_dtype = lambda x: x
|
||||
uzd = types.ModuleType("unsloth_zoo.device_type")
|
||||
uzd.device_is_bf16_supported = lambda: bf16_supported # device-aware signal stub
|
||||
|
||||
env_keys = (
|
||||
"UNSLOTH_FORCE_FLOAT32",
|
||||
"UNSLOTH_ENABLE_FULL_FINETUNING",
|
||||
"UNSLOTH_MIXED_PRECISION",
|
||||
"ACCELERATE_MIXED_PRECISION",
|
||||
)
|
||||
mod_keys = ("unsloth_zoo", "unsloth_zoo.utils", "unsloth_zoo.device_type")
|
||||
saved_env = {k: os.environ.get(k) for k in env_keys}
|
||||
saved_mods = {k: sys.modules.get(k) for k in mod_keys}
|
||||
orig_bf16 = torch.cuda.is_bf16_supported
|
||||
try:
|
||||
sys.modules.setdefault("unsloth_zoo", types.ModuleType("unsloth_zoo"))
|
||||
sys.modules["unsloth_zoo.utils"] = uzu
|
||||
sys.modules["unsloth_zoo.device_type"] = uzd
|
||||
for k in env_keys:
|
||||
os.environ.pop(k, None)
|
||||
os.environ["UNSLOTH_FORCE_FLOAT32"] = "1" if force_float32 else "0"
|
||||
os.environ["UNSLOTH_ENABLE_FULL_FINETUNING"] = "1" if full_finetuning else "0"
|
||||
os.environ["UNSLOTH_MIXED_PRECISION"] = mixed_precision
|
||||
torch.cuda.is_bf16_supported = lambda *a, **k: bf16_supported
|
||||
args = types.SimpleNamespace(fp16 = fp16, bf16 = bf16, mixed_precision = None)
|
||||
emb = types.SimpleNamespace(weight = types.SimpleNamespace(dtype = dtype))
|
||||
model = types.SimpleNamespace(
|
||||
config = types.SimpleNamespace(dtype = dtype, torch_dtype = dtype),
|
||||
get_input_embeddings = lambda: emb,
|
||||
)
|
||||
raised = None
|
||||
try:
|
||||
exec(CODE, {"torch": torch, "os": os}, {"args": args, "model": model})
|
||||
except TypeError:
|
||||
raised = "TypeError"
|
||||
return args.fp16, args.bf16, os.environ.get("ACCELERATE_MIXED_PRECISION"), raised
|
||||
finally:
|
||||
torch.cuda.is_bf16_supported = orig_bf16
|
||||
_restore(os.environ, saved_env)
|
||||
_restore(sys.modules, saved_mods)
|
||||
|
||||
|
||||
def test_v100_normal_fullft_fp16_explicit():
|
||||
# Normal model, full FT (weights upcast to float32), V100, fp16=True.
|
||||
fp16, bf16, amp, raised = _decide(
|
||||
torch.float32,
|
||||
bf16_supported = False,
|
||||
force_float32 = False,
|
||||
full_finetuning = True,
|
||||
mixed_precision = "float32",
|
||||
fp16 = True,
|
||||
bf16 = False,
|
||||
)
|
||||
assert raised is None
|
||||
assert (fp16, bf16) == (True, False) # float32 weights + fp16 forward
|
||||
|
||||
|
||||
def test_v100_normal_fullft_precision_unset():
|
||||
# Same, but user left precision unset -> must pick fp16, never bf16.
|
||||
fp16, bf16, amp, raised = _decide(
|
||||
torch.float32,
|
||||
bf16_supported = False,
|
||||
force_float32 = False,
|
||||
full_finetuning = True,
|
||||
mixed_precision = "float32",
|
||||
fp16 = False,
|
||||
bf16 = False,
|
||||
)
|
||||
assert raised is None
|
||||
assert (fp16, bf16) == (True, False)
|
||||
assert amp == "fp16"
|
||||
|
||||
|
||||
def test_force_float32_model_fullft_is_pure_float32():
|
||||
# FORCE_FLOAT32 model (Gemma3, gpt_oss, ...) in full FT -> pure float32, no autocast.
|
||||
fp16, bf16, amp, raised = _decide(
|
||||
torch.float32,
|
||||
bf16_supported = False,
|
||||
force_float32 = True,
|
||||
full_finetuning = True,
|
||||
mixed_precision = "float32",
|
||||
fp16 = True,
|
||||
bf16 = False,
|
||||
)
|
||||
assert raised is None
|
||||
assert (fp16, bf16) == (False, False)
|
||||
assert amp in (None, "no")
|
||||
|
||||
|
||||
def test_no_bf16_on_volta_in_auto_branch():
|
||||
# bf16 model dtype but no bf16 HW, precision unset -> fp16, never bf16.
|
||||
fp16, bf16, amp, raised = _decide(
|
||||
torch.bfloat16,
|
||||
bf16_supported = False,
|
||||
force_float32 = False,
|
||||
full_finetuning = False,
|
||||
mixed_precision = "float32",
|
||||
fp16 = False,
|
||||
bf16 = False,
|
||||
)
|
||||
assert bf16 is False
|
||||
|
||||
|
||||
def test_bf16_gpu_unchanged_auto_branch():
|
||||
# Regression guard: on a bf16 GPU, a float32 model with unset precision
|
||||
# still selects bf16 autocast (behavior must not change for bf16 hardware).
|
||||
fp16, bf16, amp, raised = _decide(
|
||||
torch.float32,
|
||||
bf16_supported = True,
|
||||
force_float32 = False,
|
||||
full_finetuning = True,
|
||||
mixed_precision = "float32",
|
||||
fp16 = False,
|
||||
bf16 = False,
|
||||
)
|
||||
assert raised is None
|
||||
assert (fp16, bf16) == (False, True)
|
||||
|
||||
|
||||
def test_genuine_bf16_model_with_fp16_still_raises():
|
||||
# A real bfloat16 model on bf16 HW with fp16 requested is a genuine mismatch.
|
||||
_, _, _, raised = _decide(
|
||||
torch.bfloat16,
|
||||
bf16_supported = True,
|
||||
force_float32 = False,
|
||||
full_finetuning = False,
|
||||
mixed_precision = "float32",
|
||||
fp16 = True,
|
||||
bf16 = False,
|
||||
)
|
||||
assert raised == "TypeError"
|
||||
Loading…
Add table
Add a link
Reference in a new issue