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,141 @@
|
||||
"""
|
||||
SGLang Unified Plugin Framework.
|
||||
|
||||
Supports two types of plugins via setuptools entry_points:
|
||||
1. Hardware Platform Plugins (sglang.srt.platforms) - register custom hardware platforms
|
||||
2. General Plugins (sglang.srt.plugins) - inject hooks into functions/methods, replace classes, etc.
|
||||
|
||||
Plugins are discovered automatically when installed via pip.
|
||||
- Platform plugins: use ``SGLANG_PLATFORM`` to select when multiple are installed.
|
||||
- General plugins: use ``SGLANG_PLUGINS`` (comma-separated) to restrict which are loaded.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from importlib.metadata import entry_points
|
||||
from typing import Any
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.plugins.hook_registry import (
|
||||
HookRegistry,
|
||||
HookSource,
|
||||
_current_plugin_source,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Entry point group names
|
||||
PLATFORM_PLUGINS_GROUP = "sglang.srt.platforms"
|
||||
GENERAL_PLUGINS_GROUP = "sglang.srt.plugins"
|
||||
|
||||
# Guard against multiple loads in the same process
|
||||
_plugins_loaded = False
|
||||
|
||||
|
||||
def load_plugins_by_group(
|
||||
group: str,
|
||||
excluded_dists: set[str] | None = None,
|
||||
) -> dict[str, tuple[Callable[[], Any], str | None]]:
|
||||
"""
|
||||
Discover and load plugins registered under the given entry point group.
|
||||
|
||||
Args:
|
||||
group: The setuptools entry_point group name.
|
||||
excluded_dists: Distribution names to skip. Plugins from these
|
||||
distributions are never ``ep.load()``-ed (avoids importing
|
||||
their modules and pulling hardware-specific dependencies).
|
||||
|
||||
Returns:
|
||||
Dictionary mapping plugin name to ``(callable, dist_name)``.
|
||||
"""
|
||||
# SGLANG_PLUGINS whitelist (comma-separated plugin names)
|
||||
allowed_set: set[str] | None = None
|
||||
allowed_str = envs.SGLANG_PLUGINS.get()
|
||||
if allowed_str:
|
||||
allowed_set = {x.strip() for x in allowed_str.split(",") if x.strip()}
|
||||
|
||||
discovered = entry_points(group=group)
|
||||
if len(discovered) == 0:
|
||||
logger.debug("No plugins found for group %s.", group)
|
||||
return {}
|
||||
|
||||
logger.info("Available plugins for group %s:", group)
|
||||
for ep in discovered:
|
||||
logger.info(" - %s -> %s", ep.name, ep.value)
|
||||
|
||||
plugins: dict[str, tuple[Callable[[], Any], str | None]] = {}
|
||||
for ep in discovered:
|
||||
if allowed_set is not None and ep.name not in allowed_set:
|
||||
logger.info("Skipping plugin %s (not in SGLANG_PLUGINS)", ep.name)
|
||||
continue
|
||||
dist_name = ep.dist.name if ep.dist else None
|
||||
if excluded_dists and dist_name in excluded_dists:
|
||||
logger.info(
|
||||
"Skipping plugin %s (dist %s excluded by SGLANG_PLATFORM)",
|
||||
ep.name,
|
||||
dist_name,
|
||||
)
|
||||
continue
|
||||
try:
|
||||
func = ep.load()
|
||||
plugins[ep.name] = (func, dist_name)
|
||||
logger.info("Loaded plugin %s from group %s", ep.name, group)
|
||||
except Exception:
|
||||
logger.exception("Failed to load plugin %s from group %s", ep.name, group)
|
||||
|
||||
return plugins
|
||||
|
||||
|
||||
def _get_excluded_dists() -> set[str]:
|
||||
"""Compute dist names to skip when ``SGLANG_PLATFORM`` is set.
|
||||
|
||||
Returns dist names that provide a platform plugin but are NOT the one
|
||||
selected by ``SGLANG_PLATFORM``. This prevents unselected platform
|
||||
packages from registering hooks that pull their hardware dependencies.
|
||||
"""
|
||||
selected = envs.SGLANG_PLATFORM.get()
|
||||
if not selected:
|
||||
return set()
|
||||
platform_eps = entry_points(group=PLATFORM_PLUGINS_GROUP)
|
||||
return {ep.dist.name for ep in platform_eps if ep.dist and ep.name != selected}
|
||||
|
||||
|
||||
def load_plugins():
|
||||
"""
|
||||
Load and execute all general plugins, then apply registered hooks.
|
||||
|
||||
Idempotent - safe to call multiple times. General plugins are functions
|
||||
whose side effects (registering hooks, replacing classes, etc.) are the
|
||||
desired behavior. Return values are ignored.
|
||||
|
||||
When ``SGLANG_PLATFORM`` is set, general plugins from unselected platform
|
||||
packages are automatically skipped (avoids pulling their dependencies).
|
||||
|
||||
After all plugins execute, ``HookRegistry.apply_hooks()`` is called
|
||||
automatically so callers only need this single function call.
|
||||
|
||||
This should be called early in every process (main, engine core, workers).
|
||||
"""
|
||||
global _plugins_loaded
|
||||
if _plugins_loaded:
|
||||
return
|
||||
_plugins_loaded = True
|
||||
|
||||
plugins = load_plugins_by_group(
|
||||
GENERAL_PLUGINS_GROUP,
|
||||
excluded_dists=_get_excluded_dists(),
|
||||
)
|
||||
|
||||
for name, (func, dist_name) in plugins.items():
|
||||
source = HookSource(plugin_name=name, dist_name=dist_name)
|
||||
token = _current_plugin_source.set(source)
|
||||
try:
|
||||
func()
|
||||
logger.info("Executed general plugin: %s", name)
|
||||
except Exception:
|
||||
logger.exception("Failed to execute general plugin: %s", name)
|
||||
finally:
|
||||
_current_plugin_source.reset(token)
|
||||
|
||||
# Apply all registered hooks (idempotent — already-patched targets are skipped).
|
||||
HookRegistry.apply_hooks()
|
||||
@@ -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