[diffusion] feat: out of tree platform support (#37547)

Co-authored-by: Mick Qian <mickqian@users.noreply.github.com>
This commit is contained in:
hmalgewatta
2026-09-19 09:35:58 +08:00
committed by GitHub
co-authored by Mick Qian
parent 986959e3c4
commit c475ac5eaf
40 changed files with 3104 additions and 333 deletions
+265 -40
View File
@@ -19,19 +19,19 @@ The framework provides two plugin types, both discovered via Python's standard `
<thead>
<tr>
<th>Plugin Type</th>
<th>Entry Point Group</th>
<th>Entry Point Groups</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Hardware Platform Plugin</strong></td>
<td><code>sglang.srt.platforms</code></td>
<td><code>sglang.srt.platforms</code><br/><code>sglang.multimodal_gen.platforms</code></td>
<td>Register a custom hardware platform (device operations, KV cache pools, attention backends, graph capture, compilation backends, etc.)</td>
</tr>
<tr>
<td><strong>General Plugin</strong></td>
<td><code>sglang.srt.plugins</code></td>
<td><code>sglang.srt.plugins</code><br/><code>sglang.multimodal_gen.plugins</code></td>
<td>Inject hooks (before/after/around/replace) into any function/method, or replace entire classes</td>
</tr>
</tbody>
@@ -39,44 +39,38 @@ The framework provides two plugin types, both discovered via Python's standard `
### Principles
- **Non-intrusive**: Existing CUDA/ROCm/NPU/XPU code remains unchanged. OOT code paths are added alongside existing hardware-specific logic.
- **Zero configuration**: Plugins are automatically discovered after `pip install`, no sglang code changes required.
- **Environment variable control**: `SGLANG_PLATFORM` selects or validates the active platform plugin; `SGLANG_PLUGINS` (comma-separated) controls which general plugins to load.
- **Non-intrusive**: Built-in platforms remain the fallback when no OOT platform activates.
- **Install-time discovery**: Plugins are discovered from Python entry points after installation.
- **Environment variable control**: `SGLANG_PLATFORM` selects an SRT platform, `SGLANG_DIFFUSION_PLATFORM_OVERRIDE` selects a diffusion platform, and `SGLANG_PLUGINS` filters general plugins in either hook group.
### Current Scope & Future Direction
### Current scope
The plugin system currently targets **out-of-tree (OOT) hardware platforms** — enabling new devices to integrate with SGLang without any changes to the main repository. The main-repo hardware paths (CUDA, ROCm, NPU, XPU, etc.) continue to use the existing `is_cuda()`/`is_npu()`/… utility functions.
As the plugin interfaces mature and stabilize, in-tree hardware backends can be gradually migrated to the same plugin architecture. This would replace the scattered `if device == "cuda" … elif device == "npu" …` branches throughout the codebase with a single polymorphic dispatch through the platform interface, making each hardware backend self-contained and the core engine hardware-agnostic.
The platform plugin system targets **out-of-tree (OOT) hardware platforms**. Diffusion support is experimental and covers the seams documented below, not every device-specific branch.
## Architecture
### Platform Hierarchy
### Runtime-specific platform interfaces
The platform hierarchy uses a DeviceMixin pattern to share device operations between SRT (LLM inference) and Multimodal subsystems:
SRT and diffusion have separate platform base classes and platform identity types in `sglang.srt.platforms` and `sglang.multimodal_gen.runtime.platforms`. A package supporting both runtimes should define separate platform classes and use each runtime's own `PlatformEnum.OOT` value.
```
DeviceMixin (shared device identity + operations)
├── SRTPlatform(DeviceMixin) # + graph runner, KV pool, …
│ └── MySRTPlatform(SRTPlatform, MyDeviceMixin) # OOT plugin
└── MMPlatform(DeviceMixin) # + attention backend, VAE, … (future)
└── MyMMPlatform(MMPlatform, MyDeviceMixin) # OOT plugin
Each platform entry point resolves to a zero-argument activation callback:
```python
def activate() -> str | None:
"""Return the platform class qualname when this hardware is available."""
```
Key design points:
- **DeviceMixin** provides platform identity queries (`is_cuda()`, `is_npu()`, etc.) and device operations (`set_device()`, `get_device_name()`, etc.)
- **SRTPlatform** adds SRT-specific factory methods, capability flags, and lifecycle hooks
- OOT plugins implement a **device mixin** (vendor-specific operations) and compose it with **SRTPlatform** via multiple inheritance
- All methods are **instance methods** (not classmethods), called through the `current_platform` singleton
- Device operations and factory methods raise `NotImplementedError` by default (fail-fast)
- Capability flags use safe conservative defaults (`False`/`pass`)
- Methods are annotated `[Active]` (called by SGLang core) or `[Planned]` (reserved for future migration)
Return a fully qualified class name when the provider can run, or `None` otherwise. Keep activation import-safe: do not access `current_platform` or initialize runtime or device state. Put required backend setup in the platform's `init_backend()` method and reserve general plugins for hooks that the platform interface cannot express.
### Platform Discovery (`current_platform`)
Explicit selection enumerates entry-point metadata and imports only the selected callback. Automatic selection invokes installed platform callbacks to determine which provider is active.
Diffusion reserves `cpu`, `cuda`, `rocm`, `xpu`, `mps`, `npu`, and `musa`. Automatic discovery also rejects duplicate entry-point names; explicit selection validates only the selected name and does not import unrelated providers.
#### SRT selection
`current_platform` is a **lazy singleton** in `sglang.srt.platforms`. On first access it resolves the active platform through the following priority chain:
```
```text
entry_points("sglang.srt.platforms") → Enumerate ALL plugins by name (metadata only)
├─ SGLANG_PLATFORM set (front-loading filter):
@@ -94,9 +88,43 @@ entry_points("sglang.srt.platforms") → Enumerate ALL plugins by name (metadat
└─ N activated → RuntimeError (must set SGLANG_PLATFORM)
```
#### Diffusion selection
SGLang Diffusion resolves its platform in this order:
1. When `SGLANG_DIFFUSION_PLATFORM_OVERRIDE` names `cpu`, `cuda`, `rocm`, `mps`, `npu`, or `musa`, select that built-in platform without hardware probing. XPU remains automatic-only, preserving the existing selector behavior.
2. When it contains another name, load only the matching `sglang.multimodal_gen.platforms` entry point. An unknown name or a callback that returns `None` is an error.
3. When it is unset, activate installed diffusion platform plugins. No active plugin continues to built-in detection, one selects that plugin, and multiple active plugins raise an error that asks you to set the selector. An activation callback that raises aborts startup rather than falling back, so a broken vendor runtime cannot silently run the job on a built-in platform.
4. Try built-in platforms in order: MPS, XPU, ROCm, CUDA, NPU, MUSA, then CPU.
The existing override variable is therefore the single explicit selector for supported built-in aliases and OOT entry-point names.
Selection resolves lazily, the first time anything in a process touches `current_platform`, so it needs no call site and happens in every process automatically. SGLang Diffusion also records which distribution supplied the selected platform, and skips the hooks of every other installed platform package.
#### Required platform initialization vs. optional hooks
The two mechanisms have different failure semantics, and it matters which one you use:
| | Platform contract | General plugin |
| --- | --- | --- |
| Entry-point group | `sglang.multimodal_gen.platforms` | `sglang.multimodal_gen.plugins` |
| Delivery | methods on your `Platform` subclass | hooks that monkey-patch a target |
| Activation | lazy `current_platform`, plus guarded `init_backend()` in each worker | registration with `load_plugins()`, then explicit `apply_plugin_hooks()` per process |
| A failure | aborts startup | aborts startup if the plugin ships in the selected platform's distribution; otherwise logged |
Anything your hardware needs in order to be correct belongs on the `Platform` subclass, so a broken platform cannot silently serve. Reach for a general plugin only when the `Platform` interface has no seam for what you need — and please report that gap.
A plugin shipped in the selected platform's own distribution is treated as part of that platform's contract: a failure to load it, to run its callback, or to apply any hook it registered aborts startup rather than leaving the platform half-initialized. Plugins from any other installed package stay best-effort, so a broken third party cannot take the server down. An explicit `SGLANG_PLUGINS` allowlist can disable any general plugin, including one from the selected platform package; required hardware setup therefore belongs in `init_backend()`.
### Plugin Loading Flow
`load_plugins()` discovers and executes general plugins, then applies all registered hooks. It is called at four points:
Each runtime has a process-local hook registry. SRT retains its single
`load_plugins()` activation step. Diffusion separates registration
(`load_plugins()`) from target resolution (`apply_plugin_hooks()`), because
resolving a dotted hook target can import that target's entire module graph.
Both runtimes honor `SGLANG_PLUGINS`.
The loader is called at these SRT entry points:
<table style={{width: "100%", borderCollapse: "collapse", tableLayout: "fixed"}}>
<colgroup>
@@ -113,7 +141,7 @@ entry_points("sglang.srt.platforms") → Enumerate ALL plugins by name (metadat
</thead>
<tbody>
<tr>
<td><code>cli/serve.py</code> serve()</td>
<td><code>cli/serve.py</code> <code>serve()</code></td>
<td>Main</td>
<td>Before <code>prepare_server_args()</code></td>
</tr>
@@ -135,9 +163,9 @@ entry_points("sglang.srt.platforms") → Enumerate ALL plugins by name (metadat
</tbody>
</table>
> **Note**: `load_plugins()` is idempotent (guarded by `_plugins_loaded` flag). In spawn'd subprocesses the flag resets, so plugins are correctly re-loaded.
> **Note**: Diffusion plugin registration and hook application each run once per process. Spawned subprocesses start from a blank interpreter and establish their own hooks — nothing the parent patched survives the spawn boundary. SRT's `load_plugins()` performs both phases in one call.
```
```text
load_plugins()
├── _get_excluded_dists() → compute dists to skip (via SGLANG_PLATFORM)
├── load_plugins_by_group("sglang.srt.plugins", → discover entry_points, filter by SGLANG_PLUGINS
@@ -147,15 +175,76 @@ load_plugins()
└── HookRegistry.apply_hooks() → monkey-patch targets
```
Diffusion launchers call `apply_plugin_hooks()`, which first performs registration
if necessary and then resolves and patches targets. Scheduler children use a
stricter lifecycle in `runtime/managers/worker_bootstrap.py`:
```text
spawn unpickles SchedulerProcessSpec (stdlib types + opaque ServerArgs bytes)
-> initialize_current_platform()
-> load_plugins() # discover callbacks and register hooks only
-> apply_plugin_hooks() # target imports are allowed from here onward
-> materialize ServerArgs
-> resolve and invoke the patched run_scheduler_process
```
HTTP children likewise finish plugin registration and hook application before
importing `runtime.launch_server` or materializing `ServerArgs`. This matters for
class replacements and other hooks whose effects cannot be retroactively applied
to classes or registrations created while importing the server module graph.
The bootstrap module is the `mp.Process` target and imports no diffusion runtime
module at module scope. `ServerArgs` is serialized inside `ServerArgsPayload`, so
the multiprocessing unpickler cannot import pipeline configuration modules
before the target starts. Workers always use a local `spawn` context, independent
of an embedding application's global multiprocessing setting.
This ordering makes backend initialization precede plugin callback imports,
hook-target resolution, `ServerArgs` materialization, and worker imports. A
platform activation module necessarily loads before its own `init_backend()`;
activation modules must therefore remain import-safe. Prefer platform methods
and registries for required behavior.
Each activation phase runs once per process behind a lock: a re-entrant call
from a plugin callback returns and lets the outer call finish, another thread
waits for it, and a failure is terminal. A callback must not hand activation to
a second thread and join it — that deadlocks.
#### Offline scripts and the spawn boundary
`spawn` re-executes the launching script's module scope in every child *before*
it unpickles the target's arguments, so an offline script's own imports run
ahead of that child's platform initialization and `ServerArgsPayload` cannot
help. The supported script layout is:
```python
from sglang.multimodal_gen import DiffGenerator # a proxy: imports nothing yet
if __name__ == "__main__":
generator = DiffGenerator.from_pretrained(model_path="...")
```
`DiffGenerator` on the `sglang.multimodal_gen` facade is a lazy proxy, so
binding it at module scope costs no diffusion import and the child still reaches
`initialize_current_platform()` with a clean module table. Module scope also
stays open to `envs` and `runtime.platforms`, including `runtime.platforms.plugins`.
These modules remain import-safe so a plugin can subclass `Platform` and register
hooks before backend initialization. Everything else, including `SamplingParams` and `PipelineConfig`
from the same facade, belongs inside the `if __name__ == "__main__":` guard or
inside the function that uses it. A child
that finds runtime modules already imported names them in a warning: hook
application can still patch those modules, but classes and registrations
created while importing them are already past reach.
---
## Plugin Type 1: Hardware Platform Plugin
### Description
A hardware platform plugin registers an `SRTPlatform` subclass that tells SGLang how to interact with a specific hardware backend.
A hardware platform plugin registers an SRT `SRTPlatform` subclass, a diffusion `Platform` subclass, or both. The selected class tells that runtime how to interact with a specific hardware backend.
### Quick Start
### SRT quick start
**1. Create a minimal package:**
@@ -230,7 +319,7 @@ pip install -e my_platform_plugin/
python -c "from sglang.srt.platforms import current_platform; print(current_platform)"
```
### Platform Interface Reference
### SRT platform interface reference
#### Identity Queries (from DeviceMixin)
@@ -578,7 +667,7 @@ python -c "from sglang.srt.platforms import current_platform; print(current_plat
</tbody>
</table>
### Environment Variables
### Platform and plugin environment variables
<table style={{width: "100%", borderCollapse: "collapse", tableLayout: "fixed"}}>
<colgroup>
@@ -598,18 +687,138 @@ python -c "from sglang.srt.platforms import current_platform; print(current_plat
</tr>
<tr>
<td><code>SGLANG_PLUGINS</code></td>
<td>Comma-separated whitelist of general plugin names to load (group: <code>sglang.srt.plugins</code>). If unset, all discovered general plugins are loaded.</td>
<td>Comma-separated whitelist of general plugin names to load from either hook group. It also filters automatic SRT platform discovery when <code>SGLANG_PLATFORM</code> is unset; explicit platform selection ignores it.</td>
</tr>
</tbody>
</table>
### Add diffusion support
A package may support SRT, SGLang Diffusion, or both. Diffusion uses a zero-argument `Platform` subclass and separate platform and hook entry points:
```toml
[project.entry-points."sglang.multimodal_gen.platforms"]
my_device = "my_platform_plugin:activate_diffusion"
[project.entry-points."sglang.multimodal_gen.plugins"]
my_device = "my_platform_plugin:register_hooks"
```
Keep activation import-safe and return the fully qualified class name only when the backend is available:
```python
def activate_diffusion() -> str | None:
try:
import my_device_runtime
except ImportError:
return None
if not my_device_runtime.is_available():
return None
return "my_platform_plugin.diffusion_platform.MyDiffusionPlatform"
```
The referenced class must be zero-argument constructible:
```python
from sglang.multimodal_gen.runtime.platforms import Platform, PlatformEnum
class MyDiffusionPlatform(Platform):
_enum = PlatformEnum.OOT
device_name = "my_device"
device_type = "my_device"
dispatch_key = "PrivateUse1"
def get_dispatch_key_name(self) -> str:
return "my_device"
```
Set `dispatch_key` to the PyTorch dispatcher key used by direct `torch.library` registrations. Keep it separate from `get_dispatch_key_name()`, which selects `CustomOp` implementations. Bring up the remaining contract in dependency order:
1. Implement `get_device_name()`, `get_device_total_memory()`, and `get_available_gpu_memory()` before constructing `ServerArgs`.
2. Implement `get_device()` and `get_local_torch_device()` before worker binding.
3. Configure distributed initialization, attention selection, and custom-op implementations for supported workloads.
4. Run an end-to-end workload and audit model-specific kernels, compilation, and remaining device-family branches.
Register custom-op implementations from `init_backend()`, which runs once in each worker before any pipeline module is constructed:
```python
class MyDiffusionPlatform(Platform):
...
def init_backend(self) -> None:
from my_platform_plugin.ops import rms_norm_forward
from sglang.multimodal_gen.runtime.layers.custom_op import CustomOp
from sglang.multimodal_gen.runtime.layers.layernorm import RMSNorm
CustomOp.register_oot_forward(
RMSNorm, fn=rms_norm_forward, platform_key="my_device"
)
```
`init_backend()` runs at most once per worker process, before the worker implementation is imported. Raising aborts startup and the failed initialization is not retried in that process, because registrations and other backend side effects may be only partially reversible. Custom-op dispatch is resolved when each operation is constructed, after backend initialization has completed. The selected callable therefore remains stable when the operation is compiled instead of changing on the first compiled call.
The function receives the operation instance before its normal arguments. Registration matches the exact operation class and the value returned by `get_dispatch_key_name()`. Without a registration, SGLang looks for a matching `forward_<key>()` implementation on the operation, then falls back to `forward_oot()`, whose base implementation calls `forward_native()`. Returning `"cuda"`, for example, lets a CUDA-compatible OOT platform reuse operation-specific `forward_cuda()` implementations without reporting CUDA platform identity.
The `sglang.multimodal_gen.plugins` entry point remains for seams the `Platform` interface does not cover. Its callbacks run in launchers and workers before scheduler or model construction, so use a `BEFORE` or `AROUND` hook on `GPUWorker.init_device_and_model()` for rank-aware initialization. Hooks from platform distributions other than the selected one are skipped. Because these ship in your platform's distribution, a failure at any stage — load, callback, or hook application — aborts startup, the same as `init_backend()`.
#### Diffusion platform contract
**Active** interfaces are called through `current_platform`. **Compatibility** interfaces are retained adapters; unlisted `Platform` methods are not a stable OOT contract.
| Status | Area | Interface |
| --- | --- | --- |
| Active | Initialization | `init_backend()` |
| Active | Configuration | `apply_server_args_defaults()` |
| Active | Device and memory | `get_device()`, `set_device()`, `get_local_torch_device()`, `get_device_name()`, `get_device_uuid()`, `get_device_total_memory()`, `get_available_gpu_memory()`, `get_device_capability()` |
| Active | Distributed | `get_torch_distributed_backend_str()`, `supports_distributed_device_id()`, `get_all_to_all_communicator_cls()`, `get_cpu_architecture()` |
| Active | Dispatch and model | `dispatch_key` (read by `get_torch_library_dispatch_key()`), `get_dispatch_key_name()`, `get_attn_backend_cls_str()`, `verify_model_arch()`, `optimize_vae()` |
| Active | Execution | `get_compile_backend()`, `get_compile_options()`, `inference_mode()`, `seed_everything()`, `enable_dit_layerwise_offload_by_default()` |
| Compatibility | Communicator | `get_device_communicator_cls()` remains the fallback used by `get_all_to_all_communicator_cls()` for existing platform subclasses. New OOT platforms should override the latter. |
Install the package, restart Python to refresh entry-point metadata, and verify explicit selection:
```bash
pip install -e my_platform_plugin/
SGLANG_DIFFUSION_PLATFORM_OVERRIDE=my_device python -c \
"from sglang.multimodal_gen.runtime.platforms import current_platform; print(current_platform)"
```
### Diffusion platform limitations
- SRT and diffusion require separate platform classes.
- Only one external platform can be active per process; set the runtime selector if multiple callbacks activate.
- `get_all_to_all_communicator_cls()` controls only `all_to_all_4D()`, not every collective, graph-capture, or synchronization path; `get_device_communicator_cls()` remains for compatibility.
- Compile settings affect only `build_torch_compile_kwargs()` callers; audit static `@torch.compile` decorators and other direct compile paths.
- Explicit attention selectors accept only `AttentionBackendEnum` names. Returning a custom backend works when none is selected; selecting it by name requires a hook or downstream patch.
- Device-family branches remain outside the interface. Each supported workload needs a native fallback or an actionable unsupported-feature error.
- The diffusion `PlatformEnum.OOT` identifies an external provider. Override built-in identity predicates only after auditing every enabled branch.
---
## Plugin Type 2: General Plugin
### Description
General function plugins inject behavior into sglang **without requiring a custom platform**. Use cases include:
General function plugins inject behavior into SRT or diffusion **without requiring a custom platform**. The two runtimes have separate entry-point groups and hook registries:
```python
# SRT plugin
from sglang.srt.plugins.hook_registry import HookType, plugin_hook
# Diffusion plugin
from sglang.multimodal_gen.runtime.platforms.plugins import HookType, plugin_hook
```
Register an SRT plugin under `sglang.srt.plugins` and a diffusion plugin under
`sglang.multimodal_gen.plugins`, and import the hook API from the matching
module above. Each runtime applies only its own registry, so a hook registered
through the other runtime's API never runs. The examples below use SRT.
The diffusion hook API lives alongside platform discovery in
`sglang.multimodal_gen.runtime.platforms.plugins`;
`sglang.multimodal_gen.plugins` is its entry-point group, not a Python module path.
Use cases include:
- **Observability**: Add logging, metrics, and tracing to any function
- **Behavior modification**: Modify function arguments or return values
@@ -766,7 +975,7 @@ Target paths use fully-qualified dotted notation. Both formats are supported:
- **Dotted**: `sglang.srt.managers.scheduler.Scheduler.__init__`
- **Entry-points style**: `sglang.srt.managers.scheduler:Scheduler.__init__` (colon treated as dot)
### Common Hook Targets
### Common SRT Hook Targets
<table style={{width: "100%", borderCollapse: "collapse", tableLayout: "fixed"}}>
<colgroup>
@@ -837,7 +1046,7 @@ Target paths use fully-qualified dotted notation. Both formats are supported:
<tbody>
<tr>
<td><code>sglang/srt/platforms/device_mixin.py</code></td>
<td><code>PlatformEnum</code> + <code>DeviceMixin</code> base class</td>
<td><code>DeviceMixin</code> base class and SRT platform identity types</td>
</tr>
<tr>
<td><code>sglang/srt/platforms/interface.py</code></td>
@@ -847,6 +1056,22 @@ Target paths use fully-qualified dotted notation. Both formats are supported:
<td><code>sglang/srt/platforms/__init__.py</code></td>
<td><code>current_platform</code> lazy singleton + discovery logic</td>
</tr>
<tr>
<td><code>sglang/multimodal_gen/runtime/platforms/interface.py</code></td>
<td>Diffusion <code>Platform</code> base class</td>
</tr>
<tr>
<td><code>sglang/multimodal_gen/runtime/platforms/__init__.py</code></td>
<td>Diffusion <code>current_platform</code> lazy singleton and built-in fallback order</td>
</tr>
<tr>
<td><code>sglang/multimodal_gen/runtime/platforms/plugins.py</code></td>
<td>Diffusion plugin registration, explicit hook-application phase, and hook registry</td>
</tr>
<tr>
<td><code>sglang/multimodal_gen/runtime/managers/worker_bootstrap.py</code></td>
<td>Import-neutral process specifications and spawn targets that initialize the backend before resolving runtime hooks</td>
</tr>
<tr>
<td><code>sglang/srt/plugins/__init__.py</code></td>
<td><code>load_plugins()</code> + <code>load_plugins_by_group()</code></td>
+11 -1
View File
@@ -11,7 +11,7 @@ This guide outlines the requirements for contributing to the SGLang Diffusion mo
- [Support New Models](/docs/sglang-diffusion/support_new_models): implementation guide for adding new diffusion pipelines
- [Helper ownership](/docs/sglang-diffusion/support_new_models#place-helpers-with-their-owners): where to put shared and model-specific utilities
- [CI Performance](./ci_perf): update and regenerate perf baselines
- [SGLang Plugin System](/docs/hardware-platforms/plugin): package a hardware platform outside the SGLang repository
## On AI-Assisted ("Vibe Coding") PRs
@@ -76,3 +76,13 @@ Consider adding tests to the `pr-test` or `nightly-test` suites to safeguard you
Please run the according testcase, then update/add the baseline to `perf_baselines.json` by following the instruction in console if applicable.
See [test](https://github.com/sgl-project/sglang/tree/main/python/sglang/multimodal_gen/test) for examples
## Out-of-tree platform changes
Use the `sglang.multimodal_gen.platforms` entry-point group when a hardware integration can live in a separately installed package. The [plugin guide](/docs/hardware-platforms/plugin#add-diffusion-support) describes the activation callback, diffusion platform contract, and selection behavior.
When you change the platform interface or discovery implementation in SGLang:
- Add focused CPU-only tests with a fake OOT platform.
- Run the relevant SRT plugin tests when changing shared hook-registry behavior.
- Document workload-specific kernels or optimized paths that still require vendor integration.
@@ -424,11 +424,12 @@ If not specified, parallelism is auto-derived from `--num-gpus`.
## Python API
For programmatic single-machine deployment, `launch_pool_disagg_server()` is available:
For programmatic single-machine deployment, `launch_pool_disagg_server()` is available. It spawns its workers, and each child re-executes this script's module scope before initializing its platform, so keep the diffusion imports inside the guard:
```python
from sglang.multimodal_gen.runtime.server_args import ServerArgs
if __name__ == "__main__":
from sglang.multimodal_gen.runtime.launch_server import launch_pool_disagg_server
from sglang.multimodal_gen.runtime.server_args import ServerArgs
server_args = ServerArgs.from_kwargs(
model_path="Wan-AI/Wan2.1-T2V-14B-Diffusers",
@@ -23,6 +23,16 @@ description: "Configure SGLang diffusion behavior with environment variables."
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>not set</td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}>Installed package that registers out-of-tree diffusion pipelines and component models. The package is imported once in every process.</td>
</tr>
<tr>
<td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}><code>SGLANG_DIFFUSION_PLATFORM_OVERRIDE</code></td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>not set</td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}>Select <code>cpu</code>, <code>cuda</code>, <code>rocm</code>, <code>mps</code>, <code>npu</code>, <code>musa</code>, or an installed <code>sglang.multimodal_gen.platforms</code> entry-point name. XPU remains automatic-only. When unset, SGLang Diffusion detects installed platform plugins before trying built-in platforms.</td>
</tr>
<tr>
<td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}><code>SGLANG_PLUGINS</code></td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>not set</td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}>Comma-separated allowlist for installed <code>sglang.multimodal_gen.plugins</code> hooks. When unset, all discovered diffusion hooks load.</td>
</tr>
<tr>
<td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}><code>SGLANG_DIFFUSION_TARGET_DEVICE</code></td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}><code>cuda</code></td>
@@ -68,11 +78,6 @@ description: "Configure SGLang diffusion behavior with environment variables."
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}><code>INFO</code></td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}>Default logging level</td>
</tr>
<tr>
<td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}><code>SGLANG_DIFFUSION_WORKER_MULTIPROC_METHOD</code></td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}><code>fork</code></td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}>Multiprocess context for workers (<code>fork</code> or <code>spawn</code>)</td>
</tr>
<tr>
<td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}><code>SGLANG_DIFFUSION_IPC_A2A</code></td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}><code>true</code></td>
+4
View File
@@ -112,6 +112,10 @@ def _run_diffusion(request: ServeRequest) -> None:
_print_diffusion_help(request)
return
from sglang.multimodal_gen.runtime.platforms.plugins import apply_plugin_hooks
apply_plugin_hooks()
from sglang.multimodal_gen.runtime.entrypoints.cli.serve import (
add_multimodal_gen_serve_args,
execute_serve_cmd,
+33 -1
View File
@@ -1,8 +1,40 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from sglang.utils import LazyImport
if TYPE_CHECKING:
from sglang.multimodal_gen.configs.pipeline_configs import PipelineConfig
from sglang.multimodal_gen.configs.sample import SamplingParams
from sglang.multimodal_gen.runtime.entrypoints.diffusion_generator import DiffGenerator
__all__ = ["DiffGenerator", "PipelineConfig", "SamplingParams"]
DiffGenerator = LazyImport(
"sglang.multimodal_gen.runtime.entrypoints.diffusion_generator",
"DiffGenerator",
)
def __getattr__(name: str) -> Any:
if name == "PipelineConfig":
from sglang.multimodal_gen.configs.pipeline_configs import PipelineConfig
value = PipelineConfig
elif name == "SamplingParams":
from sglang.multimodal_gen.configs.sample import SamplingParams
value = SamplingParams
else:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
globals()[name] = value
return value
def __dir__() -> list[str]:
return sorted({*globals(), *__all__})
# Trigger multimodal CI tests
@@ -21,13 +21,6 @@ from sglang.multimodal_gen.configs.pipeline_configs.base import (
from sglang.multimodal_gen.configs.pipeline_configs.model_deployment_config import (
ModelDeploymentConfig,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionRequirements,
)
from sglang.multimodal_gen.runtime.layers.attention.selector import (
get_attn_backend,
get_global_forced_attn_backend,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.component_residency import (
LAYERWISE_OFFLOAD,
)
@@ -108,6 +101,10 @@ class MiniMaxH3PipelineConfig(PipelineConfig):
self, server_args
) -> AttentionBackendEnum | None:
"""Resolve the H3 DiT backend using the selector's precedence."""
from sglang.multimodal_gen.runtime.layers.attention.selector import (
get_global_forced_attn_backend,
)
selected_backend = get_global_forced_attn_backend()
if selected_backend is None:
selected_backend, _ = server_args.resolve_component_attention_backend(
@@ -301,6 +298,13 @@ class MiniMaxH3PipelineConfig(PipelineConfig):
"validated under torch.compile or the breakable CUDA "
"graph; disable them or use --attention-backend fa."
)
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionRequirements,
)
from sglang.multimodal_gen.runtime.layers.attention.selector import (
get_attn_backend,
)
get_attn_backend(
self.dit_config.arch_config.attention_head_dim,
torch.bfloat16,
+2 -8
View File
@@ -28,7 +28,6 @@ if TYPE_CHECKING:
SGLANG_DIFFUSION_DEBUG_HOST_MEMORY: bool = False
SGLANG_DIFFUSION_DEBUG_LAYERWISE_TIMING: bool = False
SGLANG_DIFFUSION_DISABLE_LORA_MERGE_CACHE: bool = False
SGLANG_DIFFUSION_WORKER_MULTIPROC_METHOD: str = "fork"
SGLANG_DIFFUSION_TARGET_DEVICE: str = "cuda"
SGLANG_DIFFUSION_PLATFORM_OVERRIDE: str = ""
SGLANG_EXTERNAL_MODEL_PACKAGE: str = ""
@@ -243,13 +242,8 @@ environment_variables: dict[str, Callable[[], Any]] = {
"SGLANG_DIFFUSION_MXFP8_FA_HEAD_CHUNK_SIZE": _lazy_int(
"SGLANG_DIFFUSION_MXFP8_FA_HEAD_CHUNK_SIZE", 4
),
# Use dedicated multiprocess context for workers.
# Both spawn and fork work
"SGLANG_DIFFUSION_WORKER_MULTIPROC_METHOD": _lazy_str(
"SGLANG_DIFFUSION_WORKER_MULTIPROC_METHOD", "fork"
),
# Internal per-worker platform override used by disaggregated role launch.
# Empty means normal platform auto-detection.
# Select a built-in platform or an installed platform entry point.
# Empty means automatic plugin activation followed by built-in detection.
"SGLANG_DIFFUSION_PLATFORM_OVERRIDE": _lazy_str(
"SGLANG_DIFFUSION_PLATFORM_OVERRIDE", ""
),
@@ -11,6 +11,7 @@ import pickle
from collections import namedtuple
from contextlib import contextmanager, nullcontext
from dataclasses import dataclass
from pkgutil import resolve_name
from typing import Any, Dict, List, Optional, Tuple, Union
import torch
@@ -20,9 +21,6 @@ from torch.distributed import Backend, ProcessGroup
from sglang.multimodal_gen.runtime.distributed.device_communicators.base_device_communicator import (
DeviceCommunicatorBase,
)
from sglang.multimodal_gen.runtime.distributed.device_communicators.cpu_communicator import (
CpuCommunicator,
)
from sglang.multimodal_gen.runtime.distributed.utils import all_gather_single
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.multimodal_gen.runtime.utils.logging_utils import (
@@ -52,6 +50,16 @@ def get_local_torch_device() -> torch.device:
return current_platform.get_local_torch_device()
def _resolve_all_to_all_communicator_cls() -> type[DeviceCommunicatorBase]:
qualname = current_platform.get_all_to_all_communicator_cls()
communicator_cls = resolve_name(qualname)
if not isinstance(communicator_cls, type) or not issubclass(
communicator_cls, DeviceCommunicatorBase
):
raise TypeError(f"Expected a DeviceCommunicatorBase subclass: {qualname}")
return communicator_cls
def _get_unique_name(name: str) -> str:
"""Get a unique name for the group.
Example:
@@ -162,7 +170,7 @@ class GroupCoordinator:
cpu_group: ProcessGroup # group for CPU communication
device_group: ProcessGroup # group for device communication
use_device_communicator: bool # whether to use device communicator
device_communicator: DeviceCommunicatorBase # device communicator
device_communicator: DeviceCommunicatorBase # all_to_all_4D communicator
def __init__(
self,
@@ -202,21 +210,8 @@ class GroupCoordinator:
self.use_device_communicator = use_device_communicator
self.device_communicator: DeviceCommunicatorBase = None # type: ignore
if use_device_communicator and self.world_size > 1:
# Platform-aware device communicator selection
if current_platform.is_cuda_alike():
from sglang.multimodal_gen.runtime.distributed.device_communicators.cuda_communicator import (
CudaCommunicator,
)
self.device_communicator = CudaCommunicator(
cpu_group=self.cpu_group,
device=self.device,
device_group=self.device_group,
unique_name=self.unique_name,
)
else:
# For MPS and CPU, use the CPU communicator
self.device_communicator = CpuCommunicator(
communicator_cls = _resolve_all_to_all_communicator_cls()
self.device_communicator = communicator_cls(
cpu_group=self.cpu_group,
device=self.device,
device_group=self.device_group,
@@ -252,17 +252,10 @@ def init_distributed_environment(
"distributed environment"
)
# For MPS, MUSA, and XPU, don't pass device_id as it doesn't support device indices
extra_args = (
{}
if (
current_platform.is_mps()
or current_platform.is_musa()
or current_platform.is_npu()
or current_platform.is_cpu()
or current_platform.is_xpu()
)
else dict(device_id=device_id)
dict(device_id=device_id)
if current_platform.supports_distributed_device_id()
else {}
)
if timeout is not None:
@@ -4,12 +4,20 @@
# adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/entrypoints/cli/main.py
from sglang.multimodal_gen.runtime.entrypoints.cli.cli_types import CLISubcommand
from sglang.multimodal_gen.runtime.entrypoints.cli.generate import GenerateSubcommand
from sglang.multimodal_gen.runtime.entrypoints.cli.serve import ServeSubcommand
from sglang.multimodal_gen.runtime.platforms.plugins import apply_plugin_hooks
from sglang.multimodal_gen.runtime.utils.argparse import FlexibleArgumentParser
def generate_cmd_init() -> list[CLISubcommand]:
# Command modules import the runtime graph. Activate plugins first so OOT
# platforms can prepare that graph before its modules are evaluated.
apply_plugin_hooks()
from sglang.multimodal_gen.runtime.entrypoints.cli.generate import (
GenerateSubcommand,
)
from sglang.multimodal_gen.runtime.entrypoints.cli.serve import ServeSubcommand
return [GenerateSubcommand(), ServeSubcommand()]
@@ -21,6 +29,8 @@ def cmd_init() -> list[CLISubcommand]:
def main() -> None:
apply_plugin_hooks()
parser = FlexibleArgumentParser(description="sglang-diffusion CLI")
parser.add_argument("-v", "--version", action="version", version="0.1.0")
@@ -34,9 +34,9 @@ from sglang.multimodal_gen.runtime.entrypoints.utils import (
prepare_request,
save_outputs,
)
from sglang.multimodal_gen.runtime.launch_server import launch_server
from sglang.multimodal_gen.runtime.pipelines_core import Req
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch
from sglang.multimodal_gen.runtime.platforms.plugins import apply_plugin_hooks
from sglang.multimodal_gen.runtime.scheduler_client import sync_scheduler_client
from sglang.multimodal_gen.runtime.server_args import PortArgs, ServerArgs
from sglang.multimodal_gen.runtime.server_warmup import (
@@ -58,15 +58,6 @@ from sglang.multimodal_gen.runtime.utils.trace_wrapper import (
logger = init_logger(__name__)
try:
# Set the start method to 'spawn' to avoid CUDA errors in forked processes.
# This must be done at the top level of the module, before any CUDA context
# or other processes are initialized.
mp.set_start_method("spawn", force=True)
except RuntimeError:
# The start method can only be set once per program execution.
pass
def _replace_sampling_params_for_prompt(
sampling_params_orig: SamplingParams,
@@ -137,6 +128,10 @@ class DiffGenerator:
Priority level: Default pipeline config < User's pipeline config < User's kwargs
"""
# Not shared with from_server_args: the ServerArgs built below runs
# Platform.apply_server_args_defaults, which hooks must precede.
apply_plugin_hooks()
# If users also provide some kwargs, it will override the ServerArgs and PipelineConfig.
if (server_args := kwargs.get("server_args", None)) is not None:
@@ -147,7 +142,7 @@ class DiffGenerator:
else:
server_args = ServerArgs.from_kwargs(**kwargs)
return cls.from_server_args(server_args, local_mode=local_mode)
return cls._create(server_args, local_mode=local_mode)
@classmethod
def from_server_args(
@@ -162,6 +157,16 @@ class DiffGenerator:
Returns:
The created DiffGenerator
"""
apply_plugin_hooks()
return cls._create(server_args, local_mode=local_mode)
@classmethod
def _create(cls, server_args: ServerArgs, *, local_mode: bool) -> "DiffGenerator":
"""Build and connect a generator, assuming hooks are already applied.
Each public constructor owns that step itself, so this shared body must
not repeat it.
"""
globally_suppress_loggers()
instance = cls(
server_args=server_args,
@@ -184,6 +189,9 @@ class DiffGenerator:
self,
) -> list[mp.Process]:
"""Check if a local server is running; if not, start it and return the process handles."""
# Not module scope: launch_server pulls in the whole worker graph.
from sglang.multimodal_gen.runtime.launch_server import launch_server
# First, we need a client to test the server. Initialize it temporarily.
sync_scheduler_client.initialize(self.server_args)
@@ -14,11 +14,17 @@ from sglang.multimodal_gen.runtime.disaggregation.orchestrator import (
from sglang.multimodal_gen.runtime.disaggregation.roles import RoleType
from sglang.multimodal_gen.runtime.entrypoints.control_requests import ShutdownReq
from sglang.multimodal_gen.runtime.entrypoints.http_server import create_app
from sglang.multimodal_gen.runtime.managers.gpu_worker import run_scheduler_process
from sglang.multimodal_gen.runtime.managers.worker_bootstrap import (
SchedulerProcessSpec,
ServerArgsPayload,
bootstrap_http_server_process,
bootstrap_scheduler_process,
)
from sglang.multimodal_gen.runtime.observability.metrics import (
configure_metrics,
start_role_metrics_server,
)
from sglang.multimodal_gen.runtime.platforms.plugins import apply_plugin_hooks
from sglang.multimodal_gen.runtime.scheduler_client import SchedulerClient
from sglang.multimodal_gen.runtime.server_args import (
ServerArgs,
@@ -28,7 +34,6 @@ from sglang.multimodal_gen.runtime.server_args import (
from sglang.multimodal_gen.runtime.utils.common import is_port_available
from sglang.multimodal_gen.runtime.utils.logging_utils import configure_logger, logger
from sglang.multimodal_gen.runtime.utils.process import (
kill_itself_when_parent_died,
kill_process_tree,
)
from sglang.multimodal_gen.runtime.utils.trace_wrapper import init_diffusion_tracing
@@ -97,11 +102,6 @@ def _kill_alive_processes(processes, timeout_s: float) -> None:
_join_processes_with_deadline(alive, timeout_s)
def _run_http_server_process(server_args: ServerArgs) -> None:
kill_itself_when_parent_died()
launch_http_server_only(server_args)
def _request_monolithic_scheduler_shutdown(server_args: ServerArgs) -> None:
if server_args.disagg_role != RoleType.MONOLITHIC:
return
@@ -138,6 +138,7 @@ def launch_server(server_args: ServerArgs, launch_http_server: bool = True):
Args:
launch_http_server: False for offline local mode
"""
apply_plugin_hooks()
configure_logger(server_args)
# Start a new server with multiple worker processes
@@ -156,17 +157,28 @@ def launch_server(server_args: ServerArgs, launch_http_server: bool = True):
rank_offset = node_rank * local_num_gpus
processes = []
# A local spawn context makes the worker boundary deterministic even when
# an embedding application selected a different global start method.
worker_context = mp.get_context("spawn")
server_args_payload = ServerArgsPayload.capture(server_args)
# Launch this node's local worker processes.
scheduler_pipe_readers = []
scheduler_pipe_writers = []
for i in range(local_num_gpus):
rank = rank_offset + i
reader, writer = mp.Pipe(duplex=False)
reader, writer = worker_context.Pipe(duplex=False)
scheduler_pipe_writers.append(writer)
process = mp.Process(
target=run_scheduler_process,
args=(i, rank, server_args, writer),
spec = SchedulerProcessSpec(
local_rank=i,
rank=rank,
server_args=server_args_payload,
pipe_writer=writer,
)
process = worker_context.Process(
target=bootstrap_scheduler_process,
args=(spec,),
name=f"sglang-diffusionWorker-{rank}",
daemon=True,
)
@@ -226,9 +238,9 @@ def launch_server(server_args: ServerArgs, launch_http_server: bool = True):
logger.info("Starting FastAPI server.")
if server_args.webui:
logger.info("Launch FastAPI server in another process because of webui.")
http_server_process = mp.Process(
target=_run_http_server_process,
args=(server_args,),
http_server_process = worker_context.Process(
target=bootstrap_http_server_process,
args=(server_args_payload,),
name="sglang-diffusion-webui",
daemon=True,
)
@@ -370,6 +382,7 @@ def launch_pool_disagg_server(
base_dict.update(role_overrides)
base_dict.pop("pipeline_config", None)
role_args = ServerArgs.from_kwargs(**base_dict)
role_args_payload = ServerArgsPayload.capture(role_args)
pool_ctx = mp.get_context("spawn")
inst_readers = []
@@ -379,9 +392,18 @@ def launch_pool_disagg_server(
reader, writer = pool_ctx.Pipe(duplex=False)
gpu_id = gpu_ids[rank_idx]
# Physical GPU index as local_rank: torch.cuda.set_device() must
# not depend on CUDA_VISIBLE_DEVICES remapping, which can be
# stale if CUDA was already initialized in the parent.
spec = SchedulerProcessSpec(
local_rank=gpu_id,
rank=rank_idx,
server_args=role_args_payload,
pipe_writer=writer,
)
process = pool_ctx.Process(
target=_run_disagg_role_process,
args=(gpu_id, rank_idx, role_args, writer),
target=bootstrap_scheduler_process,
args=(spec,),
name=f"sglang-pool-{role_type.value}-{inst_idx}-r{rank_idx}",
daemon=True,
)
@@ -454,27 +476,6 @@ def launch_pool_disagg_server(
return all_processes
def _run_disagg_role_process(
gpu_id: int,
rank: int,
server_args: ServerArgs,
pipe_writer: mp.connection.Connection,
):
"""Entry point for a disagg role process.
Uses the physical GPU index (gpu_id) as local_rank so that
torch.cuda.set_device(local_rank) selects the correct GPU.
This avoids relying on CUDA_VISIBLE_DEVICES remapping, which
may not work if CUDA was pre-initialized in the parent process.
"""
run_scheduler_process(
local_rank=gpu_id,
rank=rank,
server_args=server_args,
pipe_writer=pipe_writer,
)
def launch_http_server_only(server_args):
init_diffusion_tracing(server_args, "DiffHTTPServer")
@@ -702,6 +703,7 @@ def launch_disagg_role(server_args: ServerArgs):
base_dict.update(role_overrides)
base_dict.pop("pipeline_config", None)
role_args = ServerArgs.from_kwargs(**base_dict)
role_args_payload = ServerArgsPayload.capture(role_args)
# Spawn GPU worker processes
# NOTE: All ranks must be spawned before waiting for ready signals,
@@ -716,9 +718,18 @@ def launch_disagg_role(server_args: ServerArgs):
reader, writer = pool_ctx.Pipe(duplex=False)
gpu_id = base_gpu_id + rank_idx
# Physical GPU index as local_rank: torch.cuda.set_device() must not
# depend on CUDA_VISIBLE_DEVICES remapping, which can be stale if CUDA
# was already initialized in the parent.
spec = SchedulerProcessSpec(
local_rank=gpu_id,
rank=rank_idx,
server_args=role_args_payload,
pipe_writer=writer,
)
process = pool_ctx.Process(
target=_run_disagg_role_process,
args=(gpu_id, rank_idx, role_args, writer),
target=bootstrap_scheduler_process,
args=(spec,),
name=f"sglang-{role_type.value}-r{rank_idx}",
daemon=True,
)
@@ -764,6 +775,8 @@ def launch_disagg_role(server_args: ServerArgs):
def dispatch_launch(server_args: ServerArgs):
"""Route to the correct launch function based on --disagg-role."""
apply_plugin_hooks()
if "NCCL_NVLS_ENABLE" not in os.environ or server_args.enable_nccl_nvls:
os.environ["NCCL_NVLS_ENABLE"] = str(int(server_args.enable_nccl_nvls))
@@ -779,6 +792,7 @@ def dispatch_launch(server_args: ServerArgs):
if __name__ == "__main__":
apply_plugin_hooks()
server_args = prepare_server_args(sys.argv[1:])
try:
@@ -4,16 +4,16 @@
# Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/model_executor/custom_op.py
from collections.abc import Callable
from typing import Any
from functools import partial
from typing import Any, ClassVar
import torch.nn as nn
import sglang.multimodal_gen.runtime.platforms as platforms
from sglang.kernels.kernel_api_logging import debug_kernel_api
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
_is_cuda = current_platform.is_cuda()
class CustomOp(nn.Module):
@@ -22,6 +22,15 @@ class CustomOp(nn.Module):
Dispatches the forward method to the appropriate backend.
"""
_oot_forward_registry: ClassVar[dict[str, dict[type["CustomOp"], Callable]]] = {}
@staticmethod
def register_oot_forward(
op_cls: type["CustomOp"], *, fn: Callable, platform_key: str
) -> None:
"""Register ``fn`` for an exact op class and behavioral dispatch key."""
CustomOp._oot_forward_registry.setdefault(platform_key, {})[op_cls] = fn
def __init__(self) -> None:
super().__init__()
self._forward_method = self.dispatch_forward()
@@ -72,18 +81,45 @@ class CustomOp(nn.Module):
def forward_xpu(self, *args, **kwargs) -> Any:
return self.forward_native(*args, **kwargs)
def _defined_forward(self, method_name: str) -> Callable | None:
"""Return an implementation defined below ``CustomOp`` in the MRO."""
for op_cls in type(self).__mro__:
if op_cls is CustomOp:
return None
if method_name in op_cls.__dict__:
return getattr(self, method_name)
return None
def dispatch_forward(self) -> Callable:
if _is_cuda:
platform = platforms.current_platform
if platform.is_out_of_tree():
# An empty key would silently skip the platform forward below and
# dispatch everything to forward_oot instead.
platform_key = platform.get_dispatch_key_name().strip()
if not platform_key:
raise ValueError(
"Out-of-tree diffusion platforms must return a non-empty "
"get_dispatch_key_name()"
)
forward = self._oot_forward_registry.get(platform_key, {}).get(type(self))
if forward is not None:
return partial(forward, self)
if platform_key.isidentifier():
platform_forward = self._defined_forward(f"forward_{platform_key}")
if platform_forward is not None:
return platform_forward
return self.forward_oot
elif platform.is_cuda():
return self.forward_cuda
elif current_platform.is_hip():
elif platform.is_hip():
return self.forward_hip
elif current_platform.is_npu():
elif platform.is_npu():
return self.forward_npu
elif current_platform.is_xpu():
elif platform.is_xpu():
return self.forward_xpu
elif current_platform.is_musa():
elif platform.is_musa():
return self.forward_musa
elif current_platform.is_cpu():
elif platform.is_cpu():
return self.forward_cpu
else:
return self.forward_native
@@ -10,8 +10,8 @@ from typing import Any, Callable, List, Optional
import torch
from torch.library import Library
import sglang.multimodal_gen.runtime.platforms as platforms
from sglang.kernels.kernel_api_logging import debug_torch_op
from sglang.multimodal_gen.runtime.platforms import current_platform
def get_group_size(group) -> int:
@@ -45,7 +45,7 @@ def direct_register_custom_op(
"""
`torch.library.custom_op` can have significant overhead because it
needs to consider complicated dispatching logic. This function
directly registers a custom op and dispatches it to the CUDA backend.
directly registers a custom op for the active platform's dispatch key.
See https://gist.github.com/youkaichao/ecbea9ec9fc79a45d2adce1784d7a9a5
for more details.
@@ -90,7 +90,9 @@ def direct_register_custom_op(
try:
my_lib.define(op_name + schema_str)
my_lib.impl(
op_name, op_func, "CUDA" if not current_platform.is_npu() else "PrivateUse1"
op_name,
op_func,
platforms.current_platform.get_torch_library_dispatch_key(),
)
if fake_impl is not None:
my_lib._register_fake(op_name, fake_impl)
@@ -78,7 +78,10 @@ from sglang.multimodal_gen.runtime.pipelines_core import (
build_pipeline,
)
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.multimodal_gen.runtime.platforms import (
current_platform,
initialize_current_platform,
)
from sglang.multimodal_gen.runtime.post_training.gpu_worker_post_training_mixin import (
GPUWorkerPostTrainingMixin,
)
@@ -1573,9 +1576,14 @@ def run_scheduler_process(
pipe_writer: mp.connection.Connection,
) -> None:
"""Run a rank's scheduler and report readiness to the launching process."""
# Idempotent safeguard for direct callers; process bootstraps already
# initialized the platform before this module was imported.
initialize_current_platform()
kill_itself_when_parent_died()
configure_logger(server_args)
globally_suppress_loggers()
if current_platform.is_cuda():
set_cuda_arch()
elif current_platform.is_musa():
@@ -0,0 +1,176 @@
# SPDX-License-Identifier: Apache-2.0
"""Import-safe specifications and entry points for diffusion child processes.
``multiprocessing`` unpickles a target's arguments before calling the target.
Runtime objects therefore cannot cross this boundary directly: merely passing
``ServerArgs`` used to import the diffusion configuration graph before worker
bootstrap began. ``ServerArgsPayload`` keeps that object graph opaque until the
child reaches the explicit runtime-activation phase.
``spawn`` re-executes the launching script's module scope earlier still, before
any argument is unpickled, so an offline script may bind only the
``DiffGenerator`` proxy and ``_PRE_ACTIVATION_MODULES`` at module scope; every
other diffusion import belongs inside its ``if __name__ == "__main__":`` guard.
A violation is reported rather than silently tolerated.
Keep module scope limited to the standard library and import-neutral types.
"""
from __future__ import annotations
import logging
import pickle
import sys
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from multiprocessing.connection import Connection
from sglang.multimodal_gen.runtime.server_args import ServerArgs
_DIFFUSION_PREFIX = "sglang.multimodal_gen."
_RUNTIME_NAMESPACES = (
"sglang.multimodal_gen.runtime",
"sglang.multimodal_gen.runtime.managers",
)
# What may legitimately be imported this early: bootstrap's own imports, and the
# platform and plugin modules that every plugin loads and the contract keeps
# import-safe. Listing these rather than their complement keeps the check
# complete as subpackages are added.
_PRE_ACTIVATION_MODULES = (
"sglang.multimodal_gen.envs",
"sglang.multimodal_gen.runtime.platforms",
"sglang.multimodal_gen.runtime.utils",
"sglang.multimodal_gen.runtime.managers.worker_bootstrap",
)
_MAX_REPORTED_MODULES = 5
def _warn_if_runtime_imported_early() -> None:
"""Name the modules that this child imported ahead of its own lifecycle."""
early = sorted(
name
for name in list(sys.modules)
if name.startswith(_DIFFUSION_PREFIX)
and name not in _RUNTIME_NAMESPACES
and not name.startswith(_PRE_ACTIVATION_MODULES)
)
if not early:
return
listed = ", ".join(early[:_MAX_REPORTED_MODULES])
if len(early) > _MAX_REPORTED_MODULES:
listed += f" (+{len(early) - _MAX_REPORTED_MODULES} more)"
# In a spawned child __main__ is the re-executed launching script, which is
# the file whose imports have to move.
script = vars(sys.modules["__main__"]).get("__file__", "the launching script")
logging.getLogger(__name__).warning(
"Diffusion runtime modules were imported before this worker initialized "
"its platform: %s. spawn re-executes %s at module scope in every child, "
"so these were built ahead of platform initialization and hook "
"application, and the classes and registrations they created are "
'already past reach. Move the import inside if __name__ == "__main__": '
"or into the function that uses it.",
listed,
script,
)
@dataclass(frozen=True, slots=True)
class ServerArgsPayload:
"""A deferred ``ServerArgs`` snapshot safe to unpickle before bootstrap."""
_pickle: bytes = field(repr=False)
@classmethod
def capture(cls, server_args: ServerArgs) -> ServerArgsPayload:
return cls(pickle.dumps(server_args, protocol=pickle.HIGHEST_PROTOCOL))
def materialize(self) -> ServerArgs:
# Importing ServerArgs pulls in pipeline configuration modules. This
# method must only be called after the process lifecycle is initialized.
from sglang.multimodal_gen.runtime.server_args import ServerArgs
server_args = pickle.loads(self._pickle)
if not isinstance(server_args, ServerArgs):
raise TypeError("Bootstrap payload did not contain diffusion ServerArgs")
return server_args
@dataclass(frozen=True, slots=True)
class SchedulerProcessSpec:
"""Everything a scheduler child needs, without eagerly importing runtime state."""
local_rank: int
rank: int
server_args: ServerArgsPayload
pipe_writer: Connection
def bootstrap_scheduler_process(spec: SchedulerProcessSpec) -> None:
"""Initialize a child in dependency order, then invoke its worker."""
# Arm PDEATHSIG before any vendor code runs: everything below can block,
# and a child that hangs there would outlive a dead launcher.
from sglang.multimodal_gen.runtime.utils.process import (
kill_itself_when_parent_died,
)
kill_itself_when_parent_died()
# Every rank re-executes the same script, so one rank reporting is enough.
if spec.rank == 0:
_warn_if_runtime_imported_early()
# Platform initialization is the first extensible runtime action. In
# particular it precedes plugin callbacks and hook target resolution, both
# of which may import arbitrary runtime modules.
from sglang.multimodal_gen.runtime.platforms import initialize_current_platform
initialize_current_platform()
from sglang.multimodal_gen.runtime.platforms.plugins import (
apply_plugin_hooks,
load_plugins,
)
load_plugins()
apply_plugin_hooks()
server_args = spec.server_args.materialize()
# Resolve the function from its module after hook application. A ``from``
# binding created earlier would retain the unpatched callable.
from sglang.multimodal_gen.runtime.managers import gpu_worker
gpu_worker.run_scheduler_process(
local_rank=spec.local_rank,
rank=spec.rank,
server_args=server_args,
pipe_writer=spec.pipe_writer,
)
def bootstrap_http_server_process(server_args: ServerArgsPayload) -> None:
from sglang.multimodal_gen.runtime.utils.process import (
kill_itself_when_parent_died,
)
kill_itself_when_parent_died()
_warn_if_runtime_imported_early()
# No initialize_current_platform() here: this child serves HTTP and never
# touches the device, so it has no reason to bring up a vendor backend.
from sglang.multimodal_gen.runtime.platforms.plugins import (
apply_plugin_hooks,
load_plugins,
)
load_plugins()
apply_plugin_hooks()
from sglang.multimodal_gen.runtime import launch_server
launch_server.launch_http_server_only(server_args.materialize())
@@ -9,7 +9,6 @@ This package contains diffusion pipelines for generating videos and images.
from typing import cast
from sglang.multimodal_gen.registry import get_model_info
from sglang.multimodal_gen.runtime.pipelines_core.composed_pipeline_base import (
ComposedPipelineBase,
)
@@ -43,6 +42,8 @@ def build_pipeline(
2. verify the model config and directory
3. based on the config, determine the pipeline class
"""
from sglang.multimodal_gen.registry import get_model_info
model_path = server_args.model_path
# Check if pipeline class is explicitly specified
@@ -45,8 +45,7 @@ from sglang.multimodal_gen.runtime.utils.precision import (
)
from sglang.multimodal_gen.runtime.utils.torch_compile import (
ActiveTargetCompiledCallable,
build_torch_compile_kwargs,
resolve_torch_compile_mode,
resolve_torch_compile_kwargs,
)
logger = init_logger(__name__)
@@ -185,17 +184,16 @@ class DecodingStage(PipelineStage):
compiled_callable.target_id != id(vae)
or compiled_callable.compiled_module is None
)
if current_platform.is_npu():
compile_kwargs = build_torch_compile_kwargs(mode=None)
if will_compile:
logger.info("Compiling VAE decode with torchair backend on NPU")
else:
mode = resolve_torch_compile_mode(
compile_kwargs, mode = resolve_torch_compile_kwargs(
"SGLANG_VAE_TORCH_COMPILE_MODE",
"SGLANG_TORCH_COMPILE_MODE",
default="default",
module=vae,
)
compile_kwargs = build_torch_compile_kwargs(mode=mode)
if current_platform.is_npu():
if will_compile:
logger.info("Compiling VAE decode with torchair backend on NPU")
else:
if will_compile:
logger.info("Compiling VAE decode with mode: %s", mode)
@@ -171,9 +171,7 @@ from sglang.multimodal_gen.runtime.utils.precision import (
from sglang.multimodal_gen.runtime.utils.profiler import SGLDiffusionProfiler
from sglang.multimodal_gen.runtime.utils.torch_compile import (
CompiledModuleRegistry,
build_torch_compile_kwargs,
maybe_enable_inductor_compute_comm_overlap,
resolve_torch_compile_mode,
resolve_torch_compile_kwargs,
)
logger = init_logger(__name__)
@@ -553,18 +551,17 @@ class DenoisingStage(PipelineStage, RolloutDenoisingMixin):
if self._torch_compile_registry.is_compiled(module):
return
if current_platform.is_npu():
compile_kwargs = build_torch_compile_kwargs(mode=None)
logger.info("Compiling transformer with torchair backend on NPU")
else:
maybe_enable_inductor_compute_comm_overlap()
dit_config = getattr(self.server_args.pipeline_config, "dit_config", None)
mode = resolve_torch_compile_mode(
compile_kwargs, mode = resolve_torch_compile_kwargs(
"SGLANG_TORCH_COMPILE_MODE",
config=dit_config,
default="max-autotune-no-cudagraphs",
module=module,
enable_inductor_compute_comm_overlap=True,
)
compile_kwargs = build_torch_compile_kwargs(mode=mode, module=module)
if current_platform.is_npu():
logger.info("Compiling transformer with torchair backend on NPU")
else:
logger.info(f"Compiling transformer with mode: {mode}")
if getattr(self.server_args, "regional_compile", False):
@@ -13,7 +13,6 @@ from __future__ import annotations
import functools
import inspect
import os
import torch
import torch.nn as nn
@@ -73,7 +72,9 @@ from sglang.multimodal_gen.runtime.utils.precision import (
)
from sglang.multimodal_gen.runtime.utils.precision_types import PRECISION_TO_TYPE
from sglang.multimodal_gen.runtime.utils.profiler import SGLDiffusionProfiler
from sglang.srt.utils.common import get_compiler_backend
from sglang.multimodal_gen.runtime.utils.torch_compile import (
resolve_torch_compile_kwargs,
)
_is_npu = current_platform.is_npu()
logger = init_logger(__name__)
@@ -265,29 +266,19 @@ class MOVADenoisingStage(PipelineStage):
module.__class__.__name__,
)
return
compile_kwargs: dict[str, object] = {"fullgraph": False, "dynamic": None}
compile_kwargs, mode = resolve_torch_compile_kwargs(
"SGLANG_TORCH_COMPILE_MODE",
config=model_config,
default="max-autotune-no-cudagraphs",
module=module,
enable_inductor_compute_comm_overlap=True,
)
if current_platform.is_npu():
backend = get_compiler_backend()
compile_kwargs["backend"] = backend
compile_kwargs["dynamic"] = False
logger.info(
"Compiling %s with torchair backend on NPU",
module.__class__.__name__,
)
else:
try:
import torch._inductor.config as _inductor_cfg
_inductor_cfg.reorder_for_compute_comm_overlap = True
except ImportError:
pass
mode = os.environ.get("SGLANG_TORCH_COMPILE_MODE") or getattr(
model_config,
"torch_compile_mode",
"max-autotune-no-cudagraphs",
)
compile_kwargs["mode"] = mode
logger.info("Compiling %s with mode: %s", module.__class__.__name__, mode)
# TODO(triple-mu): support customized fullgraph and dynamic in the future
@@ -5,8 +5,14 @@
import os
import traceback
from collections import Counter
from collections.abc import Iterable
from dataclasses import dataclass
from importlib.metadata import EntryPoint, entry_points
from pkgutil import resolve_name
from sglang.multimodal_gen import envs
# imported by other files, do not remove
from sglang.multimodal_gen.runtime.platforms.interface import ( # noqa: F401
AttentionBackendEnum,
@@ -18,6 +24,18 @@ from sglang.multimodal_gen.third_party import pynvml
logger = init_logger(__name__)
PLATFORM_PLUGINS_GROUP = "sglang.multimodal_gen.platforms"
_BUILTIN_PLATFORM_QUALNAMES = {
"cpu": "sglang.multimodal_gen.runtime.platforms.cpu.CpuPlatform",
"cuda": "sglang.multimodal_gen.runtime.platforms.cuda.CudaPlatform",
"rocm": "sglang.multimodal_gen.runtime.platforms.rocm.RocmPlatform",
"xpu": "sglang.multimodal_gen.runtime.platforms.xpu.XpuPlatform",
"mps": "sglang.multimodal_gen.runtime.platforms.mps.MpsPlatform",
"npu": "sglang.multimodal_gen.runtime.platforms.npu.NPUPlatformBase",
"musa": "sglang.multimodal_gen.runtime.platforms.musa.MusaPlatform",
}
BUILTIN_PLATFORM_NAMES = frozenset(_BUILTIN_PLATFORM_QUALNAMES)
def cuda_platform_plugin() -> str | None:
is_cuda = False
@@ -72,9 +90,7 @@ def cuda_platform_plugin() -> str | None:
if is_cuda:
logger.debug("CUDA is available")
return (
"sglang.multimodal_gen.runtime.platforms.cuda.CudaPlatform" if is_cuda else None
)
return _BUILTIN_PLATFORM_QUALNAMES["cuda"] if is_cuda else None
def mps_platform_plugin() -> str | None:
@@ -90,13 +106,13 @@ def mps_platform_plugin() -> str | None:
except Exception as e:
logger.debug("MPS detection failed: %s", e)
return "sglang.multimodal_gen.runtime.platforms.mps.MpsPlatform" if is_mps else None
return _BUILTIN_PLATFORM_QUALNAMES["mps"] if is_mps else None
def cpu_platform_plugin() -> str | None:
def cpu_platform_plugin() -> str:
"""Detect if CPU platform should be used."""
# CPU is always available as a fallback
return "sglang.multimodal_gen.runtime.platforms.cpu.CpuPlatform"
return _BUILTIN_PLATFORM_QUALNAMES["cpu"]
def rocm_platform_plugin() -> str | None:
@@ -115,9 +131,7 @@ def rocm_platform_plugin() -> str | None:
except Exception as e:
logger.debug("ROCm platform is unavailable: %s", e)
return (
"sglang.multimodal_gen.runtime.platforms.rocm.RocmPlatform" if is_rocm else None
)
return _BUILTIN_PLATFORM_QUALNAMES["rocm"] if is_rocm else None
def npu_platform_plugin() -> str | None:
@@ -131,11 +145,7 @@ def npu_platform_plugin() -> str | None:
logger.debug("NPU is available")
except Exception as e:
logger.debug("NPU detection failed: %s", e)
return (
"sglang.multimodal_gen.runtime.platforms.npu.NPUPlatformBase"
if is_npu
else None
)
return _BUILTIN_PLATFORM_QUALNAMES["npu"] if is_npu else None
def musa_platform_plugin() -> str | None:
@@ -152,9 +162,7 @@ def musa_platform_plugin() -> str | None:
except Exception as e:
logger.debug("MUSA platform is unavailable: %s", e)
return (
"sglang.multimodal_gen.runtime.platforms.musa.MusaPlatform" if is_musa else None
)
return _BUILTIN_PLATFORM_QUALNAMES["musa"] if is_musa else None
def xpu_platform_plugin() -> str | None:
@@ -175,104 +183,273 @@ def xpu_platform_plugin() -> str | None:
except Exception as e:
logger.info("Intel XPU platform is unavailable: %s", e)
return "sglang.multimodal_gen.runtime.platforms.xpu.XpuPlatform" if is_xpu else None
return _BUILTIN_PLATFORM_QUALNAMES["xpu"] if is_xpu else None
builtin_platform_plugins = {
"cuda": cuda_platform_plugin,
"rocm": rocm_platform_plugin,
"xpu": xpu_platform_plugin,
"mps": mps_platform_plugin,
"cpu": cpu_platform_plugin,
"xpu": xpu_platform_plugin,
"rocm": rocm_platform_plugin,
"cuda": cuda_platform_plugin,
"npu": npu_platform_plugin,
"musa": musa_platform_plugin,
"cpu": cpu_platform_plugin,
}
def resolve_current_platform_cls_qualname() -> str:
forced_platform = os.environ.get("SGLANG_DIFFUSION_PLATFORM_OVERRIDE", "").strip()
if forced_platform:
forced_map = {
"cpu": "sglang.multimodal_gen.runtime.platforms.cpu.CpuPlatform",
"cuda": "sglang.multimodal_gen.runtime.platforms.cuda.CudaPlatform",
"rocm": "sglang.multimodal_gen.runtime.platforms.rocm.RocmPlatform",
"mps": "sglang.multimodal_gen.runtime.platforms.mps.MpsPlatform",
"npu": "sglang.multimodal_gen.runtime.platforms.npu.NPUPlatformBase",
"musa": "sglang.multimodal_gen.runtime.platforms.musa.MusaPlatform",
"xpu": "sglang.multimodal_gen.runtime.platforms.xpu.XpuPlatform",
}
qualname = forced_map.get(forced_platform.lower())
if qualname is None:
raise ValueError(
f"Unsupported SGLANG_DIFFUSION_PLATFORM_OVERRIDE={forced_platform!r}"
@dataclass(frozen=True)
class _PlatformSelection:
qualname: str
plugin_name: str | None = None
distribution_name: str | None = None
@property
def is_external(self) -> bool:
return self.plugin_name is not None
def _select_current_platform() -> _PlatformSelection:
selected = envs.SGLANG_DIFFUSION_PLATFORM_OVERRIDE.strip()
if selected:
builtin_name = selected.lower()
if builtin_name in BUILTIN_PLATFORM_NAMES:
return _PlatformSelection(_BUILTIN_PLATFORM_QUALNAMES[builtin_name])
return _resolve_selected_platform(_discover_platform_plugin_entries(), selected)
platform_selection = _resolve_automatic_platform(
_discover_platform_plugin_entries()
)
return qualname
if platform_selection is not None:
return platform_selection
# TODO(will): if we need to support other platforms, we should consider if
# vLLM's plugin architecture is suitable for our needs.
# Try MPS first on macOS
platform_cls_qualname = mps_platform_plugin()
for detect in builtin_platform_plugins.values():
platform_cls_qualname = detect()
if platform_cls_qualname is not None:
return platform_cls_qualname
# Try Intel XPU
platform_cls_qualname = xpu_platform_plugin()
if platform_cls_qualname is not None:
return platform_cls_qualname
# Fall back to ROCm
platform_cls_qualname = rocm_platform_plugin()
if platform_cls_qualname is not None:
return platform_cls_qualname
# Fall back to CUDA
platform_cls_qualname = cuda_platform_plugin()
if platform_cls_qualname is not None:
return platform_cls_qualname
# Fall back to NPU
platform_cls_qualname = npu_platform_plugin()
if platform_cls_qualname is not None:
return platform_cls_qualname
# Fall back to MUSA
platform_cls_qualname = musa_platform_plugin()
if platform_cls_qualname is not None:
return platform_cls_qualname
# Fall back to CPU as last resort
platform_cls_qualname = cpu_platform_plugin()
if platform_cls_qualname is not None:
return platform_cls_qualname
return _PlatformSelection(platform_cls_qualname)
raise RuntimeError("No platform plugin found. Please check your installation.")
def resolve_current_platform_cls_qualname() -> str:
"""Resolve the selected class name without mutating singleton state."""
return _select_current_platform().qualname
def _discover_platform_plugin_entries() -> tuple[EntryPoint, ...]:
entries = tuple(entry_points(group=PLATFORM_PLUGINS_GROUP))
if entries:
logger.info("Available diffusion platform plugins:")
for entry_point in entries:
logger.info(" - %s -> %s", entry_point.name, entry_point.value)
return entries
def _reject_platform_names(names: Iterable[str], *, reason: str) -> None:
# Sorted so the message does not depend on entry-point iteration order.
offenders = sorted(names)
if offenders:
raise RuntimeError(f"{reason}: " + ", ".join(repr(name) for name in offenders))
def _validate_platform_entries(entries: tuple[EntryPoint, ...]) -> None:
counts = Counter(entry_point.name for entry_point in entries)
_reject_platform_names(
(name for name, count in counts.items() if count > 1),
reason="Diffusion platform entry-point names must be unique",
)
_reject_platform_names(
(name for name in counts if name.lower() in BUILTIN_PLATFORM_NAMES),
reason="Diffusion platform entry points cannot use built-in names",
)
def _platform_selection(
entry_point: EntryPoint, qualname: object
) -> _PlatformSelection | None:
if qualname is None:
return None
# activate() is third-party, so a bad return is named rather than left to
# surface as an AttributeError from some later attribute access.
selected = qualname.strip() if isinstance(qualname, str) else ""
if not selected:
raise TypeError(
f"Diffusion platform plugin {entry_point.name!r} must return a "
"non-empty class qualname or None"
)
return _PlatformSelection(
qualname=selected,
plugin_name=entry_point.name,
distribution_name=entry_point.dist.name if entry_point.dist else None,
)
def _resolve_selected_platform(
entries: tuple[EntryPoint, ...],
selected: str,
) -> _PlatformSelection:
matches = tuple(
entry_point for entry_point in entries if entry_point.name == selected
)
if not matches:
available = ", ".join(repr(entry_point.name) for entry_point in entries)
raise ValueError(
f"Unsupported SGLANG_DIFFUSION_PLATFORM_OVERRIDE={selected!r}; "
"entry point not found in group "
f"{PLATFORM_PLUGINS_GROUP!r} (available: "
f"{available or 'none'})."
)
_validate_platform_entries(matches)
logger.info(
"Selecting platform plugin %s via SGLANG_DIFFUSION_PLATFORM_OVERRIDE",
selected,
)
selection = _platform_selection(matches[0], matches[0].load()())
if selection is None:
raise RuntimeError(
f"Platform plugin {selected!r} is installed but activate() "
"returned None (hardware not available on this machine?)."
)
logger.info("OOT platform plugin activated: %s -> %s", selected, selection.qualname)
return selection
def _resolve_automatic_platform(
entries: tuple[EntryPoint, ...],
) -> _PlatformSelection | None:
_validate_platform_entries(entries)
activated: list[_PlatformSelection] = []
for entry_point in entries:
# A raising activate() propagates: silently skipping it would fall back
# to a built-in platform and run the whole job on the wrong hardware.
selection = _platform_selection(entry_point, entry_point.load()())
if selection is not None:
activated.append(selection)
logger.info(
"OOT platform plugin activated: %s -> %s",
entry_point.name,
selection.qualname,
)
if not activated:
return None
if len(activated) == 1:
return activated[0]
names = ", ".join(repr(selection.plugin_name) for selection in activated)
raise RuntimeError(
f"Multiple platform plugins activated: {names}. "
"Set SGLANG_DIFFUSION_PLATFORM_OVERRIDE to select one."
)
def _load_platform_class(
qualname: str, *, external: bool | None = None
) -> type[Platform]:
platform_cls = resolve_name(qualname)
if not isinstance(platform_cls, type) or not issubclass(platform_cls, Platform):
raise TypeError(f"Expected a Platform subclass: {qualname}")
if external is None:
external = qualname not in _BUILTIN_PLATFORM_QUALNAMES.values()
if external and platform_cls._enum is not PlatformEnum.OOT:
raise TypeError(
f"External diffusion platform {qualname} must set "
"_enum = sglang.multimodal_gen.runtime.platforms.PlatformEnum.OOT"
)
return platform_cls
_current_platform: Platform | None = None
_current_platform_selection: _PlatformSelection | None = None
_init_trace: str = ""
_backend_init_done = False
_backend_init_error: BaseException | None = None
current_platform: Platform
def _resolve_current_platform() -> Platform:
# Platform plugins import this module to subclass Platform, so resolution
# must remain lazy.
global _current_platform, _current_platform_selection, _init_trace
if _current_platform is not None:
return _current_platform
selection = _select_current_platform()
platform_cls = _load_platform_class(
selection.qualname, external=selection.is_external
)
platform = platform_cls()
if selection.is_external:
for attribute in ("device_name", "device_type"):
value = getattr(platform, attribute, None)
if not isinstance(value, str) or not value.strip():
raise TypeError(
f"External diffusion platform {selection.qualname} must "
f"define a non-empty {attribute}"
)
# Publish the instance and its provenance together, only after the
# complete external contract has passed validation.
_current_platform_selection = selection
_current_platform = platform
_init_trace = "".join(traceback.format_stack())
return platform
def get_selected_platform_dist() -> str | None:
_resolve_current_platform()
assert _current_platform_selection is not None
return _current_platform_selection.distribution_name
def initialize_current_platform() -> None:
"""Run backend initialization once per process.
Only worker entry points call this, so a launcher never marks itself
initialized and every worker starts from clean module state.
A failed initialization is terminal for that process: retrying arbitrary
backend side effects can duplicate registrations and leave a worker in a
state that reflects neither attempt.
"""
global _backend_init_error, _backend_init_done
if _backend_init_done:
if _backend_init_error is not None:
raise RuntimeError(
"Diffusion platform backend initialization previously failed: "
f"{_backend_init_error}"
) from _backend_init_error
return
try:
_resolve_current_platform().init_backend()
except BaseException as exc:
# BaseException too: the finally below marks this attempt done, so an
# unrecorded interrupt would let the next call report success.
_backend_init_error = exc
raise
finally:
_backend_init_done = True
def __getattr__(name: str):
if name == "current_platform":
# lazy init current_platform.
# 1. out-of-tree platform plugins need `from sglang.multimodal_gen.runtime.platforms import
# Platform` so that they can inherit `Platform` class. Therefore,
# we cannot resolve `current_platform` during the import of
# `sglang.multimodal_gen.runtime.platforms`.
global _current_platform
if _current_platform is None:
platform_cls_qualname = resolve_current_platform_cls_qualname()
_current_platform = resolve_name(platform_cls_qualname)()
global _init_trace
_init_trace = "".join(traceback.format_stack())
return _current_platform
return _resolve_current_platform()
elif name in globals():
return globals()[name]
else:
raise AttributeError(f"No attribute named '{name}' exists in {__name__}.")
__all__ = ["Platform", "PlatformEnum", "current_platform", "_init_trace"]
__all__ = [
"BUILTIN_PLATFORM_NAMES",
"PLATFORM_PLUGINS_GROUP",
"Platform",
"PlatformEnum",
"current_platform",
"get_selected_platform_dist",
"initialize_current_platform",
"_init_trace",
]
@@ -20,6 +20,7 @@ if TYPE_CHECKING:
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionImpl,
)
from sglang.multimodal_gen.runtime.server_args.server_args import ServerArgs
logger = init_logger(__name__)
@@ -112,15 +113,14 @@ class DeviceCapability(NamedTuple):
class Platform:
_enum: PlatformEnum
_enum: PlatformEnum = PlatformEnum.UNSPECIFIED
device_name: str
device_type: str
device: torch.device | None = None # Dummy attribute for compatibility
# available dispatch keys:
# check https://github.com/pytorch/pytorch/blob/313dac6c1ca0fa0cde32477509cce32089f8532a/torchgen/model.py#L134 # noqa
# use "CPU" as a fallback for platforms not registered in PyTorch
dispatch_key: str = "CPU"
dispatch_key: str = ""
# The torch.compile backend for compiling simple and
# standalone functions. The default value is "inductor" to keep
@@ -131,6 +131,17 @@ class Platform:
supported_quantization: list[str] = []
def init_backend(self) -> None:
"""One-time backend initialization, in each worker; raising aborts startup.
Where out-of-tree platforms register their custom-op forwards.
"""
pass
def apply_server_args_defaults(self, server_args: ServerArgs) -> None:
"""Apply defaults before argument normalization and validation."""
pass
def get_compile_backend(self, mode: str | None = None) -> str:
"""Return the backend used to compile diffusion modules."""
return self.simple_compile_backend
@@ -139,6 +150,27 @@ class Platform:
"""Return backend-specific options for a diffusion module."""
return None
def get_dispatch_key_name(self) -> str:
"""Return the behavioral dispatch key used by :class:`CustomOp`.
This is intentionally separate from ``dispatch_key``, which names a
PyTorch dispatcher key such as ``PrivateUse1``. An out-of-tree backend
can return an existing key such as ``cuda`` to reuse compatible
``forward_cuda`` implementations, or a vendor key backed by registered
forwards and ``forward_<key>`` methods.
"""
return "native"
def get_torch_library_dispatch_key(self) -> str:
"""Return the key used for direct ``torch.library`` registrations."""
if self.is_out_of_tree():
if not self.dispatch_key:
raise NotImplementedError(
"Out-of-tree diffusion platforms must define dispatch_key"
)
return self.dispatch_key
return "PrivateUse1" if self.is_npu() else "CUDA"
@lru_cache(maxsize=1)
def is_cuda(self) -> bool:
return self.is_cuda_static()
@@ -178,11 +210,11 @@ class Platform:
@classmethod
def is_cuda_static(cls) -> bool:
return getattr(cls, "_enum", None) == PlatformEnum.CUDA
return cls._enum == PlatformEnum.CUDA
@classmethod
def is_rocm_static(cls) -> bool:
return getattr(cls, "_enum", None) == PlatformEnum.ROCM
return cls._enum == PlatformEnum.ROCM
@lru_cache(maxsize=1)
def is_hpu(self) -> bool:
@@ -190,11 +222,19 @@ class Platform:
@lru_cache(maxsize=1)
def is_xpu(self) -> bool:
return hasattr(torch, "xpu") and torch.xpu.is_available()
return (
not self.is_out_of_tree()
and hasattr(torch, "xpu")
and torch.xpu.is_available()
)
@lru_cache(maxsize=1)
def is_npu(self) -> bool:
return hasattr(torch, "npu") and torch.npu.is_available()
return (
not self.is_out_of_tree()
and hasattr(torch, "npu")
and torch.npu.is_available()
)
def is_out_of_tree(self) -> bool:
return self._enum == PlatformEnum.OOT
@@ -215,7 +255,11 @@ class Platform:
@lru_cache(maxsize=1)
def is_musa(self):
try:
return hasattr(torch, "musa") and torch.musa.is_available()
return (
not self.is_out_of_tree()
and hasattr(torch, "musa")
and torch.musa.is_available()
)
except ModuleNotFoundError:
return False
@@ -312,6 +356,10 @@ class Platform:
@lru_cache(maxsize=1)
def get_device(self, local_rank: int) -> torch.device:
if self.is_out_of_tree():
raise NotImplementedError(
"Out-of-tree diffusion platforms must implement get_device()"
)
if self.is_cuda() or self.is_rocm():
return torch.device("cuda", local_rank)
elif self.is_npu():
@@ -344,6 +392,17 @@ class Platform:
"No Accelerators(AMD/NV/MTT GPU, AMD MI instinct accelerators) available"
)
def supports_distributed_device_id(self) -> bool:
"""Whether torch.distributed accepts this platform's device ID."""
return not (
self.is_out_of_tree()
or self.is_mps()
or self.is_musa()
or self.is_npu()
or self.is_cpu()
or self.is_xpu()
)
@classmethod
def is_async_output_supported(cls, enforce_eager: bool | None) -> bool:
"""
@@ -421,11 +480,23 @@ class Platform:
@classmethod
def get_device_communicator_cls(cls) -> str:
"""
Get device specific communicator class for distributed communication.
"""
"""Return the platform's default device communicator class."""
return "sglang.multimodal_gen.runtime.distributed.device_communicators.base_device_communicator.DeviceCommunicatorBase" # noqa
@classmethod
def get_all_to_all_communicator_cls(cls) -> str:
"""Return the communicator used by ``all_to_all_4D``."""
qualname = cls.get_device_communicator_cls()
if (
cls._enum is PlatformEnum.OOT
and qualname == Platform.get_device_communicator_cls()
):
raise NotImplementedError(
"Out-of-tree diffusion platforms must implement "
"get_all_to_all_communicator_cls()"
)
return qualname
@classmethod
def get_cpu_architecture(cls) -> CpuArchEnum:
"""Get the CPU architecture of the current platform."""
@@ -121,6 +121,13 @@ class MpsPlatform(Platform):
# Use base communicator for MPS
return "sglang.multimodal_gen.runtime.distributed.device_communicators.base_device_communicator.DeviceCommunicatorBase"
@classmethod
def get_all_to_all_communicator_cls(cls) -> str:
return (
"sglang.multimodal_gen.runtime.distributed.device_communicators."
"cpu_communicator.CpuCommunicator"
)
@classmethod
def seed_everything(cls, seed: int | None = None) -> None:
"""Set the seed for MPS device."""
@@ -197,6 +197,13 @@ class NPUPlatformBase(Platform):
def get_device_communicator_cls(cls) -> str:
return "sglang.multimodal_gen.runtime.distributed.device_communicators.cuda_communicator.CudaCommunicator" # noqa
@classmethod
def get_all_to_all_communicator_cls(cls) -> str:
return (
"sglang.multimodal_gen.runtime.distributed.device_communicators."
"cpu_communicator.CpuCommunicator"
)
@classmethod
def enable_dit_layerwise_offload_by_default(cls) -> bool:
"""Whether automatic DiT layerwise offload is enabled on this platform."""
@@ -0,0 +1,274 @@
# SPDX-License-Identifier: Apache-2.0
import logging
import threading
from collections import Counter, defaultdict
from collections.abc import Callable
from enum import Enum
from importlib.metadata import EntryPoint, entry_points
from typing import Any
from sglang.multimodal_gen.runtime.platforms import (
PLATFORM_PLUGINS_GROUP,
get_selected_platform_dist,
)
from sglang.srt.environ import envs
from sglang.srt.plugins.hook_registry import HookRegistry as _HookRegistry
from sglang.srt.plugins.hook_registry import (
HookSource,
HookType,
_current_plugin_source,
)
logger = logging.getLogger(__name__)
GENERAL_PLUGINS_GROUP = "sglang.multimodal_gen.plugins"
class _OnceState(Enum):
NOT_STARTED = "not_started"
RUNNING = "running"
COMPLETE = "complete"
FAILED = "failed"
class _Once:
"""A process-local initialization gate.
The lock spans the action, so RUNNING means the caller is the thread already
inside: a callback that hands activation to another thread and joins it
deadlocks rather than racing. A failure is terminal.
"""
def __init__(self, name: str) -> None:
self.name = name
self.state = _OnceState.NOT_STARTED
self.error: BaseException | None = None
self._lock = threading.RLock()
def run(self, action: Callable[[], None]) -> bool:
"""Run *action* once; return False only for a re-entrant call."""
with self._lock:
if self.state is _OnceState.COMPLETE:
return True
if self.state is _OnceState.FAILED:
raise RuntimeError(
f"{self.name} previously failed: {self.error}"
) from self.error
if self.state is _OnceState.RUNNING:
return False
self.state = _OnceState.RUNNING
try:
action()
except BaseException as exc:
self.error = exc
self.state = _OnceState.FAILED
raise
self.state = _OnceState.COMPLETE
return True
def reset(self) -> None:
with self._lock:
self.state = _OnceState.NOT_STARTED
self.error = None
_plugin_registration = _Once("Diffusion plugin registration")
_hook_application = _Once("Diffusion hook application")
_required_dist: str | None = None
class HookRegistry(_HookRegistry):
# Rebound so diffusion hooks do not land in SRT's registry.
_hooks = defaultdict(list)
_patched = set()
def plugin_hook(target: str, type: HookType = HookType.AFTER) -> Callable:
def decorator(hook: Callable) -> Callable:
HookRegistry.register(target, hook, type)
return hook
return decorator
def _discard_hooks_from_source(source: HookSource) -> None:
"""Discard hooks registered by a plugin that failed to load or execute."""
for target in tuple(HookRegistry._hooks):
remaining = [hook for hook in HookRegistry._hooks[target] if hook[2] != source]
if remaining:
HookRegistry._hooks[target] = remaining
else:
del HookRegistry._hooks[target]
def _get_excluded_dists() -> set[str]:
selected_dist = get_selected_platform_dist()
platform_dists = {
entry_point.dist.name
for entry_point in entry_points(group=PLATFORM_PLUGINS_GROUP)
if entry_point.dist
}
if selected_dist is None:
return platform_dists
return platform_dists - {selected_dist}
def _discover() -> dict[str, tuple[Callable[[], Any], str | None]]:
allowed: set[str] | None = None
allowed_str = envs.SGLANG_PLUGINS.get()
if allowed_str:
allowed = {name.strip() for name in allowed_str.split(",") if name.strip()}
discovered = tuple(entry_points(group=GENERAL_PLUGINS_GROUP))
if not discovered:
logger.debug("No diffusion plugins found for group %s.", GENERAL_PLUGINS_GROUP)
return {}
excluded_dists = _get_excluded_dists()
required_dist = get_selected_platform_dist()
candidates: list[EntryPoint] = []
for entry_point in discovered:
dist_name = entry_point.dist.name if entry_point.dist else None
if allowed is not None and entry_point.name not in allowed:
logger.info(
"Skipping diffusion plugin %s (not in SGLANG_PLUGINS)",
entry_point.name,
)
continue
if dist_name in excluded_dists:
logger.info(
"Skipping diffusion plugin %s (dist %s is not the selected platform)",
entry_point.name,
dist_name,
)
continue
candidates.append(entry_point)
counts = Counter(entry_point.name for entry_point in candidates)
duplicates = sorted(name for name, count in counts.items() if count > 1)
if duplicates:
raise RuntimeError(
"Diffusion plugin entry-point names must be unique: "
+ ", ".join(repr(name) for name in duplicates)
)
plugins: dict[str, tuple[Callable[[], Any], str | None]] = {}
for entry_point in candidates:
dist_name = entry_point.dist.name if entry_point.dist else None
source = HookSource(plugin_name=entry_point.name, dist_name=dist_name)
token = _current_plugin_source.set(source)
try:
callback = entry_point.load()
if not callable(callback):
raise TypeError(
f"Diffusion plugin {entry_point.name!r} must resolve to a callable"
)
plugins[entry_point.name] = (callback, dist_name)
logger.info("Loaded diffusion plugin %s", entry_point.name)
except Exception:
_discard_hooks_from_source(source)
if required_dist and dist_name == required_dist:
raise
logger.exception("Failed to load diffusion plugin %s", entry_point.name)
finally:
_current_plugin_source.reset(token)
return plugins
def _require_hooks_applied(required_dist: str) -> None:
unapplied = sorted(
target
for target, hooks in HookRegistry._hooks.items()
if target not in HookRegistry._patched
and any(source and source.dist_name == required_dist for _, _, source in hooks)
)
if unapplied:
raise RuntimeError(
f"Selected platform package {required_dist!r} could not apply hooks on: "
+ ", ".join(unapplied)
)
def _register_plugins_once() -> str | None:
plugins = _discover()
# The selected platform's own plugins carry its hardware contract, so their
# failures abort startup; third-party ones stay best-effort.
required_dist = get_selected_platform_dist() if plugins else None
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 diffusion plugin: %s", name)
except Exception:
_discard_hooks_from_source(source)
if required_dist and dist_name == required_dist:
raise
logger.exception("Failed to execute diffusion plugin: %s", name)
finally:
_current_plugin_source.reset(token)
return required_dist
def load_plugins() -> None:
"""Discover and execute diffusion plugin callbacks once per process.
This phase only registers hooks. It deliberately does not resolve hook
targets: resolving a dotted target imports its module, which makes a
seemingly harmless plugin-discovery call capable of importing the entire
worker runtime.
Re-entrant calls from a plugin callback return immediately; a caller on
another thread waits for the in-flight registration. A failed load is
terminal for the process because arbitrary callback side effects cannot be
rolled back safely.
"""
_ensure_plugins_loaded()
def _ensure_plugins_loaded() -> bool:
def register() -> None:
global _required_dist
_required_dist = _register_plugins_once()
return _plugin_registration.run(register)
def apply_plugin_hooks() -> None:
"""Apply registered hooks once, at an explicit runtime-safe boundary.
Hook target resolution is allowed to import target modules. Callers that
require import ordering, notably spawned accelerator workers, must finish
platform initialization before entering this phase.
"""
if not _ensure_plugins_loaded():
# The outer activation applies the complete registry after registration.
return
def apply() -> None:
HookRegistry.apply_hooks()
if _required_dist:
_require_hooks_applied(_required_dist)
_hook_application.run(apply)
def _reset_lifecycle_for_tests() -> None:
global _required_dist
_plugin_registration.reset()
_hook_application.reset()
_required_dist = None
__all__ = [
"HookRegistry",
"HookType",
"apply_plugin_hooks",
"load_plugins",
"plugin_hook",
]
@@ -191,3 +191,10 @@ class XpuPlatform(Platform):
"""Get device communicator class for Intel XPU distributed communication."""
# Use base communicator for now; can be updated to use oneCCL-based communicator
return "sglang.multimodal_gen.runtime.distributed.device_communicators.base_device_communicator.DeviceCommunicatorBase"
@classmethod
def get_all_to_all_communicator_cls(cls) -> str:
return (
"sglang.multimodal_gen.runtime.distributed.device_communicators."
"cpu_communicator.CpuCommunicator"
)
@@ -1897,6 +1897,7 @@ class ServerArgs(DisaggServerArgsMixin):
self
)
current_platform.apply_server_args_defaults(self)
# configure logger before use
configure_logger(server_args=self)
@@ -20,7 +20,10 @@ def maybe_enable_inductor_compute_comm_overlap() -> None:
def build_torch_compile_kwargs(
*, mode: str | None, module: nn.Module | None = None
*,
mode: str | None,
module: nn.Module | None = None,
enable_inductor_compute_comm_overlap: bool = False,
) -> dict[str, object]:
compile_kwargs: dict[str, object] = {"fullgraph": False, "dynamic": None}
if current_platform.is_out_of_tree():
@@ -43,6 +46,11 @@ def build_torch_compile_kwargs(
compile_kwargs["dynamic"] = False
elif mode is not None:
compile_kwargs["mode"] = mode
if (
enable_inductor_compute_comm_overlap
and compile_kwargs.get("backend", "inductor") == "inductor"
):
maybe_enable_inductor_compute_comm_overlap()
return compile_kwargs
@@ -61,6 +69,24 @@ def resolve_torch_compile_mode(
return default
def resolve_torch_compile_kwargs(
*env_names: str,
config: object | None = None,
default: str,
module: nn.Module | None = None,
enable_inductor_compute_comm_overlap: bool = False,
) -> tuple[dict[str, object], str | None]:
mode = None
if not current_platform.is_npu():
mode = resolve_torch_compile_mode(*env_names, config=config, default=default)
compile_kwargs = build_torch_compile_kwargs(
mode=mode,
module=module,
enable_inductor_compute_comm_overlap=enable_inductor_compute_comm_overlap,
)
return compile_kwargs, mode
def compile_matching_submodules(
module: nn.Module,
*,
@@ -0,0 +1,14 @@
# SPDX-License-Identifier: Apache-2.0
"""The supported offline-script layout: only the facade at module scope.
``spawn`` re-executes this in every child, so binding ``DiffGenerator`` here
must not import the diffusion runtime.
"""
from offline_script_runner import main
from sglang.multimodal_gen import DiffGenerator # noqa: F401
if __name__ == "__main__":
main()
@@ -0,0 +1,58 @@
# SPDX-License-Identifier: Apache-2.0
"""Shared body of the offline-script fixtures in ``test_worker_bootstrap``.
Held apart so the scripts differ only in the module-scope import each is named
for; nothing here may import what the child's bootstrap has to precede.
"""
import json
import multiprocessing as mp
import sys
from sglang.multimodal_gen.runtime.managers import worker_bootstrap
_CHILD_REPLY_TIMEOUT_S = 120
_CHILD_JOIN_TIMEOUT_S = 10
def run_offline_script(result_path: str) -> None:
"""Spawn one scheduler child and write back what it observed."""
# Inside the guarded call on purpose: this is the import whose absence from
# the child's module scope the test is measuring.
from sglang.multimodal_gen.runtime.server_args import ServerArgs
reader, writer = mp.Pipe(duplex=False)
spec = worker_bootstrap.SchedulerProcessSpec(
local_rank=0,
rank=0,
server_args=worker_bootstrap.ServerArgsPayload.capture(
ServerArgs.__new__(ServerArgs)
),
pipe_writer=writer,
)
process = mp.get_context("spawn").Process(
target=worker_bootstrap.bootstrap_scheduler_process,
args=(spec,),
)
process.start()
writer.close()
observed = None
if reader.poll(_CHILD_REPLY_TIMEOUT_S):
try:
observed = reader.recv()
except EOFError:
pass
process.join(_CHILD_JOIN_TIMEOUT_S)
if process.is_alive():
process.kill()
process.join(_CHILD_JOIN_TIMEOUT_S)
with open(result_path, "w") as result_file:
json.dump({"observed": observed, "exitcode": process.exitcode}, result_file)
def main() -> None:
run_offline_script(sys.argv[1])
@@ -0,0 +1,16 @@
# SPDX-License-Identifier: Apache-2.0
"""An offline script that reaches past the facade at module scope.
The child therefore imports the runtime before it can initialize its platform.
Nothing in-tree can reorder that; the child is expected to say so.
"""
from offline_script_runner import main
from sglang.multimodal_gen.runtime.entrypoints.diffusion_generator import ( # noqa: F401
DiffGenerator,
)
if __name__ == "__main__":
main()
@@ -0,0 +1,86 @@
# SPDX-License-Identifier: Apache-2.0
"""Fake out-of-tree platform package used by ``test_worker_bootstrap``.
The test copies this file into a temporary directory alongside a generated
``sgl_fake_plugin-0.1.dist-info`` and puts that directory on ``sys.path``, so a
spawned child discovers it through real entry-point metadata.
It records which diffusion modules were already imported at each bootstrap
boundary, which is why it must stay free of diffusion runtime imports beyond
``Platform``: importing an observed module here would corrupt the measurement.
"""
import sys
from sglang.multimodal_gen.runtime.platforms import Platform, PlatformEnum
WORKER_MODULE = "sglang.multimodal_gen.runtime.managers.gpu_worker"
GENERATOR_MODULE = "sglang.multimodal_gen.runtime.entrypoints.diffusion_generator"
SERVER_ARGS_MODULE = "sglang.multimodal_gen.runtime.server_args.server_args"
worker_imported_when_plugin_ran = None
generator_imported_when_plugin_ran = None
server_args_imported_when_plugin_ran = None
backend_initialized_when_plugin_ran = None
worker_imported_when_backend_initialized = None
server_args_imported_when_backend_initialized = None
backend_initialized = False
class FakePlatform(Platform):
_enum = PlatformEnum.OOT
device_name = "fake"
device_type = "fake"
dispatch_key = "PrivateUse1"
def init_backend(self):
global backend_initialized, server_args_imported_when_backend_initialized
global worker_imported_when_backend_initialized
worker_imported_when_backend_initialized = WORKER_MODULE in sys.modules
server_args_imported_when_backend_initialized = (
SERVER_ARGS_MODULE in sys.modules
)
backend_initialized = True
def activate():
return "sgl_fake_plugin.FakePlatform"
def replacement(pipe_writer, *args, **kwargs):
pipe_writer.send(
{
"override_ran": True,
"worker_imported_when_plugin_ran": worker_imported_when_plugin_ran,
"generator_imported_when_plugin_ran": generator_imported_when_plugin_ran,
"server_args_imported_when_plugin_ran": (
server_args_imported_when_plugin_ran
),
"backend_initialized_when_plugin_ran": backend_initialized_when_plugin_ran,
"worker_imported_when_backend_initialized": (
worker_imported_when_backend_initialized
),
"server_args_imported_when_backend_initialized": (
server_args_imported_when_backend_initialized
),
"backend_initialized": backend_initialized,
}
)
pipe_writer.close()
def register():
global backend_initialized_when_plugin_ran
global generator_imported_when_plugin_ran, server_args_imported_when_plugin_ran
global worker_imported_when_plugin_ran
backend_initialized_when_plugin_ran = backend_initialized
worker_imported_when_plugin_ran = WORKER_MODULE in sys.modules
generator_imported_when_plugin_ran = GENERATOR_MODULE in sys.modules
server_args_imported_when_plugin_ran = SERVER_ARGS_MODULE in sys.modules
from sglang.multimodal_gen.runtime.platforms.plugins import HookRegistry, HookType
HookRegistry.register(
WORKER_MODULE + ".run_scheduler_process", replacement, HookType.REPLACE
)
@@ -0,0 +1,505 @@
# SPDX-License-Identifier: Apache-2.0
import os
import threading
import unittest
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from sglang.multimodal_gen.runtime.platforms import plugins
from sglang.srt.plugins.hook_registry import HookRegistry as SrtHookRegistry
from sglang.srt.plugins.hook_registry import (
HookSource,
HookType,
)
_THREAD_TIMEOUT_S = 10
class _Caller(threading.Thread):
"""Runs one activation call on its own thread, keeping what it raised."""
def __init__(self, call):
super().__init__(daemon=True)
self._call = call
self.error = None
def run(self):
try:
self._call()
except BaseException as exc:
self.error = exc
class _GateProbe:
"""A gate lock that reports when a thread genuinely has to wait on it.
The non-blocking attempt fails only for a non-owner, so the arrival is
observable instead of guessed at with a sleep that can silently miss.
"""
def __init__(self, lock):
self._lock = lock
self.blocked = threading.Event()
def __enter__(self):
if not self._lock.acquire(blocking=False):
self.blocked.set()
self._lock.acquire()
def __exit__(self, *exc_info):
self._lock.release()
def _entry_point(name, distribution):
entry_point = MagicMock(name=f"entry_point_{name}")
entry_point.name = name
entry_point.value = f"test_plugin:{name}"
entry_point.dist = SimpleNamespace(name=distribution)
entry_point.load.return_value = MagicMock(name=f"plugin_{name}")
return entry_point
class _ThreadedTestCase(unittest.TestCase):
def setUp(self):
super().setUp()
self._callers = []
# A failed assertion can leave a caller unjoined; it must not run on
# into the next test.
self.addCleanup(self._join_started_callers)
def _join_started_callers(self):
for caller in self._callers:
caller.join(_THREAD_TIMEOUT_S)
def _finish(self):
for caller in self._callers:
caller.join(_THREAD_TIMEOUT_S)
self.assertFalse(caller.is_alive(), "activation thread never finished")
def _start_caller(self, call):
caller = _Caller(call)
self._callers.append(caller)
caller.start()
return caller
def _start_second_caller(self, call, probe):
"""Start *call* elsewhere and wait until it is provably blocked at the gate."""
caller = self._start_caller(call)
self.assertTrue(
probe.blocked.wait(_THREAD_TIMEOUT_S),
"second thread never reached the gate",
)
return caller
class TestOnceGate(_ThreadedTestCase):
"""Gate semantics on a fresh instance, clear of the module-global phases."""
def test_a_waiting_thread_inherits_the_failure(self):
"""A failure must reach a caller that arrived while the phase ran."""
once = plugins._Once("test gate")
probe = _GateProbe(once._lock)
once._lock = probe
inside = threading.Event()
release = threading.Event()
def boom():
inside.set()
self.assertTrue(
release.wait(_THREAD_TIMEOUT_S), "release was never signalled"
)
raise RuntimeError("vendor plugin exploded")
first = self._start_caller(lambda: once.run(boom))
try:
self.assertTrue(inside.wait(_THREAD_TIMEOUT_S))
second = self._start_second_caller(lambda: once.run(lambda: None), probe)
finally:
release.set()
self._finish()
self.assertIsInstance(first.error, RuntimeError)
self.assertIn("exploded", str(first.error))
self.assertIsInstance(second.error, RuntimeError)
self.assertIn("previously failed", str(second.error))
class TestDiffusionPluginBarrier(_ThreadedTestCase):
def setUp(self):
super().setUp()
state = (
plugins._plugin_registration.state,
plugins._plugin_registration.error,
plugins._hook_application.state,
plugins._hook_application.error,
plugins._required_dist,
)
self.addCleanup(self._restore_lifecycle, state)
plugins._reset_lifecycle_for_tests()
self._callers = []
# A failed assertion can leave a caller unjoined; it must not run on
# into the next test.
self.addCleanup(self._join_started_callers)
@staticmethod
def _restore_lifecycle(state):
(
plugins._plugin_registration.state,
plugins._plugin_registration.error,
plugins._hook_application.state,
plugins._hook_application.error,
plugins._required_dist,
) = state
def test_body_runs_once_across_repeated_calls(self):
with patch.object(plugins, "_register_plugins_once") as load_once:
plugins.load_plugins()
plugins.load_plugins()
load_once.assert_called_once_with()
def test_failure_is_terminal_instead_of_replaying_partial_side_effects(self):
with patch.object(
plugins,
"_register_plugins_once",
side_effect=RuntimeError("plugin init exploded"),
) as load_once:
with self.assertRaisesRegex(RuntimeError, "exploded"):
plugins.load_plugins()
with self.assertRaisesRegex(RuntimeError, "previously failed"):
plugins.load_plugins()
load_once.assert_called_once_with()
def test_reentrant_call_returns_instead_of_recursing(self):
calls = []
def reentrant():
calls.append(1)
plugins.load_plugins()
with patch.object(plugins, "_register_plugins_once", reentrant):
plugins.load_plugins()
self.assertEqual(len(calls), 1)
def test_hook_application_is_a_separate_once_only_phase(self):
with (
patch.object(plugins, "_register_plugins_once", return_value="vendor-pkg"),
patch.object(plugins.HookRegistry, "apply_hooks") as apply_hooks,
patch.object(plugins, "_require_hooks_applied") as require_hooks,
):
plugins.load_plugins()
apply_hooks.assert_not_called()
plugins.apply_plugin_hooks()
plugins.apply_plugin_hooks()
apply_hooks.assert_called_once_with()
require_hooks.assert_called_once_with("vendor-pkg")
def test_hook_application_failure_is_terminal(self):
with (
patch.object(plugins, "_register_plugins_once", return_value=None),
patch.object(
plugins.HookRegistry,
"apply_hooks",
side_effect=RuntimeError("hook application exploded"),
) as apply_hooks,
):
with self.assertRaisesRegex(RuntimeError, "application exploded"):
plugins.apply_plugin_hooks()
with self.assertRaisesRegex(RuntimeError, "previously failed"):
plugins.apply_plugin_hooks()
apply_hooks.assert_called_once_with()
def test_hooks_from_a_reentrant_callback_are_applied_by_the_outer_call(self):
def reentrant():
plugins.apply_plugin_hooks()
with (
patch.object(plugins, "_register_plugins_once", reentrant),
patch.object(plugins.HookRegistry, "apply_hooks") as apply_hooks,
):
plugins.apply_plugin_hooks()
apply_hooks.assert_called_once_with()
def _probe_registration_gate(self):
probe = _GateProbe(plugins._plugin_registration._lock)
patcher = patch.object(plugins._plugin_registration, "_lock", probe)
patcher.start()
self.addCleanup(patcher.stop)
return probe
def test_a_second_thread_does_not_skip_hook_application(self):
"""A caller that arrives during registration must not return before the
registry is applied."""
inside = threading.Event()
release = threading.Event()
applied_on_return = []
probe = self._probe_registration_gate()
def slow_register():
inside.set()
self.assertTrue(
release.wait(_THREAD_TIMEOUT_S), "release was never signalled"
)
with (
patch.object(plugins, "_register_plugins_once", slow_register),
patch.object(plugins.HookRegistry, "apply_hooks") as apply_hooks,
):
def apply_and_report():
plugins.apply_plugin_hooks()
applied_on_return.append(apply_hooks.call_count)
first = self._start_caller(plugins.load_plugins)
try:
self.assertTrue(inside.wait(_THREAD_TIMEOUT_S))
second = self._start_second_caller(apply_and_report, probe)
finally:
release.set()
self._finish()
apply_hooks.assert_called_once_with()
self.assertIsNone(first.error)
self.assertIsNone(second.error)
self.assertEqual(
applied_on_return,
[1],
"second thread returned before the registry was applied",
)
class TestDiffusionPlugins(unittest.TestCase):
def setUp(self):
# _discover() reads this live, so an allowlist set in the environment
# would filter the mocked entry points out from under these tests.
patcher = patch.dict(os.environ, {"SGLANG_PLUGINS": ""})
patcher.start()
self.addCleanup(patcher.stop)
def test_load_executes_callbacks_without_resolving_hook_targets(self):
register = MagicMock()
with (
patch.object(
plugins, "_discover", return_value={"test": (register, "test-package")}
),
patch.object(
plugins, "get_selected_platform_dist", return_value="test-package"
),
patch.object(plugins.HookRegistry, "apply_hooks") as apply_hooks,
):
required_dist = plugins._register_plugins_once()
register.assert_called_once_with()
apply_hooks.assert_not_called()
self.assertEqual(required_dist, "test-package")
def test_a_failing_callback_does_not_stop_the_others(self):
healthy = MagicMock()
with (
patch.object(
plugins,
"_discover",
return_value={
"broken": (MagicMock(side_effect=RuntimeError("boom")), "a"),
"healthy": (healthy, "b"),
},
),
# Unpatched, this runs real platform detection.
patch.object(plugins, "get_selected_platform_dist", return_value=None),
):
plugins._register_plugins_once()
healthy.assert_called_once_with()
def test_hooks_registered_while_importing_a_plugin_keep_their_source(self):
target = "test_diffusion_plugins.import_time_target"
hook = MagicMock()
entry_point = _entry_point("vendor", "vendor-pkg")
def load():
plugins.plugin_hook(target)(hook)
return MagicMock()
entry_point.load.side_effect = load
self.addCleanup(plugins.HookRegistry._hooks.pop, target, None)
with (
patch.object(plugins, "entry_points", return_value=[entry_point]),
patch.object(plugins, "_get_excluded_dists", return_value=set()),
patch.object(
plugins, "get_selected_platform_dist", return_value="vendor-pkg"
),
):
plugins._discover()
self.assertEqual(
plugins.HookRegistry._hooks[target][0][2],
HookSource("vendor", "vendor-pkg"),
)
def test_failing_optional_callback_discards_its_registered_hooks(self):
target = "test_diffusion_plugins.partial_callback_target"
def register_then_fail():
plugins.plugin_hook(target)(lambda result: result)
raise RuntimeError("boom")
self.addCleanup(plugins.HookRegistry._hooks.pop, target, None)
with (
patch.object(
plugins,
"_discover",
return_value={"broken": (register_then_fail, "optional-pkg")},
),
patch.object(
plugins, "get_selected_platform_dist", return_value="vendor-pkg"
),
):
plugins._register_plugins_once()
self.assertNotIn(target, plugins.HookRegistry._hooks)
def test_failing_optional_import_discards_its_registered_hooks(self):
target = "test_diffusion_plugins.partial_import_target"
entry_point = _entry_point("broken", "optional-pkg")
def load_then_fail():
plugins.plugin_hook(target)(lambda result: result)
raise RuntimeError("boom")
entry_point.load.side_effect = load_then_fail
self.addCleanup(plugins.HookRegistry._hooks.pop, target, None)
with (
patch.object(plugins, "entry_points", return_value=[entry_point]),
patch.object(plugins, "_get_excluded_dists", return_value=set()),
patch.object(
plugins, "get_selected_platform_dist", return_value="vendor-pkg"
),
):
self.assertEqual(plugins._discover(), {})
self.assertNotIn(target, plugins.HookRegistry._hooks)
def test_duplicate_plugin_names_fail_before_import(self):
entries = [_entry_point("duplicate", "one"), _entry_point("duplicate", "two")]
with (
patch.object(plugins, "entry_points", return_value=entries),
patch.object(plugins, "_get_excluded_dists", return_value=set()),
patch.object(plugins, "get_selected_platform_dist", return_value=None),
self.assertRaisesRegex(RuntimeError, "must be unique"),
):
plugins._discover()
for entry_point in entries:
entry_point.load.assert_not_called()
def test_a_failing_load_from_the_selected_platform_aborts_startup(self):
broken = _entry_point("vendor", "vendor-pkg")
broken.load.side_effect = RuntimeError("vendor wheel is broken")
unrelated = _entry_point("other", "other-pkg")
unrelated.load.side_effect = RuntimeError("third party is broken")
with (
patch.object(plugins, "entry_points", return_value=[unrelated]),
patch.object(plugins, "_get_excluded_dists", return_value=set()),
patch.object(
plugins, "get_selected_platform_dist", return_value="vendor-pkg"
),
):
self.assertEqual(plugins._discover(), {})
with (
patch.object(plugins, "entry_points", return_value=[broken]),
patch.object(plugins, "_get_excluded_dists", return_value=set()),
patch.object(
plugins, "get_selected_platform_dist", return_value="vendor-pkg"
),
self.assertRaisesRegex(RuntimeError, "vendor wheel is broken"),
):
plugins._discover()
def test_a_failing_callback_from_the_selected_platform_aborts_startup(self):
boom = MagicMock(side_effect=RuntimeError("vendor hook is broken"))
with (
patch.object(plugins, "_discover", return_value={"x": (boom, "other-pkg")}),
patch.object(
plugins, "get_selected_platform_dist", return_value="vendor-pkg"
),
):
plugins._register_plugins_once()
with (
patch.object(
plugins, "_discover", return_value={"v": (boom, "vendor-pkg")}
),
patch.object(
plugins, "get_selected_platform_dist", return_value="vendor-pkg"
),
self.assertRaisesRegex(RuntimeError, "vendor hook is broken"),
):
plugins._register_plugins_once()
def test_an_unapplied_hook_from_the_selected_platform_aborts_startup(self):
# apply_hooks() logs and moves on, so a required target can go unpatched.
target = "test_diffusion_plugins.unappliable"
self.addCleanup(plugins.HookRegistry._hooks.pop, target, None)
self.addCleanup(plugins.HookRegistry._patched.discard, target)
plugins.HookRegistry._hooks[target] = [
(HookType.AFTER, lambda r: r, HookSource("v", "vendor-pkg"))
]
with self.assertRaisesRegex(RuntimeError, "could not apply hooks"):
plugins._require_hooks_applied("vendor-pkg")
plugins._require_hooks_applied("other-pkg")
plugins.HookRegistry._patched.add(target)
plugins._require_hooks_applied("vendor-pkg")
def test_plugin_hook_uses_the_diffusion_registry(self):
target = "test_diffusion_plugins.target"
def hook():
pass
self.addCleanup(plugins.HookRegistry._hooks.pop, target, None)
plugins.plugin_hook(target)(hook)
self.assertIs(plugins.HookRegistry._hooks[target][0][1], hook)
self.assertNotIn(target, SrtHookRegistry._hooks)
def test_excludes_every_unselected_platform_distribution(self):
entries = [
_entry_point("selected", "selected-package"),
_entry_point("selected_extra", "selected-package"),
_entry_point("other", "other-package"),
]
# None is a built-in platform: nothing installed is in use.
cases = (
("selected-package", {"other-package"}),
(None, {"selected-package", "other-package"}),
)
for selected_dist, expected in cases:
with (
self.subTest(selected_dist=selected_dist),
patch.object(
plugins, "get_selected_platform_dist", return_value=selected_dist
),
patch.object(plugins, "entry_points", return_value=entries),
):
self.assertEqual(plugins._get_excluded_dists(), expected)
if __name__ == "__main__":
unittest.main()
@@ -550,7 +550,7 @@ def test_validate_server_args_requires_packed_varlen_backend():
resolve_component_attention_backend=lambda *_names: (None, None),
)
with patch(
"sglang.multimodal_gen.configs.pipeline_configs.minimax_h3.get_attn_backend"
"sglang.multimodal_gen.runtime.layers.attention.selector.get_attn_backend"
) as get_attn_backend:
MiniMaxH3PipelineConfig.validate_server_args(config, server_args)
get_attn_backend.assert_called_once_with(
@@ -560,7 +560,7 @@ def test_validate_server_args_requires_packed_varlen_backend():
attention_requirements=AttentionRequirements(packed_varlen=True),
)
with patch(
"sglang.multimodal_gen.configs.pipeline_configs.minimax_h3.get_attn_backend",
"sglang.multimodal_gen.runtime.layers.attention.selector.get_attn_backend",
side_effect=ValueError("does not implement packed varlen attention"),
):
with pytest.raises(ValueError, match="does not implement packed varlen"):
@@ -590,7 +590,7 @@ def test_validate_server_args_accepts_transformer_backend_override():
)
with patch(
"sglang.multimodal_gen.configs.pipeline_configs.minimax_h3.get_attn_backend"
"sglang.multimodal_gen.runtime.layers.attention.selector.get_attn_backend"
) as get_attn_backend:
MiniMaxH3PipelineConfig.validate_server_args(config, server_args)
get_attn_backend.assert_called_once_with(
@@ -621,7 +621,7 @@ def test_resolve_transformer_attention_backend_uses_selector_precedence():
),
)
with patch(
"sglang.multimodal_gen.configs.pipeline_configs.minimax_h3."
"sglang.multimodal_gen.runtime.layers.attention.selector."
"get_global_forced_attn_backend",
return_value=forced_backend,
):
@@ -0,0 +1,345 @@
# SPDX-License-Identifier: Apache-2.0
import unittest
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import torch
import sglang.multimodal_gen.runtime.platforms as runtime_platforms
from sglang.multimodal_gen.runtime.distributed import group_coordinator, parallel_state
from sglang.multimodal_gen.runtime.distributed.device_communicators.base_device_communicator import (
DeviceCommunicatorBase,
)
from sglang.multimodal_gen.runtime.distributed.device_communicators.cpu_communicator import (
CpuCommunicator,
)
from sglang.multimodal_gen.runtime.layers import custom_op
from sglang.multimodal_gen.runtime.layers.activation import SiluAndMul
from sglang.multimodal_gen.runtime.layers.custom_op import CustomOp
from sglang.multimodal_gen.runtime.layers.layernorm import RMSNorm
from sglang.multimodal_gen.runtime.managers import gpu_worker
from sglang.multimodal_gen.runtime.platforms.interface import (
Platform,
PlatformEnum,
)
class _OotPlatform(Platform):
_enum = PlatformEnum.OOT
device_name = "test"
device_type = "test"
def get_dispatch_key_name(self) -> str:
return "test"
class _DispatchKeyOotPlatform(_OotPlatform):
dispatch_key = "PrivateUse1"
class _ExistingCommunicatorPlatform(_OotPlatform):
@classmethod
def get_device_communicator_cls(cls) -> str:
return "test.LegacyCommunicator"
class _TestOp(CustomOp):
def forward_native(self, value):
return ("native", value)
class _CudaCompatibleTestOp(_TestOp):
def forward_cuda(self, value):
return ("cuda", value)
class _TestCommunicator(DeviceCommunicatorBase):
pass
class TestOotCustomOpDispatch(unittest.TestCase):
def tearDown(self):
CustomOp._oot_forward_registry.pop("test", None)
CustomOp._oot_forward_registry.pop("other", None)
def test_registered_forward_is_used(self):
forward = MagicMock(return_value=("registered", 7))
CustomOp.register_oot_forward(_TestOp, fn=forward, platform_key="test")
with patch.object(custom_op.platforms, "_current_platform", _OotPlatform()):
op = _TestOp()
self.assertEqual(op(7), ("registered", 7))
forward.assert_called_once_with(op, 7)
def test_missing_registration_uses_native_fallback(self):
CustomOp.register_oot_forward(_TestOp, fn=MagicMock(), platform_key="other")
with patch.object(custom_op.platforms, "_current_platform", _OotPlatform()):
op = _TestOp()
self.assertEqual(op(7), ("native", 7))
def test_compiled_forward_does_not_recompile_after_dispatch(self):
def oot_forward(op, value):
return op.forward_native(value)
CustomOp.register_oot_forward(SiluAndMul, fn=oot_forward, platform_key="test")
cases = (
(SiluAndMul, torch.randn(2, 8)),
(lambda: RMSNorm(4), torch.randn(2, 4)),
)
for factory, value in cases:
with self.subTest(op=factory):
compile_count = 0
def counting_backend(graph_module, _example_inputs):
nonlocal compile_count
compile_count += 1
return graph_module.forward
with patch.object(
custom_op.platforms, "_current_platform", _OotPlatform()
):
op = factory()
selected_forward = op._forward_method
compiled = torch.compile(op, backend=counting_backend, fullgraph=True)
expected = op.forward_native(value)
torch.testing.assert_close(compiled(value), expected)
torch.testing.assert_close(compiled(value), expected)
self.assertEqual(compile_count, 1)
self.assertIs(op._forward_method, selected_forward)
def test_platform_dispatch_key_can_reuse_an_existing_forward(self):
platform = _OotPlatform()
platform.get_dispatch_key_name = lambda: "cuda"
with patch.object(custom_op.platforms, "_current_platform", platform):
self.assertEqual(_CudaCompatibleTestOp()(7), ("cuda", 7))
def test_platform_dispatch_key_must_be_nonempty(self):
platform = _OotPlatform()
platform.get_dispatch_key_name = lambda: " "
with (
patch.object(custom_op.platforms, "_current_platform", platform),
self.assertRaisesRegex(ValueError, "non-empty"),
):
_TestOp()(7)
class TestOotBackendInit(unittest.TestCase):
def setUp(self):
state = (
runtime_platforms._backend_init_done,
runtime_platforms._backend_init_error,
)
self.addCleanup(self._restore_backend_state, state)
runtime_platforms._backend_init_done = False
runtime_platforms._backend_init_error = None
@staticmethod
def _restore_backend_state(state):
(
runtime_platforms._backend_init_done,
runtime_platforms._backend_init_error,
) = state
def test_backend_initialization_runs_once(self):
platform = _OotPlatform()
with (
patch.object(runtime_platforms, "_current_platform", platform),
patch.object(platform, "init_backend") as init_backend,
):
runtime_platforms.initialize_current_platform()
runtime_platforms.initialize_current_platform()
init_backend.assert_called_once_with()
def test_backend_initialization_failure_is_not_retried(self):
platform = _OotPlatform()
error = RuntimeError("backend unavailable")
with (
patch.object(runtime_platforms, "_current_platform", platform),
patch.object(platform, "init_backend", side_effect=error) as init_backend,
):
with self.assertRaisesRegex(RuntimeError, "backend unavailable"):
runtime_platforms.initialize_current_platform()
with self.assertRaisesRegex(RuntimeError, "previously failed"):
runtime_platforms.initialize_current_platform()
init_backend.assert_called_once_with()
def test_interrupted_backend_initialization_is_not_reported_as_success(self):
"""An interrupt must leave the process failed, not silently initialized."""
platform = _OotPlatform()
with (
patch.object(runtime_platforms, "_current_platform", platform),
patch.object(
platform, "init_backend", side_effect=KeyboardInterrupt
) as init_backend,
):
with self.assertRaises(KeyboardInterrupt):
runtime_platforms.initialize_current_platform()
with self.assertRaisesRegex(RuntimeError, "previously failed"):
runtime_platforms.initialize_current_platform()
init_backend.assert_called_once_with()
def test_worker_runs_init_backend_before_building_the_scheduler(self):
order = []
platform = MagicMock()
platform.is_cuda.return_value = False
platform.is_musa.return_value = False
with (
patch.object(gpu_worker, "current_platform", platform),
patch.object(
gpu_worker,
"initialize_current_platform",
side_effect=lambda: order.append("init_backend"),
),
patch.object(gpu_worker, "kill_itself_when_parent_died"),
patch.object(gpu_worker, "configure_logger"),
patch.object(gpu_worker, "globally_suppress_loggers"),
patch.object(
gpu_worker,
"init_diffusion_tracing",
side_effect=lambda *a, **k: order.append("tracing"),
),
patch.object(
gpu_worker.PortArgs,
"from_server_args",
side_effect=RuntimeError("stop before Scheduler"),
),
self.assertRaisesRegex(RuntimeError, "stop before Scheduler"),
):
gpu_worker.run_scheduler_process(
local_rank=0,
rank=0,
server_args=MagicMock(),
pipe_writer=None,
)
self.assertEqual(order, ["init_backend", "tracing"])
class TestOotRequiredConfiguration(unittest.TestCase):
def test_device_and_dispatch_defaults_fail_loudly(self):
platform = type("Oot", (Platform,), {"_enum": PlatformEnum.OOT})()
with self.assertRaisesRegex(NotImplementedError, "implement get_device"):
platform.get_device(0)
with self.assertRaisesRegex(NotImplementedError, "define dispatch_key"):
platform.get_torch_library_dispatch_key()
with self.assertRaisesRegex(
NotImplementedError, "implement get_all_to_all_communicator_cls"
):
platform.get_all_to_all_communicator_cls()
self.assertEqual(
_DispatchKeyOotPlatform().get_torch_library_dispatch_key(),
"PrivateUse1",
)
def test_builtin_torch_library_dispatch_is_preserved(self):
platform = Platform()
for is_npu, expected in ((False, "CUDA"), (True, "PrivateUse1")):
with (
self.subTest(is_npu=is_npu),
patch.object(platform, "is_out_of_tree", return_value=False),
patch.object(platform, "is_npu", return_value=is_npu),
):
self.assertEqual(platform.get_torch_library_dispatch_key(), expected)
from sglang.multimodal_gen.runtime.platforms.xpu import XpuPlatform
self.assertEqual(XpuPlatform().get_torch_library_dispatch_key(), "CUDA")
def test_existing_communicator_override_remains_the_fallback(self):
self.assertEqual(
_ExistingCommunicatorPlatform.get_all_to_all_communicator_cls(),
"test.LegacyCommunicator",
)
class TestOotRuntimeHooks(unittest.TestCase):
def test_builtin_overrides_keep_non_cuda_all_to_all_on_cpu(self):
from sglang.multimodal_gen.runtime.platforms.mps import MpsPlatform
from sglang.multimodal_gen.runtime.platforms.npu import NPUPlatformBase
from sglang.multimodal_gen.runtime.platforms.xpu import XpuPlatform
for platform_cls in (MpsPlatform, NPUPlatformBase, XpuPlatform):
with self.subTest(platform=platform_cls.__name__):
with patch.object(
group_coordinator, "current_platform", platform_cls()
):
self.assertIs(
group_coordinator._resolve_all_to_all_communicator_cls(),
CpuCommunicator,
)
def test_platform_selects_all_to_all_communicator(self):
platform = MagicMock()
platform.get_all_to_all_communicator_cls.return_value = "vendor.Communicator"
with (
patch.object(group_coordinator, "current_platform", platform),
patch.object(
group_coordinator,
"resolve_name",
return_value=_TestCommunicator,
) as resolve_name,
):
self.assertIs(
group_coordinator._resolve_all_to_all_communicator_cls(),
_TestCommunicator,
)
resolve_name.assert_called_once_with("vendor.Communicator")
def test_rejects_invalid_all_to_all_communicator(self):
platform = MagicMock()
platform.get_all_to_all_communicator_cls.return_value = "vendor.Communicator"
with (
patch.object(group_coordinator, "current_platform", platform),
patch.object(group_coordinator, "resolve_name", return_value=object),
self.assertRaisesRegex(TypeError, "DeviceCommunicatorBase subclass"),
):
group_coordinator._resolve_all_to_all_communicator_cls()
def test_platform_controls_distributed_device_id(self):
device_id = object()
for supported in (False, True):
platform = MagicMock(device_name="test")
platform.get_torch_distributed_backend_str.return_value = "gloo"
platform.supports_distributed_device_id.return_value = supported
with (
self.subTest(supported=supported),
patch.object(runtime_platforms, "_current_platform", platform),
patch.object(parallel_state, "_WORLD", SimpleNamespace(world_size=1)),
patch.object(
parallel_state.torch.distributed,
"is_initialized",
return_value=False,
),
patch.object(
parallel_state.torch.distributed, "init_process_group"
) as init_process_group,
patch.object(
parallel_state.torch.distributed,
"get_world_size",
return_value=1,
),
patch.object(parallel_state, "_sync_srt_world_group"),
):
parallel_state.init_distributed_environment(device_id=device_id)
kwargs = init_process_group.call_args.kwargs
if supported:
self.assertIs(kwargs["device_id"], device_id)
else:
self.assertNotIn("device_id", kwargs)
if __name__ == "__main__":
unittest.main()
@@ -1,7 +1,9 @@
# SPDX-License-Identifier: Apache-2.0
import os
import unittest
from unittest.mock import patch
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import torch
@@ -12,6 +14,15 @@ class NVMLUnavailableError(Exception):
pass
def _entry_point(name: str, result: str | None, dist: str | None = None):
entry_point = MagicMock(name=f"entry_point_{name}")
entry_point.name = name
entry_point.value = f"test_plugin:{name}"
entry_point.dist = SimpleNamespace(name=dist) if dist else None
entry_point.load.return_value = MagicMock(return_value=result)
return entry_point
class TestCudaPlatformDetection(unittest.TestCase):
def test_torch_fallback_excludes_hip(self):
cases = (
@@ -29,8 +40,8 @@ class TestCudaPlatformDetection(unittest.TestCase):
"sglang.multimodal_gen.runtime.platforms.pynvml.nvmlInit",
side_effect=NVMLUnavailableError,
),
patch.object(platforms.os.path, "isfile", return_value=False),
patch.object(platforms.os.path, "exists", return_value=False),
patch.object(os.path, "isfile", return_value=False),
patch.object(os.path, "exists", return_value=False),
patch.object(torch.version, "hip", hip_version, create=True),
patch.object(torch.cuda, "is_available", return_value=True),
patch.object(torch.cuda, "device_count", return_value=1),
@@ -38,5 +49,233 @@ class TestCudaPlatformDetection(unittest.TestCase):
self.assertEqual(platforms.cuda_platform_plugin(), expected)
class TestDiffusionPlatformPlugins(unittest.TestCase):
def setUp(self):
self.env = patch.dict(
os.environ,
{"SGLANG_DIFFUSION_PLATFORM_OVERRIDE": ""},
)
self.env.start()
self.addCleanup(self.env.stop)
current_platform = platforms._current_platform
current_selection = platforms._current_platform_selection
self.addCleanup(setattr, platforms, "_current_platform", current_platform)
self.addCleanup(
setattr, platforms, "_current_platform_selection", current_selection
)
self._reset_current_platform()
@patch.object(platforms, "entry_points")
def test_selected_platform_records_its_distribution(self, entry_points):
class _FakeOot(platforms.Platform):
_enum = platforms.PlatformEnum.OOT
device_name = "fake"
device_type = "fake"
cases = (("selected", "explicit override"), ("", "automatic discovery"))
for override, description in cases:
with (
self.subTest(description=description),
patch.object(platforms, "resolve_name", return_value=_FakeOot),
):
self._reset_current_platform()
entry_points.return_value = [
_entry_point("selected", "vendor.platform.Platform", "vendor-pkg"),
_entry_point("inactive", None, "other-pkg"),
]
os.environ["SGLANG_DIFFUSION_PLATFORM_OVERRIDE"] = override
self.assertEqual(platforms.get_selected_platform_dist(), "vendor-pkg")
self.assertIsInstance(platforms._current_platform, _FakeOot)
def test_accessor_reports_no_distribution_for_a_builtin(self):
os.environ["SGLANG_DIFFUSION_PLATFORM_OVERRIDE"] = "cpu"
self._reset_current_platform()
self.assertIsNone(platforms.get_selected_platform_dist())
def _reset_current_platform(self):
platforms._current_platform = None
platforms._current_platform_selection = None
@patch.object(platforms, "entry_points")
def test_explicit_selection_loads_only_selected_plugin(self, entry_points):
selected = _entry_point("selected", "vendor.platform.Platform")
ignored = [
_entry_point("duplicate", None),
_entry_point("duplicate", None),
_entry_point("cuda", "squatter.Platform"),
]
entry_points.return_value = [selected, *ignored]
os.environ["SGLANG_DIFFUSION_PLATFORM_OVERRIDE"] = "selected"
self.assertEqual(
platforms.resolve_current_platform_cls_qualname(),
"vendor.platform.Platform",
)
selected.load.assert_called_once_with()
for entry_point in ignored:
entry_point.load.assert_not_called()
@patch.object(platforms, "entry_points")
def test_auto_detection_requires_one_active_plugin(self, entry_points):
entry_points.return_value = [
_entry_point("inactive", None),
_entry_point("active", "vendor.platform.Platform"),
]
self.assertEqual(
platforms.resolve_current_platform_cls_qualname(),
"vendor.platform.Platform",
)
entry_points.return_value = [
_entry_point("first", "first.Platform"),
_entry_point("second", "second.Platform"),
]
with self.assertRaisesRegex(RuntimeError, "Multiple platform plugins"):
platforms.resolve_current_platform_cls_qualname()
@patch.object(platforms, "entry_points")
def test_invalid_entry_point_names_fail_before_import(self, entry_points):
cases = (
([_entry_point("same", None), _entry_point("same", None)], "", "unique"),
(
[_entry_point("same", None), _entry_point("same", None)],
"same",
"unique",
),
([_entry_point("XPU", None)], "", "built-in"),
)
for entries, selected, message in cases:
with self.subTest(message=message):
entry_points.return_value = entries
os.environ["SGLANG_DIFFUSION_PLATFORM_OVERRIDE"] = selected
with self.assertRaisesRegex(RuntimeError, message):
platforms.resolve_current_platform_cls_qualname()
for entry_point in entries:
entry_point.load.assert_not_called()
@patch.object(platforms, "entry_points")
def test_explicit_selection_requires_active_match(self, entry_points):
cases = (
([], ValueError, "not found"),
([_entry_point("selected", None)], RuntimeError, "returned None"),
)
os.environ["SGLANG_DIFFUSION_PLATFORM_OVERRIDE"] = "selected"
for entries, error_type, message in cases:
with self.subTest(message=message):
entry_points.return_value = entries
with self.assertRaisesRegex(error_type, message):
platforms.resolve_current_platform_cls_qualname()
def test_builtin_override_bypasses_plugin_selection(self):
expected = {
"cpu": "sglang.multimodal_gen.runtime.platforms.cpu.CpuPlatform",
"cuda": "sglang.multimodal_gen.runtime.platforms.cuda.CudaPlatform",
"rocm": "sglang.multimodal_gen.runtime.platforms.rocm.RocmPlatform",
"mps": "sglang.multimodal_gen.runtime.platforms.mps.MpsPlatform",
"npu": "sglang.multimodal_gen.runtime.platforms.npu.NPUPlatformBase",
"musa": "sglang.multimodal_gen.runtime.platforms.musa.MusaPlatform",
"xpu": "sglang.multimodal_gen.runtime.platforms.xpu.XpuPlatform",
}
for name, qualname in expected.items():
with (
self.subTest(name=name),
patch.object(platforms, "entry_points") as entry_points,
):
os.environ["SGLANG_DIFFUSION_PLATFORM_OVERRIDE"] = name
self.assertEqual(
platforms.resolve_current_platform_cls_qualname(),
qualname,
)
entry_points.assert_not_called()
@patch.object(platforms, "entry_points", return_value=[])
def test_xpu_keeps_automatic_detection_priority(self, _entry_points):
xpu_qualname = "sglang.multimodal_gen.runtime.platforms.xpu.XpuPlatform"
detectors = {
"mps": MagicMock(return_value=None),
"xpu": MagicMock(return_value=xpu_qualname),
"rocm": MagicMock(return_value=None),
"cuda": MagicMock(return_value=None),
"npu": MagicMock(return_value=None),
"musa": MagicMock(return_value=None),
"cpu": MagicMock(return_value=None),
}
with patch.object(platforms, "builtin_platform_plugins", detectors):
self.assertEqual(
platforms.resolve_current_platform_cls_qualname(), xpu_qualname
)
detectors["mps"].assert_called_once_with()
detectors["xpu"].assert_called_once_with()
for name in ("rocm", "cuda", "npu", "musa", "cpu"):
detectors[name].assert_not_called()
@patch.object(platforms, "entry_points")
def test_external_plugin_cannot_return_a_builtin_platform(self, entry_points):
entry_points.return_value = [
_entry_point(
"selected",
"sglang.multimodal_gen.runtime.platforms.cpu.CpuPlatform",
"vendor-pkg",
)
]
os.environ["SGLANG_DIFFUSION_PLATFORM_OVERRIDE"] = "selected"
with self.assertRaisesRegex(TypeError, "PlatformEnum.OOT"):
platforms.get_selected_platform_dist()
self.assertIsNone(platforms._current_platform)
self.assertIsNone(platforms._current_platform_selection)
def test_external_platform_identity_is_validated_before_publication(self):
class BadPlatform(platforms.Platform):
_enum = platforms.PlatformEnum.OOT
device_name = "fake"
device_type = "fake"
selection = platforms._PlatformSelection(
"vendor.BadPlatform", "selected", "vendor-pkg"
)
for attribute in ("device_name", "device_type"):
with (
self.subTest(attribute=attribute),
patch.object(BadPlatform, attribute, " "),
patch.object(
platforms, "_select_current_platform", return_value=selection
),
patch.object(
platforms, "_load_platform_class", return_value=BadPlatform
),
self.assertRaisesRegex(TypeError, attribute),
):
platforms._resolve_current_platform()
self.assertIsNone(platforms._current_platform)
self.assertIsNone(platforms._current_platform_selection)
@patch.object(platforms, "entry_points")
def test_explicit_plugin_must_return_a_class_qualname(self, entry_points):
entry_points.return_value = [_entry_point("selected", 42, "vendor-pkg")]
os.environ["SGLANG_DIFFUSION_PLATFORM_OVERRIDE"] = "selected"
with self.assertRaisesRegex(TypeError, "non-empty class qualname"):
platforms.resolve_current_platform_cls_qualname()
@patch.object(platforms, "entry_points")
def test_a_failing_activation_is_not_downgraded_to_a_builtin(self, entry_points):
# Skipping the plugin here would run the whole job on the wrong hardware.
broken = _entry_point("broken", None)
broken.load.return_value = MagicMock(
side_effect=RuntimeError("vendor runtime is broken")
)
entry_points.return_value = [broken]
with self.assertRaisesRegex(RuntimeError, "vendor runtime is broken"):
platforms.resolve_current_platform_cls_qualname()
if __name__ == "__main__":
unittest.main()
@@ -1,5 +1,6 @@
import os
from types import SimpleNamespace
from unittest.mock import patch
from unittest.mock import MagicMock, patch
import pytest
import torch.nn as nn
@@ -7,9 +8,13 @@ import torch.nn as nn
from sglang.multimodal_gen.runtime.models.dits.ltx_2 import (
LTX2VideoTransformer3DModel,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.decoding import DecodingStage
from sglang.multimodal_gen.runtime.pipelines_core.stages.denoising import (
DenoisingStage,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.mova import (
MOVADenoisingStage,
)
from sglang.multimodal_gen.runtime.utils.torch_compile import (
CompiledModuleRegistry,
build_torch_compile_kwargs,
@@ -99,6 +104,84 @@ def test_out_of_tree_platform_controls_compile_kwargs(backend, options, expected
get_compile_options.assert_called_once_with(module)
def test_mova_uses_platform_compile_kwargs():
stage = MOVADenoisingStage.__new__(MOVADenoisingStage)
module = _CompilableModule()
server_args = SimpleNamespace(enable_torch_compile=True)
module_path = (
"sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.mova"
)
compile_path = "sglang.multimodal_gen.runtime.utils.torch_compile"
with (
# This test exercises the real mode resolver, which reads the env first.
patch.dict(os.environ, {}, clear=False),
patch(f"{module_path}.current_platform.is_hip", return_value=False),
patch(f"{module_path}.current_platform.is_npu", return_value=False),
patch(
f"{compile_path}.current_platform.is_npu",
return_value=False,
),
patch(
f"{compile_path}.current_platform.is_out_of_tree",
return_value=True,
),
patch(
f"{compile_path}.current_platform.get_compile_backend",
return_value="custom_backend",
) as get_compile_backend,
patch(
f"{compile_path}.current_platform.get_compile_options",
return_value={"max_autotune": True},
) as get_compile_options,
):
os.environ.pop("SGLANG_TORCH_COMPILE_MODE", None)
stage._maybe_enable_torch_compile(module, server_args)
get_compile_backend.assert_called_once_with("max-autotune-no-cudagraphs")
get_compile_options.assert_called_once_with(module)
assert module.compile_calls == [
{
"backend": "custom_backend",
"dynamic": None,
"fullgraph": False,
"options": {"max_autotune": True},
}
]
def test_vae_compile_options_receive_the_target_module():
stage = DecodingStage.__new__(DecodingStage)
vae = _CompilableModule()
decode = MagicMock()
compiled_callable = MagicMock(target_id=None, compiled_module=None)
compiled_callable.get_or_compile.return_value = decode
server_args = SimpleNamespace(enable_torch_compile=True)
module_path = "sglang.multimodal_gen.runtime.pipelines_core.stages.decoding"
with (
patch(f"{module_path}.current_platform.is_npu", return_value=False),
patch(
f"{module_path}.resolve_torch_compile_kwargs",
return_value=({"backend": "custom_backend"}, "default"),
) as resolve_compile_kwargs,
):
result = stage._get_vae_decode_fn(
vae,
server_args,
decode_fn=decode,
compiled_callable=compiled_callable,
)
assert result is decode
resolve_compile_kwargs.assert_called_once_with(
"SGLANG_VAE_TORCH_COMPILE_MODE",
"SGLANG_TORCH_COMPILE_MODE",
default="default",
module=vae,
)
def test_ltx2_compile_conditions_match_only_direct_blocks():
conditions = LTX2VideoTransformer3DModel._compile_conditions
@@ -179,17 +262,19 @@ def test_denoising_stage_selects_regional_compile():
),
patch(
"sglang.multimodal_gen.runtime.pipelines_core.stages.denoising."
"maybe_enable_inductor_compute_comm_overlap"
),
patch(
"sglang.multimodal_gen.runtime.pipelines_core.stages.denoising."
"build_torch_compile_kwargs",
return_value=compile_kwargs,
) as build_compile_kwargs,
"resolve_torch_compile_kwargs",
return_value=(compile_kwargs, "default"),
) as resolve_compile_kwargs,
):
stage._maybe_torch_compile(model)
build_compile_kwargs.assert_called_once_with(mode="default", module=model)
resolve_compile_kwargs.assert_called_once_with(
"SGLANG_TORCH_COMPILE_MODE",
config=stage.server_args.pipeline_config.dit_config,
default="max-autotune-no-cudagraphs",
module=model,
enable_inductor_compute_comm_overlap=True,
)
assert [len(block.compile_calls) for block in model.transformer_blocks] == [1, 1]
assert [block.compile_calls for block in model.transformer_blocks] == [
[compile_kwargs],
@@ -146,6 +146,18 @@ def _from_dict_without_model_resolution(
return ServerArgs.from_dict(kwargs)
class TestPlatformLifecycleHooks(unittest.TestCase):
def test_server_args_applies_platform_defaults(self):
with patch.object(
current_platform, "apply_server_args_defaults"
) as apply_defaults:
server_args = _from_dict_without_model_resolution(
{"model_path": "test/model"}
)
apply_defaults.assert_called_once_with(server_args)
class TestServerArgsPathExpansion(unittest.TestCase):
def _from_dict_without_model_resolution(self, kwargs):
return _from_dict_without_model_resolution(kwargs)
@@ -0,0 +1,337 @@
# SPDX-License-Identifier: Apache-2.0
import importlib
import json
import multiprocessing as mp
import os
import pathlib
import shutil
import subprocess
import sys
import tempfile
import unittest
from types import SimpleNamespace
from unittest.mock import patch
from sglang.multimodal_gen.runtime.managers import worker_bootstrap
WORKER_MODULE = "sglang.multimodal_gen.runtime.managers.gpu_worker"
GENERATOR_MODULE = "sglang.multimodal_gen.runtime.entrypoints.diffusion_generator"
SERVER_ARGS_MODULE = "sglang.multimodal_gen.runtime.server_args.server_args"
HTTP_SERVER_MODULE = "sglang.multimodal_gen.runtime.launch_server"
METADATA = "Metadata-Version: 2.1\nName: sgl-fake-plugin\nVersion: 0.1\n"
ENTRY_POINTS = """\
[sglang.multimodal_gen.platforms]
fake = sgl_fake_plugin:activate
[sglang.multimodal_gen.plugins]
fake = sgl_fake_plugin:register
"""
# Real modules, not embedded source strings, so they are linted like any other
# file. The plugin imports no diffusion module of its own, which is what keeps
# the import-order measurement honest.
FIXTURES_DIR = pathlib.Path(__file__).parent / "fixtures"
FAKE_PLUGIN_FIXTURE = FIXTURES_DIR / "sgl_fake_plugin.py"
FACADE_IMPORT_SCRIPT = FIXTURES_DIR / "offline_script_facade_import.py"
RUNTIME_IMPORT_SCRIPT = FIXTURES_DIR / "offline_script_runtime_import.py"
PYTHON_ROOT = pathlib.Path(__file__).parents[4]
EARLY_IMPORT_WARNING = "imported before this worker initialized its platform"
SCRIPT_TIMEOUT_S = 300
def _install_fake_plugin_dist(root: pathlib.Path) -> None:
shutil.copy(FAKE_PLUGIN_FIXTURE, root / "sgl_fake_plugin.py")
dist_info = root / "sgl_fake_plugin-0.1.dist-info"
dist_info.mkdir()
(dist_info / "METADATA").write_text(METADATA)
(dist_info / "entry_points.txt").write_text(ENTRY_POINTS)
def _check_cli_import_order(pipe_writer) -> None:
from sglang.multimodal_gen.runtime.entrypoints.cli import main as cli_main
imported_before_activation = GENERATOR_MODULE in sys.modules
class StopAtPluginBoundary(Exception):
pass
def stop_before_command_imports():
raise StopAtPluginBoundary
cli_main.apply_plugin_hooks = stop_before_command_imports
try:
cli_main.generate_cmd_init()
except StopAtPluginBoundary:
pass
pipe_writer.send(
{
"imported_before_activation": imported_before_activation,
"imported_after_failed_activation": GENERATOR_MODULE in sys.modules,
}
)
pipe_writer.close()
def _check_http_server_import_order(pipe_writer) -> None:
from sglang.multimodal_gen.runtime.platforms import plugins
class StopAtPluginBoundary(Exception):
pass
observed = {
"http_server_imported_before_bootstrap": HTTP_SERVER_MODULE in sys.modules,
"server_args_imported_before_bootstrap": SERVER_ARGS_MODULE in sys.modules,
}
def stop_before_runtime_imports():
observed.update(
http_server_imported_when_hooks_applied=(HTTP_SERVER_MODULE in sys.modules),
server_args_imported_when_hooks_applied=(SERVER_ARGS_MODULE in sys.modules),
)
raise StopAtPluginBoundary
plugins.load_plugins = lambda: None
plugins.apply_plugin_hooks = stop_before_runtime_imports
try:
worker_bootstrap.bootstrap_http_server_process(None)
except StopAtPluginBoundary:
pass
pipe_writer.send(observed)
pipe_writer.close()
class TestBootstrapImportBoundary(unittest.TestCase):
def test_manager_namespace_does_not_hide_early_worker_imports(self):
for module, warned in (
("sglang.multimodal_gen.runtime.managers", False),
("sglang.multimodal_gen.runtime.platforms.plugins", False),
(worker_bootstrap.__name__, False),
(WORKER_MODULE, True),
):
modules = {
"__main__": SimpleNamespace(__file__="offline.py"),
module: None,
}
with (
self.subTest(module=module),
patch.object(worker_bootstrap, "sys", SimpleNamespace(modules=modules)),
patch.object(worker_bootstrap.logging, "getLogger") as get_logger,
):
worker_bootstrap._warn_if_runtime_imported_early()
if warned:
warning = get_logger.return_value.warning
warning.assert_called_once()
self.assertEqual(warning.call_args.args[1], module)
else:
get_logger.assert_not_called()
class TestSpawnedWorkerReceivesPluginOverride(unittest.TestCase):
"""End-to-end over a real spawn, with a real entry-point distribution.
A spawned child re-imports from a blank interpreter, so nothing the parent
patched survives. The child must initialize its backend, register its own
hooks, and apply them before invoking the worker.
"""
def test_lifecycle_precedes_worker_import_and_argument_materialization(self):
# The parent has a real ServerArgs object; the process boundary must
# keep its class opaque until bootstrap chooses to materialize it.
# Imported before the fake dist reaches sys.path: this resolves the
# platform, which must not land on a class living in a temp directory.
from sglang.multimodal_gen.runtime.server_args import ServerArgs
with tempfile.TemporaryDirectory() as tmp:
root = pathlib.Path(tmp)
_install_fake_plugin_dist(root)
# Spawn ships sys.path to the child, so the dist is discoverable there.
sys.path.insert(0, str(root))
self.addCleanup(sys.path.remove, str(root))
importlib.invalidate_caches()
server_args = ServerArgs.__new__(ServerArgs)
reader, writer = mp.Pipe(duplex=False)
spec = worker_bootstrap.SchedulerProcessSpec(
local_rank=0,
rank=0,
server_args=worker_bootstrap.ServerArgsPayload.capture(server_args),
pipe_writer=writer,
)
process = mp.get_context("spawn").Process(
target=worker_bootstrap.bootstrap_scheduler_process,
args=(spec,),
)
process.start()
writer.close()
self.addCleanup(process.join, 10)
self.addCleanup(process.kill)
result = None
if reader.poll(120):
try:
result = reader.recv()
except EOFError:
pass
if result is None:
process.join(10)
self.fail(
"child sent nothing back, so the override never ran "
f"(exit code {process.exitcode})"
)
self.assertTrue(result["override_ran"], "plugin override did not run")
self.assertIs(
result["worker_imported_when_plugin_ran"],
False,
"plugins loaded after the worker module was already imported",
)
self.assertIs(
result["generator_imported_when_plugin_ran"],
False,
"the package facade imported the diffusion runtime before plugins loaded",
)
self.assertIs(
result["worker_imported_when_backend_initialized"],
False,
"worker imports preceded platform backend initialization",
)
self.assertIs(
result["server_args_imported_when_backend_initialized"],
False,
"spawn unpickled ServerArgs before platform backend initialization",
)
self.assertIs(
result["server_args_imported_when_plugin_ran"],
False,
"spawn materialized ServerArgs before plugin registration",
)
self.assertTrue(
result["backend_initialized_when_plugin_ran"],
"plugin registration ran before platform backend initialization",
)
self.assertTrue(
result["backend_initialized"],
"platform backend initialized after the worker override ran",
)
def test_cli_activates_plugins_before_importing_commands(self):
reader, writer = mp.Pipe(duplex=False)
process = mp.get_context("spawn").Process(
target=_check_cli_import_order,
args=(writer,),
)
process.start()
writer.close()
self.addCleanup(process.join, 10)
self.addCleanup(process.kill)
self.assertTrue(reader.poll(30), "child did not report CLI import state")
result = reader.recv()
self.assertFalse(result["imported_before_activation"])
self.assertFalse(result["imported_after_failed_activation"])
def test_http_hooks_apply_before_runtime_imports(self):
reader, writer = mp.Pipe(duplex=False)
process = mp.get_context("spawn").Process(
target=_check_http_server_import_order,
args=(writer,),
)
process.start()
writer.close()
self.addCleanup(process.join, 10)
self.addCleanup(process.kill)
self.assertTrue(reader.poll(30), "child did not report HTTP import state")
result = reader.recv()
self.assertEqual(
result,
{
"http_server_imported_before_bootstrap": False,
"server_args_imported_before_bootstrap": False,
"http_server_imported_when_hooks_applied": False,
"server_args_imported_when_hooks_applied": False,
},
)
class TestOfflineScriptImportContract(unittest.TestCase):
"""Real scripts in a real interpreter, because spawn re-executes the
launching script's module scope before it unpickles anything."""
def _run_offline_script(self, script: pathlib.Path):
with tempfile.TemporaryDirectory() as tmp:
root = pathlib.Path(tmp)
dist_root = root / "site"
dist_root.mkdir()
_install_fake_plugin_dist(dist_root)
result_path = root / "observed.json"
env = dict(os.environ)
env["PYTHONPATH"] = os.pathsep.join(
path
for path in (
str(dist_root),
str(FIXTURES_DIR),
str(PYTHON_ROOT),
env.get("PYTHONPATH", ""),
)
if path
)
completed = subprocess.run(
[sys.executable, str(script), str(result_path)],
env=env,
capture_output=True,
text=True,
timeout=SCRIPT_TIMEOUT_S,
check=False,
)
self.assertEqual(completed.returncode, 0, completed.stderr)
payload = json.loads(result_path.read_text())
self.assertIsNotNone(
payload["observed"],
f"child sent nothing back (exit code {payload['exitcode']})",
)
return payload["observed"], completed.stderr
def test_a_module_scope_facade_import_leaves_the_child_lifecycle_intact(self):
observed, stderr = self._run_offline_script(FACADE_IMPORT_SCRIPT)
self.assertTrue(observed["override_ran"], "plugin override did not run")
self.assertIs(
observed["generator_imported_when_plugin_ran"],
False,
"re-executing the script imported the generator before plugins loaded",
)
self.assertNotIn(EARLY_IMPORT_WARNING, stderr)
def test_a_module_scope_runtime_import_is_reported_by_the_child(self):
observed, stderr = self._run_offline_script(RUNTIME_IMPORT_SCRIPT)
self.assertTrue(observed["override_ran"], "plugin override did not run")
self.assertIs(
observed["generator_imported_when_plugin_ran"],
True,
"the script layout under test no longer imports the runtime early",
)
self.assertIn(
EARLY_IMPORT_WARNING,
stderr,
"the child accepted a mis-ordered import without reporting it",
)
self.assertIn(
RUNTIME_IMPORT_SCRIPT.name,
stderr,
"the report did not name the script whose imports have to move",
)
if __name__ == "__main__":
unittest.main()