chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user