Multi platform Plugin (#21388)

Co-authored-by: root <root@tjzj-inf-sci-k8s-bzz2-0183.tjzj.baidu.com>
Co-authored-by: Alex Nails <alex.nails@radixark.ai>
Co-authored-by: Alex Nails <alexj.nails@gmail.com>
Co-authored-by: root <root@tjzj-inf-sci-k8s-bzz2-0000.tjzj.baidu.com>
Co-authored-by: Mick <mickjagger19@icloud.com>
This commit is contained in:
Baidu-AIAK
2026-04-19 17:23:51 -07:00
committed by GitHub
co-authored by root Alex Nails Alex Nails root Mick
parent ebcc2b3eec
commit 7ca3566130
22 changed files with 2811 additions and 17 deletions
+4
View File
@@ -86,6 +86,10 @@ def serve(args, extra_argv):
)
return
from sglang.srt.plugins import load_plugins
load_plugins()
model_type, dispatch_argv = _extract_model_type_override(extra_argv)
model_path = get_model_path(dispatch_argv)
try:
+4
View File
@@ -56,6 +56,10 @@ if __name__ == "__main__":
stacklevel=1,
)
from sglang.srt.plugins import load_plugins
load_plugins()
server_args = prepare_server_args(sys.argv[1:])
try:
+7 -1
View File
@@ -22,6 +22,7 @@ from sglang.srt.compilation.cuda_piecewise_backend import CUDAPiecewiseBackend
from sglang.srt.compilation.npu_piecewise_backend import NPUPiecewiseBackend
from sglang.srt.compilation.pass_manager import PostGradPassManager
from sglang.srt.environ import envs
from sglang.srt.platforms import current_platform
from sglang.srt.utils.common import is_npu
logger = logging.getLogger(__name__)
@@ -48,7 +49,12 @@ def make_backend(
sglang_backend,
):
backend_cls = CUDAPiecewiseBackend if not is_npu() else NPUPiecewiseBackend
if current_platform.is_out_of_tree():
backend_cls = current_platform.get_piecewise_backend_cls()
elif is_npu():
backend_cls = NPUPiecewiseBackend
else:
backend_cls = CUDAPiecewiseBackend
return backend_cls(
graph,
compile_config,
+10
View File
@@ -84,6 +84,7 @@ from sglang.srt.managers.scheduler import run_scheduler_process
from sglang.srt.managers.template_manager import TemplateManager
from sglang.srt.managers.tokenizer_manager import TokenizerManager
from sglang.srt.observability.trace import process_tracing_init, trace_set_thread_info
from sglang.srt.plugins import load_plugins
from sglang.srt.server_args import PortArgs, ServerArgs
from sglang.srt.utils import (
MultiprocessingSerializer,
@@ -167,6 +168,10 @@ class Engine(EngineScoreMixin, EngineBase):
Please refer to `ServerArgs` for the documentation.
"""
# Ensure plugins are loaded before ServerArgs construction,
# so hooks on ServerArgs.__post_init__ fire correctly.
load_plugins()
# Parse server_args
if "server_args" in kwargs:
# Directly load server_args
@@ -647,6 +652,11 @@ class Engine(EngineScoreMixin, EngineBase):
# Configure global environment
configure_logger(server_args)
_set_envs_and_config(server_args)
# Defensive: ensure plugins loaded (may already be loaded by
# Engine.__init__ or CLI entry).
load_plugins()
server_args.check_server_args()
_set_gc(server_args)
+4
View File
@@ -554,6 +554,10 @@ class Envs:
# Sglang Cache Dir
SGLANG_CACHE_DIR = EnvStr(os.path.expanduser("~/.cache/sglang"))
# Plugin system
SGLANG_PLATFORM = EnvStr("")
SGLANG_PLUGINS = EnvStr("")
envs = Envs()
EnvField._allow_set_name = False
@@ -1,8 +1,9 @@
from typing import Callable
from typing import Callable, ClassVar
from torch import nn
from sglang.kernel_api_logging import debug_kernel_api
from sglang.srt.platforms import current_platform
from sglang.srt.utils import (
cpu_has_amx_support,
is_cpu,
@@ -23,6 +24,15 @@ _is_musa = is_musa()
class MultiPlatformOp(nn.Module):
# OOT forward registry: maps dispatch_key -> {op_cls -> forward_fn}
_oot_forward_registry: ClassVar[dict[str, dict[type, Callable]]] = {}
@classmethod
def register_oot_forward(cls, op_cls: type, fn: Callable, platform_key: str):
"""Register an OOT forward implementation for a specific op class and platform."""
cls._oot_forward_registry.setdefault(platform_key, {})[op_cls] = fn
def __init__(self):
super().__init__()
self._forward_method: Callable = self.dispatch_forward()
@@ -100,6 +110,17 @@ class MultiPlatformOp(nn.Module):
return self.forward_native(*args, **kwargs)
def dispatch_forward(self):
# OOT platform dispatch: check registry then method lookup
if current_platform.is_out_of_tree():
key = current_platform.get_dispatch_key_name()
oot = self._oot_forward_registry.get(key, {})
if type(self) in oot:
return oot[type(self)].__get__(self)
method = getattr(self, f"forward_{key}", None)
if method is not None:
return method
return self.forward_native
if _is_cuda:
return self.forward_cuda
elif _is_hip:
+3
View File
@@ -204,6 +204,7 @@ from sglang.srt.observability.scheduler_metrics_mixin import (
)
from sglang.srt.observability.trace import process_tracing_init, trace_set_thread_info
from sglang.srt.parser.reasoning_parser import ReasoningParser
from sglang.srt.plugins import load_plugins
from sglang.srt.sampling.sampling_batch_info import SamplingBatchInfo
from sglang.srt.server_args import PortArgs, ServerArgs, get_global_server_args
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
@@ -3743,6 +3744,8 @@ def run_scheduler_process(
dp_rank: Optional[int],
pipe_writer,
):
# Load plugins so hooks can override Scheduler and its dependencies.
load_plugins()
dp_rank = configure_scheduler_process(
server_args,
gpu_id,
+12 -3
View File
@@ -54,6 +54,7 @@ from sglang.srt.mem_cache.utils import (
set_mla_kv_buffer_triton_fp8_quant,
set_mla_kv_scale_buffer_triton,
)
from sglang.srt.platforms import current_platform
from sglang.srt.utils import (
cpu_has_amx_support,
is_cpu,
@@ -780,8 +781,12 @@ class MHATokenToKVPool(KVCache):
self._create_buffers()
self.device_module = torch.get_device_module(self.device)
_use_alt_stream = _is_cuda or current_platform.is_cuda_alike()
self.alt_stream = (
self.device_module.Stream() if _is_cuda and enable_alt_stream else None
self.device_module.Stream()
if _use_alt_stream and enable_alt_stream
else None
)
if enable_kv_cache_copy:
@@ -1262,7 +1267,9 @@ class HybridLinearKVPool(KVCache):
TokenToKVPoolClass = MHATokenToKVPool
if _is_npu:
if current_platform.is_out_of_tree():
TokenToKVPoolClass = current_platform.get_mha_kv_pool_cls()
elif _is_npu:
from sglang.srt.hardware_backend.npu.memory_pool_npu import (
NPUMHATokenToKVPool,
)
@@ -1283,7 +1290,9 @@ class HybridLinearKVPool(KVCache):
TokenToKVPoolClass = MLATokenToKVPool
if _is_npu:
if current_platform.is_out_of_tree():
TokenToKVPoolClass = current_platform.get_mla_kv_pool_cls()
elif _is_npu:
from sglang.srt.hardware_backend.npu.memory_pool_npu import (
NPUMLATokenToKVPool,
)
@@ -150,6 +150,7 @@ from sglang.srt.model_loader.remote_instance_weight_loader_utils import (
)
from sglang.srt.model_loader.utils import set_default_torch_dtype
from sglang.srt.model_loader.weight_utils import default_weight_loader
from sglang.srt.platforms import current_platform
from sglang.srt.sampling.sampling_batch_info import SamplingBatchInfo
from sglang.srt.server_args import (
ServerArgs,
@@ -207,6 +208,8 @@ if _is_npu:
from sglang.srt.hardware_backend.npu.utils import init_npu_backend
init_npu_backend()
elif current_platform.is_out_of_tree():
current_platform.init_backend()
MLA_ATTENTION_BACKENDS = [
"aiter",
@@ -702,6 +705,7 @@ class ModelRunner(ModelRunnerKVCacheMixin):
# Init routed experts capturer
self.init_routed_experts_capturer()
# TODO: Refactor device-specific init branches into platform interface (separate PR).
# Must be called BEFORE init_device_graphs() so CUDA graph capture
# runs with aux hidden state capture enabled.
self.init_aux_hidden_state_capture()
@@ -714,6 +718,13 @@ class ModelRunner(ModelRunnerKVCacheMixin):
elif self.device in ["npu", "cpu"]:
self.init_attention_backend()
self.init_device_graphs()
elif current_platform.is_out_of_tree():
self.init_attention_backend()
if current_platform.support_cuda_graph():
self.init_device_graphs()
else:
self.graph_runner = None
self.graph_mem_usage = 0
else:
self.graph_runner = None
self.graph_mem_usage = 0
@@ -1483,7 +1494,14 @@ class ModelRunner(ModelRunnerKVCacheMixin):
self.server_args.load_format = load_format
self.load_config = load_config
if recapture_cuda_graph and (self.device == "cuda" or self.device == "musa"):
if recapture_cuda_graph and (
self.device == "cuda"
or self.device == "musa"
or (
current_platform.is_out_of_tree()
and current_platform.support_cuda_graph()
)
):
self.init_device_graphs()
logger.info("Update weights end.")
@@ -2532,8 +2550,10 @@ class ModelRunner(ModelRunnerKVCacheMixin):
tic = time.perf_counter()
before_mem = get_available_gpu_memory(self.device, self.gpu_id)
graph_backend = defaultdict(
lambda: "cuda graph",
lambda: f"{current_platform.device_name} graph",
{
"cuda": "cuda graph",
"musa": "cuda graph",
"cpu": "cpu graph",
"npu": "npu graph",
},
@@ -2541,14 +2561,18 @@ class ModelRunner(ModelRunnerKVCacheMixin):
logger.info(
f"Capture {graph_backend[self.device]} begin. This can take up to several minutes. avail mem={before_mem:.2f} GB"
)
graph_runners = defaultdict(
lambda: CudaGraphRunner,
{
"cpu": CPUGraphRunner,
"npu": NPUGraphRunner,
},
)
self.graph_runner = graph_runners[self.device](self)
if current_platform.is_out_of_tree():
GraphRunnerCls = current_platform.get_graph_runner_cls()
self.graph_runner = GraphRunnerCls(self)
else:
graph_runners = defaultdict(
lambda: CudaGraphRunner,
{
"cpu": CPUGraphRunner,
"npu": NPUGraphRunner,
},
)
self.graph_runner = graph_runners[self.device](self)
after_mem = get_available_gpu_memory(self.device, self.gpu_id)
self.graph_mem_usage = before_mem - after_mem
@@ -282,7 +282,63 @@ class ModelRunnerKVCacheMixin:
# Initialize token_to_kv_pool
is_nsa_model = is_deepseek_nsa(self.model_config.hf_config)
if self.server_args.attention_backend == "ascend" and not self.mambaish_config:
# Check out-of-tree platform (plugin system) first
from sglang.srt.platforms import current_platform
if current_platform.is_out_of_tree() and not self.mambaish_config:
if self.use_mla_backend and is_nsa_model:
PoolCls = current_platform.get_nsa_kv_pool_cls()
self.token_to_kv_pool = PoolCls(
self.max_total_num_tokens,
page_size=self.page_size,
dtype=self.kv_cache_dtype,
kv_lora_rank=self.model_config.kv_lora_rank,
qk_rope_head_dim=self.model_config.qk_rope_head_dim,
layer_num=self.num_effective_layers,
device=self.device,
kv_cache_dim=self.calculate_mla_kv_cache_dim(),
enable_memory_saver=self.server_args.enable_memory_saver,
start_layer=self.start_layer,
end_layer=self.end_layer,
index_head_dim=get_nsa_index_head_dim(self.model_config.hf_config),
)
elif self.use_mla_backend:
PoolCls = current_platform.get_mla_kv_pool_cls()
self.token_to_kv_pool = PoolCls(
self.max_total_num_tokens,
page_size=self.page_size,
dtype=self.kv_cache_dtype,
kv_lora_rank=self.model_config.kv_lora_rank,
qk_rope_head_dim=self.model_config.qk_rope_head_dim,
index_head_dim=(
self.model_config.index_head_dim if is_nsa_model else None
),
layer_num=self.num_effective_layers,
device=self.device,
enable_memory_saver=self.server_args.enable_memory_saver,
start_layer=self.start_layer,
end_layer=self.end_layer,
)
else:
PoolCls = current_platform.get_mha_kv_pool_cls()
self.token_to_kv_pool = PoolCls(
self.max_total_num_tokens,
page_size=self.page_size,
dtype=self.kv_cache_dtype,
head_num=self.model_config.get_num_kv_heads(
get_attention_tp_size()
),
head_dim=self.model_config.head_dim,
layer_num=self.num_effective_layers,
device=self.device,
enable_memory_saver=self.server_args.enable_memory_saver,
start_layer=self.start_layer,
end_layer=self.end_layer,
)
elif (
self.server_args.attention_backend == "ascend" and not self.mambaish_config
):
if self.is_hybrid_swa:
from sglang.srt.hardware_backend.npu.memory_pool_npu import (
NPUMHATokenToKVPool,
@@ -513,7 +569,17 @@ class ModelRunnerKVCacheMixin:
# Initialize token_to_kv_pool_allocator
need_sort = self.server_args.disaggregation_mode in ("decode", "prefill")
if self.token_to_kv_pool_allocator is None:
if _is_npu and (
if current_platform.is_out_of_tree():
AllocatorCls = current_platform.get_paged_allocator_cls()
self.token_to_kv_pool_allocator = AllocatorCls(
self.max_total_num_tokens,
page_size=self.page_size,
dtype=self.kv_cache_dtype,
device=self.device,
kvcache=self.token_to_kv_pool,
need_sort=need_sort,
)
elif _is_npu and (
self.server_args.attention_backend == "ascend"
or self.hybrid_gdn_config is not None
):
+125
View File
@@ -0,0 +1,125 @@
"""
SGLang Platform Discovery and Lazy Initialization.
Provides `current_platform` as a module-level lazy singleton. On first access,
it discovers platform plugins via entry_points and instantiates the appropriate
SRTPlatform subclass.
Usage:
from sglang.srt.platforms import current_platform
print(current_platform.device_name)
"""
import logging
import pkgutil
from importlib.metadata import entry_points
from sglang.srt.environ import envs
from sglang.srt.platforms.interface import SRTPlatform
from sglang.srt.plugins import PLATFORM_PLUGINS_GROUP, load_plugins_by_group
logger = logging.getLogger(__name__)
_current_platform: SRTPlatform | None = None
def _resolve_platform() -> SRTPlatform:
"""
Discover and instantiate the active platform.
Discovery flow:
1. Branch on SGLANG_PLATFORM:
SGLANG_PLATFORM set (front-loading filter):
- Enumerate entry_points without importing any plugin modules
- Only ep.load() + activate() the named plugin
- Other plugins are never imported (avoids pulling their dependencies)
- Plugin name not found RuntimeError
- activate() returns None RuntimeError (hardware unavailable)
SGLANG_PLATFORM unset (auto-discover):
- Import and activate all discovered plugins
- 0 activated fallback base SRTPlatform
- 1 activated use it
- N activated RuntimeError (must set SGLANG_PLATFORM)
SGLANG_PLATFORM matches against entry_point names.
"""
selected = envs.SGLANG_PLATFORM.get()
if selected:
# Front-loading filter: only import and activate the specified plugin.
# Other plugins' modules are never loaded — avoids pulling their deps.
discovered = entry_points(group=PLATFORM_PLUGINS_GROUP)
ep_map = {ep.name: ep for ep in discovered}
if selected not in ep_map:
available = ", ".join(f"'{n}'" for n in ep_map) if ep_map else "none"
raise RuntimeError(
f"SGLANG_PLATFORM={selected!r} not found in discovered platform plugins "
f"(available: {available}). Install the plugin with 'pip install -e' "
f"to register its entry_points."
)
try:
plugin_fn = ep_map[selected].load()
result = plugin_fn()
except Exception:
logger.exception("Failed to activate platform plugin: %s", selected)
raise
if result is None:
raise RuntimeError(
f"Platform plugin {selected!r} is installed but activate() returned None "
f"(hardware not available on this machine?)."
)
logger.info("OOT platform plugin activated: %s -> %s", selected, result)
return _load_platform_class(result)()
# Auto-discover: import and activate all plugins, expect exactly one
all_plugins = load_plugins_by_group(PLATFORM_PLUGINS_GROUP)
activated: dict[str, str] = {}
for name, (plugin_fn, _dist) in all_plugins.items():
try:
result = plugin_fn()
if result is not None:
activated[name] = result
logger.info("OOT platform plugin activated: %s -> %s", name, result)
except Exception:
logger.exception("Failed to activate platform plugin: %s", name)
if len(activated) == 0:
logger.warning("No platform detected. Using base SRTPlatform with defaults.")
return SRTPlatform()
if len(activated) == 1:
name, qualname = next(iter(activated.items()))
return _load_platform_class(qualname)()
# Multiple activated without SGLANG_PLATFORM
names_str = ", ".join(f"'{n}'" for n in activated)
raise RuntimeError(
f"Multiple platform plugins activated: {names_str}. "
f"Set SGLANG_PLATFORM to select one."
)
def _load_platform_class(qualname: str) -> type:
"""Load an SRTPlatform subclass from its fully-qualified class name."""
cls = pkgutil.resolve_name(qualname)
if not isinstance(cls, type) or not issubclass(cls, SRTPlatform):
raise TypeError(
f"Expected an SRTPlatform subclass, got {type(cls)}: {qualname}"
)
return cls
def __getattr__(name: str):
"""Lazy initialization of current_platform on first access."""
if name == "current_platform":
global _current_platform
if _current_platform is None:
_current_platform = _resolve_platform()
return _current_platform
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+244
View File
@@ -0,0 +1,244 @@
"""
Shared device abstraction for SGLang platforms.
DeviceMixin provides the common device identity queries and operations
shared between the SRT (LLM inference) and Multimodal (diffusion)
platform hierarchies. Concrete per-device mixins (e.g. MyDeviceMixin)
implement the abstract operations; subsystem-specific platforms
(SRTPlatform, MMPlatform) inherit DeviceMixin and add their own methods.
Hierarchy example (OOT plugin)::
DeviceMixin
MyDeviceMixin(DeviceMixin) # vendor-specific device operations
SRTPlatform(DeviceMixin) # + graph runner, KV pool, …
MySRTPlatform(SRTPlatform, MyDeviceMixin)
MMPlatform(DeviceMixin) # + attention backend, VAE, …
MyMMPlatform(MMPlatform, MyDeviceMixin)
Method status annotations:
- ``[Active]`` SGLang core calls this method through ``current_platform``.
OOT implementations take effect immediately.
- ``[Planned]`` Reserved interface. SGLang core still uses hardcoded calls
(e.g. ``torch.cuda.empty_cache()``). OOT implementations will NOT take
effect until the core is migrated in a future PR.
"""
import enum
from typing import TYPE_CHECKING, NamedTuple, Optional
if TYPE_CHECKING:
import torch
class PlatformEnum(enum.Enum):
"""Enumeration of known platform types.
Superset of both SRT and MM enums so that a single PlatformEnum can
be shared across subsystems.
"""
CUDA = enum.auto()
ROCM = enum.auto()
CPU = enum.auto()
XPU = enum.auto()
MUSA = enum.auto()
NPU = enum.auto()
TPU = enum.auto()
MPS = enum.auto()
OOT = enum.auto() # Out-of-tree (external plugin)
UNSPECIFIED = enum.auto()
class CpuArchEnum(enum.Enum):
"""CPU architecture enumeration."""
X86 = enum.auto()
ARM = enum.auto()
UNSPECIFIED = enum.auto()
class DeviceCapability(NamedTuple):
"""Device compute capability (major, minor).
Uses NamedTuple for built-in comparison support:
``DeviceCapability(9, 0) >= DeviceCapability(8, 9)`` works naturally.
"""
major: int
minor: int
def as_version_str(self) -> str:
return f"{self.major}.{self.minor}"
def to_int(self) -> int:
"""Express capability as ``<major><minor>`` (minor is single digit)."""
assert 0 <= self.minor < 10
return self.major * 10 + self.minor
class DeviceMixin:
"""Mixin providing device identity queries and basic device operations.
Class-level attributes (override in subclasses):
_enum: PlatformEnum identifying this platform.
device_name: Human-readable short name (e.g. "cuda", "npu").
device_type: ``torch.device`` type string (e.g. "cuda", "npu").
"""
_enum: PlatformEnum = PlatformEnum.UNSPECIFIED
device_name: str = "unknown"
device_type: str = "cpu"
# ------------------------------------------------------------------
# Platform identity queries
# ------------------------------------------------------------------
def is_cuda(self) -> bool:
return self._enum == PlatformEnum.CUDA
def is_rocm(self) -> bool:
return self._enum == PlatformEnum.ROCM
def is_cpu(self) -> bool:
return self._enum == PlatformEnum.CPU
def is_xpu(self) -> bool:
return self._enum == PlatformEnum.XPU
def is_musa(self) -> bool:
return self._enum == PlatformEnum.MUSA
def is_npu(self) -> bool:
return self._enum == PlatformEnum.NPU
def is_tpu(self) -> bool:
return self._enum == PlatformEnum.TPU
def is_mps(self) -> bool:
return self._enum == PlatformEnum.MPS
def is_cuda_alike(self) -> bool:
"""True for CUDA, ROCm, or MUSA (all expose CUDA-like APIs)."""
return self._enum in (
PlatformEnum.CUDA,
PlatformEnum.ROCM,
PlatformEnum.MUSA,
)
def is_out_of_tree(self) -> bool:
"""True for externally-registered OOT platforms."""
return self._enum == PlatformEnum.OOT
# ------------------------------------------------------------------
# Active methods — core calls these through current_platform.
# OOT implementations take effect immediately.
# ------------------------------------------------------------------
def get_device_total_memory(self, device_id: int = 0) -> int:
"""[Active] Get total device memory in bytes."""
raise NotImplementedError
def get_current_memory_usage(
self, device: Optional["torch.device"] = None
) -> float:
"""[Active] Get current peak memory usage in bytes."""
raise NotImplementedError
# ------------------------------------------------------------------
# Planned methods — reserved interface. Core still uses hardcoded
# calls (e.g. torch.cuda.*). OOT implementations will NOT take
# effect until the core is migrated in a future PR.
# ------------------------------------------------------------------
# ---- Device management ----
def get_device(self, local_rank: int) -> "torch.device":
"""[Planned] Return ``torch.device`` for the given local rank."""
raise NotImplementedError
def set_device(self, device: "torch.device") -> None:
"""[Planned] Set the current device."""
raise NotImplementedError
def get_device_name(self, device_id: int = 0) -> str:
"""[Planned] Get human-readable device name."""
raise NotImplementedError
def get_device_uuid(self, device_id: int = 0) -> str:
"""[Planned] Get unique device identifier string."""
raise NotImplementedError
def get_device_capability(self, device_id: int = 0) -> Optional["DeviceCapability"]:
"""[Planned] Get device compute capability. None if N/A."""
raise NotImplementedError
def empty_cache(self) -> None:
"""[Planned] Release cached device memory. No-op for CPU-like platforms."""
pass
def synchronize(self) -> None:
"""[Planned] Synchronize device operations. No-op for CPU-like platforms."""
pass
# ---- Memory ----
def get_available_memory(self, device_id: int = 0) -> tuple[int, int]:
"""[Planned] Return ``(free_bytes, total_bytes)``."""
raise NotImplementedError
# ---- Distributed ----
def get_torch_distributed_backend_str(self) -> str:
"""[Planned] Return the torch.distributed backend string (e.g. "nccl", "hccl")."""
raise NotImplementedError
def get_communicator_class(self) -> type | None:
"""[Planned] Return platform-specific communicator class, or None for default."""
return None
# ---- Misc ----
@classmethod
def inference_mode(cls):
"""[Planned] Return inference mode context manager."""
import torch
return torch.inference_mode(mode=True)
@classmethod
def seed_everything(cls, seed: int | None = None) -> None:
"""[Planned] Set random seeds for reproducibility across all libraries."""
if seed is not None:
import random
import numpy as np
import torch
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
def verify_quantization(self, quant: str) -> None:
"""[Planned] Validate that a quantization method is supported. No-op by default."""
pass
@classmethod
def get_cpu_architecture(cls) -> "CpuArchEnum":
"""[Planned] Detect CPU architecture."""
import platform as _platform
machine = _platform.machine().lower()
if machine in ("x86_64", "amd64", "i386", "i686"):
return CpuArchEnum.X86
elif machine in ("arm64", "aarch64"):
return CpuArchEnum.ARM
return CpuArchEnum.UNSPECIFIED
# ------------------------------------------------------------------
# Dunder helpers
# ------------------------------------------------------------------
def __repr__(self) -> str:
return f"{self.__class__.__name__}(device={self.device_name})"
+133
View File
@@ -0,0 +1,133 @@
"""
SGLang SRT Hardware Platform Abstraction.
Defines SRTPlatform the base class for SRT (LLM inference) platform
backends. SRTPlatform inherits DeviceMixin for shared device operations
and adds SRT-specific subsystem factory methods, capability flags, and
configuration lifecycle hooks.
Out-of-tree platforms register via setuptools entry_points under the
"sglang.platform_plugins" group and should subclass SRTPlatform.
"""
from typing import TYPE_CHECKING
from sglang.srt.platforms.device_mixin import DeviceMixin, PlatformEnum
if TYPE_CHECKING:
pass
# Re-export for convenience
__all__ = ["SRTPlatform", "PlatformEnum"]
class SRTPlatform(DeviceMixin):
"""
Base class for SRT hardware platform backends.
Inherits device identity queries and operations from DeviceMixin.
Adds SRT-specific factory methods, capability flags, and lifecycle hooks.
OOT platforms should subclass SRTPlatform and override the methods
relevant to their hardware.
"""
# SRT-specific class-level attribute
supported_quantization: list[str] = []
# ------------------------------------------------------------------
# Configuration lifecycle
# ------------------------------------------------------------------
def apply_server_args_defaults(self, server_args) -> None:
"""Apply platform-specific default values to server arguments.
Called after ServerArgs is parsed.
"""
pass
# ------------------------------------------------------------------
# Subsystem factory methods
# ------------------------------------------------------------------
def get_default_attention_backend(self) -> str:
"""Return the default attention backend name for this platform."""
raise NotImplementedError
def get_graph_runner_cls(self) -> type:
"""Return the graph runner class for this platform."""
raise NotImplementedError
def get_mha_kv_pool_cls(self) -> type:
"""Return the MHA KV pool class for this platform."""
raise NotImplementedError
def get_mla_kv_pool_cls(self) -> type:
"""Return the MLA KV pool class for this platform."""
raise NotImplementedError
def get_nsa_kv_pool_cls(self) -> type:
"""Return the NSA KV pool class for this platform (DeepSeek V3.2)."""
raise NotImplementedError
def get_paged_allocator_cls(self) -> type:
"""Return the paged allocator class for this platform."""
raise NotImplementedError
def get_compile_backend(self, mode: str | None = None) -> str:
"""Return the compilation backend identifier.
``mode`` is an optional hint for the platform (e.g. "npugraph_ex").
"""
return "inductor"
def get_piecewise_backend_cls(self) -> type:
"""Return the piecewise compilation backend class for this platform."""
raise NotImplementedError
# ------------------------------------------------------------------
# Capability flags (safe conservative defaults)
# ------------------------------------------------------------------
def supports_fp8(self) -> bool:
"""Whether this platform supports FP8 quantization."""
return False
def is_pin_memory_available(self) -> bool:
"""Whether pinned memory is available on this platform."""
return True
def support_cuda_graph(self) -> bool:
"""Whether this platform supports device graph capture and replay.
Controls CUDA graph (CudaGraphRunner) for the decode path.
OOT platforms that support graph-style capture should return True.
"""
return False
def support_piecewise_cuda_graph(self) -> bool:
"""Whether this platform supports piecewise CUDA graph.
Controls PiecewiseCudaGraphRunner for the prefill/extend path
(torch.compile backend).
"""
return False
# ------------------------------------------------------------------
# Initialization
# ------------------------------------------------------------------
def init_backend(self) -> None:
"""One-time backend initialization. Called in each worker."""
pass
# ------------------------------------------------------------------
# MultiPlatformOp integration
# ------------------------------------------------------------------
def get_dispatch_key_name(self) -> str:
"""Return the dispatch key name for MultiPlatformOp.
Determines which ``forward_<key>()`` method is selected.
E.g. "cuda", "npu", "hip", "xpu", "cpu".
"""
return "native"
+141
View File
@@ -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()
+430
View File
@@ -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
+17
View File
@@ -790,6 +790,11 @@ class ServerArgs:
self._handle_mps_backends()
self._handle_xpu_backends()
# Allow OOT platform plugins to apply server args defaults.
from sglang.srt.platforms import current_platform
current_platform.apply_server_args_defaults(self)
# Handle piecewise CUDA graph.
self._handle_piecewise_cuda_graph()
@@ -1157,6 +1162,12 @@ class ServerArgs:
# 5. Non-CUDA hardware (AMD, NPU, CPU, MPS, XPU, etc.)
if is_hip() or is_npu() or is_cpu() or is_mps() or is_xpu():
self.disable_piecewise_cuda_graph = True
# 5b. OOT platforms that don't support piecewise cuda graph
from sglang.srt.platforms import current_platform
if current_platform.is_out_of_tree():
if not current_platform.support_piecewise_cuda_graph():
self.disable_piecewise_cuda_graph = True
# 6. MoE A2A backend
if self.moe_a2a_backend != "none":
self.disable_piecewise_cuda_graph = True
@@ -2326,6 +2337,12 @@ class ServerArgs:
2.2 We will use Flashinfer backend on blackwell.
2.3 Otherwise, we will use triton backend.
"""
# OOT platforms provide their own default attention backend.
from sglang.srt.platforms import current_platform
if current_platform.is_out_of_tree():
return current_platform.get_default_attention_backend()
# Whisper requires flashinfer for cross-attention CUDA graph support
if "WhisperForConditionalGeneration" in (
model_config.hf_config.architectures or []
+26
View File
@@ -590,6 +590,18 @@ def get_available_gpu_memory(
free_gpu_memory, total_gpu_memory = torch.musa.mem_get_info()
elif device == "mps":
free_gpu_memory = psutil.virtual_memory().available
else:
from sglang.srt.platforms import current_platform
if not current_platform.is_out_of_tree():
raise ValueError(
f"Unsupported device type: {device!r}. "
"If this is an OOT platform, ensure it is properly registered "
"via the 'sglang.platform_plugins' entry point."
)
total_mem = current_platform.get_device_total_memory(gpu_id)
used_mem = current_platform.get_current_memory_usage()
free_gpu_memory = total_mem - used_mem
if distributed:
tensor = torch.tensor(free_gpu_memory, dtype=torch.float32)
@@ -1671,6 +1683,14 @@ def get_mtgpu_memory_capacity():
def get_device_memory_capacity(device: str = None):
# OOT platforms provide their own memory query via the platform class.
from sglang.srt.platforms import current_platform
if current_platform.is_out_of_tree():
mem_bytes = current_platform.get_device_total_memory()
if mem_bytes:
return mem_bytes / (1 << 20) # bytes -> MiB
return None
if is_cuda():
gpu_mem = get_nvgpu_memory_capacity()
elif is_hip():
@@ -1913,6 +1933,12 @@ def get_device_capability(device_id: int = 0) -> Tuple[int, int]:
def get_compiler_backend(mode=None) -> str:
# OOT platforms provide their own compile backend.
from sglang.srt.platforms import current_platform
if current_platform.is_out_of_tree():
return current_platform.get_compile_backend(mode)
if hasattr(torch, "hpu") and torch.hpu.is_available():
return "hpu_backend"