* Pin the newer-mapper FP8 probe with tests that can fail The two identity assertions added in #7478 compare the returned FP8 tables against the installed ones, but the fixture serves the same mapper.py as both the installed and the fetched source and exec always allocates fresh dicts, so they pin allocation rather than provenance and hold for any new dict. Replace them with two tests that drive get_model_name end to end: one splices an FP8 entry into the fetched source only and asserts the upgrade error still fires, the other serves a mapper.py with no FP8 tables and asserts the 4bit half of the probe survives, which is the regression #7497 fixed. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Tighten the resolver stub for PR #7516 - Restore the fp8_block/fp8_row identity assert alongside the new provenance test. It is weak, not vacuous: it still catches a probe that hands back the installed table objects, and it costs nothing to keep. - Bind Version and transformers_version in the stub namespace. Both are unreached under the current gates, so a change to either would fail with a bare NameError instead of the assertion. Merged main, which clears the unrelated test_runtime_text_encoding failure the branch inherited from its base. * Cover the FP8 row-scaling path instead of duplicating the block one The two tests this PR originally added were already covered by tests/test_new_mapper_fetched_fp8.py from #7497. An 8-mutant matrix over loader_utils.py found nothing they caught that the existing file did not, so they are dropped and test_new_mapper_no_global_leak.py goes back to main. Two real gaps were open, both on the row branch that load_in_fp8 = True plus UNSLOTH_HAS_FBGEMM selects ahead of block: - the FBGEMM row branch in __get_model_name could be deleted outright with every test still green - _resolve_with_mappers could ignore its fp8_row argument and silently fall back to the installed row table Adds two tests to the existing file, reusing its _load_resolver rather than a second harness. The row-only fixture splices into the fetched row table alone, since an entry the block table also knows lets the block branch answer and masks the regression. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: danielhanchen <danielhanchen@gmail.com>
208 lines
8.5 KiB
Python
208 lines
8.5 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
|
|
|
"""Regression tests for what ``_get_new_mapper`` hands back to the upgrade probe.
|
|
|
|
``test_new_mapper_no_global_leak.py`` serves the repo's own ``mapper.py`` as both installed
|
|
and fetched source, so it cannot tell a fetched table from a fresh copy of the installed one.
|
|
Two gaps it misses:
|
|
|
|
1. The probe must answer for an fp8 repo only the FETCHED mapper knows, so an extra ``"8"``
|
|
entry is spliced into the fetched source only. Isolating the exec without returning the
|
|
fetched fp8 tables would silently drop the fp8 half of the upgrade check.
|
|
2. The probe must survive a fetched ``mapper.py`` with no fp8 tables (anything older, or a
|
|
future rename): reading them with ``[]`` raises ``KeyError`` into the bare ``except``,
|
|
taking the 4bit half, the probe's whole purpose, down with it.
|
|
|
|
Both of the above only reach the block table. The last two tests take the row branch, which
|
|
``load_in_fp8 = True`` plus ``UNSLOTH_HAS_FBGEMM`` selects ahead of block: deleting that branch,
|
|
or dropping ``_resolve_with_mappers``' ``fp8_row`` argument so it falls back to the installed
|
|
table, both leave every other test here green.
|
|
|
|
``loader_utils`` imports torch, so ast-extract the resolvers and run them against a stubbed
|
|
``requests``, as in ``tests/test_bad_mappings_redirect.py``.
|
|
"""
|
|
|
|
import ast
|
|
import os
|
|
import sys
|
|
import types
|
|
|
|
_MODELS = os.path.join(os.path.dirname(__file__), os.pardir, "unsloth", "models")
|
|
|
|
_WANTED = {"__get_model_name", "_resolve_with_mappers", "_get_new_mapper", "get_model_name"}
|
|
|
|
# An fp8 ("8") model, spliced into the FETCHED mapper only.
|
|
_NEW_KEY = "unsloth/Zeta-9B-Only-On-Main"
|
|
_NEW_OFFICIAL = "zeta-org/Zeta-9B-Only-On-Main-FP8"
|
|
_NEW_BLOCK = "unsloth/Zeta-9B-Only-On-Main-FP8-Block"
|
|
_NEW_ROW = "unsloth/Zeta-9B-Only-On-Main-FP8-Row"
|
|
_ANCHOR = ' "unsloth/Kimi-K2-Instruct-BF16" : ('
|
|
# Row table only, so the block branch cannot answer for it and mask a row-path regression.
|
|
_ROW_ONLY = "zeta-org/Zeta-9B-Row-Only-FP8"
|
|
|
|
|
|
def _mapper_source():
|
|
with open(os.path.join(_MODELS, "mapper.py"), encoding = "utf-8") as f:
|
|
return f.read()
|
|
|
|
|
|
def _with_extra_fp8_model(source):
|
|
assert _ANCHOR in source, "anchor moved; update this test"
|
|
entry = (
|
|
f' "{_NEW_KEY}" : {{\n'
|
|
f' "16" : ("{_NEW_KEY}", "zeta-org/Zeta-9B-Only-On-Main"),\n'
|
|
f' "8" : ("{_NEW_OFFICIAL}", "{_NEW_BLOCK}", "{_NEW_ROW}"),\n'
|
|
f" }},\n"
|
|
)
|
|
return source.replace(_ANCHOR, entry + _ANCHOR, 1)
|
|
|
|
|
|
def _with_row_only_fp8_model(source):
|
|
"""Fetched row table only. Block must not know it, or the block branch answers instead."""
|
|
return source + f'\nFLOAT_TO_FP8_ROW_MAPPER["{_ROW_ONLY.lower()}"] = "{_NEW_ROW}"\n'
|
|
|
|
|
|
def _without_fp8_tables(source):
|
|
"""A mapper.py from before the fp8 tables existed."""
|
|
return source.replace("FLOAT_TO_FP8_BLOCK_MAPPER", "SOME_OTHER_BLOCK_TABLE").replace(
|
|
"FLOAT_TO_FP8_ROW_MAPPER", "SOME_OTHER_ROW_TABLE"
|
|
)
|
|
|
|
|
|
class _FakeResponse:
|
|
def __init__(self, text):
|
|
self.text = text
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *exc):
|
|
return False
|
|
|
|
|
|
def _install_fake_requests(monkeypatch, text):
|
|
module = types.ModuleType("requests")
|
|
module.get = lambda url, timeout = None: _FakeResponse(text)
|
|
monkeypatch.setitem(sys.modules, "requests", module)
|
|
|
|
|
|
def _install_fake_vllm_absent(monkeypatch, namespace):
|
|
"""vllm >= 0.12.0 returns early from __get_model_name, leaving the probe unreachable."""
|
|
monkeypatch.delitem(sys.modules, "vllm", raising = False)
|
|
fake = types.ModuleType("importlib")
|
|
fake.util = types.SimpleNamespace(find_spec = lambda name: None)
|
|
namespace["importlib"] = fake
|
|
|
|
|
|
def _load_resolver(installed_source):
|
|
"""Stand-in for loader_utils' module globals, built from `installed_source`."""
|
|
from unsloth_zoo.utils import Version
|
|
|
|
mapper_ns = {}
|
|
exec(compile(installed_source, "mapper.py", "exec"), mapper_ns)
|
|
|
|
namespace = {
|
|
"INT_TO_FLOAT_MAPPER": mapper_ns["INT_TO_FLOAT_MAPPER"],
|
|
"FLOAT_TO_INT_MAPPER": mapper_ns["FLOAT_TO_INT_MAPPER"],
|
|
"MAP_TO_UNSLOTH_16bit": mapper_ns["MAP_TO_UNSLOTH_16bit"],
|
|
"FLOAT_TO_FP8_BLOCK_MAPPER": mapper_ns["FLOAT_TO_FP8_BLOCK_MAPPER"],
|
|
"FLOAT_TO_FP8_ROW_MAPPER": mapper_ns["FLOAT_TO_FP8_ROW_MAPPER"],
|
|
"SUPPORTS_FOURBIT": True,
|
|
"transformers_version": Version("4.57.6"),
|
|
"Version": Version,
|
|
"os": os,
|
|
}
|
|
with open(os.path.join(_MODELS, "loader_utils.py"), encoding = "utf-8") as f:
|
|
tree = ast.parse(f.read())
|
|
for node in tree.body:
|
|
if isinstance(node, ast.Assign) and any(
|
|
getattr(t, "id", None) in ("BAD_MAPPINGS", "_OFFLINE_ENV_VALUES", "_OFFLINE_ENV_KEYS")
|
|
for t in node.targets
|
|
):
|
|
exec(compile(ast.Module([node], []), "<assign>", "exec"), namespace)
|
|
elif isinstance(node, ast.FunctionDef) and (
|
|
node.name in _WANTED or node.name == "_env_says_offline"
|
|
):
|
|
exec(compile(ast.Module([node], []), node.name, "exec"), namespace)
|
|
return namespace
|
|
|
|
|
|
def test_probe_answers_for_an_fp8_repo_only_the_fetched_mapper_knows(monkeypatch):
|
|
installed = _mapper_source()
|
|
namespace = _load_resolver(installed)
|
|
installed_block = namespace["FLOAT_TO_FP8_BLOCK_MAPPER"]
|
|
installed_row = namespace["FLOAT_TO_FP8_ROW_MAPPER"]
|
|
assert _NEW_OFFICIAL.lower() not in installed_block, "the installed table must not know it"
|
|
|
|
_install_fake_requests(monkeypatch, _with_extra_fp8_model(installed))
|
|
_install_fake_vllm_absent(monkeypatch, namespace)
|
|
|
|
try:
|
|
resolved = namespace["get_model_name"](
|
|
_NEW_OFFICIAL, load_in_4bit = False, load_in_fp8 = "block"
|
|
)
|
|
except NotImplementedError as error:
|
|
assert "not supported in your current Unsloth version" in str(error)
|
|
else:
|
|
raise AssertionError(
|
|
f"a fetched-only fp8 repo must raise the upgrade error, got {resolved!r}"
|
|
)
|
|
|
|
# Answering must not have adopted the fetched tables.
|
|
assert namespace["FLOAT_TO_FP8_BLOCK_MAPPER"] is installed_block
|
|
assert namespace["FLOAT_TO_FP8_ROW_MAPPER"] is installed_row
|
|
assert _NEW_OFFICIAL.lower() not in namespace["FLOAT_TO_FP8_BLOCK_MAPPER"]
|
|
|
|
|
|
def test_probe_survives_a_fetched_mapper_without_the_fp8_tables(monkeypatch):
|
|
installed = _mapper_source()
|
|
namespace = _load_resolver(installed)
|
|
_install_fake_requests(monkeypatch, _without_fp8_tables(installed))
|
|
|
|
int_to_float, float_to_int, map_to_16bit = namespace["_get_new_mapper"]()[:3]
|
|
|
|
assert (
|
|
int_to_float and float_to_int and map_to_16bit
|
|
), "a fetched mapper.py without the fp8 tables must not take the 4bit upgrade check down"
|
|
|
|
|
|
def test_fbgemm_prefers_the_row_table_over_the_block_one(monkeypatch):
|
|
"""With FBGEMM, `load_in_fp8 = True` must resolve row-scaled, not blockwise."""
|
|
monkeypatch.setenv("UNSLOTH_HAS_FBGEMM", "1")
|
|
namespace = _load_resolver(_mapper_source())
|
|
row = namespace["FLOAT_TO_FP8_ROW_MAPPER"]
|
|
block = namespace["FLOAT_TO_FP8_BLOCK_MAPPER"]
|
|
|
|
key = next(k for k in row if k in block and row[k] != block[k])
|
|
resolved = namespace["get_model_name"](key, load_in_4bit = False, load_in_fp8 = True)
|
|
|
|
assert resolved == row[key], (
|
|
f"FBGEMM must take the row branch for {key!r}, got {resolved!r} "
|
|
f"(the blockwise answer is {block[key]!r})"
|
|
)
|
|
|
|
|
|
def test_probe_answers_for_a_row_only_repo_the_fetched_mapper_knows(monkeypatch):
|
|
"""The row half of the probe needs the FETCHED row table, same as the block half."""
|
|
monkeypatch.setenv("UNSLOTH_HAS_FBGEMM", "1")
|
|
installed = _mapper_source()
|
|
namespace = _load_resolver(installed)
|
|
installed_row = namespace["FLOAT_TO_FP8_ROW_MAPPER"]
|
|
key = _ROW_ONLY.lower()
|
|
assert key not in installed_row, "the installed row table must not know it"
|
|
assert key not in namespace["FLOAT_TO_FP8_BLOCK_MAPPER"], "no block entry, or block answers"
|
|
|
|
_install_fake_requests(monkeypatch, _with_row_only_fp8_model(installed))
|
|
_install_fake_vllm_absent(monkeypatch, namespace)
|
|
|
|
try:
|
|
resolved = namespace["get_model_name"](_ROW_ONLY, load_in_4bit = False, load_in_fp8 = True)
|
|
except NotImplementedError as error:
|
|
assert "not supported in your current Unsloth version" in str(error)
|
|
else:
|
|
raise AssertionError(
|
|
f"a fetched-only row-scaled repo must raise the upgrade error, got {resolved!r}"
|
|
)
|
|
|
|
assert namespace["FLOAT_TO_FP8_ROW_MAPPER"] is installed_row
|