94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
431 lines
16 KiB
Python
431 lines
16 KiB
Python
"""
|
|
Hook registry for SGLang plugins.
|
|
|
|
Provides before/after/around/replace hooks that can be applied to any
|
|
function, method, or class in the sglang codebase. Hooks are registered
|
|
during plugin loading and applied before the engine starts.
|
|
|
|
Usage:
|
|
from sglang.srt.plugins.hook_registry import HookRegistry, HookType
|
|
|
|
def my_timer(original_fn, *args, **kwargs):
|
|
start = time.perf_counter()
|
|
result = original_fn(*args, **kwargs)
|
|
print(f"Elapsed: {time.perf_counter() - start:.3f}s")
|
|
return result
|
|
|
|
HookRegistry.register(
|
|
"sglang.srt.managers.scheduler.Scheduler.schedule",
|
|
my_timer,
|
|
HookType.AROUND,
|
|
)
|
|
"""
|
|
|
|
import contextvars
|
|
import functools
|
|
import logging
|
|
import pkgutil
|
|
import sys
|
|
import types
|
|
from collections import defaultdict
|
|
from collections.abc import Callable
|
|
from enum import Enum
|
|
from typing import NamedTuple
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class HookSource(NamedTuple):
|
|
"""Identifies which plugin registered a hook."""
|
|
|
|
plugin_name: str # entry_point name, e.g. "xpu_hooks"
|
|
dist_name: str | None # distribution name, e.g. "sglang_xpu_platform"
|
|
|
|
|
|
# Set by load_plugins() around each plugin's func() call, read by register().
|
|
_current_plugin_source: contextvars.ContextVar[HookSource | None] = (
|
|
contextvars.ContextVar("_current_plugin_source", default=None)
|
|
)
|
|
|
|
|
|
def _format_source(source: HookSource | None) -> str:
|
|
"""Format source info for log messages."""
|
|
if source is None:
|
|
return "unknown"
|
|
if source.dist_name:
|
|
return f"plugin={source.plugin_name}, dist={source.dist_name}"
|
|
return f"plugin={source.plugin_name}"
|
|
|
|
|
|
class HookType(Enum):
|
|
"""Types of hooks that can be applied to functions or classes."""
|
|
|
|
BEFORE = "before" # Execute before original; can modify args
|
|
AFTER = "after" # Execute after original; can modify return value
|
|
AROUND = "around" # Wrap original; full control over execution
|
|
REPLACE = "replace" # Replace the original function or class entirely
|
|
|
|
|
|
class HookRegistry:
|
|
"""
|
|
Global registry for function/method/class hooks.
|
|
|
|
Thread safety: All registration should happen during load_plugins()
|
|
phase (single-threaded). apply_hooks() should be called once before the
|
|
engine starts serving requests.
|
|
"""
|
|
|
|
_hooks: dict[str, list[tuple[HookType, Callable, HookSource | None]]] = defaultdict(
|
|
list
|
|
)
|
|
_patched: set[str] = set()
|
|
|
|
@classmethod
|
|
def register(
|
|
cls,
|
|
target: str,
|
|
hook: Callable,
|
|
hook_type: HookType = HookType.AFTER,
|
|
*,
|
|
source: HookSource | None = None,
|
|
):
|
|
"""
|
|
Register a hook on a target function, method, or class.
|
|
|
|
Args:
|
|
target: Fully-qualified dotted path to the target.
|
|
e.g. "sglang.srt.managers.scheduler.Scheduler.schedule"
|
|
or "sglang.srt.managers.scheduler.Scheduler" (class)
|
|
hook: The hook callable (function or class). Signature depends on hook_type:
|
|
- BEFORE: fn(*args, **kwargs) -> (args, kwargs) or None
|
|
- AFTER: fn(result, *args, **kwargs) -> new_result or None
|
|
- AROUND: fn(original_fn, *args, **kwargs) -> result
|
|
- REPLACE: fn(*args, **kwargs) -> result (function replacement)
|
|
MyClass (class replacement)
|
|
hook_type: Type of hook (default: AFTER).
|
|
source: Optional source info. If None, auto-read from context var
|
|
set by ``load_plugins()``.
|
|
|
|
Raises:
|
|
TypeError: If a class is passed with a hook_type other than REPLACE.
|
|
"""
|
|
if isinstance(hook, type) and hook_type != HookType.REPLACE:
|
|
raise TypeError(
|
|
f"Class {hook.__name__} can only be used with HookType.REPLACE, "
|
|
f"got HookType.{hook_type.name}. "
|
|
f"Use a function for BEFORE/AFTER/AROUND hooks."
|
|
)
|
|
resolved_source = source or _current_plugin_source.get()
|
|
# Warn on duplicate REPLACE for the same target
|
|
if hook_type == HookType.REPLACE:
|
|
existing_replace = [
|
|
(h, src) for ht, h, src in cls._hooks[target] if ht == HookType.REPLACE
|
|
]
|
|
if existing_replace:
|
|
prev, prev_src = existing_replace[-1]
|
|
prev_name = getattr(prev, "__qualname__", None) or repr(prev)
|
|
new_name = getattr(hook, "__qualname__", None) or repr(hook)
|
|
logger.warning(
|
|
"Multiple REPLACE hooks on '%s': previous (%s [%s]) will be "
|
|
"overridden by (%s [%s]). The last registered REPLACE takes effect.",
|
|
target,
|
|
prev_name,
|
|
_format_source(prev_src),
|
|
new_name,
|
|
_format_source(resolved_source),
|
|
)
|
|
cls._hooks[target].append((hook_type, hook, resolved_source))
|
|
logger.debug(
|
|
"Registered %s hook on %s [%s]",
|
|
hook_type.value,
|
|
target,
|
|
_format_source(resolved_source),
|
|
)
|
|
|
|
@classmethod
|
|
def apply_hooks(cls):
|
|
"""
|
|
Apply all registered hooks to their target functions/classes.
|
|
|
|
This performs the actual monkey-patching. Should be called once after
|
|
all plugins have been loaded and before the engine starts.
|
|
|
|
Targets with class REPLACE hooks are applied first, so that
|
|
subsequent method-level hooks (AROUND, BEFORE, AFTER) on child
|
|
attributes resolve against the *replaced* class rather than the
|
|
original.
|
|
"""
|
|
sorted_items = sorted(cls._hooks.items(), key=cls._target_sort_key)
|
|
for target, hooks in sorted_items:
|
|
if target in cls._patched:
|
|
continue
|
|
try:
|
|
cls._apply_target(target, hooks)
|
|
cls._patched.add(target)
|
|
except Exception:
|
|
logger.exception("Failed to apply hooks to %s", target)
|
|
|
|
@staticmethod
|
|
def _target_sort_key(item):
|
|
"""Sort key: class REPLACE targets (tier 0) before all others (tier 1).
|
|
|
|
This ensures that when a class is replaced, subsequent method-level
|
|
hooks on ``ClassName.method`` resolve against the replacement class.
|
|
"""
|
|
_target, hooks = item
|
|
has_class_replace = any(
|
|
isinstance(h, type) and ht == HookType.REPLACE for ht, h, _ in hooks
|
|
)
|
|
return (0 if has_class_replace else 1, _target)
|
|
|
|
@classmethod
|
|
def _apply_target(cls, target: str, hooks: list):
|
|
"""Resolve target, build wrapper chain, and replace the original."""
|
|
parts = target.rsplit(".", 1)
|
|
if len(parts) != 2:
|
|
raise ValueError(
|
|
f"Invalid target path (need at least module.attr): {target}"
|
|
)
|
|
|
|
obj_path, attr_name = parts
|
|
obj = pkgutil.resolve_name(obj_path)
|
|
|
|
# Check if the original is a classmethod or staticmethod by
|
|
# inspecting __dict__ before getattr() triggers the descriptor
|
|
# protocol (which would lose the wrapper type for classmethod).
|
|
original = getattr(obj, attr_name)
|
|
is_classmethod = False
|
|
is_staticmethod = False
|
|
if isinstance(obj, type):
|
|
raw_attr = obj.__dict__.get(attr_name)
|
|
if isinstance(raw_attr, classmethod):
|
|
is_classmethod = True
|
|
original = raw_attr.__func__
|
|
elif isinstance(raw_attr, staticmethod):
|
|
is_staticmethod = True
|
|
original = raw_attr.__func__
|
|
|
|
# Cross-target conflict detection: if the parent object is a class
|
|
# that was already class-REPLACE'd, and the replacement class defines
|
|
# its own version of this method, a method REPLACE here will silently
|
|
# override the replacement class's implementation.
|
|
if isinstance(obj, type) and obj_path in cls._patched:
|
|
has_method_replace = any(ht == HookType.REPLACE for ht, _, _ in hooks)
|
|
if has_method_replace and attr_name in obj.__dict__:
|
|
replace_sources = [
|
|
_format_source(src)
|
|
for ht, _, src in hooks
|
|
if ht == HookType.REPLACE
|
|
]
|
|
logger.warning(
|
|
"Method REPLACE on '%s' will override the class REPLACE's "
|
|
"own implementation of '%s'. If this is unintended, remove "
|
|
"the method REPLACE and modify the replacement class "
|
|
"directly, or use AROUND to wrap it. (from: %s)",
|
|
target,
|
|
attr_name,
|
|
", ".join(replace_sources),
|
|
)
|
|
|
|
# Guard: if the target is a class, only REPLACE is safe. Wrapping a
|
|
# class in a function would break isinstance/issubclass/inheritance.
|
|
if isinstance(original, type):
|
|
bad = [ht for ht, _, _ in hooks if ht != HookType.REPLACE]
|
|
if bad:
|
|
raise TypeError(
|
|
f"Target '{target}' is a class. Only HookType.REPLACE is "
|
|
f"allowed for class targets (got {bad[0].value}). "
|
|
f"To hook a method, use '{target}.<method_name>' instead."
|
|
)
|
|
|
|
# Warn about risky hook combinations
|
|
hook_types = [ht for ht, _, _ in hooks]
|
|
around_count = hook_types.count(HookType.AROUND)
|
|
has_replace = HookType.REPLACE in hook_types
|
|
has_others = any(ht != HookType.REPLACE for ht in hook_types)
|
|
|
|
if around_count > 1:
|
|
around_sources = [
|
|
_format_source(src) for ht, _, src in hooks if ht == HookType.AROUND
|
|
]
|
|
logger.warning(
|
|
"Multiple AROUND hooks on '%s' (%d hooks, from: %s). If any AROUND hook "
|
|
"skips calling original_fn, inner hooks will be bypassed.",
|
|
target,
|
|
around_count,
|
|
", ".join(around_sources),
|
|
)
|
|
if has_replace and has_others:
|
|
logger.info(
|
|
"Target '%s' has both REPLACE and %s hooks. "
|
|
"REPLACE will be applied first, then wrapped by other hooks.",
|
|
target,
|
|
", ".join(
|
|
sorted({ht.value for ht in hook_types if ht != HookType.REPLACE})
|
|
),
|
|
)
|
|
|
|
# Build the wrapper chain.
|
|
# Sort: REPLACE hooks first (stable sort preserves registration order
|
|
# within the same type). This ensures AROUND/BEFORE/AFTER always wrap
|
|
# the replaced function, regardless of registration order.
|
|
sorted_hooks = sorted(
|
|
hooks, key=lambda h: (0 if h[0] == HookType.REPLACE else 1)
|
|
)
|
|
wrapped = original
|
|
for hook_type, hook, _src in sorted_hooks:
|
|
if isinstance(hook, type) and hook_type == HookType.REPLACE:
|
|
# Class replacement: direct substitution to preserve type identity.
|
|
# This keeps isinstance(), issubclass(), and inheritance working.
|
|
wrapped = hook
|
|
else:
|
|
wrapped = _wrap_fn(wrapped, hook, hook_type)
|
|
|
|
# Restore classmethod/staticmethod decorator if the original had one.
|
|
if is_classmethod:
|
|
wrapped = classmethod(wrapped)
|
|
logger.debug("Preserved @classmethod decorator for %s", target)
|
|
elif is_staticmethod:
|
|
wrapped = staticmethod(wrapped)
|
|
logger.debug("Preserved @staticmethod decorator for %s", target)
|
|
|
|
setattr(obj, attr_name, wrapped)
|
|
|
|
# Propagate the patch to all other modules that imported the original
|
|
# via ``from source_module import name``. Python's ``from X import Y``
|
|
# copies the reference at import time; patching X alone leaves
|
|
# importers with a stale binding.
|
|
if wrapped is not original:
|
|
extra = _propagate_patch(original, wrapped, obj)
|
|
if extra:
|
|
logger.debug(
|
|
"Propagated patch for %s to %d additional module(s)",
|
|
target,
|
|
extra,
|
|
)
|
|
|
|
sources = sorted({_format_source(src) for _, _, src in hooks})
|
|
logger.info(
|
|
"Applied %d hook(s) to %s (from: %s)",
|
|
len(hooks),
|
|
target,
|
|
", ".join(sources),
|
|
)
|
|
|
|
@classmethod
|
|
def reset(cls):
|
|
"""Reset all hooks and patches. Primarily for testing."""
|
|
cls._hooks.clear()
|
|
cls._patched.clear()
|
|
|
|
|
|
def _propagate_patch(original: object, wrapped: object, source_module: object) -> int:
|
|
"""Propagate a monkey-patch to all modules holding a stale ``from X import Y`` binding.
|
|
|
|
After ``setattr(source_module, name, wrapped)`` updates the defining module,
|
|
other modules that did ``from source_module import name`` still hold a direct
|
|
reference to the old *original* object. This walks ``sys.modules`` and
|
|
replaces every such stale binding with *wrapped*.
|
|
|
|
Returns the number of additional module attributes that were patched.
|
|
"""
|
|
patched_count = 0
|
|
for mod in list(sys.modules.values()):
|
|
if mod is source_module or mod is None:
|
|
continue
|
|
if not isinstance(mod, types.ModuleType):
|
|
continue
|
|
try:
|
|
mod_vars = vars(mod)
|
|
except TypeError:
|
|
continue
|
|
for attr_name, attr_value in list(mod_vars.items()):
|
|
if attr_value is original:
|
|
try:
|
|
setattr(mod, attr_name, wrapped)
|
|
patched_count += 1
|
|
except (AttributeError, TypeError):
|
|
pass
|
|
return patched_count
|
|
|
|
|
|
def _wrap_fn(original_fn: Callable, hook: Callable, hook_type: HookType) -> Callable:
|
|
"""Create a wrapper function based on the hook type."""
|
|
if hook_type == HookType.REPLACE:
|
|
|
|
@functools.wraps(original_fn)
|
|
def wrapper(*args, **kwargs):
|
|
return hook(*args, **kwargs)
|
|
|
|
wrapper.__wrapped__ = original_fn
|
|
return wrapper
|
|
|
|
elif hook_type == HookType.BEFORE:
|
|
|
|
@functools.wraps(original_fn)
|
|
def wrapper(*args, **kwargs):
|
|
result = hook(*args, **kwargs)
|
|
if result is not None:
|
|
args, kwargs = result
|
|
return original_fn(*args, **kwargs)
|
|
|
|
wrapper.__wrapped__ = original_fn
|
|
return wrapper
|
|
|
|
elif hook_type == HookType.AFTER:
|
|
|
|
@functools.wraps(original_fn)
|
|
def wrapper(*args, **kwargs):
|
|
result = original_fn(*args, **kwargs)
|
|
modified = hook(result, *args, **kwargs)
|
|
return modified if modified is not None else result
|
|
|
|
wrapper.__wrapped__ = original_fn
|
|
return wrapper
|
|
|
|
elif hook_type == HookType.AROUND:
|
|
|
|
@functools.wraps(original_fn)
|
|
def wrapper(*args, **kwargs):
|
|
return hook(original_fn, *args, **kwargs)
|
|
|
|
wrapper.__wrapped__ = original_fn
|
|
return wrapper
|
|
|
|
else:
|
|
raise ValueError(f"Unknown hook type: {hook_type}")
|
|
|
|
|
|
def plugin_hook(
|
|
target: str,
|
|
type: HookType = HookType.AFTER,
|
|
) -> Callable:
|
|
"""Decorator that registers a function or class as a hook on *target*.
|
|
|
|
Usage::
|
|
|
|
# Function hook (AROUND)
|
|
@plugin_hook("sglang.srt.managers.scheduler.Scheduler.schedule",
|
|
type=HookType.AROUND)
|
|
def my_timer(original_fn, *args, **kwargs):
|
|
start = time.perf_counter()
|
|
result = original_fn(*args, **kwargs)
|
|
print(f"Elapsed: {time.perf_counter() - start:.3f}s")
|
|
return result
|
|
|
|
# Class replacement (REPLACE)
|
|
@plugin_hook("sglang.srt.managers.scheduler.Scheduler",
|
|
type=HookType.REPLACE)
|
|
class MyScheduler(Scheduler):
|
|
...
|
|
|
|
The decorated function/class is returned unchanged so it can still be
|
|
used directly if needed.
|
|
"""
|
|
|
|
def decorator(hook: Callable) -> Callable:
|
|
HookRegistry.register(target, hook, type)
|
|
return hook
|
|
|
|
return decorator
|