Files
jundot--omlx/tests/test_vlm_mtp_proxy.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

217 lines
7.5 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Tests for VLM MTP proxy classes.
Validates that _VLMAdapterMTPProxy and _MTPResetBindingProxy correctly
control attribute visibility so that:
- _mtp_rounds / _mtp_rounds_batch see a target that does NOT expose
``language_model`` (forcing ``lm = model`` and routing verify through
the adapter);
- drafter.reset() can temporarily expose ``language_model`` for bind().
"""
from types import SimpleNamespace
import mlx.core as mx
import mlx.nn as nn
import pytest
from omlx.speculative.vlm_mtp import (
_MTPResetBindingProxy,
_VLMAdapterMTPProxy,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
class FakeLanguageModel:
"""Mimics the patched LanguageModel with rollback_speculative_cache."""
def __init__(self):
self.rollback_called = False
self.model = object()
def __call__(self, *args, **kwargs):
from mlx_vlm.models.base import LanguageModelOutput
return LanguageModelOutput(
logits=mx.zeros((1, 1, 4)),
hidden_states=[mx.zeros((1, 1, 8))],
gdn_states=[],
shared_kv_states={},
)
def rollback_speculative_cache(self, caches, gdn_states, accepted, block_size):
self.rollback_called = True
return accepted
def speculative_logits_from_hidden(self, hidden):
return hidden
class FakeVLMAdapter:
"""Mimics VLMModelAdapter with _language_model and patched methods."""
def __init__(self, expose_rollback: bool = True, uses_mrope: bool = False):
self._language_model = FakeLanguageModel()
self._uses_mrope = uses_mrope
self.forward_called = False
if expose_rollback:
# Mimic _patch_vlm_model_adapter() which delegates to _language_model.
self.rollback_speculative_cache = (
self._language_model.rollback_speculative_cache
)
def __call__(self, *args, **kwargs):
self.forward_called = True
return self._language_model(*args, **kwargs)
def set_batch_rope_deltas(self, deltas):
pass
class FakeDrafter(nn.Module):
def __init__(self):
super().__init__()
self.reset_target = None
self.reset_called = False
def reset(self, target_model, *args, **kwargs):
self.reset_target = target_model
self.reset_called = True
# bind() accesses target_model.language_model.model.embed_tokens
_ = target_model.language_model
return []
# ---------------------------------------------------------------------------
# _VLMAdapterMTPProxy tests
# ---------------------------------------------------------------------------
class TestVLMAdapterMTPProxy:
def test_language_model_not_exposed_by_default(self):
adapter = FakeVLMAdapter()
proxy = _VLMAdapterMTPProxy(adapter, adapter._language_model)
assert not hasattr(proxy, "language_model")
with pytest.raises(AttributeError):
_ = proxy.language_model
def test_language_model_exposed_when_flag_set(self):
adapter = FakeVLMAdapter()
proxy = _VLMAdapterMTPProxy(adapter, adapter._language_model)
proxy._expose_language_model = True
assert hasattr(proxy, "language_model")
assert proxy.language_model is adapter._language_model
def test_call_delegates_to_adapter(self):
adapter = FakeVLMAdapter()
proxy = _VLMAdapterMTPProxy(adapter, adapter._language_model)
proxy(mx.array([1]), cache=[])
assert adapter.forward_called
def test_non_language_model_attrs_delegate_to_adapter(self):
adapter = FakeVLMAdapter()
proxy = _VLMAdapterMTPProxy(adapter, adapter._language_model)
assert proxy._language_model is adapter._language_model
def test_rollback_speculative_cache_delegates(self):
adapter = FakeVLMAdapter()
proxy = _VLMAdapterMTPProxy(adapter, adapter._language_model)
proxy.rollback_speculative_cache([], [], 0, 4)
assert adapter._language_model.rollback_called
def test_rollback_falls_back_to_language_model_when_adapter_lacks_passthrough(self):
adapter = FakeVLMAdapter(expose_rollback=False)
proxy = _VLMAdapterMTPProxy(adapter, adapter._language_model)
proxy.rollback_speculative_cache([], [], 0, 4)
assert adapter._language_model.rollback_called
def test_mrope_proxy_hides_fast_path_attrs_but_keeps_rollback(self):
adapter = FakeVLMAdapter(expose_rollback=False, uses_mrope=True)
proxy = _VLMAdapterMTPProxy(adapter, adapter._language_model)
assert hasattr(proxy, "rollback_speculative_cache")
assert not hasattr(proxy, "model")
assert not hasattr(proxy, "speculative_logits_from_hidden")
def test_mtp_rounds_sees_no_language_model(self):
"""Simulates the hasattr check in _mtp_rounds / _mtp_rounds_batch."""
adapter = FakeVLMAdapter()
proxy = _VLMAdapterMTPProxy(adapter, adapter._language_model)
# _mtp_rounds line 547: lm = model.language_model if hasattr(...) else model
lm = proxy.language_model if hasattr(proxy, "language_model") else proxy
assert lm is proxy
# ---------------------------------------------------------------------------
# _MTPResetBindingProxy tests
# ---------------------------------------------------------------------------
class TestMTPResetBindingProxy:
def test_reset_temporarily_exposes_language_model(self):
adapter = FakeVLMAdapter()
proxy = _VLMAdapterMTPProxy(adapter, adapter._language_model)
drafter = FakeDrafter()
reset_proxy = _MTPResetBindingProxy(drafter, proxy)
# Before reset: language_model not exposed
assert not hasattr(proxy, "language_model")
reset_proxy.reset(proxy)
# Drafter's bind() saw language_model during reset
assert drafter.reset_called
assert drafter.reset_target is proxy
# After reset: language_model hidden again
assert not hasattr(proxy, "language_model")
def test_reset_non_proxy_target_passes_through(self):
adapter = FakeVLMAdapter()
proxy = _VLMAdapterMTPProxy(adapter, adapter._language_model)
drafter = FakeDrafter()
reset_proxy = _MTPResetBindingProxy(drafter, proxy)
other_target = SimpleNamespace(language_model=object())
reset_proxy.reset(other_target)
assert drafter.reset_called
assert drafter.reset_target is other_target
def test_other_attrs_delegate_to_drafter(self):
adapter = FakeVLMAdapter()
proxy = _VLMAdapterMTPProxy(adapter, adapter._language_model)
drafter = FakeDrafter()
reset_proxy = _MTPResetBindingProxy(drafter, proxy)
assert reset_proxy.reset_called is False # getattr on drafter
def test_reset_exception_restores_hidden_state(self):
"""language_model must be hidden again even if reset() raises."""
adapter = FakeVLMAdapter()
proxy = _VLMAdapterMTPProxy(adapter, adapter._language_model)
class FailingDrafter:
def reset(self, target_model, *args, **kwargs):
_ = target_model.language_model # needs it exposed
raise RuntimeError("bind failed")
reset_proxy = _MTPResetBindingProxy(FailingDrafter(), proxy)
with pytest.raises(RuntimeError, match="bind failed"):
reset_proxy.reset(proxy)
# Must be hidden even after exception
assert not hasattr(proxy, "language_model")