"""Unit tests for the speculative algorithm plugin registry.""" import unittest from types import SimpleNamespace from unittest.mock import MagicMock from sglang.srt.arg_groups.speculative_hook import handle_speculative_decoding from sglang.srt.speculative.spec_info import SpeculativeAlgorithm from sglang.srt.speculative.spec_registry import ( _REGISTRY, CustomSpecAlgo, _assert_custom_spec_algo_conforms, _reserved_names, ) from sglang.test.ci.ci_register import register_cpu_ci from sglang.test.test_utils import CustomTestCase register_cpu_ci(est_time=5, suite="base-a-test-cpu") class _RegistryIsolated(CustomTestCase): """Snapshot and restore the global registry so tests don't leak.""" def setUp(self): self._snapshot = _REGISTRY.copy() _REGISTRY.clear() def tearDown(self): _REGISTRY.clear() _REGISTRY.update(self._snapshot) class TestFromString(_RegistryIsolated): def test_none_input_returns_none_member(self): self.assertIs(SpeculativeAlgorithm.from_string(None), SpeculativeAlgorithm.NONE) def test_builtin_name_returns_enum(self): self.assertIs( SpeculativeAlgorithm.from_string("EAGLE"), SpeculativeAlgorithm.EAGLE ) self.assertIs( SpeculativeAlgorithm.from_string("NGRAM"), SpeculativeAlgorithm.NGRAM ) def test_builtin_name_is_case_insensitive(self): self.assertIs( SpeculativeAlgorithm.from_string("eagle"), SpeculativeAlgorithm.EAGLE ) def test_unknown_name_raises(self): with self.assertRaisesRegex(ValueError, "Unknown speculative algorithm"): SpeculativeAlgorithm.from_string("NOT_REGISTERED") def test_registered_plugin_returns_custom_spec(self): @SpeculativeAlgorithm.register("MY_FOO") def _factory(server_args): return MagicMock algo = SpeculativeAlgorithm.from_string("MY_FOO") self.assertIsInstance(algo, CustomSpecAlgo) self.assertEqual(algo.name, "MY_FOO") def test_registered_plugin_lookup_is_case_insensitive(self): @SpeculativeAlgorithm.register("MY_FOO") def _factory(server_args): return MagicMock self.assertIs( SpeculativeAlgorithm.from_string("my_foo"), SpeculativeAlgorithm.from_string("MY_FOO"), ) class TestRegister(_RegistryIsolated): def test_register_returns_factory_unchanged(self): def _factory(server_args): return MagicMock decorated = SpeculativeAlgorithm.register("MY_FOO")(_factory) self.assertIs(decorated, _factory) def test_two_distinct_registrations_are_independent(self): @SpeculativeAlgorithm.register("FOO") def _foo_factory(server_args): return MagicMock @SpeculativeAlgorithm.register("BAR") def _bar_factory(server_args): return MagicMock foo = SpeculativeAlgorithm.from_string("FOO") bar = SpeculativeAlgorithm.from_string("BAR") self.assertIsNot(foo, bar) self.assertNotEqual(foo, bar) self.assertEqual(foo.name, "FOO") self.assertEqual(bar.name, "BAR") def test_duplicate_name_raises(self): @SpeculativeAlgorithm.register("MY_FOO") def _factory(server_args): return MagicMock with self.assertRaisesRegex(ValueError, "already registered"): @SpeculativeAlgorithm.register("MY_FOO") def _factory2(server_args): return MagicMock def test_reserved_name_raises(self): for reserved in _reserved_names(): with self.assertRaisesRegex(ValueError, "reserved"): SpeculativeAlgorithm.register(reserved) def test_reserved_names_cover_all_enum_members(self): # Reserved names are derived from the enum, so every builtin (including # FROZEN_KV_MTP, which a hand-maintained list had omitted) is reserved. for member in SpeculativeAlgorithm: self.assertIn(member.name, _reserved_names()) self.assertIn("NEXTN", _reserved_names()) # CLI alias def test_reserved_name_is_case_insensitive(self): with self.assertRaisesRegex(ValueError, "reserved"): SpeculativeAlgorithm.register("frozen_kv_mtp") def test_register_is_case_insensitive_on_collision(self): @SpeculativeAlgorithm.register("MY_FOO") def _factory(server_args): return MagicMock with self.assertRaisesRegex(ValueError, "already registered"): @SpeculativeAlgorithm.register("my_foo") def _factory2(server_args): return MagicMock class TestCustomSpecAlgoInterface(_RegistryIsolated): """CustomSpecAlgo must duck-type SpeculativeAlgorithm enum values.""" def setUp(self): super().setUp() @SpeculativeAlgorithm.register("MY_FOO", supports_overlap=False) def _factory(server_args): return MagicMock self.algo = SpeculativeAlgorithm.from_string("MY_FOO") def test_is_predicates_all_false_except_speculative(self): self.assertFalse(self.algo.is_none()) self.assertFalse(self.algo.is_eagle()) self.assertFalse(self.algo.is_eagle3()) self.assertFalse(self.algo.is_frozen_kv_mtp()) self.assertFalse(self.algo.is_dflash()) self.assertFalse(self.algo.is_standalone()) self.assertFalse(self.algo.is_ngram()) self.assertTrue(self.algo.is_speculative()) # A registered plugin is never NONE -> is_some() mirrors the enum. self.assertTrue(self.algo.is_some()) def test_is_some_matches_enum_semantics(self): # is_some() is called on spec algos in overlap_utils.py; a CustomSpecAlgo # must answer it the same way the enum does (True iff not NONE). self.assertEqual(self.algo.is_some(), not self.algo.is_none()) self.assertEqual(SpeculativeAlgorithm.EAGLE.is_some(), self.algo.is_some()) def test_supports_overlap_false_warns_deprecation(self): # supports_overlap=False plugins run the V2 schema synchronously; the # removed V1 path is surfaced as a deprecation warning at create time. server_args = MagicMock() server_args.disable_overlap_schedule = True with self.assertLogs("sglang.srt.speculative.spec_registry", "WARNING") as logs: self.algo.create_worker(server_args) self.assertTrue(any("deprecated" in line for line in logs.output)) @SpeculativeAlgorithm.register("MY_V2", supports_overlap=True) def _factory(server_args): return MagicMock v2 = SpeculativeAlgorithm.from_string("MY_V2") server_args.disable_overlap_schedule = False self.assertIs(v2.create_worker(server_args), MagicMock) def test_create_worker_calls_factory(self): server_args = MagicMock() server_args.disable_overlap_schedule = True worker_cls = self.algo.create_worker(server_args) self.assertIs(worker_cls, MagicMock) def test_create_worker_raises_on_overlap_mismatch(self): server_args = MagicMock() server_args.disable_overlap_schedule = False with self.assertRaisesRegex(ValueError, "does not support overlap"): self.algo.create_worker(server_args) class TestValidatorHook(_RegistryIsolated): def test_validator_invocation_is_caller_driven(self): validator = MagicMock() @SpeculativeAlgorithm.register("MY_FOO", validate_server_args=validator) def _factory(server_args): return MagicMock algo = SpeculativeAlgorithm.from_string("MY_FOO") self.assertIs(algo.validate_server_args, validator) # Callers (e.g. ServerArgs.__post_init__) must invoke the hook themselves; # CustomSpecAlgo does not call it from create_worker. validator.assert_not_called() class TestServerArgsHook(_RegistryIsolated): def test_handle_speculative_decoding_invokes_custom_handle_server_args(self): class CustomHandleServerArgs(CustomSpecAlgo): def handle_server_args(self, server_args): server_args.custom_spec_handle_seen = self.name server_args.speculative_num_draft_tokens = 7 @SpeculativeAlgorithm.register( "MY_HANDLE_ARGS", supports_overlap=True, spec_class=CustomHandleServerArgs ) def _factory(server_args): return MagicMock server_args = SimpleNamespace( speculative_draft_model_path=None, speculative_draft_model_revision=None, speculative_moe_runner_backend=None, moe_runner_backend="auto", speculative_algorithm="my_handle_args", decrypted_draft_config_file=None, trust_remote_code=False, speculative_draft_window_size=None, speculative_skip_dp_mlp_sync=False, speculative_adaptive=False, ) handle_speculative_decoding(server_args) self.assertEqual(server_args.speculative_algorithm, "MY_HANDLE_ARGS") self.assertEqual(server_args.custom_spec_handle_seen, "MY_HANDLE_ARGS") self.assertEqual(server_args.speculative_num_draft_tokens, 7) class TestSubclassOverride(_RegistryIsolated): """Plugins can subclass CustomSpecAlgo to override is_*() / create_worker.""" def test_subclass_overrides_is_eagle(self): class EagleLike(CustomSpecAlgo): def is_eagle(self) -> bool: return True @SpeculativeAlgorithm.register( "MY_LIKE_EAGLE", supports_overlap=True, spec_class=EagleLike ) def _factory(server_args): return MagicMock algo = SpeculativeAlgorithm.from_string("MY_LIKE_EAGLE") self.assertIsInstance(algo, EagleLike) self.assertIsInstance(algo, CustomSpecAlgo) self.assertTrue(algo.is_eagle()) # Other predicates default to False self.assertFalse(algo.is_ngram()) self.assertFalse(algo.is_dflash()) def test_subclass_overrides_create_worker(self): class CustomDispatch(CustomSpecAlgo): def create_worker(self, server_args): return "custom-dispatched" @SpeculativeAlgorithm.register("MY_CUSTOM", spec_class=CustomDispatch) def _factory(server_args): return MagicMock algo = SpeculativeAlgorithm.from_string("MY_CUSTOM") # Custom dispatch bypasses default overlap check self.assertEqual(algo.create_worker(MagicMock()), "custom-dispatched") class TestConformanceGuard(_RegistryIsolated): """register_algorithm rejects spec classes that drift from the enum's is_*() / supports_*() duck-typing interface.""" def test_base_custom_spec_algo_conforms(self): # The shipped base class must satisfy its own contract. _assert_custom_spec_algo_conforms(CustomSpecAlgo) def test_conforming_subclass_passes(self): class Good(CustomSpecAlgo): def is_eagle(self) -> bool: return True _assert_custom_spec_algo_conforms(Good) # does not raise @staticmethod def _spec_class_missing(method: str) -> type: # Build a class exposing the full enum interface except `method`. A real # subclass can't be "missing" an inherited method, so the failure mode # the guard catches is the base class itself dropping one — simulated # here with a standalone class. interface = { name for name in vars(SpeculativeAlgorithm) if name.startswith(("is_", "supports_")) } body = {m: (lambda self: False) for m in interface if m != method} return type("Broken", (), body) def test_missing_predicate_raises(self): Broken = self._spec_class_missing("is_some") with self.assertRaisesRegex(TypeError, "is_some"): _assert_custom_spec_algo_conforms(Broken) def test_register_rejects_nonconforming_spec_class(self): Broken = self._spec_class_missing("is_some") with self.assertRaisesRegex(TypeError, "missing duck-typed methods"): @SpeculativeAlgorithm.register("MY_BROKEN", spec_class=Broken) def _factory(server_args): return MagicMock def test_enum_interface_subset_of_custom_spec_algo(self): # Every is_*/supports_* method on the enum exists on CustomSpecAlgo. # vars() (not dir()) because EnumMeta.__dir__ hides instance methods. interface = { name for name in vars(SpeculativeAlgorithm) if name.startswith(("is_", "supports_")) } self.assertTrue(interface) # guard against an empty (no-op) interface self.assertEqual(interface - set(dir(CustomSpecAlgo)), set()) class TestCrossTypeIdentity(_RegistryIsolated): """A plugin algo and a builtin enum value must never compare equal.""" def test_plugin_not_equal_to_builtin(self): @SpeculativeAlgorithm.register("MY_FOO") def _factory(server_args): return MagicMock algo = SpeculativeAlgorithm.from_string("MY_FOO") self.assertNotEqual(algo, SpeculativeAlgorithm.EAGLE) self.assertNotEqual(algo, SpeculativeAlgorithm.NONE) self.assertIsNot(algo, SpeculativeAlgorithm.EAGLE) if __name__ == "__main__": unittest.main(verbosity=3)