Files
jundot--omlx/tests/test_deepseek_v4_patch.py
wehub-resource-sync e9a2f726c9
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:29:51 +08:00

1356 lines
51 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPDX-License-Identifier: Apache-2.0
"""Tests for the DeepSeek V4 monkey-patch (PR 1192 port)."""
import importlib
import inspect
import sys
from types import SimpleNamespace
import pytest
@pytest.fixture(scope="module")
def applied_patch():
"""Apply the patch once for the whole module. The patch itself is
idempotent so repeated calls are safe."""
from omlx.patches.deepseek_v4 import apply_deepseek_v4_patch
apply_deepseek_v4_patch()
return True
class TestPatchOrchestration:
"""Top-level apply / idempotency / module registration checks."""
def test_apply_returns_true_first_time(self):
from omlx.patches.deepseek_v4 import apply_deepseek_v4_patch, is_applied
# The patch may have been applied by a previous test run in the
# same process; force-reset is_applied to validate the flow.
# The module-level _APPLIED guard means we cannot un-apply, so
# this test is informational about the *current* state.
if is_applied():
assert apply_deepseek_v4_patch() is False
else:
assert apply_deepseek_v4_patch() is True
assert is_applied() is True
def test_apply_is_idempotent(self, applied_patch):
from omlx.patches.deepseek_v4 import apply_deepseek_v4_patch
# After fixture has applied the patch, a second call must return False.
assert apply_deepseek_v4_patch() is False
def test_hyper_connection_registered(self, applied_patch):
assert "mlx_lm.models.hyper_connection" in sys.modules
def test_deepseek_v4_registered(self, applied_patch):
assert "mlx_lm.models.deepseek_v4" in sys.modules
def test_deepseek_v4_module_package(self, applied_patch):
mod = sys.modules["mlx_lm.models.deepseek_v4"]
# __package__ must be mlx_lm.models so relative imports inside
# the loaded file resolve through the real mlx_lm package.
assert mod.__package__ == "mlx_lm.models"
def test_deepseek_v4_mtp_alias_registered(self, applied_patch):
assert (
sys.modules["mlx_lm.models.deepseek_v4_mtp"]
is sys.modules["mlx_lm.models.deepseek_v4"]
)
class TestCacheInjection:
"""PoolingCache / BatchPoolingCache injected into mlx_lm.models.cache."""
def test_pooling_cache_attribute(self, applied_patch):
import mlx_lm.models.cache as cache_mod
assert hasattr(cache_mod, "PoolingCache")
assert hasattr(cache_mod, "BatchPoolingCache")
def test_pooling_cache_module_attribute(self, applied_patch):
from mlx_lm.models.cache import BatchPoolingCache, PoolingCache
# The injected classes claim to live in mlx_lm.models.cache so
# any introspection (e.g. type(c).__module__) sees the right name.
assert PoolingCache.__module__ == "mlx_lm.models.cache"
assert BatchPoolingCache.__module__ == "mlx_lm.models.cache"
def test_pooling_cache_instantiation(self, applied_patch):
from mlx_lm.models.cache import PoolingCache
cache = PoolingCache(ratio=4)
assert cache.ratio == 4
assert cache.empty()
assert cache.size() == 0
assert cache.offset == 0
class TestUtilsPatch:
"""mlx_lm.utils.load_model + _load_safetensors + SAFETENSORS_DTYPE_FALLBACKS."""
def test_load_model_replaced(self, applied_patch):
import mlx_lm.utils as utils_mod
# The replaced function carries our docstring marker via its
# bound name; just check it's not the upstream one by virtue of
# the new attributes around it.
assert hasattr(utils_mod, "_load_safetensors")
assert hasattr(utils_mod, "SAFETENSORS_DTYPE_FALLBACKS")
def test_dtype_fallback_map(self, applied_patch):
import mlx_lm.utils as utils_mod
assert utils_mod.SAFETENSORS_DTYPE_FALLBACKS == {"F8_E8M0": "U8"}
def test_load_safetensors_passthrough_for_normal_dtype(
self, applied_patch, tmp_path
):
"""A safetensors file with a standard dtype must round-trip
through _load_safetensors unchanged (no header rewrite)."""
import mlx.core as mx
from mlx_lm.utils import _load_safetensors
path = tmp_path / "model.safetensors"
data = {"x": mx.zeros((4, 4), dtype=mx.float32)}
mx.save_safetensors(str(path), data)
loaded = _load_safetensors(str(path))
assert "x" in loaded
assert loaded["x"].shape == (4, 4)
class TestGeneratePatch:
"""mlx_lm.generate._make_cache replaced."""
def test_make_cache_replaced(self, applied_patch):
gen_mod = importlib.import_module("mlx_lm.generate")
assert hasattr(gen_mod, "_make_cache")
# Source must include PoolingCache → BatchPoolingCache branch.
# We can't easily compare functions, so just verify the new
# behavior: passing a model with a PoolingCache in make_cache
# produces a BatchPoolingCache.
from mlx_lm.models.cache import BatchPoolingCache, PoolingCache
class FakeModel:
def __init__(self):
self.layers = [None]
def make_cache(self):
return [PoolingCache(ratio=4)]
result = gen_mod._make_cache(FakeModel(), [0], None)
assert len(result) == 1
assert isinstance(result[0], BatchPoolingCache)
class TestTokenizerPatch:
"""mlx_lm.tokenizer_utils.AutoTokenizer wrapped with deepseek_v4 fallback."""
def test_autotokenizer_wrapped(self, applied_patch):
import mlx_lm.tokenizer_utils as tu
# Wrapped class still exposes from_pretrained.
assert hasattr(tu.AutoTokenizer, "from_pretrained")
# Class name preserved for any introspection.
assert tu.AutoTokenizer.__name__ == "AutoTokenizer"
def test_passthrough_on_success(self, applied_patch):
"""When upstream AutoTokenizer.from_pretrained succeeds, the wrapper
must return its result unmodified — no fallback path taken."""
from unittest.mock import patch as mock_patch
from omlx.patches.deepseek_v4 import tokenizer_patch
sentinel = object()
class _FakeUpstream:
calls = []
@staticmethod
def from_pretrained(model_path, *args, **kwargs):
_FakeUpstream.calls.append((model_path, args, kwargs))
return sentinel
with mock_patch("transformers.AutoTokenizer", _FakeUpstream):
wrapper = tokenizer_patch._build_wrapper()
result = wrapper.from_pretrained("/fake/path", trust_remote_code=True)
assert result is sentinel
assert len(_FakeUpstream.calls) == 1
# Fallback never injected its own config kwarg.
assert "config" not in _FakeUpstream.calls[0][2]
def test_fallback_on_max_position_embeddings_error(self, applied_patch):
"""The exact AttributeError that transformers raises when it cannot
recognize deepseek_v4 must trigger a retry with PreTrainedConfig()."""
import pytest as _pytest
from unittest.mock import patch as mock_patch
from omlx.patches.deepseek_v4 import tokenizer_patch
class _FakeUpstream:
calls = []
@staticmethod
def from_pretrained(model_path, *args, **kwargs):
_FakeUpstream.calls.append((model_path, args, kwargs))
if "config" in kwargs:
return "FALLBACK_OK"
raise AttributeError(
"'PreTrainedConfig' object has no attribute "
"'max_position_embeddings'"
)
with mock_patch("transformers.AutoTokenizer", _FakeUpstream):
wrapper = tokenizer_patch._build_wrapper()
with _pytest.warns(
RuntimeWarning, match="Falling back to generic tokenizer config"
):
result = wrapper.from_pretrained("/fake/path")
assert result == "FALLBACK_OK"
assert len(_FakeUpstream.calls) == 2
# Second call must inject config=PreTrainedConfig().
assert "config" in _FakeUpstream.calls[1][2]
def test_fallback_on_deepseek_v4_value_error(self, applied_patch):
"""ValueError mentioning deepseek_v4 also triggers fallback."""
import pytest as _pytest
from unittest.mock import patch as mock_patch
from omlx.patches.deepseek_v4 import tokenizer_patch
class _FakeUpstream:
calls = []
@staticmethod
def from_pretrained(model_path, *args, **kwargs):
_FakeUpstream.calls.append((model_path, args, kwargs))
if "config" in kwargs:
return "FALLBACK_OK"
raise ValueError("Unrecognized configuration class for deepseek_v4")
with mock_patch("transformers.AutoTokenizer", _FakeUpstream):
wrapper = tokenizer_patch._build_wrapper()
with _pytest.warns(
RuntimeWarning, match="Falling back to generic tokenizer config"
):
result = wrapper.from_pretrained("/fake/path")
assert result == "FALLBACK_OK"
assert len(_FakeUpstream.calls) == 2
def test_unrelated_error_reraises(self, applied_patch):
"""Errors outside the deepseek_v4 / max_position_embeddings signature
must NOT be swallowed."""
from unittest.mock import patch as mock_patch
import pytest as _pytest
from omlx.patches.deepseek_v4 import tokenizer_patch
class _FakeUpstream:
@staticmethod
def from_pretrained(*args, **kwargs):
raise ValueError("totally unrelated error")
with mock_patch("transformers.AutoTokenizer", _FakeUpstream):
wrapper = tokenizer_patch._build_wrapper()
with _pytest.raises(ValueError, match="totally unrelated"):
wrapper.from_pretrained("/fake/path")
def test_explicit_config_skips_fallback(self, applied_patch):
"""If the caller already passed config=, we must not override it
even when the inner call raises a matching error."""
from unittest.mock import patch as mock_patch
import pytest as _pytest
from omlx.patches.deepseek_v4 import tokenizer_patch
class _FakeUpstream:
@staticmethod
def from_pretrained(*args, **kwargs):
# Caller-provided config is in kwargs; we still raise the
# max_position_embeddings error to verify the wrapper does
# not silently retry.
raise AttributeError(
"'PreTrainedConfig' object has no attribute "
"'max_position_embeddings'"
)
with mock_patch("transformers.AutoTokenizer", _FakeUpstream):
wrapper = tokenizer_patch._build_wrapper()
with _pytest.raises(AttributeError, match="max_position_embeddings"):
wrapper.from_pretrained("/fake/path", config="caller_supplied")
def test_class_attribute_forwarding(self, applied_patch):
"""Class-level attribute access (e.g. AutoTokenizer.register) must
forward to the upstream class so mlx-lm's NewlineTokenizer
registration still works."""
import mlx_lm.tokenizer_utils as tu
from transformers import AutoTokenizer as upstream_at
# register is an upstream classmethod — wrapped class must expose it.
assert tu.AutoTokenizer.register is upstream_at.register
class TestDSMLToolParser:
"""tool_parser_v4 — DSML invoke / parameter grammar parsing."""
def test_single_invoke_typed_args(self, applied_patch):
from omlx.patches.deepseek_v4 import tool_parser_v4 as tp
text = (
'<DSMLinvoke name="get_weather">\n'
'<DSMLparameter name="city" string="true">Seoul</DSMLparameter>\n'
'<DSMLparameter name="days" string="false">7</DSMLparameter>\n'
'<DSMLparameter name="imperial" string="false">false</DSMLparameter>\n'
"</DSMLinvoke>"
)
result = tp.parse_tool_call(text)
assert result["name"] == "get_weather"
assert result["arguments"] == {
"city": "Seoul",
"days": 7,
"imperial": False,
}
def test_multiple_invokes_returns_list(self, applied_patch):
from omlx.patches.deepseek_v4 import tool_parser_v4 as tp
text = (
'<DSMLinvoke name="a">'
'<DSMLparameter name="x" string="false">1</DSMLparameter>'
"</DSMLinvoke>\n"
'<DSMLinvoke name="b">'
'<DSMLparameter name="y" string="true">hello</DSMLparameter>'
"</DSMLinvoke>"
)
result = tp.parse_tool_call(text)
assert isinstance(result, list)
assert len(result) == 2
assert result[0] == {"name": "a", "arguments": {"x": 1}}
assert result[1] == {"name": "b", "arguments": {"y": "hello"}}
def test_object_and_array_parameters(self, applied_patch):
from omlx.patches.deepseek_v4 import tool_parser_v4 as tp
text = (
'<DSMLinvoke name="search">\n'
'<DSMLparameter name="filters" string="false">'
'{"category": "books", "min_price": 10}'
"</DSMLparameter>\n"
'<DSMLparameter name="ids" string="false">[1, 2, 3]</DSMLparameter>\n'
"</DSMLinvoke>"
)
result = tp.parse_tool_call(text)
assert result["arguments"]["filters"] == {"category": "books", "min_price": 10}
assert result["arguments"]["ids"] == [1, 2, 3]
def test_no_invoke_raises(self, applied_patch):
import pytest as _pytest
from omlx.patches.deepseek_v4 import tool_parser_v4 as tp
with _pytest.raises(ValueError, match="No.*invoke.*block"):
tp.parse_tool_call("just some plain text without DSML markup")
def test_outer_markers_exposed(self, applied_patch):
from omlx.patches.deepseek_v4 import tool_parser_v4 as tp
# mlx-lm reads these as module attributes for stream detection.
assert tp.tool_call_start == "<DSMLtool_calls>"
assert tp.tool_call_end == "</DSMLtool_calls>"
class TestChatTemplateV4:
"""chat_template_v4 — DSML system prompt + tool_calls render."""
def test_outer_marker_uses_tool_calls_not_function_calls(self, applied_patch):
from omlx.patches.deepseek_v4 import chat_template_v4 as ct
# vllm's DeepSeekV4ToolParser overrides only the outer marker
# name (tool_calls vs V3.2's function_calls). Verify our copy
# made that one edit.
assert "function_calls" not in ct.tool_calls_template
assert "tool_calls" in ct.tool_calls_template
assert "function_calls" not in ct.TOOLS_SYSTEM_TEMPLATE
def test_inner_grammar_unchanged_from_v32(self, applied_patch):
from omlx.patches.deepseek_v4 import chat_template_v4 as ct
# Inner markers must still be invoke / parameter — V4 reuses V3.2's
# invoke/parameter grammar.
assert "invoke" in ct.tool_call_template
assert "parameter" in ct.encode_arguments_to_dsml(
{"name": "x", "arguments": '{"k": "v"}'}
)
def test_round_trip_encode_then_parse(self, applied_patch):
from omlx.patches.deepseek_v4 import chat_template_v4 as ct
from omlx.patches.deepseek_v4 import tool_parser_v4 as tp
encoded_args = ct.encode_arguments_to_dsml(
{"name": "f", "arguments": '{"a": 1, "b": "hi", "c": [1, 2]}'}
)
invoke = ct.tool_call_template.format(
dsml_token=ct.dsml_token, name="f", arguments=encoded_args
)
block = ct.tool_calls_template.format(
dsml_token=ct.dsml_token, tool_calls=invoke
)
# Strip the outer markers as TokenizerWrapper would.
inner = (
block.replace(tp.tool_call_start, "").replace(tp.tool_call_end, "").strip()
)
parsed = tp.parse_tool_call(inner)
assert parsed == {"name": "f", "arguments": {"a": 1, "b": "hi", "c": [1, 2]}}
def test_user_only_request_with_tools_injects_dsml(self, applied_patch):
"""User-only message + tools must still emit the DSML tools block.
Regression guard for the case where a Claude Code or OpenAI client
passes ``tools`` without a system message. ``render_message`` only
injects tools on system / developer roles, so ``encode_messages``
synthesises an empty system message up front when the first
message is a plain user. Without this fix the rendered prompt
omits the ``<functions>`` schema entirely and the model never
emits a tool_calls block.
"""
from omlx.patches.deepseek_v4 import chat_template_v4 as ct
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the weather",
"parameters": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
},
},
}
]
prompt = ct.apply_chat_template(
[{"role": "user", "content": "Weather in Seoul?"}],
tools=tools,
add_generation_prompt=True,
)
assert "<functions>" in prompt
assert "get_weather" in prompt
assert ct.dsml_token in prompt
def test_system_user_request_with_tools_unchanged(self, applied_patch):
"""When a system message is already present, the synthetic prepend
path must not fire — the rendered prompt keeps the original system
content verbatim and only injects the tools schema once.
"""
from omlx.patches.deepseek_v4 import chat_template_v4 as ct
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the weather",
"parameters": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
},
},
}
]
prompt = ct.apply_chat_template(
[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Weather in Seoul?"},
],
tools=tools,
add_generation_prompt=True,
)
assert "You are a helpful assistant." in prompt
# Only one tools block — no double-injection from synthetic prepend.
assert prompt.count("<functions>") == 1
def test_user_only_no_tools_no_prepend(self, applied_patch):
"""No tools → no synthetic system. Plain user-only request renders
with just the BOS + user wrapper, matching V3.2 baseline."""
from omlx.patches.deepseek_v4 import chat_template_v4 as ct
prompt = ct.apply_chat_template(
[{"role": "user", "content": "Hi"}],
add_generation_prompt=True,
)
assert "<functions>" not in prompt
assert "## Tools" not in prompt
def test_encode_arguments_accepts_dict(self, applied_patch):
"""Anthropic /v1/messages history stores tool_call arguments as
a dict (anthropic_utils.py decodes the input before saving).
encode_arguments_to_dsml must accept that shape — not just the
OpenAI JSON-string convention — so multi-turn renders don't
raise TypeError when the assistant history is from Claude Code.
"""
from omlx.patches.deepseek_v4 import chat_template_v4 as ct
encoded = ct.encode_arguments_to_dsml(
{"name": "f", "arguments": {"location": "Seoul", "n": 3}}
)
assert 'name="location"' in encoded and "Seoul" in encoded
assert 'name="n"' in encoded and ">3<" in encoded
# string="true" for string params, "false" for non-string.
assert 'string="true"' in encoded
assert 'string="false"' in encoded
def test_assistant_tool_call_dict_arguments_round_trip(self, applied_patch):
"""End-to-end multi-turn: assistant message history contains a
tool_use whose arguments came in as dict (Anthropic shape). The
rendered prompt must include the assistant's prior tool_call
block in DSML form so the model can continue the conversation
coherently.
"""
from omlx.patches.deepseek_v4 import chat_template_v4 as ct
messages = [
{"role": "user", "content": "Weather in Seoul?"},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "get_weather",
"arguments": {"location": "Seoul"},
},
}
],
},
{"role": "tool", "content": "sunny, 22C"},
]
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the weather",
"parameters": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
},
},
}
]
prompt = ct.apply_chat_template(
messages, tools=tools, add_generation_prompt=True
)
assert "<DSMLtool_calls>" in prompt
assert 'invoke name="get_weather"' in prompt
assert "Seoul" in prompt
assert "sunny, 22C" in prompt
class TestChatTemplateModuleRegistration:
"""sys.modules registration so mlx-lm's importlib path picks up our types."""
def test_chat_template_module_registered(self, applied_patch):
import sys
assert "mlx_lm.chat_templates.deepseek_v4" in sys.modules
mod = sys.modules["mlx_lm.chat_templates.deepseek_v4"]
assert hasattr(mod, "apply_chat_template")
def test_tool_parser_module_registered(self, applied_patch):
import sys
assert "mlx_lm.tool_parsers.deepseek_v4" in sys.modules
mod = sys.modules["mlx_lm.tool_parsers.deepseek_v4"]
assert hasattr(mod, "parse_tool_call")
assert mod.tool_call_start == "<DSMLtool_calls>"
assert mod.tool_call_end == "</DSMLtool_calls>"
class TestModelClassResolution:
"""mlx_lm.utils._get_classes resolves deepseek_v4 to our injected classes."""
def test_get_classes_returns_injected_module(self, applied_patch):
from mlx_lm.utils import _get_classes
model_class, args_class = _get_classes({"model_type": "deepseek_v4"})
assert model_class.__module__ == "mlx_lm.models.deepseek_v4"
assert args_class.__module__ == "mlx_lm.models.deepseek_v4"
assert model_class.__name__ == "Model"
assert args_class.__name__ == "ModelArgs"
def test_get_classes_returns_injected_module_for_mtp_variant(self, applied_patch):
from mlx_lm.utils import _get_classes
model_class, args_class = _get_classes({"model_type": "deepseek_v4_mtp"})
assert model_class.__module__ == "mlx_lm.models.deepseek_v4"
assert args_class.__module__ == "mlx_lm.models.deepseek_v4"
class TestPatchedLoadModelTrustRemoteCode:
"""DeepSeek's patched load_model must mirror mlx-lm's custom-code gate."""
def test_signature_accepts_trust_remote_code(self, applied_patch):
from mlx_lm.utils import load_model
assert "trust_remote_code" in inspect.signature(load_model).parameters
def test_model_file_requires_trust_remote_code(self, tmp_path, applied_patch):
config_path = tmp_path / "config.json"
config_path.write_text(
'{"model_type": "custom", "model_file": "custom_arch.py"}'
)
(tmp_path / "custom_arch.py").write_text(
"\n".join(
[
"from pathlib import Path",
"import mlx.nn as nn",
"Path(__file__).with_name('executed.txt').write_text('yes')",
"",
"class ModelArgs:",
" @classmethod",
" def from_dict(cls, config):",
" return cls()",
"",
"class Model(nn.Module):",
" def __init__(self, args):",
" super().__init__()",
]
)
)
from mlx_lm.utils import load_model
with pytest.raises(ValueError, match="trust_remote_code=True"):
load_model(tmp_path, strict=False, lazy=True)
assert not (tmp_path / "executed.txt").exists()
load_model(
tmp_path,
strict=False,
lazy=True,
trust_remote_code=True,
)
assert (tmp_path / "executed.txt").read_text() == "yes"
class TestCacheHandlerRegistration:
"""omlx CacheTypeRegistry resolves the new cache types to their handlers."""
def test_pooling_cache_resolves_to_handler(self, applied_patch):
from omlx.cache.type_registry import CacheTypeRegistry
handler = CacheTypeRegistry.get_handler_by_class_name("PoolingCache")
assert type(handler).__name__ == "PoolingCacheHandler"
def test_batch_pooling_cache_resolves_to_handler(self, applied_patch):
from omlx.cache.type_registry import CacheTypeRegistry
handler = CacheTypeRegistry.get_handler_by_class_name("BatchPoolingCache")
assert type(handler).__name__ == "BatchPoolingCacheHandler"
def test_pooling_cache_not_block_sliceable(self, applied_patch):
from omlx.cache.type_registry import CacheTypeRegistry
handler = CacheTypeRegistry.get_handler_by_class_name("PoolingCache")
assert handler.supports_block_slicing is False
def test_batch_pooling_cache_not_block_sliceable(self, applied_patch):
from omlx.cache.type_registry import CacheTypeRegistry
handler = CacheTypeRegistry.get_handler_by_class_name("BatchPoolingCache")
assert handler.supports_block_slicing is False
def test_detect_cache_type_pooling(self, applied_patch):
from mlx_lm.models.cache import PoolingCache
from omlx.cache.type_handlers import CacheType
from omlx.cache.type_registry import CacheTypeRegistry
cache = PoolingCache(ratio=4)
assert CacheTypeRegistry.detect_cache_type(cache) == CacheType.POOLING_CACHE
class TestPoolingCacheStateRoundTrip:
"""Handler extract_state → reconstruct_cache must preserve the pool tensor."""
def test_round_trip_with_pooled_tensor(self, applied_patch):
import mlx.core as mx
from mlx_lm.models.cache import PoolingCache
from omlx.cache.type_registry import CacheTypeRegistry
# Build a PoolingCache with a known pool.
ratio = 4
cache = PoolingCache(ratio=ratio)
# Simulate update_and_fetch having stuffed the pool with 8
# compressed tokens of dim 32.
pooled = mx.arange(1 * 8 * 32, dtype=mx.float32).reshape(1, 8, 32)
cache.pooled = pooled
handler = CacheTypeRegistry.get_handler_by_class_name("PoolingCache")
state = handler.extract_state(cache)
assert state["pooled"] is not None
assert state["pooled"].shape == (1, 8, 32)
restored = handler.reconstruct_cache(state, meta_state=ratio)
assert restored is not None
assert restored.ratio == ratio
assert restored.pooled.shape == (1, 8, 32)
# Verify content matches.
diff = mx.max(mx.abs(restored.pooled - pooled)).item()
assert diff == 0.0
def test_round_trip_empty_cache(self, applied_patch):
from mlx_lm.models.cache import PoolingCache
from omlx.cache.type_registry import CacheTypeRegistry
cache = PoolingCache(ratio=8)
handler = CacheTypeRegistry.get_handler_by_class_name("PoolingCache")
state = handler.extract_state(cache)
assert state["pooled"] is None
assert state["buf_kv"] is None
restored = handler.reconstruct_cache(state, meta_state=8)
assert restored is not None
assert restored.empty()
assert restored.ratio == 8
def test_seq_len_from_state(self, applied_patch):
import mlx.core as mx
from mlx_lm.models.cache import PoolingCache
from omlx.cache.type_registry import CacheTypeRegistry
cache = PoolingCache(ratio=4)
cache.pooled = mx.zeros((1, 12, 16), dtype=mx.float32)
handler = CacheTypeRegistry.get_handler_by_class_name("PoolingCache")
state = handler.extract_state(cache)
assert handler.get_seq_len(state) == 12
class TestCacheMaterialization:
"""DeepSeek-V4 cache arrays are materialized after forward updates."""
def test_helper_collects_plain_and_cachelist_leaf_arrays(
self, applied_patch, monkeypatch
):
import mlx.core as mx
from mlx_lm.models.cache import CacheList
dsv4 = sys.modules["mlx_lm.models.deepseek_v4"]
class Leaf:
def __init__(self, arr):
self.arr = arr
self.none_value = None
self.scalar = 7
leaf_a = Leaf(mx.array([1], dtype=mx.int32))
leaf_b = Leaf(mx.array([2], dtype=mx.int32))
leaf_c = Leaf(mx.array([3], dtype=mx.int32))
calls = []
def fake_eval(*arrays):
calls.append(arrays)
monkeypatch.setattr(dsv4.mx, "eval", fake_eval)
dsv4._materialize_cache_arrays([CacheList(leaf_a, leaf_b), leaf_c, None])
assert len(calls) == 1
assert calls[0] == (leaf_a.arr, leaf_b.arr, leaf_c.arr)
def test_model_call_materializes_cache_after_layer_loop(self, applied_patch):
dsv4 = sys.modules["mlx_lm.models.deepseek_v4"]
source = inspect.getsource(dsv4.DeepseekV4Model.__call__)
loop_pos = source.index("for layer, layer_cache in zip")
materialize_pos = source.index("_materialize_cache_arrays(cache)")
pipeline_send_pos = source.index("if pipeline_rank != 0")
assert loop_pos < materialize_pos < pipeline_send_pos
class TestDeepseekV4SwitchGLU:
"""DeepSeek-V4 SwitchGLU execution guards."""
def test_shared_expert_uses_configured_swiglu_limit(self, applied_patch):
dsv4 = sys.modules["mlx_lm.models.deepseek_v4"]
config = dsv4.ModelArgs(
vocab_size=16,
hidden_size=8,
intermediate_size=16,
moe_intermediate_size=4,
num_hidden_layers=1,
num_attention_heads=2,
num_key_value_heads=1,
n_shared_experts=1,
n_routed_experts=2,
num_experts_per_tok=1,
num_hash_layers=0,
q_lora_rank=0,
qk_rope_head_dim=4,
head_dim=4,
o_lora_rank=0,
index_n_heads=2,
index_head_dim=4,
index_topk=2,
swiglu_limit=10.0,
)
moe = dsv4.DeepseekV4MoE(config, layer_idx=0)
assert moe.switch_mlp.activation.limit == config.swiglu_limit
assert moe.shared_experts.swiglu_limit == config.swiglu_limit
def test_skips_fused_weighted_sum_for_cache_stability(
self, applied_patch, monkeypatch
):
mx = pytest.importorskip("mlx.core")
from omlx.patches.deepseek_v4 import switch_layers
monkeypatch.setattr(
switch_layers.glm_fast,
"has_symbol",
lambda name: name == "glm_moe_weighted_sum",
)
def fail_weighted_sum(*args, **kwargs):
raise AssertionError("DeepSeek V4 must not use fused weighted sum")
monkeypatch.setattr(
switch_layers.glm_fast,
"glm_moe_weighted_sum",
fail_weighted_sum,
raising=False,
)
mx.random.seed(11)
layer = switch_layers.SwitchGLU(
input_dims=16,
hidden_dims=32,
num_experts=4,
bias=False,
)
x = mx.random.normal((1, 8, 16), dtype=mx.float32)
indices = mx.array(
[
[
[0, 1, 2, 3, 0, 1, 2, 3],
[1, 2, 3, 0, 1, 2, 3, 0],
[2, 3, 0, 1, 2, 3, 0, 1],
[3, 0, 1, 2, 3, 0, 1, 2],
[0, 2, 1, 3, 0, 2, 1, 3],
[1, 3, 2, 0, 1, 3, 2, 0],
[2, 0, 3, 1, 2, 0, 3, 1],
[3, 1, 0, 2, 3, 1, 0, 2],
]
],
dtype=mx.int32,
)
scores = mx.softmax(
mx.random.normal((1, 8, 8), dtype=mx.float32),
axis=-1,
)
y = layer(x, indices, scores=scores)
mx.eval(y)
assert y.shape == (1, 8, 8, 16)
class TestPreLoadDispatch:
"""maybe_apply_pre_load_patches gates correctly on config.json model_type."""
def test_no_dispatch_for_other_model_type(self, tmp_path):
# Create a fake model dir with a non-deepseek config.
config_path = tmp_path / "config.json"
config_path.write_text('{"model_type": "llama"}')
from omlx.utils.model_loading import maybe_apply_pre_load_patches
# Should be a no-op (no exception). We can't easily assert that
# apply_deepseek_v4_patch was NOT called because earlier tests
# may have applied it already. Just verify no crash.
maybe_apply_pre_load_patches(str(tmp_path))
def test_no_dispatch_for_missing_config(self, tmp_path):
# No config.json present.
from omlx.utils.model_loading import maybe_apply_pre_load_patches
maybe_apply_pre_load_patches(str(tmp_path))
def test_dispatch_for_deepseek_v4(self, tmp_path):
config_path = tmp_path / "config.json"
config_path.write_text('{"model_type": "deepseek_v4"}')
from omlx.patches.deepseek_v4 import is_applied
from omlx.utils.model_loading import maybe_apply_pre_load_patches
maybe_apply_pre_load_patches(str(tmp_path))
# Patch must be applied after this dispatch (or already applied).
assert is_applied() is True
def test_dispatch_for_deepseek_v4_mtp_variant(self, tmp_path):
config_path = tmp_path / "config.json"
config_path.write_text('{"model_type": "deepseek_v4_mtp"}')
from omlx.patches.deepseek_v4 import is_applied
from omlx.utils.model_loading import maybe_apply_pre_load_patches
maybe_apply_pre_load_patches(str(tmp_path))
assert is_applied() is True
class TestMakeQuantizationConfigMtp:
"""make_quantization_config must cover the MTP fusion projections.
Without explicit entries, mtp.<i>.e_proj / mtp.<i>.h_proj fall through
to the affine default, whose QuantizedLinear expects a .biases tensor
the fp8 checkpoint doesn't ship, and strict load fails."""
def test_mtp_projections_get_mxfp8(self, applied_patch):
import mlx.nn as nn
dsv4 = sys.modules["mlx_lm.models.deepseek_v4"]
class _MTPStub(nn.Module):
def __init__(self):
super().__init__()
self.e_proj = nn.Linear(8, 8, bias=False)
self.h_proj = nn.Linear(8, 8, bias=False)
class _ModelStub(nn.Module):
def __init__(self):
super().__init__()
self.mtp = [_MTPStub()]
self.lm_head = nn.Linear(8, 8, bias=False)
qcfg = dsv4.make_quantization_config(_ModelStub())
mxfp8 = {"group_size": 32, "bits": 8, "mode": "mxfp8"}
assert qcfg["mtp.0.e_proj"] == mxfp8
assert qcfg["mtp.0.h_proj"] == mxfp8
# Non-MTP paths keep the affine default (no per-path entry).
assert "lm_head" not in qcfg
def test_no_mtp_no_entries(self, applied_patch):
import mlx.nn as nn
dsv4 = sys.modules["mlx_lm.models.deepseek_v4"]
class _ModelStub(nn.Module):
def __init__(self):
super().__init__()
self.lm_head = nn.Linear(8, 8, bias=False)
qcfg = dsv4.make_quantization_config(_ModelStub())
assert not any(k.startswith("mtp.") for k in qcfg)
class TestDeepSeekV4SanitizeAffineSwitchMLP:
"""Sanitize should enable the FP16 affine routed-MoE fast path."""
def test_affine_switch_mlp_scale_bias_cast_to_fp16(self, applied_patch):
mx = pytest.importorskip("mlx.core")
dsv4 = sys.modules["mlx_lm.models.deepseek_v4"]
fake_model = SimpleNamespace(
args=SimpleNamespace(
num_hidden_layers=1,
n_routed_experts=2,
o_groups=1,
o_lora_rank=1,
)
)
weights = {
"model.layers.0.ffn.switch_mlp.up_proj.weight": mx.zeros(
(2, 4, 2), dtype=mx.uint32
),
"model.layers.0.ffn.switch_mlp.up_proj.scales": mx.zeros(
(2, 4, 1), dtype=mx.bfloat16
),
"model.layers.0.ffn.switch_mlp.up_proj.biases": mx.zeros(
(2, 4, 1), dtype=mx.bfloat16
),
"model.layers.0.ffn.switch_mlp.down_proj.weight": mx.zeros(
(2, 4, 2), dtype=mx.uint32
),
"model.layers.0.ffn.switch_mlp.down_proj.scales": mx.zeros(
(2, 4, 1), dtype=mx.bfloat16
),
"model.layers.0.ffn.switch_mlp.down_proj.biases": mx.zeros(
(2, 4, 1), dtype=mx.bfloat16
),
"model.layers.0.ffn.shared_experts.up_proj.scales": mx.zeros(
(4, 1), dtype=mx.bfloat16
),
}
out = dsv4.Model.sanitize(fake_model, dict(weights))
assert out["model.layers.0.ffn.switch_mlp.up_proj.scales"].dtype == mx.float16
assert out["model.layers.0.ffn.switch_mlp.up_proj.biases"].dtype == mx.float16
assert out["model.layers.0.ffn.switch_mlp.down_proj.scales"].dtype == mx.float16
assert out["model.layers.0.ffn.switch_mlp.down_proj.biases"].dtype == mx.float16
assert (
out["model.layers.0.ffn.shared_experts.up_proj.scales"].dtype == mx.bfloat16
)
class TestDeepSeekV4SanitizeHcAliases:
"""Sanitize accepts both upstream HC key spellings for V4 checkpoints."""
@staticmethod
def _fake_model():
return SimpleNamespace(
args=SimpleNamespace(
num_hidden_layers=1,
n_routed_experts=0,
o_groups=1,
o_lora_rank=1,
)
)
def test_dotted_hc_aliases_remap_to_model_modules(self, applied_patch):
mx = pytest.importorskip("mlx.core")
dsv4 = sys.modules["mlx_lm.models.deepseek_v4"]
weights = {
"model.layers.0.hc_attn.base": mx.zeros((1,), dtype=mx.float32),
"model.layers.0.hc_attn.fn": mx.zeros((1, 1), dtype=mx.float32),
"model.layers.0.hc_attn.scale": mx.zeros((3,), dtype=mx.float32),
"model.layers.0.hc_ffn.base": mx.zeros((1,), dtype=mx.float32),
"model.layers.0.hc_ffn.fn": mx.zeros((1, 1), dtype=mx.float32),
"model.layers.0.hc_ffn.scale": mx.zeros((3,), dtype=mx.float32),
}
out = dsv4.Model.sanitize(self._fake_model(), dict(weights))
assert "model.layers.0.attn_hc.base" in out
assert "model.layers.0.attn_hc.fn" in out
assert "model.layers.0.attn_hc.scale" in out
assert "model.layers.0.ffn_hc.base" in out
assert "model.layers.0.ffn_hc.fn" in out
assert "model.layers.0.ffn_hc.scale" in out
assert not any(".hc_attn." in key or ".hc_ffn." in key for key in out)
def test_dotted_hc_alias_does_not_override_canonical_key(self, applied_patch):
mx = pytest.importorskip("mlx.core")
dsv4 = sys.modules["mlx_lm.models.deepseek_v4"]
weights = {
"model.layers.0.hc_attn.base": mx.zeros((1,), dtype=mx.float32),
"model.layers.0.attn_hc.base": mx.zeros((2,), dtype=mx.float32),
}
out = dsv4.Model.sanitize(self._fake_model(), dict(weights))
assert out["model.layers.0.attn_hc.base"].shape == (2,)
assert "model.layers.0.hc_attn.base" not in out
class TestMtpSanitizeWoAReshape:
"""The MTP patch sanitize must reshape mtp.<i>.block.attn.wo_a from the
2D nn.Linear layout to the 3D MultiLinear layout, like the backbone."""
@pytest.fixture()
def patched_sanitize(self, applied_patch):
import omlx.patches.mlx_lm_mtp.deepseek_v4_model as mtp_dsv4
mtp_dsv4.apply()
dsv4 = sys.modules["mlx_lm.models.deepseek_v4"]
return dsv4.Model.sanitize
@staticmethod
def _fake_model(with_mtp=True):
class _Args:
num_hidden_layers = 1
num_nextn_predict_layers = 1
o_groups = 2
o_lora_rank = 4
n_routed_experts = 2
class _Fake:
args = _Args()
fake = _Fake()
if with_mtp:
fake.mtp = [object()]
return fake
def test_mtp_wo_a_2d_reshaped_to_3d(self, patched_sanitize):
import mlx.core as mx
weights = {
"mtp.0.attn.wo_a.weight": mx.zeros((8, 16), dtype=mx.bfloat16),
}
out = patched_sanitize(self._fake_model(), weights)
assert out["mtp.0.block.attn.wo_a.weight"].shape == (2, 4, 16)
def test_mtp_wo_a_3d_unchanged(self, patched_sanitize):
import mlx.core as mx
weights = {
"mtp.0.block.attn.wo_a.weight": mx.zeros((2, 4, 16), dtype=mx.bfloat16),
}
out = patched_sanitize(self._fake_model(), weights)
assert out["mtp.0.block.attn.wo_a.weight"].shape == (2, 4, 16)
def test_mtp_dotted_hc_alias_nested_under_block(self, patched_sanitize):
import mlx.core as mx
weights = {
"mtp.0.hc_attn.base": mx.zeros((1,), dtype=mx.float32),
"mtp.0.hc_ffn.scale": mx.zeros((3,), dtype=mx.float32),
}
out = patched_sanitize(self._fake_model(), weights)
assert "mtp.0.block.attn_hc.base" in out
assert "mtp.0.block.ffn_hc.scale" in out
assert "mtp.0.hc_attn.base" not in out
assert "mtp.0.hc_ffn.scale" not in out
class TestMtpBackboneInterface:
"""The patched DSv4 Model.__call__ must accept the full patched-backbone
interface — batch_generator._call_backbone passes n_confirmed=1 during
MTP verify cycles (crashed with TypeError before the fix)."""
def test_call_accepts_n_confirmed(self, applied_patch):
import omlx.patches.mlx_lm_mtp.deepseek_v4_model as mtp_dsv4
mtp_dsv4.apply()
dsv4 = sys.modules["mlx_lm.models.deepseek_v4"]
sig = inspect.signature(dsv4.Model.__call__)
assert "n_confirmed" in sig.parameters
assert sig.parameters["n_confirmed"].default == 0
assert "return_hidden" in sig.parameters
class TestPoolingCacheTrimRollback:
"""trim(1) must exactly undo the last (draft) token of an MTP verify
update, including the pool-boundary case where the draft completed a
compression window. Equivalence is checked behaviorally: a trimmed
cache must evolve identically to a reference cache that never saw the
rejected token."""
@staticmethod
def _push(cache, tokens, offset):
"""Feed raw per-token rows through the PoolingCache contract,
compressing completed windows with a deterministic stand-in
(mean over the window) like Compressor does."""
import mlx.core as mx
kv = tokens
gate = tokens * 0.5
r_kv, _r_gate, _ = cache.accumulate_windows(kv, gate, offset)
if r_kv.size == 0:
rows = mx.zeros((kv.shape[0], 0, kv.shape[-1]), dtype=kv.dtype)
else:
rows = mx.unflatten(r_kv, 1, (-1, cache.ratio)).mean(axis=2)
return cache.update_and_fetch(rows)
@staticmethod
def _tok(values):
import mlx.core as mx
arr = mx.array(values, dtype=mx.float32)
return mx.broadcast_to(arr[None, :, None], (1, len(values), 8))
def _equivalence(self, cache_cls, prefix, verify, post, applied):
"""Drive cache through prefix + 2-token verify, trim the draft,
push `post`; compare against a reference that never saw the draft."""
import mlx.core as mx
ratio = 4
if cache_cls.__name__ == "BatchPoolingCache":
cache = cache_cls(ratio, [0])
ref = cache_cls(ratio, [0])
else:
cache = cache_cls(ratio)
ref = cache_cls(ratio)
pos = 0
for chunk in prefix:
self._push(cache, self._tok(chunk), pos)
self._push(ref, self._tok(chunk), pos)
pos += len(chunk)
# Verify forward: [confirmed, draft] on cache; confirmed only on ref.
self._push(cache, self._tok(verify), pos)
assert cache.is_trimmable()
assert cache.trim(1) == 1
self._push(ref, self._tok(verify[:1]), pos)
pos += 1
out = self._push(cache, self._tok(post), pos)
ref_out = self._push(ref, self._tok(post), pos)
if out is None or getattr(out, "size", 0) == 0:
assert ref_out is None or getattr(ref_out, "size", 0) == 0
else:
pl = getattr(cache, "_pool_lengths", None)
n = pl[0] if pl is not None else out.shape[1]
ref_n = ref._pool_lengths[0] if pl is not None else ref_out.shape[1]
assert n == ref_n
assert mx.allclose(out[:, :n], ref_out[:, :n]).item()
assert (
cache.remainder
if isinstance(cache.remainder, int)
else list(cache.remainder)
) == (ref.remainder if isinstance(ref.remainder, int) else list(ref.remainder))
def test_easy_case_draft_in_buffer(self, applied_patch):
from mlx_lm.models.cache import PoolingCache
# After verify: remainder = (1 + 2) % 4 = 3 >= 1 -> buffer trim.
self._equivalence(PoolingCache, [[1.0]], [2.0, 3.0], [4.0], applied_patch)
def test_boundary_case_draft_completed_window(self, applied_patch):
from mlx_lm.models.cache import PoolingCache
# remainder before verify = 2; verify adds 2 -> window completes on
# the draft token -> undo log path (drop pooled row, replay
# confirmed into the buffer).
self._equivalence(
PoolingCache, [[1.0, 2.0]], [3.0, 4.0], [5.0, 6.0, 7.0], applied_patch
)
def test_boundary_case_with_existing_pool(self, applied_patch):
from mlx_lm.models.cache import PoolingCache
# One full window already pooled, then the boundary case again.
self._equivalence(
PoolingCache,
[[1.0, 2.0, 3.0, 4.0], [5.0, 6.0]],
[7.0, 8.0],
[9.0, 10.0, 11.0],
applied_patch,
)
def test_batch_easy_case(self, applied_patch):
from mlx_lm.models.cache import BatchPoolingCache
self._equivalence(BatchPoolingCache, [[1.0]], [2.0, 3.0], [4.0], applied_patch)
def test_batch_boundary_case(self, applied_patch):
from mlx_lm.models.cache import BatchPoolingCache
self._equivalence(
BatchPoolingCache,
[[1.0, 2.0, 3.0, 4.0], [5.0, 6.0]],
[7.0, 8.0],
[9.0, 10.0, 11.0],
applied_patch,
)
def test_untrimmable_when_no_undo_after_prompt(self, applied_patch):
"""Prompt-sized updates (L > 8) don't stash an undo log; a trim at
a pool boundary right after one must report not-trimmable instead
of corrupting state. (Updates up to L == 8 keep an undo so depth-k
MTP verify windows can roll back.)"""
from mlx_lm.models.cache import PoolingCache
cache = PoolingCache(4)
self._push(
cache,
self._tok([float(v) for v in range(1, 13)]), # L = 12 > 8
0,
)
assert cache.remainder == 0
assert cache.pooled is not None
assert not cache.is_trimmable()
assert cache.trim(1) == 0
def test_verify_sized_update_keeps_undo(self, applied_patch):
"""MTP verify windows (2 < L <= 8) stash an undo log: a trim right
after one rolls back across the pool boundary instead of failing."""
from mlx_lm.models.cache import PoolingCache
cache = PoolingCache(4)
self._push(cache, self._tok([1.0, 2.0, 3.0, 4.0]), 0)
assert cache.remainder == 0
assert cache.pooled is not None
assert cache.is_trimmable()
assert cache.trim(1) == 1
# The completed window is undone: its 3 surviving tokens are back
# in the remainder buffer and no pooled row remains visible.
assert cache.remainder == 3
assert cache.size() == 0
class TestNaxMoEStockRouting:
"""NAX GPUs route prefill-sized MoE gemms to stock mx.gather_qmm."""
@pytest.fixture(autouse=True)
def _nax_off_by_default(self, monkeypatch):
from omlx.patches.deepseek_v4 import switch_layers as sl
# Pin detection off so the block-kernel tests behave identically on
# M5-family machines; each test overrides what it needs.
monkeypatch.setattr(sl, "is_nax_available", lambda: False)
monkeypatch.setattr(sl, "_NAX_STOCK_MODE", "")
yield
def test_prefers_stock_for_prefill_route_counts_only(self, monkeypatch):
from omlx.patches.deepseek_v4 import switch_layers as sl
monkeypatch.setattr(sl, "is_nax_available", lambda: True)
assert not sl._nax_prefers_stock(8)
assert not sl._nax_prefers_stock(sl._NAX_STOCK_MIN_ROUTES - 1)
assert sl._nax_prefers_stock(sl._NAX_STOCK_MIN_ROUTES)
assert sl._nax_prefers_stock(1 << 20)
def test_no_stock_routing_without_nax(self, monkeypatch):
from omlx.patches.deepseek_v4 import switch_layers as sl
assert not sl._nax_prefers_stock(1 << 20)
def test_env_kill_switch_keeps_block_kernels(self, monkeypatch):
from omlx.patches.deepseek_v4 import switch_layers as sl
monkeypatch.setattr(sl, "is_nax_available", lambda: True)
monkeypatch.setattr(sl, "_NAX_STOCK_MODE", "0")
assert not sl._nax_prefers_stock(1 << 20)
def test_env_force_routes_everything(self, monkeypatch):
from omlx.patches.deepseek_v4 import switch_layers as sl
monkeypatch.setattr(sl, "is_nax_available", lambda: True)
monkeypatch.setattr(sl, "_NAX_STOCK_MODE", "1")
assert sl._nax_prefers_stock(1)
def test_native_block_kind_short_circuits_on_nax_prefill(self, monkeypatch):
import mlx.core as mx
from omlx.patches.deepseek_v4 import switch_layers as sl
linear = sl.QuantizedSwitchLinear(
64, 64, num_experts=2, bias=False, group_size=64, bits=4
)
monkeypatch.setattr(sl, "_nax_prefers_stock", lambda n: n >= 1024)
prefill_x = mx.zeros((2048, 1, 64), dtype=mx.bfloat16)
assert linear._native_block_kind(prefill_x, True) is None
# Decode-sized calls fall through to the regular block-kernel gates:
# the NAX gate must not change what they resolve to.
decode_x = mx.zeros((8, 1, 64), dtype=mx.bfloat16)
gated = linear._native_block_kind(decode_x, True)
monkeypatch.setattr(sl, "_nax_prefers_stock", lambda n: False)
assert gated == linear._native_block_kind(decode_x, True)