[diffusion] feat: measure warmup memory and layer usage per phase for residency calibration (1/4) (#37916)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -201,6 +201,9 @@ class PipelineConfig:
|
|||||||
native_only_components: ClassVar[tuple[str, ...]] = ()
|
native_only_components: ClassVar[tuple[str, ...]] = ()
|
||||||
task_type: ModelTaskType = ModelTaskType.I2I
|
task_type: ModelTaskType = ModelTaskType.I2I
|
||||||
skip_input_image_preprocess: bool = False
|
skip_input_image_preprocess: bool = False
|
||||||
|
# False when changing component placement after a calibration request is
|
||||||
|
# known to alter the pipeline's numerical path.
|
||||||
|
supports_auto_residency: bool = True
|
||||||
# Components that cannot fall back to a native Transformers/Diffusers
|
# Components that cannot fall back to a native Transformers/Diffusers
|
||||||
# implementation because their pipeline requires SGLang-specific behavior.
|
# implementation because their pipeline requires SGLang-specific behavior.
|
||||||
native_only_components: tuple[str, ...] = ()
|
native_only_components: tuple[str, ...] = ()
|
||||||
|
|||||||
@@ -723,3 +723,6 @@ class LTX2PipelineConfig(PipelineConfig):
|
|||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class LTX23PipelineConfig(LTX2PipelineConfig):
|
class LTX23PipelineConfig(LTX2PipelineConfig):
|
||||||
"""Configuration overrides for LTX-2.3."""
|
"""Configuration overrides for LTX-2.3."""
|
||||||
|
|
||||||
|
# original-mode lora swaps invalidate post-warmup timing calibration
|
||||||
|
supports_auto_residency: bool = False
|
||||||
|
|||||||
@@ -755,6 +755,8 @@ class QwenImageEditPlus_2511_PipelineConfig(QwenImageEditPlusPipelineConfig):
|
|||||||
class QwenImageLayeredPipelineConfig(QwenImageEditPipelineConfig):
|
class QwenImageLayeredPipelineConfig(QwenImageEditPipelineConfig):
|
||||||
resolution: int = 640
|
resolution: int = 640
|
||||||
vae_precision: str = "bf16"
|
vae_precision: str = "bf16"
|
||||||
|
# promoting the auxiliary components regresses first-request latency
|
||||||
|
supports_auto_residency: bool = False
|
||||||
|
|
||||||
def get_model_deployment_config(self) -> ModelDeploymentConfig:
|
def get_model_deployment_config(self) -> ModelDeploymentConfig:
|
||||||
return ModelDeploymentConfig(
|
return ModelDeploymentConfig(
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ class SanaWMPipelineConfig(PipelineConfig):
|
|||||||
optional 6-DoF camera trajectory, optional Stage-2 LTX-2 refiner)."""
|
optional 6-DoF camera trajectory, optional Stage-2 LTX-2 refiner)."""
|
||||||
|
|
||||||
task_type: ModelTaskType = ModelTaskType.TI2V
|
task_type: ModelTaskType = ModelTaskType.TI2V
|
||||||
|
# The current two-stage path is not numerically invariant when its primary
|
||||||
|
# transformer and connectors are promoted after warmup.
|
||||||
|
supports_auto_residency: bool = False
|
||||||
|
|
||||||
# SanaWMBeforeDenoisingStage._splice_first_frame handles condition-image
|
# SanaWMBeforeDenoisingStage._splice_first_frame handles condition-image
|
||||||
# resize + VAE-encode itself, so bypass the framework's generic TI2V
|
# resize + VAE-encode itself, so bypass the framework's generic TI2V
|
||||||
|
|||||||
@@ -136,6 +136,58 @@ def _sanitize_filename(name: str, replacement: str = "_", max_length: int = 150)
|
|||||||
return ascii_name
|
return ascii_name
|
||||||
|
|
||||||
|
|
||||||
|
_SEQUENCE_SHARD_PIPELINE_FAMILIES = ("wan", "helios", "joy", "cosmos3")
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_sequence_shard(
|
||||||
|
pipeline_config: Any, enable_sequence_shard: bool | None
|
||||||
|
) -> bool:
|
||||||
|
"""Whether this pipeline shards the sequence dim instead of aligning frames.
|
||||||
|
|
||||||
|
Shared by ``SamplingParams._adjust_visual_fields`` and the synthetic
|
||||||
|
warmup builder so warmup requests follow the same frame contract as real
|
||||||
|
requests.
|
||||||
|
"""
|
||||||
|
pipeline_name_lower = pipeline_config.__class__.__name__.lower()
|
||||||
|
return any(
|
||||||
|
family in pipeline_name_lower for family in _SEQUENCE_SHARD_PIPELINE_FAMILIES
|
||||||
|
) and (enable_sequence_shard is None or enable_sequence_shard)
|
||||||
|
|
||||||
|
|
||||||
|
def align_num_frames_for_num_gpus(
|
||||||
|
num_frames: int,
|
||||||
|
*,
|
||||||
|
num_gpus: int,
|
||||||
|
vae_config: Any,
|
||||||
|
round_down: bool,
|
||||||
|
) -> int:
|
||||||
|
"""Align the latent frame count to be divisible by ``num_gpus``."""
|
||||||
|
if num_gpus <= 1:
|
||||||
|
return num_frames
|
||||||
|
use_temporal_scaling_frames = vae_config.use_temporal_scaling_frames
|
||||||
|
temporal_scale_factor = vae_config.arch_config.temporal_compression_ratio
|
||||||
|
|
||||||
|
if use_temporal_scaling_frames:
|
||||||
|
orig_latent_num_frames = (num_frames - 1) // temporal_scale_factor + 1
|
||||||
|
else:
|
||||||
|
orig_latent_num_frames = num_frames
|
||||||
|
|
||||||
|
if orig_latent_num_frames % num_gpus == 0:
|
||||||
|
return num_frames
|
||||||
|
|
||||||
|
if round_down:
|
||||||
|
# Ensure we have at least 1 batch per GPU
|
||||||
|
new_latent_num_frames = max(1, (orig_latent_num_frames // num_gpus)) * num_gpus
|
||||||
|
else:
|
||||||
|
new_latent_num_frames = math.ceil(orig_latent_num_frames / num_gpus) * num_gpus
|
||||||
|
|
||||||
|
if use_temporal_scaling_frames:
|
||||||
|
# Convert back to frames, keeping num_frames-1 a multiple of the
|
||||||
|
# temporal scale factor
|
||||||
|
return (new_latent_num_frames - 1) * temporal_scale_factor + 1
|
||||||
|
return new_latent_num_frames
|
||||||
|
|
||||||
|
|
||||||
class DataType(Enum):
|
class DataType(Enum):
|
||||||
IMAGE = auto()
|
IMAGE = auto()
|
||||||
VIDEO = auto()
|
VIDEO = auto()
|
||||||
@@ -792,14 +844,9 @@ class SamplingParams:
|
|||||||
)
|
)
|
||||||
logger.warning(error_msg)
|
logger.warning(error_msg)
|
||||||
|
|
||||||
pipeline_name_lower = server_args.pipeline_config.__class__.__name__.lower()
|
if resolve_sequence_shard(
|
||||||
|
server_args.pipeline_config, self.enable_sequence_shard
|
||||||
if (
|
):
|
||||||
"wan" in pipeline_name_lower
|
|
||||||
or "helios" in pipeline_name_lower
|
|
||||||
or "joy" in pipeline_name_lower
|
|
||||||
or "cosmos3" in pipeline_name_lower
|
|
||||||
) and (self.enable_sequence_shard is None or self.enable_sequence_shard):
|
|
||||||
self.enable_sequence_shard = True
|
self.enable_sequence_shard = True
|
||||||
logger.debug("Automatically enabled enable_sequence_shard")
|
logger.debug("Automatically enabled enable_sequence_shard")
|
||||||
else:
|
else:
|
||||||
@@ -832,43 +879,13 @@ class SamplingParams:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if self.adjust_frames:
|
if self.adjust_frames:
|
||||||
# Adjust number of frames based on number of GPUs for video task
|
new_num_frames = align_num_frames_for_num_gpus(
|
||||||
use_temporal_scaling_frames = (
|
self.num_frames,
|
||||||
pipeline_config.vae_config.use_temporal_scaling_frames
|
num_gpus=server_args.num_gpus,
|
||||||
|
vae_config=pipeline_config.vae_config,
|
||||||
|
round_down=self.num_frames_round_down,
|
||||||
)
|
)
|
||||||
num_frames = self.num_frames
|
if new_num_frames != self.num_frames:
|
||||||
num_gpus = server_args.num_gpus
|
|
||||||
temporal_scale_factor = (
|
|
||||||
pipeline_config.vae_config.arch_config.temporal_compression_ratio
|
|
||||||
)
|
|
||||||
|
|
||||||
if use_temporal_scaling_frames:
|
|
||||||
orig_latent_num_frames = (
|
|
||||||
num_frames - 1
|
|
||||||
) // temporal_scale_factor + 1
|
|
||||||
else:
|
|
||||||
orig_latent_num_frames = num_frames
|
|
||||||
|
|
||||||
if orig_latent_num_frames % server_args.num_gpus != 0:
|
|
||||||
# Adjust latent frames to be divisible by number of GPUs
|
|
||||||
if self.num_frames_round_down:
|
|
||||||
# Ensure we have at least 1 batch per GPU
|
|
||||||
new_latent_num_frames = (
|
|
||||||
max(1, (orig_latent_num_frames // num_gpus)) * num_gpus
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
new_latent_num_frames = (
|
|
||||||
math.ceil(orig_latent_num_frames / num_gpus) * num_gpus
|
|
||||||
)
|
|
||||||
|
|
||||||
if use_temporal_scaling_frames:
|
|
||||||
# Convert back to number of frames, ensuring num_frames-1 is a multiple of temporal_scale_factor
|
|
||||||
new_num_frames = (
|
|
||||||
new_latent_num_frames - 1
|
|
||||||
) * temporal_scale_factor + 1
|
|
||||||
else:
|
|
||||||
new_num_frames = new_latent_num_frames
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Adjusting number of frames from %s to %s based on number of GPUs (%s)",
|
"Adjusting number of frames from %s to %s based on number of GPUs (%s)",
|
||||||
self.num_frames,
|
self.num_frames,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ if TYPE_CHECKING:
|
|||||||
SGLANG_DIFFUSION_TEST_FORCE_HOST_AVAILABLE_GIB: float | None = None
|
SGLANG_DIFFUSION_TEST_FORCE_HOST_AVAILABLE_GIB: float | None = None
|
||||||
SGLANG_DIFFUSION_TEST_CAP_DEVICE_MEMORY_GIB: float | None = None
|
SGLANG_DIFFUSION_TEST_CAP_DEVICE_MEMORY_GIB: float | None = None
|
||||||
SGLANG_DIFFUSION_STAGE_LOGGING: bool = False
|
SGLANG_DIFFUSION_STAGE_LOGGING: bool = False
|
||||||
|
SGLANG_DIFFUSION_DISABLE_AUTO_RESIDENCY: bool = False
|
||||||
SGLANG_DIFFUSION_MINIMAX_H3_ADALN_GPU_PLANS: int = 64
|
SGLANG_DIFFUSION_MINIMAX_H3_ADALN_GPU_PLANS: int = 64
|
||||||
SGLANG_DIFFUSION_MINIMAX_H3_ADALN_FP32: bool = False
|
SGLANG_DIFFUSION_MINIMAX_H3_ADALN_FP32: bool = False
|
||||||
SGLANG_DIFFUSION_CFG_GATE_STEP: float = 1.0
|
SGLANG_DIFFUSION_CFG_GATE_STEP: float = 1.0
|
||||||
@@ -265,6 +266,12 @@ environment_variables: dict[str, Callable[[], Any]] = {
|
|||||||
# If set, sgl_diffusion will enable stage logging, which will print the time
|
# If set, sgl_diffusion will enable stage logging, which will print the time
|
||||||
# taken for each stage
|
# taken for each stage
|
||||||
"SGLANG_DIFFUSION_STAGE_LOGGING": _lazy_bool("SGLANG_DIFFUSION_STAGE_LOGGING"),
|
"SGLANG_DIFFUSION_STAGE_LOGGING": _lazy_bool("SGLANG_DIFFUSION_STAGE_LOGGING"),
|
||||||
|
# Kill-switch for the warmup-calibrated auto residency promotion that runs
|
||||||
|
# under `--performance-mode auto` with server warmup. Set to disable the
|
||||||
|
# promotion without giving up the rest of the auto performance policy.
|
||||||
|
"SGLANG_DIFFUSION_DISABLE_AUTO_RESIDENCY": _lazy_bool(
|
||||||
|
"SGLANG_DIFFUSION_DISABLE_AUTO_RESIDENCY"
|
||||||
|
),
|
||||||
# Plan slots in the MiniMax-H3 --minimax-h3-adaln-online GPU slab
|
# Plan slots in the MiniMax-H3 --minimax-h3-adaln-online GPU slab
|
||||||
# (9.25 MiB per slot-timestep; 64 x width 4 = 2.31 GiB). A request needs
|
# (9.25 MiB per slot-timestep; 64 x width 4 = 2.31 GiB). A request needs
|
||||||
# up to num_inference_steps - 1 slots; the default covers the 50-step
|
# up to num_inference_steps - 1 slots; the default covers the 50-step
|
||||||
|
|||||||
@@ -140,6 +140,18 @@ class IpcA2AState:
|
|||||||
"""Drop mappings that belong to a model-parallel group being replaced."""
|
"""Drop mappings that belong to a model-parallel group being replaced."""
|
||||||
self.__init__()
|
self.__init__()
|
||||||
|
|
||||||
|
def drop_staging(self) -> None:
|
||||||
|
"""Release the cached staging buffers (both ranks call this at the same point).
|
||||||
|
|
||||||
|
Staging is keyed by message size and only evicted by count, so a warmup
|
||||||
|
probe at the full serving shape leaves buffers sized for it behind.
|
||||||
|
"""
|
||||||
|
if not self.staging:
|
||||||
|
return
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
torch.cuda.synchronize()
|
||||||
|
self.staging.clear()
|
||||||
|
|
||||||
def _share(self, t, group):
|
def _share(self, t, group):
|
||||||
"""Exchange `t` with the peer via torch IPC, re-opening the handle in
|
"""Exchange `t` with the peer via torch IPC, re-opening the handle in
|
||||||
the LOCAL device context (the mapping is only dereferenceable from the
|
the LOCAL device context (the mapping is only dereferenceable from the
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""Control-request protocol between the HTTP process and scheduler workers.
|
||||||
|
|
||||||
|
These types are cross-process IPC contracts, not utilities: the HTTP side
|
||||||
|
constructs them (scheduler_client treats them as ``_CONTROL_REQ_TYPES`` and
|
||||||
|
fans them out to every replica) and each scheduler dispatches them through
|
||||||
|
``Scheduler.request_handlers``. Keep this module import-light -- both
|
||||||
|
processes import it, and the HTTP process must not drag in torch-heavy
|
||||||
|
worker modules through it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
|
||||||
|
|
||||||
|
class SetLoraReq(msgspec.Struct):
|
||||||
|
lora_nickname: Union[str, List[str]]
|
||||||
|
lora_path: Optional[Union[str, List[Optional[str]]]] = None
|
||||||
|
target: Union[str, List[str]] = "all"
|
||||||
|
strength: Union[float, List[float]] = 1.0
|
||||||
|
merge_mode: Optional[str] = None
|
||||||
|
lora_alpha: Optional[Union[int, List[Optional[int]]]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class MergeLoraWeightsReq(msgspec.Struct):
|
||||||
|
target: str = "all"
|
||||||
|
strength: float = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
class UnmergeLoraWeightsReq(msgspec.Struct):
|
||||||
|
target: str = "all"
|
||||||
|
|
||||||
|
|
||||||
|
class ListLorasReq(msgspec.Struct):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ShutdownReq(msgspec.Struct):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseRealtimeSessionReq(msgspec.Struct):
|
||||||
|
session_id: str
|
||||||
|
|
||||||
|
|
||||||
|
class GetDisaggStatsReq(msgspec.Struct):
|
||||||
|
"""Request to get disagg pipeline metrics from the scheduler."""
|
||||||
|
|
||||||
|
pass
|
||||||
@@ -19,13 +19,15 @@ from sglang.multimodal_gen.configs.sample.sampling_params import (
|
|||||||
DataType,
|
DataType,
|
||||||
SamplingParams,
|
SamplingParams,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.utils import (
|
from sglang.multimodal_gen.runtime.entrypoints.control_requests import (
|
||||||
GenerationResult,
|
|
||||||
ListLorasReq,
|
ListLorasReq,
|
||||||
MergeLoraWeightsReq,
|
MergeLoraWeightsReq,
|
||||||
SetLoraReq,
|
SetLoraReq,
|
||||||
ShutdownReq,
|
ShutdownReq,
|
||||||
UnmergeLoraWeightsReq,
|
UnmergeLoraWeightsReq,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.entrypoints.utils import (
|
||||||
|
GenerationResult,
|
||||||
expand_request_outputs,
|
expand_request_outputs,
|
||||||
format_lora_message,
|
format_lora_message,
|
||||||
prepare_request,
|
prepare_request,
|
||||||
|
|||||||
@@ -264,7 +264,9 @@ async def stats_endpoint(request: Request):
|
|||||||
Returns queue depth, request counts, latency, throughput, etc.
|
Returns queue depth, request counts, latency, throughput, etc.
|
||||||
Sends a GetDisaggStatsReq to the scheduler via ZMQ and returns the result.
|
Sends a GetDisaggStatsReq to the scheduler via ZMQ and returns the result.
|
||||||
"""
|
"""
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.utils import GetDisaggStatsReq
|
from sglang.multimodal_gen.runtime.entrypoints.control_requests import (
|
||||||
|
GetDisaggStatsReq,
|
||||||
|
)
|
||||||
|
|
||||||
server_args: ServerArgs = request.app.state.server_args
|
server_args: ServerArgs = request.app.state.server_args
|
||||||
response: dict = {
|
response: dict = {
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ from fastapi import APIRouter, Body, HTTPException
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from sglang.multimodal_gen.registry import get_model_info
|
from sglang.multimodal_gen.registry import get_model_info
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.utils import (
|
from sglang.multimodal_gen.runtime.entrypoints.control_requests import (
|
||||||
ListLorasReq,
|
ListLorasReq,
|
||||||
MergeLoraWeightsReq,
|
MergeLoraWeightsReq,
|
||||||
SetLoraReq,
|
SetLoraReq,
|
||||||
UnmergeLoraWeightsReq,
|
UnmergeLoraWeightsReq,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.entrypoints.utils import (
|
||||||
format_lora_message,
|
format_lora_message,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch
|
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch
|
||||||
|
|||||||
+3
-3
@@ -8,6 +8,9 @@ from typing import TYPE_CHECKING
|
|||||||
import msgspec.msgpack
|
import msgspec.msgpack
|
||||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.runtime.entrypoints.control_requests import (
|
||||||
|
ReleaseRealtimeSessionReq,
|
||||||
|
)
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.openai.protocol import (
|
from sglang.multimodal_gen.runtime.entrypoints.openai.protocol import (
|
||||||
RealtimeEvent,
|
RealtimeEvent,
|
||||||
RealtimeVideoGenerationsRequest,
|
RealtimeVideoGenerationsRequest,
|
||||||
@@ -28,9 +31,6 @@ from sglang.multimodal_gen.runtime.entrypoints.openai.realtime.timer import (
|
|||||||
from sglang.multimodal_gen.runtime.entrypoints.openai.utils import (
|
from sglang.multimodal_gen.runtime.entrypoints.openai.utils import (
|
||||||
process_generation_batch,
|
process_generation_batch,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.utils import (
|
|
||||||
ReleaseRealtimeSessionReq,
|
|
||||||
)
|
|
||||||
from sglang.multimodal_gen.runtime.scheduler_client import async_scheduler_client
|
from sglang.multimodal_gen.runtime.scheduler_client import async_scheduler_client
|
||||||
from sglang.multimodal_gen.runtime.server_args import get_global_server_args
|
from sglang.multimodal_gen.runtime.server_args import get_global_server_args
|
||||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.openai.realtime.realtime_adapter import (
|
|
||||||
BaseRealtimeModelAdapter,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from sglang.multimodal_gen.runtime.entrypoints.openai.realtime.realtime_adapter import (
|
||||||
|
BaseRealtimeModelAdapter,
|
||||||
|
)
|
||||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||||
|
|
||||||
|
|
||||||
@@ -67,3 +66,12 @@ def get_realtime_model_adapter(
|
|||||||
"Realtime video is not supported for pipeline config "
|
"Realtime video is not supported for pipeline config "
|
||||||
f"{type(pipeline_config).__name__}; no realtime adapter is registered."
|
f"{type(pipeline_config).__name__}; no realtime adapter is registered."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def has_realtime_model_adapter(server_args: ServerArgs) -> bool:
|
||||||
|
"""Whether the resolved pipeline config has a registered adapter."""
|
||||||
|
_register_builtin_realtime_model_adapters()
|
||||||
|
return any(
|
||||||
|
config_cls in _REALTIME_ADAPTER_REGISTRY
|
||||||
|
for config_cls in type(server_args.pipeline_config).__mro__
|
||||||
|
)
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ from sglang.multimodal_gen.configs.sample.sampling_params import (
|
|||||||
DataType,
|
DataType,
|
||||||
SamplingParams,
|
SamplingParams,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.utils import (
|
from sglang.multimodal_gen.runtime.entrypoints.control_requests import (
|
||||||
ListLorasReq,
|
ListLorasReq,
|
||||||
MergeLoraWeightsReq,
|
MergeLoraWeightsReq,
|
||||||
SetLoraReq,
|
SetLoraReq,
|
||||||
ShutdownReq,
|
ShutdownReq,
|
||||||
UnmergeLoraWeightsReq,
|
UnmergeLoraWeightsReq,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.entrypoints.utils import (
|
||||||
format_lora_message,
|
format_lora_message,
|
||||||
save_outputs,
|
save_outputs,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -160,49 +160,6 @@ def _close_cached_cuda_video_buffer() -> None:
|
|||||||
atexit.register(_close_cached_cuda_video_buffer)
|
atexit.register(_close_cached_cuda_video_buffer)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SetLoraReq:
|
|
||||||
lora_nickname: Union[str, List[str]]
|
|
||||||
lora_path: Optional[Union[str, List[Optional[str]]]] = None
|
|
||||||
target: Union[str, List[str]] = "all"
|
|
||||||
strength: Union[float, List[float]] = 1.0
|
|
||||||
merge_mode: Optional[str] = None
|
|
||||||
lora_alpha: Optional[Union[int, List[Optional[int]]]] = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MergeLoraWeightsReq:
|
|
||||||
target: str = "all"
|
|
||||||
strength: float = 1.0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class UnmergeLoraWeightsReq:
|
|
||||||
target: str = "all"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ListLorasReq:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ShutdownReq:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ReleaseRealtimeSessionReq:
|
|
||||||
session_id: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class GetDisaggStatsReq:
|
|
||||||
"""Request to get disagg pipeline metrics from the scheduler."""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def format_lora_message(
|
def format_lora_message(
|
||||||
lora_nickname: Union[str, List[str]],
|
lora_nickname: Union[str, List[str]],
|
||||||
target: Union[str, List[str]],
|
target: Union[str, List[str]],
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ from sglang.multimodal_gen.runtime.disaggregation.orchestrator import (
|
|||||||
DiffusionServer,
|
DiffusionServer,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.runtime.disaggregation.roles import RoleType
|
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.entrypoints.http_server import create_app
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.utils import ShutdownReq
|
|
||||||
from sglang.multimodal_gen.runtime.managers.gpu_worker import run_scheduler_process
|
from sglang.multimodal_gen.runtime.managers.gpu_worker import run_scheduler_process
|
||||||
from sglang.multimodal_gen.runtime.scheduler_client import SchedulerClient
|
from sglang.multimodal_gen.runtime.scheduler_client import SchedulerClient
|
||||||
from sglang.multimodal_gen.runtime.server_args import (
|
from sglang.multimodal_gen.runtime.server_args import (
|
||||||
|
|||||||
@@ -41,6 +41,20 @@ def _maybe_wait(tensor: torch.Tensor) -> torch.Tensor:
|
|||||||
_A2A_STAGING_BUFFERS: dict[tuple[str, torch.dtype, int], torch.Tensor] = {}
|
_A2A_STAGING_BUFFERS: dict[tuple[str, torch.dtype, int], torch.Tensor] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def drop_a2a_staging_buffers() -> None:
|
||||||
|
"""Release the cached all-to-all staging buffers on this rank.
|
||||||
|
|
||||||
|
The cache only ever grows to the largest message seen, so a warmup probe
|
||||||
|
at the full serving shape leaves buffers sized for it behind; the caller
|
||||||
|
releases them at a point every rank reaches together.
|
||||||
|
"""
|
||||||
|
if not _A2A_STAGING_BUFFERS:
|
||||||
|
return
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
torch.cuda.synchronize()
|
||||||
|
_A2A_STAGING_BUFFERS.clear()
|
||||||
|
|
||||||
|
|
||||||
def _a2a_staging_buffer(
|
def _a2a_staging_buffer(
|
||||||
role: str, shape: tuple[int, ...], dtype: torch.dtype, device: torch.device
|
role: str, shape: tuple[int, ...], dtype: torch.dtype, device: torch.device
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import numpy as np
|
|||||||
import torch
|
import torch
|
||||||
from setproctitle import setproctitle
|
from setproctitle import setproctitle
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.runtime.warmup_request_builder import lighten_warmup_req
|
||||||
|
|
||||||
from sglang.multimodal_gen.runtime.utils.logging_utils import ( # isort: skip
|
from sglang.multimodal_gen.runtime.utils.logging_utils import ( # isort: skip
|
||||||
globally_suppress_loggers,
|
globally_suppress_loggers,
|
||||||
)
|
)
|
||||||
@@ -49,7 +51,17 @@ from sglang.multimodal_gen.runtime.entrypoints.utils import (
|
|||||||
post_process_sample,
|
post_process_sample,
|
||||||
save_outputs,
|
save_outputs,
|
||||||
)
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.managers.memory_managers.auto_residency import (
|
||||||
|
DefaultWorkload,
|
||||||
|
WarmupMemoryRecord,
|
||||||
|
estimate_default_workload_peak_bytes,
|
||||||
|
resolve_default_workload,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.managers.memory_managers.component_manager import (
|
||||||
|
peek_global_component_residency_manager,
|
||||||
|
)
|
||||||
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
|
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
|
||||||
|
LayerwiseUsageTracker,
|
||||||
configure_layerwise_offload_modules,
|
configure_layerwise_offload_modules,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.runtime.managers.memory_managers.memory_occupation_controller import (
|
from sglang.multimodal_gen.runtime.managers.memory_managers.memory_occupation_controller import (
|
||||||
@@ -93,14 +105,6 @@ from sglang.srt.utils.network import NetworkAddress
|
|||||||
|
|
||||||
logger = init_logger(__name__)
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
OFFLOAD_DISABLE_RECOMMENDATION_ORDER = (
|
|
||||||
"vae",
|
|
||||||
"image_encoder",
|
|
||||||
"text_encoder",
|
|
||||||
"text_encoder_2",
|
|
||||||
"transformer",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _ExpandedOutputParts:
|
class _ExpandedOutputParts:
|
||||||
@@ -129,6 +133,64 @@ def _worker_cpu_intra_op_threads(num_gpus: int) -> int | None:
|
|||||||
return max(1, min(16, cpu_count // max(1, num_gpus)))
|
return max(1, min(16, cpu_count // max(1, num_gpus)))
|
||||||
|
|
||||||
|
|
||||||
|
OFFLOAD_DISABLE_RECOMMENDATION_ORDER = (
|
||||||
|
"vae",
|
||||||
|
"image_encoder",
|
||||||
|
"text_encoder",
|
||||||
|
"text_encoder_2",
|
||||||
|
"transformer",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
PROBE_FIT_MIN_MARGIN_BYTES = 1 << 30
|
||||||
|
|
||||||
|
|
||||||
|
def _shape_label(req: Req) -> str:
|
||||||
|
return f"{req.width}x{req.height}x{req.num_frames or 1}f"
|
||||||
|
|
||||||
|
|
||||||
|
def fit_auto_residency_probe(
|
||||||
|
req: Req,
|
||||||
|
*,
|
||||||
|
records: list[WarmupMemoryRecord],
|
||||||
|
free_bytes: int,
|
||||||
|
total_bytes: int,
|
||||||
|
server_args: ServerArgs,
|
||||||
|
) -> tuple[Req, int | None, int]:
|
||||||
|
"""Shrink a full-shape probe until its extrapolated peak fits the memory left.
|
||||||
|
|
||||||
|
The probe measures the default workload under the load-safe placement, so
|
||||||
|
a probe the card cannot hold would only be found out by running out of
|
||||||
|
memory. The bounded warmup that runs before it gives one measurement to
|
||||||
|
extrapolate from; while that extrapolation exceeds free memory minus the
|
||||||
|
reserve, frames go first and then area, the ladder the OOM retry walks.
|
||||||
|
Returns the fitted request, its estimate and the number of shrink steps.
|
||||||
|
"""
|
||||||
|
# Only the probe has to fit, so the margin is allocator slack, not the
|
||||||
|
# planner's placement reserve (which held back 4 GiB of a 32 GiB card and
|
||||||
|
# shrank a probe that had 10 GiB to spare).
|
||||||
|
budget = free_bytes - max(PROBE_FIT_MIN_MARGIN_BYTES, total_bytes // 50)
|
||||||
|
# The bounded warmup already ran at the smallest measured shape; a probe
|
||||||
|
# below it measures nothing new and degenerate shapes fail inside models.
|
||||||
|
floor_units = min((record.workload_units() for record in records), default=0)
|
||||||
|
fitted, steps = req, 0
|
||||||
|
while True:
|
||||||
|
units = (
|
||||||
|
max(1, int(fitted.width or 1))
|
||||||
|
* max(1, int(fitted.height or 1))
|
||||||
|
* max(1, int(fitted.num_frames or 1))
|
||||||
|
)
|
||||||
|
estimate = estimate_default_workload_peak_bytes(
|
||||||
|
records=records, target_units=units
|
||||||
|
)
|
||||||
|
if estimate is None or estimate <= budget or units <= floor_units:
|
||||||
|
return fitted, estimate, steps
|
||||||
|
lighter = lighten_warmup_req(server_args, fitted)
|
||||||
|
if lighter is None:
|
||||||
|
return fitted, estimate, steps
|
||||||
|
fitted, steps = lighter, steps + 1
|
||||||
|
|
||||||
|
|
||||||
class GPUWorker(GPUWorkerPostTrainingMixin):
|
class GPUWorker(GPUWorkerPostTrainingMixin):
|
||||||
"""
|
"""
|
||||||
A worker that executes the model on a single GPU.
|
A worker that executes the model on a single GPU.
|
||||||
@@ -163,6 +225,10 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
|
|||||||
load_snapshot.peak_allocated_mb if load_snapshot is not None else 0.0
|
load_snapshot.peak_allocated_mb if load_snapshot is not None else 0.0
|
||||||
)
|
)
|
||||||
self._runtime_peak_reserved_mb = 0.0
|
self._runtime_peak_reserved_mb = 0.0
|
||||||
|
# Warmup probes run the default workload's full shape and may exceed any
|
||||||
|
# serving request; keep their peak out of the runtime figure.
|
||||||
|
self._warmup_peak_reserved_mb = 0.0
|
||||||
|
self._release_warmup_pool_before_serving = False
|
||||||
self._runtime_peak_allocated_mb = 0.0
|
self._runtime_peak_allocated_mb = 0.0
|
||||||
self.sp_group = get_sp_group()
|
self.sp_group = get_sp_group()
|
||||||
self.sp_cpu_group = self.sp_group.cpu_group
|
self.sp_cpu_group = self.sp_group.cpu_group
|
||||||
@@ -173,6 +239,26 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
|
|||||||
self.cfg_cpu_group = self.cfg_group.cpu_group
|
self.cfg_cpu_group = self.cfg_group.cpu_group
|
||||||
self._realtime_sessions = RealtimeSessionCache(max_sessions=1)
|
self._realtime_sessions = RealtimeSessionCache(max_sessions=1)
|
||||||
self.memory_occupation: MemoryOccupationController | None = None
|
self.memory_occupation: MemoryOccupationController | None = None
|
||||||
|
# per-rank memory measurements of server warmup forwards; consumed by
|
||||||
|
# the auto-residency placement decision before the server turns ready
|
||||||
|
self._auto_residency_warmup_records: list[WarmupMemoryRecord] = []
|
||||||
|
# default workload resolved once for the per-request residency hint
|
||||||
|
self._cached_default_workload: DefaultWorkload | None = None
|
||||||
|
self._cached_default_workload_failed = False
|
||||||
|
|
||||||
|
def _default_workload_for_hint(self) -> DefaultWorkload | None:
|
||||||
|
if (
|
||||||
|
self._cached_default_workload is None
|
||||||
|
and not self._cached_default_workload_failed
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
self._cached_default_workload = resolve_default_workload(
|
||||||
|
self.server_args
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.debug("Default workload unresolvable", exc_info=True)
|
||||||
|
self._cached_default_workload_failed = True
|
||||||
|
return self._cached_default_workload
|
||||||
|
|
||||||
def release_realtime_session(self, session_id: str) -> OutputBatch:
|
def release_realtime_session(self, session_id: str) -> OutputBatch:
|
||||||
"""release the session of a realtime connection"""
|
"""release the session of a realtime connection"""
|
||||||
@@ -364,24 +450,37 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
|
|||||||
remaining_gpu_mem_gb = (
|
remaining_gpu_mem_gb = (
|
||||||
current_platform.get_device_total_memory() / (1024**3) - peak_reserved_gb
|
current_platform.get_device_total_memory() / (1024**3) - peak_reserved_gb
|
||||||
)
|
)
|
||||||
can_stay_resident = self.get_can_stay_resident_components(remaining_gpu_mem_gb)
|
try:
|
||||||
|
can_stay_resident = self.get_can_stay_resident_components(
|
||||||
|
remaining_gpu_mem_gb
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
# a debug-only hint must never fail a completed request
|
||||||
|
logger.debug("Residency hint unavailable", exc_info=True)
|
||||||
|
can_stay_resident = []
|
||||||
|
|
||||||
pool_overhead_gb = peak_reserved_gb - peak_allocated_gb
|
pool_overhead_gb = peak_reserved_gb - peak_allocated_gb
|
||||||
pool_overhead_pct = (
|
pool_overhead_pct = (
|
||||||
pool_overhead_gb / peak_reserved_gb * 100 if peak_reserved_gb else 0.0
|
pool_overhead_gb / peak_reserved_gb * 100 if peak_reserved_gb else 0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
residency_hint = (
|
||||||
|
f" Components that can remain on GPU: {can_stay_resident}. "
|
||||||
|
"Make it explicit with --component-residency <name>=resident; "
|
||||||
|
"--performance-mode auto with server warmup applies safe "
|
||||||
|
"adjustments automatically."
|
||||||
|
if can_stay_resident
|
||||||
|
else ""
|
||||||
|
)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"GPU memory: peak=%.2f GB, allocated=%.2f GB, pool=%.2f GB (%.1f%%), "
|
"GPU memory: peak=%.2f GB, allocated=%.2f GB, pool=%.2f GB (%.1f%%), "
|
||||||
"headroom=%.2f GB. Components that can remain on GPU: %s. "
|
"headroom=%.2f GB.%s",
|
||||||
"Adjust --cpu-offload-components or --layerwise-offload-components "
|
|
||||||
"to change residency.",
|
|
||||||
peak_reserved_gb,
|
peak_reserved_gb,
|
||||||
peak_allocated_gb,
|
peak_allocated_gb,
|
||||||
pool_overhead_gb,
|
pool_overhead_gb,
|
||||||
pool_overhead_pct,
|
pool_overhead_pct,
|
||||||
remaining_gpu_mem_gb,
|
remaining_gpu_mem_gb,
|
||||||
can_stay_resident,
|
residency_hint,
|
||||||
)
|
)
|
||||||
|
|
||||||
def execute_forward(
|
def execute_forward(
|
||||||
@@ -409,6 +508,8 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
|
|||||||
return self._execute_forward_batch(batch)
|
return self._execute_forward_batch(batch)
|
||||||
|
|
||||||
req = batch[0]
|
req = batch[0]
|
||||||
|
if req.is_warmup and req.extra.get("auto_residency_full_shape_probe"):
|
||||||
|
self._fit_auto_residency_probe(req)
|
||||||
return self._execute_forward_common(
|
return self._execute_forward_common(
|
||||||
req,
|
req,
|
||||||
forward_fn=lambda: self.pipeline.forward(req, self.server_args),
|
forward_fn=lambda: self.pipeline.forward(req, self.server_args),
|
||||||
@@ -502,9 +603,57 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
|
|||||||
"""
|
"""
|
||||||
output_batch = None
|
output_batch = None
|
||||||
forward_failed = False
|
forward_failed = False
|
||||||
|
# Prewarm reqs (is_warmup=False) run a different offload layout and
|
||||||
|
# must not contaminate the calibration records. Pipelines that cannot
|
||||||
|
# apply a residency plan also skip the temporary per-layer hooks.
|
||||||
|
measure_server_warmup = (
|
||||||
|
req.is_warmup
|
||||||
|
and bool(req.extra.get("server_based_warmup"))
|
||||||
|
and self.server_args.pipeline_config.supports_auto_residency
|
||||||
|
and current_platform.is_cuda()
|
||||||
|
)
|
||||||
|
warmup_workload = (
|
||||||
|
(
|
||||||
|
int(req.width or 0),
|
||||||
|
int(req.height or 0),
|
||||||
|
int(req.num_frames or 1),
|
||||||
|
max(1, int(req.num_inference_steps or 1)),
|
||||||
|
)
|
||||||
|
if measure_server_warmup
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
warmup_baseline_allocated_bytes = 0
|
||||||
|
layerwise_usage_tracker: LayerwiseUsageTracker | None = None
|
||||||
|
layerwise_layer_uses_by_stage: dict[
|
||||||
|
str, dict[str, dict[str, tuple[int, ...]]]
|
||||||
|
] = {}
|
||||||
try:
|
try:
|
||||||
|
if measure_server_warmup:
|
||||||
|
# Drop the previous request's allocator pool so each probe
|
||||||
|
# starts from the same placement and can return released
|
||||||
|
# component storage before its allocated peak is measured.
|
||||||
|
torch.get_device_module().empty_cache()
|
||||||
|
self._release_warmup_pool(req)
|
||||||
if not current_platform.is_cpu() and not current_platform.is_mps():
|
if not current_platform.is_cpu() and not current_platform.is_mps():
|
||||||
torch.get_device_module().reset_peak_memory_stats()
|
torch.get_device_module().reset_peak_memory_stats()
|
||||||
|
if measure_server_warmup:
|
||||||
|
warmup_baseline_allocated_bytes = (
|
||||||
|
torch.get_device_module().memory_allocated()
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
self.server_args.performance_mode == "auto"
|
||||||
|
and self.pipeline is not None
|
||||||
|
):
|
||||||
|
layerwise_usage_tracker = LayerwiseUsageTracker(
|
||||||
|
self.pipeline.modules,
|
||||||
|
stage_name_provider=(
|
||||||
|
lambda: (
|
||||||
|
req.metrics.active_stage_name
|
||||||
|
if req.metrics is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
start_time = (
|
start_time = (
|
||||||
execution_start_time
|
execution_start_time
|
||||||
@@ -559,7 +708,7 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
|
|||||||
req_label = req.request_id[:8] if req.request_id else "unnamed"
|
req_label = req.request_id[:8] if req.request_id else "unnamed"
|
||||||
with maybe_record_function(f"SAVE_OUTPUTS {req_label}"):
|
with maybe_record_function(f"SAVE_OUTPUTS {req_label}"):
|
||||||
self._materialize_output_transport(output_batch, req, save_output_paths)
|
self._materialize_output_transport(output_batch, req, save_output_paths)
|
||||||
self._record_output_peak_memory(output_batch)
|
self._record_output_peak_memory(output_batch, is_warmup=req.is_warmup)
|
||||||
|
|
||||||
collect_perf = (
|
collect_perf = (
|
||||||
req.perf_dump_path is not None or envs.SGLANG_DIFFUSION_STAGE_LOGGING
|
req.perf_dump_path is not None or envs.SGLANG_DIFFUSION_STAGE_LOGGING
|
||||||
@@ -615,12 +764,115 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
|
|||||||
if output_batch is None:
|
if output_batch is None:
|
||||||
output_batch = OutputBatch()
|
output_batch = OutputBatch()
|
||||||
output_batch.error = f"Error executing {error_context}: {e}"
|
output_batch.error = f"Error executing {error_context}: {e}"
|
||||||
self._record_output_peak_memory(output_batch)
|
self._record_output_peak_memory(output_batch, is_warmup=req.is_warmup)
|
||||||
# clean cache if OOM
|
# clean cache if OOM
|
||||||
if not current_platform.is_cpu():
|
if not current_platform.is_cpu():
|
||||||
torch.get_device_module().empty_cache()
|
torch.get_device_module().empty_cache()
|
||||||
|
finally:
|
||||||
|
# also runs on the propagate_forward_errors re-raise: a warmup
|
||||||
|
# forward that never completed must still leave a failed record,
|
||||||
|
# or the estimator would plan from the remaining partial data
|
||||||
|
if measure_server_warmup:
|
||||||
|
assert warmup_workload is not None
|
||||||
|
if layerwise_usage_tracker is not None:
|
||||||
|
(
|
||||||
|
layerwise_layer_uses,
|
||||||
|
layerwise_layer_uses_by_stage,
|
||||||
|
) = layerwise_usage_tracker.finish_with_stages()
|
||||||
|
else:
|
||||||
|
layerwise_layer_uses = {}
|
||||||
|
self._record_server_warmup_memory(
|
||||||
|
req=req,
|
||||||
|
workload=warmup_workload,
|
||||||
|
baseline_allocated_bytes=warmup_baseline_allocated_bytes,
|
||||||
|
succeeded=output_batch is not None and output_batch.error is None,
|
||||||
|
layerwise_layer_uses=layerwise_layer_uses,
|
||||||
|
layerwise_layer_uses_by_stage=layerwise_layer_uses_by_stage,
|
||||||
|
)
|
||||||
return output_batch
|
return output_batch
|
||||||
|
|
||||||
|
def _record_server_warmup_memory(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
req: Req,
|
||||||
|
workload: tuple[int, int, int, int],
|
||||||
|
baseline_allocated_bytes: int,
|
||||||
|
succeeded: bool,
|
||||||
|
layerwise_layer_uses: dict[str, dict[str, tuple[int, ...]]] | None = None,
|
||||||
|
layerwise_layer_uses_by_stage: (
|
||||||
|
dict[str, dict[str, dict[str, tuple[int, ...]]]] | None
|
||||||
|
) = None,
|
||||||
|
) -> None:
|
||||||
|
phase_allocated_peaks: dict[str, int] = {}
|
||||||
|
phase_components: dict[str, tuple[str, ...]] = {}
|
||||||
|
phase_used_components: dict[str, tuple[str, ...]] = {}
|
||||||
|
phase_full_weight_transition_components: dict[str, tuple[str, ...]] = {}
|
||||||
|
untracked_active_components: tuple[str, ...] = ()
|
||||||
|
residency_manager = peek_global_component_residency_manager()
|
||||||
|
if residency_manager is not None:
|
||||||
|
for phase_name, peak in residency_manager.take_warmup_phase_peaks().items():
|
||||||
|
phase_allocated_peaks[phase_name] = peak.allocated_bytes
|
||||||
|
phase_components[phase_name] = peak.active_components
|
||||||
|
phase_used_components[phase_name] = peak.used_components
|
||||||
|
phase_full_weight_transition_components[phase_name] = (
|
||||||
|
peak.full_weight_transition_components
|
||||||
|
)
|
||||||
|
untracked_active_components = residency_manager.current_device_components()
|
||||||
|
request_allocated_peak = max(
|
||||||
|
int(torch.get_device_module().max_memory_allocated()),
|
||||||
|
max(phase_allocated_peaks.values(), default=0),
|
||||||
|
)
|
||||||
|
if request_allocated_peak > max(phase_allocated_peaks.values(), default=0):
|
||||||
|
# Work after the residency-managed stage timeline (for example,
|
||||||
|
# output materialization) must remain a placement constraint.
|
||||||
|
# A reserved-only increase is not a second live placement. Record
|
||||||
|
# it separately so post-placement validation can still require
|
||||||
|
# allocator headroom without charging cache to candidate deltas.
|
||||||
|
phase_allocated_peaks["request:untracked"] = request_allocated_peak
|
||||||
|
phase_components["request:untracked"] = untracked_active_components
|
||||||
|
phase_used_components["request:untracked"] = ()
|
||||||
|
phase_full_weight_transition_components["request:untracked"] = ()
|
||||||
|
metrics = req.metrics
|
||||||
|
width, height, num_frames, num_inference_steps = workload
|
||||||
|
self._auto_residency_warmup_records.append(
|
||||||
|
WarmupMemoryRecord(
|
||||||
|
width=width,
|
||||||
|
height=height,
|
||||||
|
num_frames=num_frames,
|
||||||
|
baseline_allocated_bytes=int(baseline_allocated_bytes),
|
||||||
|
peak_allocated_bytes=request_allocated_peak,
|
||||||
|
succeeded=succeeded,
|
||||||
|
peak_reserved_bytes=int(
|
||||||
|
torch.get_device_module().max_memory_reserved()
|
||||||
|
),
|
||||||
|
phase_peak_allocated_bytes=phase_allocated_peaks,
|
||||||
|
phase_active_components=phase_components,
|
||||||
|
phase_used_components=phase_used_components,
|
||||||
|
phase_full_weight_transition_components=(
|
||||||
|
phase_full_weight_transition_components
|
||||||
|
),
|
||||||
|
layerwise_layer_uses=layerwise_layer_uses or {},
|
||||||
|
layerwise_layer_uses_by_stage=layerwise_layer_uses_by_stage or {},
|
||||||
|
num_inference_steps=num_inference_steps,
|
||||||
|
total_duration_ms=(
|
||||||
|
float(metrics.total_duration_ms) if metrics is not None else 0.0
|
||||||
|
),
|
||||||
|
stage_duration_ms=(dict(metrics.stages) if metrics is not None else {}),
|
||||||
|
step_duration_ms=(tuple(metrics.steps) if metrics is not None else ()),
|
||||||
|
step_duration_ms_by_stage=(
|
||||||
|
{
|
||||||
|
stage_name: tuple(durations)
|
||||||
|
for stage_name, durations in metrics.steps_by_stage.items()
|
||||||
|
}
|
||||||
|
if metrics is not None
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
stage_iterations=(
|
||||||
|
dict(metrics.stage_iterations) if metrics is not None else {}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def _materialize_output_transport(
|
def _materialize_output_transport(
|
||||||
self,
|
self,
|
||||||
output_batch: OutputBatch,
|
output_batch: OutputBatch,
|
||||||
@@ -733,16 +985,100 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
|
|||||||
)
|
)
|
||||||
return np.asarray(materialized.frames)
|
return np.asarray(materialized.frames)
|
||||||
|
|
||||||
def _record_output_peak_memory(self, output_batch: OutputBatch) -> None:
|
def _fit_auto_residency_probe(self, req: Req) -> None:
|
||||||
|
"""Size the full-shape probe to what the card has left, on every rank alike."""
|
||||||
|
records = [r for r in self._auto_residency_warmup_records if r.succeeded]
|
||||||
|
if not records or not current_platform.is_cuda():
|
||||||
|
return
|
||||||
|
device = current_platform.get_device(self.local_rank)
|
||||||
|
free_bytes = int(
|
||||||
|
current_platform.get_available_gpu_memory(empty_cache=True) * (1 << 30)
|
||||||
|
)
|
||||||
|
total_bytes = int(torch.cuda.get_device_properties(device).total_memory)
|
||||||
|
_, _, steps = fit_auto_residency_probe(
|
||||||
|
req,
|
||||||
|
records=records,
|
||||||
|
free_bytes=free_bytes,
|
||||||
|
total_bytes=total_bytes,
|
||||||
|
server_args=self.server_args,
|
||||||
|
)
|
||||||
|
requested_units = (
|
||||||
|
max(1, int(req.width or 1))
|
||||||
|
* max(1, int(req.height or 1))
|
||||||
|
* max(1, int(req.num_frames or 1))
|
||||||
|
)
|
||||||
|
estimate = estimate_default_workload_peak_bytes(
|
||||||
|
records=records, target_units=requested_units
|
||||||
|
)
|
||||||
|
# Ranks see different free memory and hold different records; the
|
||||||
|
# forward must run one shape everywhere, so the most cautious rank wins.
|
||||||
|
agreed = torch.tensor([steps], dtype=torch.int64, device=device)
|
||||||
|
agreed = get_replica_group().all_reduce(
|
||||||
|
agreed, op=torch.distributed.ReduceOp.MAX
|
||||||
|
)
|
||||||
|
steps = int(agreed.item())
|
||||||
|
if steps == 0:
|
||||||
|
return
|
||||||
|
fitted = req
|
||||||
|
for _ in range(steps):
|
||||||
|
lighter = lighten_warmup_req(self.server_args, fitted)
|
||||||
|
if lighter is None:
|
||||||
|
break
|
||||||
|
fitted = lighter
|
||||||
|
if self.is_output_rank:
|
||||||
|
logger.warning(
|
||||||
|
"Auto residency probe %s would not fit: extrapolated peak %.1f GiB "
|
||||||
|
"against %.1f GiB free; probing at %s instead",
|
||||||
|
_shape_label(req),
|
||||||
|
(estimate or 0) / (1 << 30),
|
||||||
|
free_bytes / (1 << 30),
|
||||||
|
_shape_label(fitted),
|
||||||
|
)
|
||||||
|
req.sampling_params = fitted.sampling_params
|
||||||
|
|
||||||
|
def _release_warmup_pool(self, req: Req) -> None:
|
||||||
|
"""Drop what the full-shape probe left behind before the next request.
|
||||||
|
|
||||||
|
The probe runs a shape serving may never see. Its cached allocator
|
||||||
|
blocks would become the floor of every runtime peak measurement, and
|
||||||
|
the all-to-all staging buffers it created (IPC and Ulysses) stay
|
||||||
|
allocated at its message size. The request after the probe (the
|
||||||
|
bounded re-warm) regrows all of them at a serving-sized shape.
|
||||||
|
"""
|
||||||
|
if req.is_warmup and req.extra.get("auto_residency_full_shape_probe"):
|
||||||
|
self._release_warmup_pool_before_serving = True
|
||||||
|
return
|
||||||
|
if not self._release_warmup_pool_before_serving:
|
||||||
|
return
|
||||||
|
self._release_warmup_pool_before_serving = False
|
||||||
|
if current_platform.is_cpu() or current_platform.is_mps():
|
||||||
|
return
|
||||||
|
from sglang.multimodal_gen.runtime.distributed.device_communicators.ipc_a2a import (
|
||||||
|
IPC_A2A,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.layers.usp import drop_a2a_staging_buffers
|
||||||
|
|
||||||
|
IPC_A2A.drop_staging()
|
||||||
|
drop_a2a_staging_buffers()
|
||||||
|
torch.get_device_module().empty_cache()
|
||||||
|
|
||||||
|
def _record_output_peak_memory(
|
||||||
|
self, output_batch: OutputBatch, *, is_warmup: bool = False
|
||||||
|
) -> None:
|
||||||
if current_platform.is_cpu():
|
if current_platform.is_cpu():
|
||||||
return
|
return
|
||||||
snapshot = capture_memory_snapshot()
|
snapshot = capture_memory_snapshot()
|
||||||
self._runtime_peak_reserved_mb = max(
|
if is_warmup:
|
||||||
self._runtime_peak_reserved_mb, snapshot.peak_reserved_mb
|
self._warmup_peak_reserved_mb = max(
|
||||||
)
|
self._warmup_peak_reserved_mb, snapshot.peak_reserved_mb
|
||||||
self._runtime_peak_allocated_mb = max(
|
)
|
||||||
self._runtime_peak_allocated_mb, snapshot.peak_allocated_mb
|
else:
|
||||||
)
|
self._runtime_peak_reserved_mb = max(
|
||||||
|
self._runtime_peak_reserved_mb, snapshot.peak_reserved_mb
|
||||||
|
)
|
||||||
|
self._runtime_peak_allocated_mb = max(
|
||||||
|
self._runtime_peak_allocated_mb, snapshot.peak_allocated_mb
|
||||||
|
)
|
||||||
if self.is_output_rank:
|
if self.is_output_rank:
|
||||||
output_batch.peak_memory_mb = snapshot.peak_reserved_mb
|
output_batch.peak_memory_mb = snapshot.peak_reserved_mb
|
||||||
|
|
||||||
@@ -755,6 +1091,7 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
|
|||||||
[
|
[
|
||||||
self._load_peak_reserved_mb,
|
self._load_peak_reserved_mb,
|
||||||
self._runtime_peak_reserved_mb,
|
self._runtime_peak_reserved_mb,
|
||||||
|
self._warmup_peak_reserved_mb,
|
||||||
self._load_peak_allocated_mb,
|
self._load_peak_allocated_mb,
|
||||||
self._runtime_peak_allocated_mb,
|
self._runtime_peak_allocated_mb,
|
||||||
],
|
],
|
||||||
@@ -769,6 +1106,7 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
|
|||||||
(
|
(
|
||||||
load_peak_mb,
|
load_peak_mb,
|
||||||
runtime_peak_mb,
|
runtime_peak_mb,
|
||||||
|
warmup_peak_mb,
|
||||||
load_peak_allocated_mb,
|
load_peak_allocated_mb,
|
||||||
runtime_peak_allocated_mb,
|
runtime_peak_allocated_mb,
|
||||||
) = peaks.tolist()
|
) = peaks.tolist()
|
||||||
@@ -789,6 +1127,9 @@ class GPUWorker(GPUWorkerPostTrainingMixin):
|
|||||||
peak_allocated_mb=runtime_peak_allocated_mb,
|
peak_allocated_mb=runtime_peak_allocated_mb,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
metrics.record_memory_snapshot(
|
||||||
|
"warmup_peak", replace(snapshot, peak_reserved_mb=warmup_peak_mb)
|
||||||
|
)
|
||||||
|
|
||||||
def _forward_group(self, batch: list[Req]) -> OutputBatch:
|
def _forward_group(self, batch: list[Req]) -> OutputBatch:
|
||||||
assert self.pipeline is not None
|
assert self.pipeline is not None
|
||||||
|
|||||||
@@ -0,0 +1,690 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""Warmup-calibrated automatic component residency placement.
|
||||||
|
|
||||||
|
Under ``--performance-mode auto`` with server warmup, each rank measures the
|
||||||
|
peak GPU memory of bounded synthetic warmup requests and a low-step probe at
|
||||||
|
the complete default serving shape, then selects a complete serving placement
|
||||||
|
for every eligible component under the measured memory constraints.
|
||||||
|
|
||||||
|
When no full-shape measurement is available, the fallback estimate splits the
|
||||||
|
measured peak into persistent weights and workload-scaled activations. Scaling
|
||||||
|
the whole peak would multiply resident weights by the video frame/area cap
|
||||||
|
ratio (~16x for Wan-class defaults) and residency adjustment would never trigger.
|
||||||
|
|
||||||
|
The planner targets the model default workload only (default resolution,
|
||||||
|
default frames, batch=1). Larger shapes, batches, or multi-image inputs need
|
||||||
|
explicit ``--component-residency``.
|
||||||
|
|
||||||
|
Loading and serving are deliberately separate placement states. The existing
|
||||||
|
auto policy provides the initial state; when that state can complete loading
|
||||||
|
and calibration, this module optimizes the long-lived serving state and
|
||||||
|
validates the transition with a post-placement warmup. It does not force a
|
||||||
|
single placement to serve two different lifecycle objectives.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import statistics
|
||||||
|
from typing import TYPE_CHECKING, Iterable, Mapping
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.runtime.managers.memory_managers.component_residency import (
|
||||||
|
COMPONENT_OFFLOAD,
|
||||||
|
LAYERWISE_OFFLOAD,
|
||||||
|
RESIDENT,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload_components import (
|
||||||
|
is_dit_component_name,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
GIB_BYTES = 1024**3
|
||||||
|
|
||||||
|
# Activation memory rarely scales perfectly linearly with workload units;
|
||||||
|
# pad the extrapolated activation part before checking the budget.
|
||||||
|
ACTIVATION_EXTRAPOLATION_MARGIN = 1.2
|
||||||
|
# A target-shape measurement plus the mandatory post-placement warmup justifies
|
||||||
|
# a tighter reserve than an extrapolated estimate. Both retain an absolute
|
||||||
|
# floor for allocator slack, shape variance, and CUDA graph or compile pools.
|
||||||
|
MEASURED_VRAM_RESERVE_FRACTION = 0.05
|
||||||
|
EXTRAPOLATED_VRAM_RESERVE_FRACTION = 0.10
|
||||||
|
MIN_VRAM_RESERVE_BYTES = 4 * GIB_BYTES
|
||||||
|
# The absolute floor is sized for datacenter cards, where either fraction can
|
||||||
|
# dominate. On a 12 GiB card a flat 4 GiB would fence off a third of the device,
|
||||||
|
# so cap the floor as a share of what is actually there.
|
||||||
|
MAX_VRAM_RESERVE_FRACTION = 0.20
|
||||||
|
|
||||||
|
# A feasible placement is not automatically useful. Predictions inside this
|
||||||
|
# interval are treated as latency-equivalent. The joint optimizer then avoids
|
||||||
|
# changing strategy, minimizes device memory, preserves the faster estimate,
|
||||||
|
# and finally minimizes HostPin. The raw estimate is already an upper bound:
|
||||||
|
# transfer time is capped by the measured request.
|
||||||
|
ESTIMATED_PINNED_H2D_BYTES_PER_SECOND = 24 * GIB_BYTES
|
||||||
|
MIN_LATENCY_EQUIVALENCE_NS = 50_000_000
|
||||||
|
MAX_LATENCY_EQUIVALENCE_NS = 100_000_000
|
||||||
|
LATENCY_EQUIVALENCE_FRACTION = 0.01
|
||||||
|
# The transfer model ranks feasible placements; the mandatory warmup is the
|
||||||
|
# authority on whether a selected placement actually helped. Allow normal
|
||||||
|
# measurement noise, but undo a round whose calibrated request is materially
|
||||||
|
# slower than the original layout.
|
||||||
|
POST_ADJUSTMENT_REGRESSION_FRACTION = 0.05
|
||||||
|
|
||||||
|
PLACEMENT_STATUS_SKIPPED = "skipped"
|
||||||
|
PLACEMENT_STATUS_ADJUSTED = "adjusted"
|
||||||
|
PLACEMENT_STATUS_VALIDATED = "validated"
|
||||||
|
PLACEMENT_STATUS_ROLLED_BACK = "rolled_back"
|
||||||
|
PLACEMENT_STATUS_ROLLBACK_FAILED = "rollback_failed"
|
||||||
|
|
||||||
|
|
||||||
|
def describe_error(error: BaseException) -> str:
|
||||||
|
"""Never-empty error text (str(AssertionError()) is "" and would be
|
||||||
|
dropped by any truthiness filter)."""
|
||||||
|
text = str(error)
|
||||||
|
return f"{type(error).__name__}: {text}" if text else type(error).__name__
|
||||||
|
|
||||||
|
|
||||||
|
class WarmupMemoryRecord(msgspec.Struct, frozen=True):
|
||||||
|
"""Per-rank memory measurement of one server warmup forward."""
|
||||||
|
|
||||||
|
width: int
|
||||||
|
height: int
|
||||||
|
num_frames: int
|
||||||
|
baseline_allocated_bytes: int
|
||||||
|
peak_allocated_bytes: int
|
||||||
|
succeeded: bool
|
||||||
|
peak_reserved_bytes: int = 0
|
||||||
|
phase_peak_allocated_bytes: dict[str, int] = {}
|
||||||
|
phase_active_components: dict[str, tuple[str, ...]] = {}
|
||||||
|
phase_used_components: dict[str, tuple[str, ...]] = {}
|
||||||
|
phase_full_weight_transition_components: dict[str, tuple[str, ...]] = {}
|
||||||
|
layerwise_layer_uses: dict[str, dict[str, tuple[int, ...]]] = {}
|
||||||
|
layerwise_layer_uses_by_stage: dict[str, dict[str, dict[str, tuple[int, ...]]]] = {}
|
||||||
|
num_inference_steps: int = 1
|
||||||
|
total_duration_ms: float = 0.0
|
||||||
|
stage_duration_ms: dict[str, float] = {}
|
||||||
|
step_duration_ms: tuple[float, ...] = ()
|
||||||
|
step_duration_ms_by_stage: dict[str, tuple[float, ...]] = {}
|
||||||
|
stage_iterations: dict[str, tuple[int, int]] = {}
|
||||||
|
|
||||||
|
def workload_units(self) -> int:
|
||||||
|
return max(1, self.width) * max(1, self.height) * max(1, self.num_frames)
|
||||||
|
|
||||||
|
|
||||||
|
class ResidencyTarget(msgspec.Struct, frozen=True):
|
||||||
|
"""One complete target state for an auto-managed component."""
|
||||||
|
|
||||||
|
component_name: str
|
||||||
|
residency_mode: str
|
||||||
|
target_resident_weight_bytes: int
|
||||||
|
# Estimated per-request host-to-device traffic this target removes.
|
||||||
|
h2d_bytes_per_request: int
|
||||||
|
# Layerwise candidates jointly choose stage-scoped GPU residency and host
|
||||||
|
# pinning. None is used by ordinary component placement.
|
||||||
|
target_layerwise_resident_layers: tuple[int, ...] | None = None
|
||||||
|
target_layerwise_pinned_layers: tuple[tuple[int, ...], ...] | None = None
|
||||||
|
pinned_host_delta_bytes: int = 0
|
||||||
|
host_unpin_scratch_bytes: int = 0
|
||||||
|
host_pin_scratch_bytes: int = 0
|
||||||
|
host_materialize_scratch_bytes: int = 0
|
||||||
|
# Signed device-memory delta while applying the placement before the
|
||||||
|
# validation warmup. Layerwise -> resident materializes every managed
|
||||||
|
# layer immediately; a demotion can release those bytes first and fund a
|
||||||
|
# later materialization in the same transaction.
|
||||||
|
device_transition_delta_bytes: int = 0
|
||||||
|
permanent_residency: bool = False
|
||||||
|
# Device-memory delta relative to the measured placement. A component
|
||||||
|
# already loaded for its own phase has a different delta from phases where
|
||||||
|
# it is absent; keeping both avoids adding the same weights twice.
|
||||||
|
active_device_delta_bytes: int = 0
|
||||||
|
# Delta when the component is already present because of async prefetch,
|
||||||
|
# but is not the semantic owner of this phase.
|
||||||
|
present_device_delta_bytes: int = 0
|
||||||
|
inactive_device_delta_bytes: int = 0
|
||||||
|
# None preserves the historical derived target for hand-built callers:
|
||||||
|
# partial layerwise targets remain layerwise, every other option is
|
||||||
|
# resident. Generated complete-state frontiers set this explicitly.
|
||||||
|
target_residency_mode: str | None = None
|
||||||
|
current_placement: bool = False
|
||||||
|
target_device_weight_bytes: int = 0
|
||||||
|
target_pinned_host_bytes: int = 0
|
||||||
|
|
||||||
|
def target_mode(self) -> str:
|
||||||
|
if self.target_residency_mode is not None:
|
||||||
|
return self.target_residency_mode
|
||||||
|
if (
|
||||||
|
self.target_layerwise_resident_layers is not None
|
||||||
|
and not self.permanent_residency
|
||||||
|
):
|
||||||
|
return LAYERWISE_OFFLOAD
|
||||||
|
return RESIDENT
|
||||||
|
|
||||||
|
def option_key(self) -> str:
|
||||||
|
target_mode = self.target_mode()
|
||||||
|
if target_mode == COMPONENT_OFFLOAD:
|
||||||
|
return f"{self.component_name}:component-offload"
|
||||||
|
if self.target_layerwise_resident_layers is None:
|
||||||
|
return f"{self.component_name}:resident"
|
||||||
|
layer_counts = ",".join(
|
||||||
|
str(count) for count in self.target_layerwise_resident_layers
|
||||||
|
)
|
||||||
|
pinned = "|".join(
|
||||||
|
",".join(str(index) for index in indices) or "-"
|
||||||
|
for indices in self.target_layerwise_pinned_layers or ()
|
||||||
|
)
|
||||||
|
permanence = "permanent" if self.permanent_residency else "stage"
|
||||||
|
return f"{self.component_name}:{permanence}:layers={layer_counts}:pins={pinned}"
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultWorkload(msgspec.Struct, frozen=True):
|
||||||
|
"""The model-default request shape the planner is calibrated for."""
|
||||||
|
|
||||||
|
width: int | None
|
||||||
|
height: int | None
|
||||||
|
num_frames: int
|
||||||
|
num_inference_steps: int
|
||||||
|
|
||||||
|
def workload_units(self) -> int | None:
|
||||||
|
if self.width is None or self.height is None:
|
||||||
|
return None
|
||||||
|
return max(1, self.width) * max(1, self.height) * max(1, self.num_frames)
|
||||||
|
|
||||||
|
def describe(self) -> str:
|
||||||
|
if self.width is None or self.height is None:
|
||||||
|
return "model-default"
|
||||||
|
return f"{self.width}x{self.height}x{self.num_frames}f"
|
||||||
|
|
||||||
|
|
||||||
|
class RankResidencyReport(msgspec.Struct, frozen=True):
|
||||||
|
"""One rank's inputs to the replica-wide placement decision."""
|
||||||
|
|
||||||
|
rank: int
|
||||||
|
budget_bytes: int
|
||||||
|
estimated_peak_bytes: int | None
|
||||||
|
target_workload_measured: bool = False
|
||||||
|
observed_reserved_bytes: int = 0
|
||||||
|
estimated_peak_bytes_by_phase: dict[str, int] = {}
|
||||||
|
active_components_by_phase: dict[str, tuple[str, ...]] = {}
|
||||||
|
used_components_by_phase: dict[str, tuple[str, ...]] = {}
|
||||||
|
full_weight_transition_components_by_phase: dict[str, tuple[str, ...]] = {}
|
||||||
|
current_device_weight_bytes_by_component: dict[str, int] = {}
|
||||||
|
node_rank: int = 0
|
||||||
|
pinned_host_bytes: int = 0
|
||||||
|
host_pin_capacity_bytes: int = 0
|
||||||
|
host_transition_headroom_bytes: int = 0
|
||||||
|
device_transition_allocated_bytes: int = 0
|
||||||
|
estimated_request_duration_ns: int = 0
|
||||||
|
measured_request_duration_ns: int = 0
|
||||||
|
candidate_latency_savings_ns: dict[str, int] = {}
|
||||||
|
candidates: list[ResidencyTarget] = []
|
||||||
|
skip_reason: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_default_workload(server_args: ServerArgs) -> DefaultWorkload:
|
||||||
|
"""Resolve the default request shape the planner is optimized for."""
|
||||||
|
from sglang.multimodal_gen.runtime.warmup_request_builder import (
|
||||||
|
get_model_sampling_defaults,
|
||||||
|
resolve_default_workload_shape,
|
||||||
|
)
|
||||||
|
|
||||||
|
defaults = get_model_sampling_defaults(server_args)
|
||||||
|
width, height, num_frames = resolve_default_workload_shape(server_args, defaults)
|
||||||
|
return DefaultWorkload(
|
||||||
|
width=width,
|
||||||
|
height=height,
|
||||||
|
num_frames=num_frames,
|
||||||
|
num_inference_steps=defaults.num_inference_steps or 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_measured_default_workload(
|
||||||
|
workload: DefaultWorkload, records: Iterable[WarmupMemoryRecord]
|
||||||
|
) -> DefaultWorkload:
|
||||||
|
"""Fill an implicit default resolution from the executed warmup.
|
||||||
|
|
||||||
|
Image-edit pipelines can derive their output size from the input image, so
|
||||||
|
the sampling defaults legitimately omit width and height. The warmup record
|
||||||
|
is captured after input validation and therefore contains the effective
|
||||||
|
serving shape. Keep the model-default frame count because video warmup may
|
||||||
|
intentionally cap frames before measurement.
|
||||||
|
"""
|
||||||
|
if workload.workload_units() is not None:
|
||||||
|
return workload
|
||||||
|
measured = [
|
||||||
|
record
|
||||||
|
for record in records
|
||||||
|
if record.succeeded and record.width > 0 and record.height > 0
|
||||||
|
]
|
||||||
|
if not measured:
|
||||||
|
return workload
|
||||||
|
representative = max(measured, key=lambda record: record.width * record.height)
|
||||||
|
return DefaultWorkload(
|
||||||
|
width=representative.width,
|
||||||
|
height=representative.height,
|
||||||
|
num_frames=workload.num_frames,
|
||||||
|
num_inference_steps=workload.num_inference_steps,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_layerwise_layer_uses(
|
||||||
|
*,
|
||||||
|
records: Iterable[WarmupMemoryRecord],
|
||||||
|
target_units: int | None,
|
||||||
|
target_num_inference_steps: int,
|
||||||
|
) -> dict[str, dict[str, tuple[int, ...]]]:
|
||||||
|
"""Estimate per-request layer calls from the same calibration forward.
|
||||||
|
|
||||||
|
A full-shape memory probe deliberately runs only a few denoise steps.
|
||||||
|
Stage-attributed calls use that stage's measured and target iteration
|
||||||
|
counts, so independent shape, paint, refiner, and chunk loops are not all
|
||||||
|
multiplied by one request-wide ratio. Legacy records retain the repeated
|
||||||
|
DiT-layer heuristic.
|
||||||
|
"""
|
||||||
|
successful = [record for record in records if record.succeeded]
|
||||||
|
if target_units is not None:
|
||||||
|
covering = [
|
||||||
|
record for record in successful if record.workload_units() >= target_units
|
||||||
|
]
|
||||||
|
if covering:
|
||||||
|
successful = covering
|
||||||
|
|
||||||
|
estimated: dict[str, dict[str, list[int]]] = {}
|
||||||
|
for record in successful:
|
||||||
|
source_steps = max(1, record.num_inference_steps)
|
||||||
|
component_stages = _component_stages(record)
|
||||||
|
repeated_stages = _repeated_stages(record, component_stages)
|
||||||
|
for component_name, groups in record.layerwise_layer_uses.items():
|
||||||
|
component = estimated.setdefault(component_name, {})
|
||||||
|
for layer_name, counts in groups.items():
|
||||||
|
target = component.setdefault(layer_name, [0] * len(counts))
|
||||||
|
if len(target) != len(counts):
|
||||||
|
continue
|
||||||
|
stage_counts = [
|
||||||
|
(
|
||||||
|
stage_name,
|
||||||
|
stage_components[component_name][layer_name],
|
||||||
|
)
|
||||||
|
for stage_name, stage_components in (
|
||||||
|
record.layerwise_layer_uses_by_stage.items()
|
||||||
|
)
|
||||||
|
if component_name in stage_components
|
||||||
|
and layer_name in stage_components[component_name]
|
||||||
|
and len(stage_components[component_name][layer_name]) == len(counts)
|
||||||
|
]
|
||||||
|
for layer_index, count in enumerate(counts):
|
||||||
|
if stage_counts:
|
||||||
|
measured_by_stage = sum(
|
||||||
|
per_layer_counts[layer_index]
|
||||||
|
for _, per_layer_counts in stage_counts
|
||||||
|
)
|
||||||
|
scaled = max(0, count - measured_by_stage)
|
||||||
|
for stage_name, per_layer_counts in stage_counts:
|
||||||
|
measured_iterations, target_iterations = _stage_iterations(
|
||||||
|
record,
|
||||||
|
stage_name,
|
||||||
|
repeated_stages=repeated_stages,
|
||||||
|
target_num_inference_steps=(target_num_inference_steps),
|
||||||
|
)
|
||||||
|
stage_count = per_layer_counts[layer_index]
|
||||||
|
if stage_count <= 1:
|
||||||
|
scaled += stage_count
|
||||||
|
else:
|
||||||
|
scaled += (
|
||||||
|
stage_count * target_iterations
|
||||||
|
+ measured_iterations
|
||||||
|
- 1
|
||||||
|
) // measured_iterations
|
||||||
|
else:
|
||||||
|
scaled = count
|
||||||
|
component_is_repeated = is_dit_component_name(
|
||||||
|
component_name
|
||||||
|
) or any(
|
||||||
|
stage_name in repeated_stages
|
||||||
|
for stage_name in component_stages.get(component_name, ())
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
component_is_repeated
|
||||||
|
and count > 1
|
||||||
|
and target_num_inference_steps > source_steps
|
||||||
|
):
|
||||||
|
scaled = (
|
||||||
|
count * target_num_inference_steps + source_steps - 1
|
||||||
|
) // source_steps
|
||||||
|
target[layer_index] = max(target[layer_index], scaled)
|
||||||
|
return {
|
||||||
|
component_name: {
|
||||||
|
layer_name: tuple(counts) for layer_name, counts in groups.items()
|
||||||
|
}
|
||||||
|
for component_name, groups in estimated.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _component_stages(
|
||||||
|
record: WarmupMemoryRecord,
|
||||||
|
*,
|
||||||
|
timed_stage_names: set[str] | None = None,
|
||||||
|
) -> dict[str, set[str]]:
|
||||||
|
component_stages: dict[str, set[str]] = {}
|
||||||
|
phase_components = record.phase_used_components or record.phase_active_components
|
||||||
|
for phase_name, components in phase_components.items():
|
||||||
|
fields = phase_name.split(":", 2)
|
||||||
|
if len(fields) < 2 or not fields[0].isdigit():
|
||||||
|
continue
|
||||||
|
stage_name = fields[1]
|
||||||
|
if timed_stage_names is not None and stage_name not in timed_stage_names:
|
||||||
|
continue
|
||||||
|
for component_name in components:
|
||||||
|
component_stages.setdefault(component_name, set()).add(stage_name)
|
||||||
|
return component_stages
|
||||||
|
|
||||||
|
|
||||||
|
def _repeated_stages(
|
||||||
|
record: WarmupMemoryRecord,
|
||||||
|
component_stages: Mapping[str, set[str]],
|
||||||
|
) -> set[str]:
|
||||||
|
stages = {
|
||||||
|
stage_name
|
||||||
|
for component_name, stage_names in component_stages.items()
|
||||||
|
if is_dit_component_name(component_name)
|
||||||
|
for stage_name in stage_names
|
||||||
|
}
|
||||||
|
stages.update(record.stage_iterations)
|
||||||
|
stages.update(
|
||||||
|
stage_name
|
||||||
|
for stage_name in set(record.stage_duration_ms).union(
|
||||||
|
*(stage_names for stage_names in component_stages.values())
|
||||||
|
)
|
||||||
|
if stage_name.endswith("DenoisingStage")
|
||||||
|
and not stage_name.endswith("BeforeDenoisingStage")
|
||||||
|
)
|
||||||
|
return stages
|
||||||
|
|
||||||
|
|
||||||
|
def _stage_iterations(
|
||||||
|
record: WarmupMemoryRecord,
|
||||||
|
stage_name: str,
|
||||||
|
*,
|
||||||
|
repeated_stages: set[str],
|
||||||
|
target_num_inference_steps: int,
|
||||||
|
) -> tuple[int, int]:
|
||||||
|
explicit = record.stage_iterations.get(stage_name)
|
||||||
|
if explicit is not None:
|
||||||
|
return max(1, explicit[0]), max(1, explicit[1])
|
||||||
|
measured = max(1, record.num_inference_steps)
|
||||||
|
target = (
|
||||||
|
max(1, target_num_inference_steps)
|
||||||
|
if stage_name in repeated_stages
|
||||||
|
else measured
|
||||||
|
)
|
||||||
|
return measured, target
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_default_workload_timing(
|
||||||
|
*,
|
||||||
|
records: Iterable[WarmupMemoryRecord],
|
||||||
|
target_units: int | None,
|
||||||
|
target_num_inference_steps: int,
|
||||||
|
) -> tuple[int, dict[str, int], dict[str, tuple[str, ...]]]:
|
||||||
|
"""Estimate full-request and stage durations from the warmup workload.
|
||||||
|
|
||||||
|
Repeated stages scale by their own measured/default iteration counts. The
|
||||||
|
full-shape probe intentionally executes only a few steps, so using its raw
|
||||||
|
total would make every one-shot encoder transfer look important relative
|
||||||
|
to a long video request.
|
||||||
|
"""
|
||||||
|
successful = [record for record in records if record.succeeded]
|
||||||
|
if not successful:
|
||||||
|
return 0, {}, {}
|
||||||
|
if target_units is not None:
|
||||||
|
at_target = [
|
||||||
|
record for record in successful if record.workload_units() >= target_units
|
||||||
|
]
|
||||||
|
if at_target:
|
||||||
|
successful = at_target
|
||||||
|
representative = max(
|
||||||
|
successful,
|
||||||
|
key=lambda record: (
|
||||||
|
record.workload_units(),
|
||||||
|
record.total_duration_ms,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if representative.total_duration_ms <= 0 or not representative.stage_duration_ms:
|
||||||
|
return 0, {}, {}
|
||||||
|
|
||||||
|
component_stages = _component_stages(
|
||||||
|
representative,
|
||||||
|
timed_stage_names=set(representative.stage_duration_ms),
|
||||||
|
)
|
||||||
|
repeated_stages = _repeated_stages(representative, component_stages)
|
||||||
|
|
||||||
|
stage_duration_ns: dict[str, int] = {}
|
||||||
|
for stage_name, duration_ms in representative.stage_duration_ms.items():
|
||||||
|
measured_iterations, target_iterations = _stage_iterations(
|
||||||
|
representative,
|
||||||
|
stage_name,
|
||||||
|
repeated_stages=repeated_stages,
|
||||||
|
target_num_inference_steps=target_num_inference_steps,
|
||||||
|
)
|
||||||
|
step_durations = representative.step_duration_ms_by_stage.get(stage_name, ())
|
||||||
|
if not step_durations and len(repeated_stages) == 1:
|
||||||
|
step_durations = representative.step_duration_ms
|
||||||
|
if stage_name in repeated_stages and step_durations:
|
||||||
|
steady_steps = (
|
||||||
|
step_durations[1:] if len(step_durations) > 1 else step_durations
|
||||||
|
)
|
||||||
|
non_step_ms = max(0.0, duration_ms - sum(step_durations))
|
||||||
|
target_duration_ms = non_step_ms + (
|
||||||
|
statistics.median(steady_steps) * target_iterations
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
target_duration_ms = duration_ms * target_iterations / measured_iterations
|
||||||
|
stage_duration_ns[stage_name] = max(0, int(target_duration_ms * 1_000_000))
|
||||||
|
|
||||||
|
measured_stage_ms = sum(representative.stage_duration_ms.values())
|
||||||
|
untracked_ms = max(0.0, representative.total_duration_ms - measured_stage_ms)
|
||||||
|
request_duration_ns = sum(stage_duration_ns.values()) + int(
|
||||||
|
untracked_ms * 1_000_000
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
request_duration_ns,
|
||||||
|
stage_duration_ns,
|
||||||
|
{
|
||||||
|
component_name: tuple(sorted(stage_names))
|
||||||
|
for component_name, stage_names in component_stages.items()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_default_workload_peak_bytes(
|
||||||
|
*,
|
||||||
|
records: Iterable[WarmupMemoryRecord],
|
||||||
|
target_units: int | None,
|
||||||
|
constant_weight_bytes: int = 0,
|
||||||
|
) -> int | None:
|
||||||
|
"""Extrapolate live warmup memory to the default workload.
|
||||||
|
|
||||||
|
Real measurements use allocated bytes. Cached allocator blocks are
|
||||||
|
reclaimable and are covered by the explicit VRAM reserve; treating them as
|
||||||
|
live memory would charge the same storage again when adding resident
|
||||||
|
weights.
|
||||||
|
|
||||||
|
Preference order:
|
||||||
|
1. A measurement at or above the target workload bounds the peak directly.
|
||||||
|
2. Two distinct measured workload sizes fit ``peak = constant + slope *
|
||||||
|
units``; only the fitted linear part is extrapolated (and padded).
|
||||||
|
Under offload the pre-forward baseline is nearly empty, so a
|
||||||
|
single-point split cannot separate constant costs (streamed weights,
|
||||||
|
attention workspace, tiled VAE decode) from workload-linear
|
||||||
|
activations -- measured on Wan2.1-14B, single-point scaling
|
||||||
|
overestimated a ~30 GiB peak as ~183 GiB.
|
||||||
|
3. One usable size: scale everything above the pre-forward allocated
|
||||||
|
baseline (conservative; may block adjustment but never over-allocates).
|
||||||
|
|
||||||
|
Returns None when the estimate cannot be trusted: no successful records,
|
||||||
|
the target workload is unknown (an unknown target would silently equate the
|
||||||
|
area/frame-capped warmup peak with the real serving peak), or a probe at or
|
||||||
|
below the target ran out of memory. That last case is not missing data but
|
||||||
|
a measurement: the card could not hold the target as it is already
|
||||||
|
configured, so making more weights resident can only make it worse. A probe
|
||||||
|
that failed strictly above the target says nothing about the target and is
|
||||||
|
dropped instead.
|
||||||
|
"""
|
||||||
|
records = list(records)
|
||||||
|
if target_units is None:
|
||||||
|
return None
|
||||||
|
failed_units = [
|
||||||
|
record.workload_units() for record in records if not record.succeeded
|
||||||
|
]
|
||||||
|
if any(units <= target_units for units in failed_units):
|
||||||
|
return None
|
||||||
|
records = [record for record in records if record.succeeded]
|
||||||
|
if not records:
|
||||||
|
return None
|
||||||
|
|
||||||
|
peak_by_units: dict[int, int] = {}
|
||||||
|
for record in records:
|
||||||
|
units = record.workload_units()
|
||||||
|
peak = record.peak_allocated_bytes
|
||||||
|
peak_by_units[units] = max(peak_by_units.get(units, 0), peak)
|
||||||
|
|
||||||
|
covering_peaks = [
|
||||||
|
peak for units, peak in peak_by_units.items() if units >= target_units
|
||||||
|
]
|
||||||
|
if covering_peaks:
|
||||||
|
return max(covering_peaks)
|
||||||
|
|
||||||
|
if len(peak_by_units) >= 2:
|
||||||
|
# fit on the two largest sizes: closest to the target, best local slope
|
||||||
|
(large_units, large_peak), (small_units, small_peak) = sorted(
|
||||||
|
peak_by_units.items(), reverse=True
|
||||||
|
)[:2]
|
||||||
|
slope = (large_peak - small_peak) / (large_units - small_units)
|
||||||
|
if slope >= 0:
|
||||||
|
constant = max(
|
||||||
|
large_peak - slope * large_units,
|
||||||
|
min(constant_weight_bytes, large_peak),
|
||||||
|
)
|
||||||
|
return int(
|
||||||
|
constant + slope * target_units * ACTIVATION_EXTRAPOLATION_MARGIN
|
||||||
|
)
|
||||||
|
# negative slope is measurement noise; fall through to the
|
||||||
|
# conservative single-point estimate
|
||||||
|
|
||||||
|
estimates = []
|
||||||
|
for record in records:
|
||||||
|
peak = record.peak_allocated_bytes
|
||||||
|
baseline = min(
|
||||||
|
max(record.baseline_allocated_bytes, constant_weight_bytes), peak
|
||||||
|
)
|
||||||
|
activation = peak - baseline
|
||||||
|
ratio = target_units / record.workload_units()
|
||||||
|
estimates.append(
|
||||||
|
baseline + int(activation * ratio * ACTIVATION_EXTRAPOLATION_MARGIN)
|
||||||
|
)
|
||||||
|
return max(estimates)
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_workload_phase_peaks(
|
||||||
|
*,
|
||||||
|
records: Iterable[WarmupMemoryRecord],
|
||||||
|
target_units: int | None,
|
||||||
|
component_weight_bytes: Mapping[str, int],
|
||||||
|
) -> tuple[
|
||||||
|
dict[str, int],
|
||||||
|
dict[str, tuple[str, ...]],
|
||||||
|
dict[str, tuple[str, ...]],
|
||||||
|
dict[str, tuple[str, ...]],
|
||||||
|
]:
|
||||||
|
"""Estimate each measured execution phase at the target workload.
|
||||||
|
|
||||||
|
A component already active in a phase is part of that phase's measured
|
||||||
|
peak. Keeping a component resident therefore adds no weight bytes to its
|
||||||
|
own component-offload phase, while it adds the full footprint to phases
|
||||||
|
where the component was absent. Measurements with different active layouts
|
||||||
|
remain separate constraints; combining one layout's peak with another
|
||||||
|
layout's component set would describe a state that never occurred.
|
||||||
|
"""
|
||||||
|
successful = [record for record in records if record.succeeded]
|
||||||
|
if target_units is not None:
|
||||||
|
covering = [
|
||||||
|
record for record in successful if record.workload_units() >= target_units
|
||||||
|
]
|
||||||
|
if covering:
|
||||||
|
successful = covering
|
||||||
|
|
||||||
|
grouped: dict[
|
||||||
|
tuple[str, tuple[str, ...], tuple[str, ...], tuple[str, ...]],
|
||||||
|
list[WarmupMemoryRecord],
|
||||||
|
] = {}
|
||||||
|
for record in successful:
|
||||||
|
used_by_phase = record.phase_used_components or record.phase_active_components
|
||||||
|
for phase_name in record.phase_peak_allocated_bytes:
|
||||||
|
active = tuple(sorted(record.phase_active_components.get(phase_name, ())))
|
||||||
|
used = tuple(sorted(used_by_phase.get(phase_name, ())))
|
||||||
|
full_weight_transitions = tuple(
|
||||||
|
sorted(
|
||||||
|
record.phase_full_weight_transition_components.get(phase_name, ())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
grouped.setdefault(
|
||||||
|
(phase_name, active, used, full_weight_transitions), []
|
||||||
|
).append(record)
|
||||||
|
|
||||||
|
layouts_per_phase: dict[str, int] = {}
|
||||||
|
for phase_name, _, _, _ in grouped:
|
||||||
|
layouts_per_phase[phase_name] = layouts_per_phase.get(phase_name, 0) + 1
|
||||||
|
|
||||||
|
estimated_peaks: dict[str, int] = {}
|
||||||
|
active_components: dict[str, tuple[str, ...]] = {}
|
||||||
|
used_components: dict[str, tuple[str, ...]] = {}
|
||||||
|
full_weight_transition_components: dict[str, tuple[str, ...]] = {}
|
||||||
|
layout_indices: dict[str, int] = {}
|
||||||
|
for (
|
||||||
|
phase_name,
|
||||||
|
active,
|
||||||
|
used,
|
||||||
|
full_weight_transitions,
|
||||||
|
), phase_records in sorted(grouped.items()):
|
||||||
|
output_name = phase_name
|
||||||
|
if layouts_per_phase[phase_name] > 1:
|
||||||
|
index = layout_indices.get(phase_name, 0)
|
||||||
|
layout_indices[phase_name] = index + 1
|
||||||
|
output_name = f"{phase_name}:layout:{index}"
|
||||||
|
weight_floor = sum(component_weight_bytes.get(name, 0) for name in active)
|
||||||
|
phase_measurements = [
|
||||||
|
WarmupMemoryRecord(
|
||||||
|
width=record.width,
|
||||||
|
height=record.height,
|
||||||
|
num_frames=record.num_frames,
|
||||||
|
baseline_allocated_bytes=min(
|
||||||
|
record.baseline_allocated_bytes,
|
||||||
|
record.phase_peak_allocated_bytes[phase_name],
|
||||||
|
),
|
||||||
|
peak_allocated_bytes=record.phase_peak_allocated_bytes[phase_name],
|
||||||
|
succeeded=True,
|
||||||
|
)
|
||||||
|
for record in phase_records
|
||||||
|
]
|
||||||
|
estimate = estimate_default_workload_peak_bytes(
|
||||||
|
records=phase_measurements,
|
||||||
|
target_units=target_units,
|
||||||
|
constant_weight_bytes=weight_floor,
|
||||||
|
)
|
||||||
|
if estimate is None:
|
||||||
|
continue
|
||||||
|
estimated_peaks[output_name] = estimate
|
||||||
|
active_components[output_name] = active
|
||||||
|
used_components[output_name] = used
|
||||||
|
full_weight_transition_components[output_name] = full_weight_transitions
|
||||||
|
return (
|
||||||
|
estimated_peaks,
|
||||||
|
active_components,
|
||||||
|
used_components,
|
||||||
|
full_weight_transition_components,
|
||||||
|
)
|
||||||
+247
-6
@@ -1,4 +1,4 @@
|
|||||||
from collections.abc import Iterator
|
from collections.abc import Iterable, Iterator
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Mapping, MutableMapping, Protocol, Sequence
|
from typing import Mapping, MutableMapping, Protocol, Sequence
|
||||||
@@ -58,6 +58,14 @@ class ResidencyState:
|
|||||||
batch_is_warmup: bool = False
|
batch_is_warmup: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class WarmupPhasePeak:
|
||||||
|
active_components: tuple[str, ...]
|
||||||
|
allocated_bytes: int
|
||||||
|
used_components: tuple[str, ...] = ()
|
||||||
|
full_weight_transition_components: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
class ResidencyBatch(Protocol):
|
class ResidencyBatch(Protocol):
|
||||||
is_warmup: bool
|
is_warmup: bool
|
||||||
|
|
||||||
@@ -129,6 +137,13 @@ class ComponentResidencyManager:
|
|||||||
] = {}
|
] = {}
|
||||||
self._uses_seen: dict[str, ComponentUse] = {}
|
self._uses_seen: dict[str, ComponentUse] = {}
|
||||||
self._modules_seen: dict[str, nn.Module] = {}
|
self._modules_seen: dict[str, nn.Module] = {}
|
||||||
|
self._track_warmup_memory = False
|
||||||
|
self._warmup_phase_key: str | None = None
|
||||||
|
self._warmup_phase_components: tuple[str, ...] = ()
|
||||||
|
self._warmup_phase_used_components: tuple[str, ...] = ()
|
||||||
|
self._warmup_phase_full_weight_transition_components: tuple[str, ...] = ()
|
||||||
|
self._warmup_phase_peaks: dict[str, WarmupPhasePeak] = {}
|
||||||
|
self._completed_warmup_phase_peaks: dict[str, WarmupPhasePeak] = {}
|
||||||
|
|
||||||
def refresh_pipeline(self, pipeline: ComponentResidencyPipeline) -> None:
|
def refresh_pipeline(self, pipeline: ComponentResidencyPipeline) -> None:
|
||||||
custom_strategies = dict(pipeline.component_residency_strategies)
|
custom_strategies = dict(pipeline.component_residency_strategies)
|
||||||
@@ -178,6 +193,24 @@ class ComponentResidencyManager:
|
|||||||
self._ordered_uses = tuple(
|
self._ordered_uses = tuple(
|
||||||
use for uses in self._stage_uses_by_index for use in uses
|
use for uses in self._stage_uses_by_index for use in uses
|
||||||
)
|
)
|
||||||
|
self._track_warmup_memory = (
|
||||||
|
self.state.batch_is_warmup
|
||||||
|
and self.server_args.pipeline_config.supports_auto_residency
|
||||||
|
and current_platform.is_cuda()
|
||||||
|
and torch.get_device_module().is_available()
|
||||||
|
)
|
||||||
|
self._warmup_phase_key = None
|
||||||
|
self._warmup_phase_components = ()
|
||||||
|
self._warmup_phase_used_components = ()
|
||||||
|
self._warmup_phase_full_weight_transition_components = ()
|
||||||
|
self._warmup_phase_peaks = {}
|
||||||
|
self._completed_warmup_phase_peaks = {}
|
||||||
|
if self._track_warmup_memory:
|
||||||
|
# GPUWorker reset the request peak before entering the pipeline.
|
||||||
|
# Start the first interval without another reset so preprocessing
|
||||||
|
# before stage 0 remains part of the placement constraints.
|
||||||
|
self._warmup_phase_key = "request:before-stage"
|
||||||
|
self._warmup_phase_components = self._warmup_active_components()
|
||||||
self._validate_explicit_nonresident_components()
|
self._validate_explicit_nonresident_components()
|
||||||
|
|
||||||
def _validate_explicit_nonresident_components(self) -> None:
|
def _validate_explicit_nonresident_components(self) -> None:
|
||||||
@@ -224,6 +257,49 @@ class ComponentResidencyManager:
|
|||||||
self.state.stage_index = stage_index
|
self.state.stage_index = stage_index
|
||||||
self.state.stage_name = self.stage_name(stage)
|
self.state.stage_name = self.stage_name(stage)
|
||||||
self.state.next_stage_name = self._next_stage_name(stage_index)
|
self.state.next_stage_name = self._next_stage_name(stage_index)
|
||||||
|
if self._track_warmup_memory:
|
||||||
|
self._begin_warmup_phase(
|
||||||
|
key=f"{stage_index}:{self.state.stage_name}:setup",
|
||||||
|
components=self._warmup_active_components(),
|
||||||
|
used_components=(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def full_weight_transition(self, component_names: Iterable[str]) -> Iterator[None]:
|
||||||
|
"""Measure request logic that temporarily materializes complete weights."""
|
||||||
|
names = tuple(sorted(set(component_names)))
|
||||||
|
if not self._track_warmup_memory or not names:
|
||||||
|
yield
|
||||||
|
return
|
||||||
|
previous_phase = (
|
||||||
|
self._warmup_phase_key,
|
||||||
|
self._warmup_phase_components,
|
||||||
|
self._warmup_phase_used_components,
|
||||||
|
self._warmup_phase_full_weight_transition_components,
|
||||||
|
)
|
||||||
|
self._begin_warmup_phase(
|
||||||
|
key=(
|
||||||
|
f"{self.state.stage_index}:{self.state.stage_name}:"
|
||||||
|
f"full-weight-transition:{','.join(names)}"
|
||||||
|
),
|
||||||
|
components=self._warmup_active_components(),
|
||||||
|
used_components=(),
|
||||||
|
full_weight_transition_components=names,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
previous_key, components, used_components, transitions = previous_phase
|
||||||
|
if previous_key is None:
|
||||||
|
self._record_warmup_phase_peak()
|
||||||
|
self._warmup_phase_key = None
|
||||||
|
else:
|
||||||
|
self._begin_warmup_phase(
|
||||||
|
key=previous_key,
|
||||||
|
components=components,
|
||||||
|
used_components=used_components,
|
||||||
|
full_weight_transition_components=transitions,
|
||||||
|
)
|
||||||
|
|
||||||
def begin_stage(self) -> None:
|
def begin_stage(self) -> None:
|
||||||
"""Prepare a stage that declares one uninterrupted component use."""
|
"""Prepare a stage that declares one uninterrupted component use."""
|
||||||
@@ -233,6 +309,7 @@ class ComponentResidencyManager:
|
|||||||
|
|
||||||
def end_stage(self) -> None:
|
def end_stage(self) -> None:
|
||||||
"""Close the component interval owned by the current stage."""
|
"""Close the component interval owned by the current stage."""
|
||||||
|
self._record_warmup_phase_peak()
|
||||||
if self._active_use is None:
|
if self._active_use is None:
|
||||||
return
|
return
|
||||||
if self._active_use.stage_name != self.state.stage_name:
|
if self._active_use.stage_name != self.state.stage_name:
|
||||||
@@ -263,7 +340,13 @@ class ComponentResidencyManager:
|
|||||||
self._active_use_module is not None
|
self._active_use_module is not None
|
||||||
and active_module is not self._active_use_module
|
and active_module is not self._active_use_module
|
||||||
)
|
)
|
||||||
|
requires_prepare = active_module is not None and (
|
||||||
|
self._active_use_module is None
|
||||||
|
or module_changed
|
||||||
|
or use.target_dtype != previous_use.target_dtype
|
||||||
|
)
|
||||||
if module_changed:
|
if module_changed:
|
||||||
|
self._begin_warmup_transition(previous_use, None)
|
||||||
self._disable_active_nvtx()
|
self._disable_active_nvtx()
|
||||||
self._finish_use(
|
self._finish_use(
|
||||||
previous_use,
|
previous_use,
|
||||||
@@ -271,18 +354,19 @@ class ComponentResidencyManager:
|
|||||||
keep_on_warmup=False,
|
keep_on_warmup=False,
|
||||||
force=True,
|
force=True,
|
||||||
)
|
)
|
||||||
if active_module is not None and (
|
self._begin_warmup_transition(None, use)
|
||||||
self._active_use_module is None
|
elif requires_prepare:
|
||||||
or module_changed
|
self._begin_warmup_transition(previous_use, use)
|
||||||
or use.target_dtype != previous_use.target_dtype
|
if requires_prepare:
|
||||||
):
|
|
||||||
active_module = self._prepare_forward_use(use, module=active_module)
|
active_module = self._prepare_forward_use(use, module=active_module)
|
||||||
|
self._begin_warmup_use(use)
|
||||||
self._active_use = use
|
self._active_use = use
|
||||||
self._active_use_module = active_module
|
self._active_use_module = active_module
|
||||||
self.state.current_use = use
|
self.state.current_use = use
|
||||||
self._enable_nvtx_for_use(use, active_module)
|
self._enable_nvtx_for_use(use, active_module)
|
||||||
return
|
return
|
||||||
if self._active_use is not None:
|
if self._active_use is not None:
|
||||||
|
self._begin_warmup_transition(self._active_use, None)
|
||||||
self._disable_active_nvtx()
|
self._disable_active_nvtx()
|
||||||
self._finish_use(
|
self._finish_use(
|
||||||
self._active_use,
|
self._active_use,
|
||||||
@@ -292,8 +376,10 @@ class ComponentResidencyManager:
|
|||||||
self._active_use = None
|
self._active_use = None
|
||||||
self._active_use_module = None
|
self._active_use_module = None
|
||||||
self.state.current_use = None
|
self.state.current_use = None
|
||||||
|
self._begin_warmup_transition(None, use)
|
||||||
self._mark_current_use(use)
|
self._mark_current_use(use)
|
||||||
module = self._prepare_forward_use(use, module=module)
|
module = self._prepare_forward_use(use, module=module)
|
||||||
|
self._begin_warmup_use(use)
|
||||||
self._active_use = use
|
self._active_use = use
|
||||||
self._active_use_module = module
|
self._active_use_module = module
|
||||||
self._enable_nvtx_for_use(use, module)
|
self._enable_nvtx_for_use(use, module)
|
||||||
@@ -303,6 +389,7 @@ class ComponentResidencyManager:
|
|||||||
"""End one sequential component use interval."""
|
"""End one sequential component use interval."""
|
||||||
if self._active_use is None or not self._same_use(self._active_use, use):
|
if self._active_use is None or not self._same_use(self._active_use, use):
|
||||||
return
|
return
|
||||||
|
self._begin_warmup_transition(self._active_use, None)
|
||||||
self._disable_active_nvtx()
|
self._disable_active_nvtx()
|
||||||
self._finish_use(
|
self._finish_use(
|
||||||
self._active_use,
|
self._active_use,
|
||||||
@@ -316,6 +403,7 @@ class ComponentResidencyManager:
|
|||||||
self._active_use = None
|
self._active_use = None
|
||||||
self._active_use_module = None
|
self._active_use_module = None
|
||||||
self.state.current_use = None
|
self.state.current_use = None
|
||||||
|
self._begin_warmup_between_uses()
|
||||||
self._prefetch_next_memory_intensive_use()
|
self._prefetch_next_memory_intensive_use()
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
@@ -378,6 +466,7 @@ class ComponentResidencyManager:
|
|||||||
if self._active_use is None:
|
if self._active_use is None:
|
||||||
return
|
return
|
||||||
active_use = self._active_use
|
active_use = self._active_use
|
||||||
|
self._begin_warmup_transition(active_use, None)
|
||||||
self._disable_active_nvtx()
|
self._disable_active_nvtx()
|
||||||
self._finish_use(
|
self._finish_use(
|
||||||
active_use,
|
active_use,
|
||||||
@@ -387,6 +476,7 @@ class ComponentResidencyManager:
|
|||||||
self._active_use = None
|
self._active_use = None
|
||||||
self._active_use_module = None
|
self._active_use_module = None
|
||||||
self.state.current_use = None
|
self.state.current_use = None
|
||||||
|
self._begin_warmup_between_uses()
|
||||||
if prefetch_next:
|
if prefetch_next:
|
||||||
self._prefetch_next_memory_intensive_use()
|
self._prefetch_next_memory_intensive_use()
|
||||||
|
|
||||||
@@ -483,8 +573,11 @@ class ComponentResidencyManager:
|
|||||||
|
|
||||||
self._uses_seen[use.component_name] = use
|
self._uses_seen[use.component_name] = use
|
||||||
self._modules_seen[use.component_name] = module
|
self._modules_seen[use.component_name] = module
|
||||||
|
self._begin_warmup_prefetch(use)
|
||||||
if strategy.prefetch_for_use(module, use, self.state):
|
if strategy.prefetch_for_use(module, use, self.state):
|
||||||
self._prefetched_use_keys.add(self._use_key(use))
|
self._prefetched_use_keys.add(self._use_key(use))
|
||||||
|
else:
|
||||||
|
self._begin_warmup_between_uses()
|
||||||
|
|
||||||
def _finish_use(
|
def _finish_use(
|
||||||
self,
|
self,
|
||||||
@@ -534,11 +627,154 @@ class ComponentResidencyManager:
|
|||||||
not self._is_single_dit_component(component_name) or keep_single_dit
|
not self._is_single_dit_component(component_name) or keep_single_dit
|
||||||
)
|
)
|
||||||
strategy = self.strategy_for(component_name, module)
|
strategy = self.strategy_for(component_name, module)
|
||||||
|
if self._track_warmup_memory:
|
||||||
|
will_prepare = self.state.batch_is_warmup and preferred
|
||||||
|
self._begin_warmup_phase(
|
||||||
|
key=f"request:cleanup:{component_name}",
|
||||||
|
components=self._warmup_active_components(
|
||||||
|
(use,) if will_prepare else ()
|
||||||
|
),
|
||||||
|
used_components=(component_name,) if will_prepare else (),
|
||||||
|
)
|
||||||
was_on_supported_device = self._module_on_supported_device(module)
|
was_on_supported_device = self._module_on_supported_device(module)
|
||||||
strategy.finish_request(module, use, self.state, preferred=preferred)
|
strategy.finish_request(module, use, self.state, preferred=preferred)
|
||||||
self._empty_cache_after_large_release(
|
self._empty_cache_after_large_release(
|
||||||
use, strategy, module, was_on_supported_device
|
use, strategy, module, was_on_supported_device
|
||||||
)
|
)
|
||||||
|
if self._track_warmup_memory:
|
||||||
|
self._record_warmup_phase_peak()
|
||||||
|
self._warmup_phase_peaks["idle"] = WarmupPhasePeak(
|
||||||
|
active_components=self._warmup_active_components(),
|
||||||
|
allocated_bytes=int(torch.get_device_module().memory_allocated()),
|
||||||
|
used_components=(),
|
||||||
|
)
|
||||||
|
self._completed_warmup_phase_peaks = dict(self._warmup_phase_peaks)
|
||||||
|
self._track_warmup_memory = False
|
||||||
|
|
||||||
|
def _begin_warmup_phase(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
key: str,
|
||||||
|
components: tuple[str, ...],
|
||||||
|
used_components: tuple[str, ...],
|
||||||
|
full_weight_transition_components: tuple[str, ...] = (),
|
||||||
|
) -> None:
|
||||||
|
if not self._track_warmup_memory:
|
||||||
|
return
|
||||||
|
self._record_warmup_phase_peak()
|
||||||
|
self._warmup_phase_key = key
|
||||||
|
self._warmup_phase_components = tuple(sorted(set(components)))
|
||||||
|
self._warmup_phase_used_components = tuple(sorted(set(used_components)))
|
||||||
|
self._warmup_phase_full_weight_transition_components = tuple(
|
||||||
|
sorted(set(full_weight_transition_components))
|
||||||
|
)
|
||||||
|
torch.get_device_module().reset_peak_memory_stats()
|
||||||
|
|
||||||
|
def _begin_warmup_transition(
|
||||||
|
self, previous: ComponentUse | None, upcoming: ComponentUse | None
|
||||||
|
) -> None:
|
||||||
|
if not self._track_warmup_memory:
|
||||||
|
return
|
||||||
|
previous_name = previous.component_name if previous is not None else "idle"
|
||||||
|
upcoming_name = upcoming.component_name if upcoming is not None else "idle"
|
||||||
|
self._begin_warmup_phase(
|
||||||
|
key=(
|
||||||
|
f"{self.state.stage_index}:{self.state.stage_name}:transition:"
|
||||||
|
f"{previous_name}->{upcoming_name}"
|
||||||
|
),
|
||||||
|
components=self._warmup_active_components(
|
||||||
|
tuple(use for use in (previous, upcoming) if use is not None)
|
||||||
|
),
|
||||||
|
used_components=tuple(
|
||||||
|
use.component_name for use in (previous, upcoming) if use is not None
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _begin_warmup_use(self, use: ComponentUse) -> None:
|
||||||
|
phase = use.phase or use.component_name
|
||||||
|
self._begin_warmup_phase(
|
||||||
|
key=f"{self.state.stage_index}:{self.state.stage_name}:use:{phase}",
|
||||||
|
components=self._warmup_active_components((use,)),
|
||||||
|
used_components=(use.component_name,),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _begin_warmup_between_uses(self) -> None:
|
||||||
|
self._begin_warmup_phase(
|
||||||
|
key=f"{self.state.stage_index}:{self.state.stage_name}:between",
|
||||||
|
components=self._warmup_active_components(),
|
||||||
|
used_components=(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _begin_warmup_prefetch(self, use: ComponentUse) -> None:
|
||||||
|
phase = use.phase or use.component_name
|
||||||
|
self._begin_warmup_phase(
|
||||||
|
key=f"{self.state.stage_index}:{self.state.stage_name}:prefetch:{phase}",
|
||||||
|
components=self._warmup_active_components((use,)),
|
||||||
|
used_components=(use.component_name,),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _warmup_active_components(
|
||||||
|
self, active_uses: Sequence[ComponentUse] = ()
|
||||||
|
) -> tuple[str, ...]:
|
||||||
|
components = {use.component_name for use in active_uses}
|
||||||
|
for component_name, module in self.pipeline.modules.items():
|
||||||
|
if not isinstance(module, nn.Module):
|
||||||
|
continue
|
||||||
|
if is_layerwise_offloaded_module(module):
|
||||||
|
continue
|
||||||
|
if self._module_on_supported_device(module):
|
||||||
|
components.add(component_name)
|
||||||
|
return tuple(sorted(components))
|
||||||
|
|
||||||
|
def _record_warmup_phase_peak(self) -> None:
|
||||||
|
if not self._track_warmup_memory or self._warmup_phase_key is None:
|
||||||
|
return
|
||||||
|
peak = WarmupPhasePeak(
|
||||||
|
active_components=self._warmup_phase_components,
|
||||||
|
allocated_bytes=int(torch.get_device_module().max_memory_allocated()),
|
||||||
|
used_components=self._warmup_phase_used_components,
|
||||||
|
full_weight_transition_components=(
|
||||||
|
self._warmup_phase_full_weight_transition_components
|
||||||
|
),
|
||||||
|
)
|
||||||
|
previous = self._warmup_phase_peaks.get(self._warmup_phase_key)
|
||||||
|
if previous is None:
|
||||||
|
self._warmup_phase_peaks[self._warmup_phase_key] = peak
|
||||||
|
else:
|
||||||
|
self._warmup_phase_peaks[self._warmup_phase_key] = WarmupPhasePeak(
|
||||||
|
active_components=tuple(
|
||||||
|
sorted(
|
||||||
|
set(previous.active_components) & set(peak.active_components)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
allocated_bytes=max(previous.allocated_bytes, peak.allocated_bytes),
|
||||||
|
used_components=tuple(
|
||||||
|
sorted(set(previous.used_components) & set(peak.used_components))
|
||||||
|
),
|
||||||
|
full_weight_transition_components=tuple(
|
||||||
|
sorted(
|
||||||
|
set(previous.full_weight_transition_components)
|
||||||
|
& set(peak.full_weight_transition_components)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def take_warmup_phase_peaks(
|
||||||
|
self,
|
||||||
|
) -> dict[str, WarmupPhasePeak]:
|
||||||
|
"""Return and clear the most recently completed warmup phase peaks."""
|
||||||
|
peaks = self._completed_warmup_phase_peaks
|
||||||
|
self._completed_warmup_phase_peaks = {}
|
||||||
|
return peaks
|
||||||
|
|
||||||
|
def current_device_components(self) -> tuple[str, ...]:
|
||||||
|
"""Components whose complete module is currently on the device.
|
||||||
|
|
||||||
|
Dormant layerwise-managed modules are excluded because only their
|
||||||
|
resident window is present. Active layerwise uses are attributed by
|
||||||
|
the managed phase timeline instead.
|
||||||
|
"""
|
||||||
|
return self._warmup_active_components()
|
||||||
|
|
||||||
def stage_name(self, stage: ComponentResidencyStage) -> str:
|
def stage_name(self, stage: ComponentResidencyStage) -> str:
|
||||||
return self._stage_names_by_id.get(id(stage), stage.__class__.__name__)
|
return self._stage_names_by_id.get(id(stage), stage.__class__.__name__)
|
||||||
@@ -703,6 +939,11 @@ class ComponentResidencyManager:
|
|||||||
_GLOBAL_COMPONENT_RESIDENCY_MANAGER: ComponentResidencyManager | None = None
|
_GLOBAL_COMPONENT_RESIDENCY_MANAGER: ComponentResidencyManager | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def peek_global_component_residency_manager() -> ComponentResidencyManager | None:
|
||||||
|
"""Return the process-global manager without creating one."""
|
||||||
|
return _GLOBAL_COMPONENT_RESIDENCY_MANAGER
|
||||||
|
|
||||||
|
|
||||||
def get_global_component_residency_manager(
|
def get_global_component_residency_manager(
|
||||||
pipeline: ComponentResidencyPipeline,
|
pipeline: ComponentResidencyPipeline,
|
||||||
server_args: ServerArgs,
|
server_args: ServerArgs,
|
||||||
|
|||||||
+105
-1
@@ -5,7 +5,15 @@ import threading
|
|||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
from time import perf_counter
|
from time import perf_counter
|
||||||
from typing import Any, Dict, List, Optional, Set, Tuple
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
)
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from torch.distributed.tensor import DTensor
|
from torch.distributed.tensor import DTensor
|
||||||
@@ -2205,3 +2213,99 @@ def configure_layerwise_offload_modules(
|
|||||||
elif warn_missing:
|
elif warn_missing:
|
||||||
logger.debug("No selected pipeline component enabled layerwise offload")
|
logger.debug("No selected pipeline component enabled layerwise offload")
|
||||||
return configured_component_names
|
return configured_component_names
|
||||||
|
|
||||||
|
|
||||||
|
class LayerwiseUsageTracker:
|
||||||
|
"""Temporary per-layer call counters for one calibration request.
|
||||||
|
|
||||||
|
Managers cannot provide this information when layerwise offload has not
|
||||||
|
been configured yet or was disabled by a resident placement. Observe the
|
||||||
|
declared layer groups directly and remove every hook before returning, so
|
||||||
|
ordinary serving requests pay no counter or hook-dispatch cost.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
modules: Mapping[str, object],
|
||||||
|
*,
|
||||||
|
stage_name_provider: Callable[[], str | None] | None = None,
|
||||||
|
) -> None:
|
||||||
|
self._handles: list[Any] = []
|
||||||
|
self._counts: dict[str, dict[str, list[int]]] = {}
|
||||||
|
self._counts_by_stage: dict[str, dict[str, dict[str, list[int]]]] = {}
|
||||||
|
self._stage_name_provider = stage_name_provider
|
||||||
|
for component_name, module in modules.items():
|
||||||
|
if not isinstance(module, LayerwiseOffloadableModuleMixin):
|
||||||
|
continue
|
||||||
|
named_modules = dict(module.named_modules())
|
||||||
|
component_counts: dict[str, list[int]] = {}
|
||||||
|
for layer_name in module.layer_names:
|
||||||
|
layers = named_modules.get(layer_name)
|
||||||
|
if not isinstance(layers, (torch.nn.ModuleList, torch.nn.Sequential)):
|
||||||
|
continue
|
||||||
|
counts = [0] * len(layers)
|
||||||
|
if not counts:
|
||||||
|
continue
|
||||||
|
component_counts[layer_name] = counts
|
||||||
|
for layer_index, layer in enumerate(layers):
|
||||||
|
|
||||||
|
def record_use(
|
||||||
|
_module,
|
||||||
|
_inputs,
|
||||||
|
*,
|
||||||
|
target_counts=counts,
|
||||||
|
target_index=layer_index,
|
||||||
|
target_component_name=component_name,
|
||||||
|
target_layer_name=layer_name,
|
||||||
|
target_layer_count=len(layers),
|
||||||
|
) -> None:
|
||||||
|
target_counts[target_index] += 1
|
||||||
|
if self._stage_name_provider is None:
|
||||||
|
return
|
||||||
|
stage_name = self._stage_name_provider()
|
||||||
|
if stage_name is None:
|
||||||
|
return
|
||||||
|
stage_counts = self._counts_by_stage.setdefault(stage_name, {})
|
||||||
|
component_stage_counts = stage_counts.setdefault(
|
||||||
|
target_component_name, {}
|
||||||
|
)
|
||||||
|
layer_stage_counts = component_stage_counts.setdefault(
|
||||||
|
target_layer_name, [0] * target_layer_count
|
||||||
|
)
|
||||||
|
layer_stage_counts[target_index] += 1
|
||||||
|
|
||||||
|
self._handles.append(layer.register_forward_pre_hook(record_use))
|
||||||
|
if component_counts:
|
||||||
|
self._counts[component_name] = component_counts
|
||||||
|
|
||||||
|
def finish(self) -> dict[str, dict[str, tuple[int, ...]]]:
|
||||||
|
counts, _ = self.finish_with_stages()
|
||||||
|
return counts
|
||||||
|
|
||||||
|
def finish_with_stages(
|
||||||
|
self,
|
||||||
|
) -> tuple[
|
||||||
|
dict[str, dict[str, tuple[int, ...]]],
|
||||||
|
dict[str, dict[str, dict[str, tuple[int, ...]]]],
|
||||||
|
]:
|
||||||
|
for handle in self._handles:
|
||||||
|
handle.remove()
|
||||||
|
self._handles.clear()
|
||||||
|
counts = {
|
||||||
|
component_name: {
|
||||||
|
layer_name: tuple(counts)
|
||||||
|
for layer_name, counts in component_counts.items()
|
||||||
|
}
|
||||||
|
for component_name, component_counts in self._counts.items()
|
||||||
|
}
|
||||||
|
counts_by_stage = {
|
||||||
|
stage_name: {
|
||||||
|
component_name: {
|
||||||
|
layer_name: tuple(counts)
|
||||||
|
for layer_name, counts in component_counts.items()
|
||||||
|
}
|
||||||
|
for component_name, component_counts in stage_counts.items()
|
||||||
|
}
|
||||||
|
for stage_name, stage_counts in self._counts_by_stage.items()
|
||||||
|
}
|
||||||
|
return counts, counts_by_stage
|
||||||
|
|||||||
@@ -16,15 +16,7 @@ from sglang.multimodal_gen.runtime.disaggregation.roles import RoleType
|
|||||||
from sglang.multimodal_gen.runtime.disaggregation.scheduler_mixin import (
|
from sglang.multimodal_gen.runtime.disaggregation.scheduler_mixin import (
|
||||||
SchedulerDisaggMixin,
|
SchedulerDisaggMixin,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.post_training.io_struct import (
|
from sglang.multimodal_gen.runtime.entrypoints.control_requests import (
|
||||||
GetWeightsChecksumReqInput,
|
|
||||||
ReleaseMemoryOccupationReqInput,
|
|
||||||
ResumeMemoryOccupationReqInput,
|
|
||||||
UpdateWeightFromDiskReqInput,
|
|
||||||
UpdateWeightFromTensorCheckerReqInput,
|
|
||||||
UpdateWeightFromTensorReqInput,
|
|
||||||
)
|
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.utils import (
|
|
||||||
GetDisaggStatsReq,
|
GetDisaggStatsReq,
|
||||||
ListLorasReq,
|
ListLorasReq,
|
||||||
MergeLoraWeightsReq,
|
MergeLoraWeightsReq,
|
||||||
@@ -33,6 +25,14 @@ from sglang.multimodal_gen.runtime.entrypoints.utils import (
|
|||||||
ShutdownReq,
|
ShutdownReq,
|
||||||
UnmergeLoraWeightsReq,
|
UnmergeLoraWeightsReq,
|
||||||
)
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.entrypoints.post_training.io_struct import (
|
||||||
|
GetWeightsChecksumReqInput,
|
||||||
|
ReleaseMemoryOccupationReqInput,
|
||||||
|
ResumeMemoryOccupationReqInput,
|
||||||
|
UpdateWeightFromDiskReqInput,
|
||||||
|
UpdateWeightFromTensorCheckerReqInput,
|
||||||
|
UpdateWeightFromTensorReqInput,
|
||||||
|
)
|
||||||
from sglang.multimodal_gen.runtime.ipc_array import (
|
from sglang.multimodal_gen.runtime.ipc_array import (
|
||||||
is_local_endpoint,
|
is_local_endpoint,
|
||||||
spill_large_arrays_to_file_refs,
|
spill_large_arrays_to_file_refs,
|
||||||
|
|||||||
@@ -341,6 +341,7 @@ class Req:
|
|||||||
self.suppress_logs = True
|
self.suppress_logs = True
|
||||||
self.metrics.suppress_stage_breakdown = True
|
self.metrics.suppress_stage_breakdown = True
|
||||||
self.extra["cache_dit_num_inference_steps"] = self.num_inference_steps
|
self.extra["cache_dit_num_inference_steps"] = self.num_inference_steps
|
||||||
|
self.extra["warmup_target_num_inference_steps"] = self.num_inference_steps
|
||||||
self.num_inference_steps = warmup_steps
|
self.num_inference_steps = warmup_steps
|
||||||
|
|
||||||
def copy_as_warmup(self, warmup_steps: int = 1) -> Req:
|
def copy_as_warmup(self, warmup_steps: int = 1) -> Req:
|
||||||
@@ -348,6 +349,34 @@ class Req:
|
|||||||
req.set_as_warmup(warmup_steps)
|
req.set_as_warmup(warmup_steps)
|
||||||
return req
|
return req
|
||||||
|
|
||||||
|
def record_stage_iterations(
|
||||||
|
self,
|
||||||
|
measured_iterations: int,
|
||||||
|
target_iterations: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Record a stage loop against its full default-request work.
|
||||||
|
|
||||||
|
Most stages declare the count as a formula of the step count
|
||||||
|
(``PipelineStage.default_workload_iterations``) and never call this.
|
||||||
|
It is for loops whose length is only known inside them (chunked or
|
||||||
|
block-wise schedules); ``target_iterations`` defaults to scaling the
|
||||||
|
measured count from the probe's steps to the default workload's.
|
||||||
|
"""
|
||||||
|
if not self.is_warmup or self.metrics is None:
|
||||||
|
return
|
||||||
|
measured = max(1, int(measured_iterations))
|
||||||
|
if target_iterations is None:
|
||||||
|
measured_request_steps = max(1, int(self.num_inference_steps))
|
||||||
|
target_request_steps = int(
|
||||||
|
self.extra.get(
|
||||||
|
"warmup_target_num_inference_steps", measured_request_steps
|
||||||
|
)
|
||||||
|
)
|
||||||
|
target_iterations = (
|
||||||
|
measured * max(1, target_request_steps) + measured_request_steps - 1
|
||||||
|
) // measured_request_steps
|
||||||
|
self.metrics.record_stage_iterations(measured, target_iterations)
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
"""Initialize dependent fields after dataclass initialization."""
|
"""Initialize dependent fields after dataclass initialization."""
|
||||||
if getattr(self.sampling_params, "data_type", None) == DataType.ACTION:
|
if getattr(self.sampling_params, "data_type", None) == DataType.ACTION:
|
||||||
|
|||||||
@@ -55,6 +55,31 @@ class StageVerificationError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def record_default_workload_iterations(stage, batch) -> None:
|
||||||
|
"""Record how often a stage's repeated unit ran, once per warmup request.
|
||||||
|
|
||||||
|
A stage whose loop length is a formula of the request's step count
|
||||||
|
declares it in ``default_workload_iterations``; the same formula at the
|
||||||
|
default workload's step count is the target. A stage that recorded
|
||||||
|
explicitly inside its loop (a count only known there) is left alone.
|
||||||
|
"""
|
||||||
|
metrics = batch.metrics
|
||||||
|
if metrics is None or metrics.active_stage_name is None:
|
||||||
|
return
|
||||||
|
if metrics.active_stage_name in metrics.stage_iterations:
|
||||||
|
return
|
||||||
|
measured = stage.default_workload_iterations(batch, int(batch.num_inference_steps))
|
||||||
|
if measured is None:
|
||||||
|
return
|
||||||
|
target_steps = int(
|
||||||
|
batch.extra.get("warmup_target_num_inference_steps", batch.num_inference_steps)
|
||||||
|
)
|
||||||
|
target = stage.default_workload_iterations(batch, target_steps)
|
||||||
|
metrics.record_stage_iterations(
|
||||||
|
max(1, int(measured)), max(1, int(measured if target is None else target))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PipelineStage(StageDedupMixin, ABC):
|
class PipelineStage(StageDedupMixin, ABC):
|
||||||
"""
|
"""
|
||||||
Abstract base class for all pipeline stages.
|
Abstract base class for all pipeline stages.
|
||||||
@@ -163,6 +188,14 @@ class PipelineStage(StageDedupMixin, ABC):
|
|||||||
def set_profile_stage_name(self, stage_name: str) -> None:
|
def set_profile_stage_name(self, stage_name: str) -> None:
|
||||||
self._profile_stage_name = stage_name
|
self._profile_stage_name = stage_name
|
||||||
|
|
||||||
|
def default_workload_iterations(
|
||||||
|
self, batch: Req, num_inference_steps: int
|
||||||
|
) -> int | None:
|
||||||
|
"""How many times this stage's repeated unit runs for a request with
|
||||||
|
``num_inference_steps`` steps; ``None`` means the stage is not repeated
|
||||||
|
(or records its count itself with ``batch.record_stage_iterations``)."""
|
||||||
|
return None
|
||||||
|
|
||||||
def _component_stage_name(self, stage_name: str | None = None) -> str:
|
def _component_stage_name(self, stage_name: str | None = None) -> str:
|
||||||
return stage_name or self._registered_stage_name or self.__class__.__name__
|
return stage_name or self._registered_stage_name or self.__class__.__name__
|
||||||
|
|
||||||
@@ -383,7 +416,14 @@ class PipelineStage(StageDedupMixin, ABC):
|
|||||||
|
|
||||||
# Execute the actual stage logic with unified profiling.
|
# Execute the actual stage logic with unified profiling.
|
||||||
previous_batch_is_warmup = self._current_batch_is_warmup
|
previous_batch_is_warmup = self._current_batch_is_warmup
|
||||||
|
metrics = batch.metrics
|
||||||
|
warmup_metrics = metrics if batch.is_warmup else None
|
||||||
|
previous_active_stage = (
|
||||||
|
warmup_metrics.active_stage_name if warmup_metrics is not None else None
|
||||||
|
)
|
||||||
self._current_batch_is_warmup = batch.is_warmup
|
self._current_batch_is_warmup = batch.is_warmup
|
||||||
|
if warmup_metrics is not None:
|
||||||
|
warmup_metrics.active_stage_name = self._component_stage_name()
|
||||||
try:
|
try:
|
||||||
with StageProfiler(
|
with StageProfiler(
|
||||||
stage_name,
|
stage_name,
|
||||||
@@ -394,7 +434,11 @@ class PipelineStage(StageDedupMixin, ABC):
|
|||||||
perf_dump_path_provided=batch.perf_dump_path is not None,
|
perf_dump_path_provided=batch.perf_dump_path is not None,
|
||||||
):
|
):
|
||||||
result = self.forward(batch, server_args)
|
result = self.forward(batch, server_args)
|
||||||
|
if warmup_metrics is not None:
|
||||||
|
record_default_workload_iterations(self, batch)
|
||||||
finally:
|
finally:
|
||||||
|
if warmup_metrics is not None:
|
||||||
|
warmup_metrics.active_stage_name = previous_active_stage
|
||||||
self._current_batch_is_warmup = previous_batch_is_warmup
|
self._current_batch_is_warmup = previous_batch_is_warmup
|
||||||
self._current_use_nvtx = False
|
self._current_use_nvtx = False
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,12 @@ class CausalDMDRealtimeCacheContext:
|
|||||||
|
|
||||||
|
|
||||||
class CausalDMDDenoisingStage(DenoisingStage):
|
class CausalDMDDenoisingStage(DenoisingStage):
|
||||||
|
def default_workload_iterations(
|
||||||
|
self, batch: Req, num_inference_steps: int
|
||||||
|
) -> int | None:
|
||||||
|
# blocks x fixed DMD steps, known only once the block sizes are laid out
|
||||||
|
return None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Denoising stage for causal diffusion.
|
Denoising stage for causal diffusion.
|
||||||
"""
|
"""
|
||||||
@@ -218,9 +224,8 @@ class CausalDMDDenoisingStage(DenoisingStage):
|
|||||||
(scheduler.timesteps.cpu(), torch.tensor([0], dtype=torch.float32))
|
(scheduler.timesteps.cpu(), torch.tensor([0], dtype=torch.float32))
|
||||||
)
|
)
|
||||||
timesteps = scheduler_timesteps[1000 - timesteps]
|
timesteps = scheduler_timesteps[1000 - timesteps]
|
||||||
timesteps = timesteps.to(device)
|
|
||||||
logger.info("Using timesteps: %s", timesteps)
|
logger.info("Using timesteps: %s", timesteps)
|
||||||
return timesteps
|
return timesteps.to(device)
|
||||||
|
|
||||||
def _prepare_causal_dmd_image_kwargs(
|
def _prepare_causal_dmd_image_kwargs(
|
||||||
self,
|
self,
|
||||||
@@ -1254,6 +1259,9 @@ class CausalDMDDenoisingStage(DenoisingStage):
|
|||||||
block_sizes = [1] + [self.num_frames_per_block] * num_blocks
|
block_sizes = [1] + [self.num_frames_per_block] * num_blocks
|
||||||
start_index = 0
|
start_index = 0
|
||||||
|
|
||||||
|
total_iterations = len(block_sizes) * len(timesteps)
|
||||||
|
batch.record_stage_iterations(total_iterations, total_iterations)
|
||||||
|
|
||||||
def prepare_context_input(current_latents):
|
def prepare_context_input(current_latents):
|
||||||
return current_latents
|
return current_latents
|
||||||
|
|
||||||
|
|||||||
@@ -334,6 +334,11 @@ class DenoisingStage(PipelineStage, RolloutDenoisingMixin):
|
|||||||
def role_affinity(self):
|
def role_affinity(self):
|
||||||
return RoleType.DENOISER
|
return RoleType.DENOISER
|
||||||
|
|
||||||
|
def default_workload_iterations(
|
||||||
|
self, batch: Req, num_inference_steps: int
|
||||||
|
) -> int | None:
|
||||||
|
return num_inference_steps
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, transformer, scheduler, pipeline=None, transformer_2=None, vae=None
|
self, transformer, scheduler, pipeline=None, transformer_2=None, vae=None
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ logger = init_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class DmdDenoisingStage(DenoisingStage):
|
class DmdDenoisingStage(DenoisingStage):
|
||||||
|
def default_workload_iterations(
|
||||||
|
self, batch: Req, num_inference_steps: int
|
||||||
|
) -> int | None:
|
||||||
|
# a fixed distilled schedule: the same count at any requested step count
|
||||||
|
return len(self.server_args.pipeline_config.dmd_denoising_steps)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Denoising stage for DMD.
|
Denoising stage for DMD.
|
||||||
"""
|
"""
|
||||||
|
|||||||
+5
@@ -1063,6 +1063,11 @@ class Cosmos3TimestepPreparationStage(PipelineStage):
|
|||||||
|
|
||||||
|
|
||||||
class Cosmos3DenoisingStage(PipelineStage, RolloutDenoisingMixin):
|
class Cosmos3DenoisingStage(PipelineStage, RolloutDenoisingMixin):
|
||||||
|
def default_workload_iterations(
|
||||||
|
self, batch: Req, num_inference_steps: int
|
||||||
|
) -> int | None:
|
||||||
|
return num_inference_steps
|
||||||
|
|
||||||
"""Cosmos3 denoise loop, including CFG and the parallelism modes.
|
"""Cosmos3 denoise loop, including CFG and the parallelism modes.
|
||||||
|
|
||||||
The UND pathway runs once and its K/V is cached per cache_key (``cond`` /
|
The UND pathway runs once and its K/V is cached per cache_key (``cond`` /
|
||||||
|
|||||||
+4
@@ -776,5 +776,9 @@ class HeliosChunkedDenoisingStage(PipelineStage):
|
|||||||
# separately to avoid temporal artifacts at chunk boundaries.
|
# separately to avoid temporal artifacts at chunk boundaries.
|
||||||
batch.latent_chunks = chunk_latents_list
|
batch.latent_chunks = chunk_latents_list
|
||||||
batch.latents = history_latents[:, :, -total_generated_latent_frames:]
|
batch.latents = history_latents[:, :, -total_generated_latent_frames:]
|
||||||
|
batch.record_stage_iterations(
|
||||||
|
global_step_offset,
|
||||||
|
global_step_offset if is_enable_stage2 else None,
|
||||||
|
)
|
||||||
|
|
||||||
return batch
|
return batch
|
||||||
|
|||||||
+1
@@ -691,6 +691,7 @@ class LingBotWorldCausalDMDDenoisingStage(CausalDMDDenoisingStage):
|
|||||||
"Ensure ImageVAEEncodingStage runs before this stage."
|
"Ensure ImageVAEEncodingStage runs before this stage."
|
||||||
)
|
)
|
||||||
ctx = self._prepare_causal_dmd_forward_context(batch, server_args)
|
ctx = self._prepare_causal_dmd_forward_context(batch, server_args)
|
||||||
|
batch.record_stage_iterations(len(ctx.timesteps), len(ctx.timesteps))
|
||||||
latents = ctx.latents
|
latents = ctx.latents
|
||||||
cache_ctx = self._prepare_realtime_causal_caches(batch, server_args, ctx)
|
cache_ctx = self._prepare_realtime_causal_caches(batch, server_args, ctx)
|
||||||
|
|
||||||
|
|||||||
+3
@@ -421,6 +421,9 @@ class LongLive2CausalDenoisingStage(CausalDMDDenoisingStage):
|
|||||||
num_blocks = (t - 1) // self.num_frames_per_block
|
num_blocks = (t - 1) // self.num_frames_per_block
|
||||||
block_sizes = [1] + [self.num_frames_per_block] * num_blocks
|
block_sizes = [1] + [self.num_frames_per_block] * num_blocks
|
||||||
|
|
||||||
|
total_iterations = len(block_sizes) * len(timesteps)
|
||||||
|
batch.record_stage_iterations(total_iterations, total_iterations)
|
||||||
|
|
||||||
start_index = 0
|
start_index = 0
|
||||||
self._validate_block_prompt_count(batch, block_sizes)
|
self._validate_block_prompt_count(batch, block_sizes)
|
||||||
|
|
||||||
|
|||||||
+6
@@ -108,6 +108,12 @@ class LTX2AVDenoisingStage(LTX2DenoisingStage):
|
|||||||
|
|
||||||
|
|
||||||
class LTX2RefinementStage(LTX2AVDenoisingStage):
|
class LTX2RefinementStage(LTX2AVDenoisingStage):
|
||||||
|
def default_workload_iterations(
|
||||||
|
self, batch: Req, num_inference_steps: int
|
||||||
|
) -> int | None:
|
||||||
|
# the refiner runs its distilled sigma schedule regardless of the request's steps
|
||||||
|
return max(1, len(self.distilled_sigmas) - 1)
|
||||||
|
|
||||||
"""Stage-2 refinement wrapper that re-noises distilled LTX latents once."""
|
"""Stage-2 refinement wrapper that re-noises distilled LTX latents once."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|||||||
+6
@@ -443,6 +443,12 @@ def _precompute_rope_cache(
|
|||||||
|
|
||||||
|
|
||||||
class MiniMaxH3DenoisingStage(DenoisingStage):
|
class MiniMaxH3DenoisingStage(DenoisingStage):
|
||||||
|
def default_workload_iterations(
|
||||||
|
self, batch: Req, num_inference_steps: int
|
||||||
|
) -> int | None:
|
||||||
|
# one denoise per sigma interval: steps - 1
|
||||||
|
return max(1, num_inference_steps - 1)
|
||||||
|
|
||||||
def __init__(self, transformer, pipeline=None) -> None:
|
def __init__(self, transformer, pipeline=None) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
transformer=transformer,
|
transformer=transformer,
|
||||||
|
|||||||
+5
@@ -152,6 +152,11 @@ class MOVATimestepPreparationStage(PipelineStage):
|
|||||||
|
|
||||||
|
|
||||||
class MOVADenoisingStage(PipelineStage):
|
class MOVADenoisingStage(PipelineStage):
|
||||||
|
def default_workload_iterations(
|
||||||
|
self, batch: Req, num_inference_steps: int
|
||||||
|
) -> int | None:
|
||||||
|
return num_inference_steps
|
||||||
|
|
||||||
"""Run MOVA dual-tower denoising loop."""
|
"""Run MOVA dual-tower denoising loop."""
|
||||||
|
|
||||||
def __init__(self, video_dit, video_dit_2, audio_dit, dual_tower_bridge, scheduler):
|
def __init__(self, video_dit, video_dit_2, audio_dit, dual_tower_bridge, scheduler):
|
||||||
|
|||||||
+2
@@ -725,6 +725,8 @@ class SanaWMLTX2RefinerStage(PipelineStage):
|
|||||||
return batch
|
return batch
|
||||||
|
|
||||||
batch_size = int(batch.latents.shape[0])
|
batch_size = int(batch.latents.shape[0])
|
||||||
|
total_iterations = batch_size * (len(STAGE_2_DISTILLED_SIGMA_VALUES) - 1)
|
||||||
|
batch.record_stage_iterations(total_iterations, total_iterations)
|
||||||
prompts = self._prompts_for_batch(batch, batch_size)
|
prompts = self._prompts_for_batch(batch, batch_size)
|
||||||
fps = float(getattr(batch, "fps", 16) or 16)
|
fps = float(getattr(batch, "fps", 16) or 16)
|
||||||
|
|
||||||
|
|||||||
+2
@@ -630,6 +630,8 @@ class SanaWMStreamingDenoisingStage(CausalDMDDenoisingStage):
|
|||||||
len(explicit_sigmas),
|
len(explicit_sigmas),
|
||||||
do_cfg,
|
do_cfg,
|
||||||
)
|
)
|
||||||
|
total_iterations = num_chunks * len(explicit_sigmas)
|
||||||
|
batch.record_stage_iterations(total_iterations, total_iterations)
|
||||||
|
|
||||||
for chunk_idx in self.progress_bar(range(num_chunks), batch=batch):
|
for chunk_idx in self.progress_bar(range(num_chunks), batch=batch):
|
||||||
chunk_kv, sink_num = self._accumulate_kv_cache(
|
chunk_kv, sink_num = self._accumulate_kv_cache(
|
||||||
|
|||||||
+2
@@ -671,6 +671,8 @@ class SanaWMStreamingRefinerStage(SanaWMLTX2RefinerStage):
|
|||||||
)
|
)
|
||||||
return batch
|
return batch
|
||||||
n_blocks = math.ceil(n_active / self.block_size)
|
n_blocks = math.ceil(n_active / self.block_size)
|
||||||
|
total_iterations = n_blocks * (len(STAGE_2_DISTILLED_SIGMA_VALUES) - 1)
|
||||||
|
batch.record_stage_iterations(total_iterations, total_iterations)
|
||||||
self.log_info(
|
self.log_info(
|
||||||
"SANA-WM streaming refiner: latent=%s, sink=%d, block=%d, blocks=%d, kv_max=%d, seed=%d",
|
"SANA-WM streaming refiner: latent=%s, sink=%d, block=%d, blocks=%d, kv_max=%d, seed=%d",
|
||||||
tuple(latents.shape),
|
tuple(latents.shape),
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ from typing import Any, Optional
|
|||||||
import zmq
|
import zmq
|
||||||
import zmq.asyncio
|
import zmq.asyncio
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.runtime.entrypoints.control_requests import (
|
||||||
|
ListLorasReq,
|
||||||
|
MergeLoraWeightsReq,
|
||||||
|
SetLoraReq,
|
||||||
|
ShutdownReq,
|
||||||
|
UnmergeLoraWeightsReq,
|
||||||
|
)
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.post_training.io_struct import (
|
from sglang.multimodal_gen.runtime.entrypoints.post_training.io_struct import (
|
||||||
GetWeightsChecksumReqInput,
|
GetWeightsChecksumReqInput,
|
||||||
ReleaseMemoryOccupationReqInput,
|
ReleaseMemoryOccupationReqInput,
|
||||||
@@ -15,13 +22,6 @@ from sglang.multimodal_gen.runtime.entrypoints.post_training.io_struct import (
|
|||||||
UpdateWeightFromTensorCheckerReqInput,
|
UpdateWeightFromTensorCheckerReqInput,
|
||||||
UpdateWeightFromTensorReqInput,
|
UpdateWeightFromTensorReqInput,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.utils import (
|
|
||||||
ListLorasReq,
|
|
||||||
MergeLoraWeightsReq,
|
|
||||||
SetLoraReq,
|
|
||||||
ShutdownReq,
|
|
||||||
UnmergeLoraWeightsReq,
|
|
||||||
)
|
|
||||||
from sglang.multimodal_gen.runtime.ipc_array import materialize_file_refs
|
from sglang.multimodal_gen.runtime.ipc_array import materialize_file_refs
|
||||||
from sglang.multimodal_gen.runtime.pipelines_core import Req
|
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.pipelines_core.schedule_batch import OutputBatch
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ from sglang.multimodal_gen import envs
|
|||||||
from sglang.multimodal_gen.configs.pipeline_configs.model_deployment_config import (
|
from sglang.multimodal_gen.configs.pipeline_configs.model_deployment_config import (
|
||||||
ModelDeploymentConfig,
|
ModelDeploymentConfig,
|
||||||
)
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.disaggregation.roles import RoleType
|
||||||
|
from sglang.multimodal_gen.runtime.entrypoints.openai.realtime.registry import (
|
||||||
|
has_realtime_model_adapter,
|
||||||
|
)
|
||||||
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload_components import (
|
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload_components import (
|
||||||
LAYERWISE_OFFLOAD_ALL_COMPONENTS,
|
LAYERWISE_OFFLOAD_ALL_COMPONENTS,
|
||||||
LAYERWISE_OFFLOAD_DIT_GROUP,
|
LAYERWISE_OFFLOAD_DIT_GROUP,
|
||||||
@@ -26,6 +30,49 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
logger = init_logger(__name__)
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def auto_residency_args_skip_reason(server_args: ServerArgs) -> str | None:
|
||||||
|
"""Return why args cannot use warmup-calibrated residency."""
|
||||||
|
if envs.SGLANG_DIFFUSION_DISABLE_AUTO_RESIDENCY:
|
||||||
|
return "disabled via SGLANG_DIFFUSION_DISABLE_AUTO_RESIDENCY"
|
||||||
|
if server_args.performance_mode != "auto":
|
||||||
|
return f"performance_mode={server_args.performance_mode}"
|
||||||
|
if (
|
||||||
|
server_args.pipeline_class_name == "LTX2TwoStagePipeline"
|
||||||
|
and server_args.ltx2_two_stage_device_mode is None
|
||||||
|
):
|
||||||
|
return "legacy LTX-2 two-stage placement"
|
||||||
|
if server_args.ltx2_two_stage_device_mode == "original":
|
||||||
|
return "LTX-2 original two-stage placement"
|
||||||
|
if (
|
||||||
|
server_args.warmup_mode != "server"
|
||||||
|
or server_args.disagg_role != RoleType.MONOLITHIC
|
||||||
|
):
|
||||||
|
return "no synthetic server warmup to calibrate from"
|
||||||
|
task_type = server_args.pipeline_config.task_type
|
||||||
|
if not (task_type.is_visual_gen() or task_type.is_mesh_gen()):
|
||||||
|
return "no synthetic server warmup to calibrate from"
|
||||||
|
if not server_args.pipeline_config.supports_auto_residency:
|
||||||
|
return "pipeline does not support post-warmup residency changes"
|
||||||
|
if has_realtime_model_adapter(server_args):
|
||||||
|
return "realtime serving has no representative synthetic warmup"
|
||||||
|
if server_args.backend == "diffusers":
|
||||||
|
return "diffusers backend"
|
||||||
|
if server_args.enable_breakable_cuda_graph:
|
||||||
|
return "breakable CUDA graph captures during warmup"
|
||||||
|
if server_args.enable_torch_compile:
|
||||||
|
# Compile warmup temporarily evicts resident auxiliaries and may
|
||||||
|
# layerwise-offload the DiT, so its peak is not a serving peak.
|
||||||
|
return "torch.compile warmup uses a stripped memory layout"
|
||||||
|
if envs.SGLANG_CACHE_DIT_ENABLED:
|
||||||
|
return "cache-dit enabled"
|
||||||
|
if server_args.batching_max_size > 1:
|
||||||
|
return "dynamic batching enabled"
|
||||||
|
if not current_platform.is_cuda():
|
||||||
|
return "requires CUDA"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
PERFORMANCE_MODES = ("manual", "auto", "speed", "memory")
|
PERFORMANCE_MODES = ("manual", "auto", "speed", "memory")
|
||||||
|
|
||||||
DEFAULT_LAYERWISE_COMPONENT_ARG_NAMES = (
|
DEFAULT_LAYERWISE_COMPONENT_ARG_NAMES = (
|
||||||
|
|||||||
@@ -454,6 +454,10 @@ class ServerArgs(DisaggServerArgsMixin):
|
|||||||
warmup_resolutions: list[str] = None
|
warmup_resolutions: list[str] = None
|
||||||
warmup_num_frames: int | None = None
|
warmup_num_frames: int | None = None
|
||||||
warmup_steps: int = 1
|
warmup_steps: int = 1
|
||||||
|
# JSON overrides for the representative request shape used by synthetic
|
||||||
|
# warmup and automatic residency planning. Execution remains bounded by
|
||||||
|
# warmup_steps and the server warmup frame/area caps.
|
||||||
|
warmup_sampling_params: dict[str, Any] | str | None = None
|
||||||
|
|
||||||
disable_autocast: bool | None = None
|
disable_autocast: bool | None = None
|
||||||
|
|
||||||
@@ -2384,6 +2388,18 @@ class ServerArgs(DisaggServerArgsMixin):
|
|||||||
default=ServerArgs.warmup_steps,
|
default=ServerArgs.warmup_steps,
|
||||||
help="The number of warmup steps to perform for each resolution.",
|
help="The number of warmup steps to perform for each resolution.",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--warmup-sampling-params",
|
||||||
|
type=str,
|
||||||
|
default=ServerArgs.warmup_sampling_params,
|
||||||
|
help=(
|
||||||
|
"JSON object overriding model sampling defaults for synthetic "
|
||||||
|
"warmup and auto residency planning, for example "
|
||||||
|
'\'{"width":832,"height":480,"num_frames":9,'
|
||||||
|
'"num_inference_steps":4}\'. Warmup still applies its '
|
||||||
|
"bounded execution caps."
|
||||||
|
),
|
||||||
|
)
|
||||||
# component residency and legacy offload controls
|
# component residency and legacy offload controls
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--component-residency",
|
"--component-residency",
|
||||||
|
|||||||
@@ -7,15 +7,22 @@ from typing import Any, Awaitable, Callable
|
|||||||
|
|
||||||
from tqdm.auto import tqdm
|
from tqdm.auto import tqdm
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.runtime.entrypoints.openai.realtime.registry import (
|
||||||
|
has_realtime_model_adapter,
|
||||||
|
)
|
||||||
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import (
|
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import (
|
||||||
OutputBatch,
|
OutputBatch,
|
||||||
Req,
|
Req,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||||
|
from sglang.multimodal_gen.runtime.server_args.auto_tune import (
|
||||||
|
auto_residency_args_skip_reason,
|
||||||
|
)
|
||||||
from sglang.multimodal_gen.runtime.utils.image_io import save_base64_image_to_path
|
from sglang.multimodal_gen.runtime.utils.image_io import save_base64_image_to_path
|
||||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||||
from sglang.multimodal_gen.runtime.warmup_request_builder import (
|
from sglang.multimodal_gen.runtime.warmup_request_builder import (
|
||||||
build_warmup_reqs,
|
build_warmup_reqs,
|
||||||
|
lighten_warmup_req,
|
||||||
should_include_warmup_image,
|
should_include_warmup_image,
|
||||||
supports_synthetic_warmup,
|
supports_synthetic_warmup,
|
||||||
)
|
)
|
||||||
@@ -74,15 +81,7 @@ def should_run_server_warmup(server_args: ServerArgs) -> bool:
|
|||||||
|
|
||||||
def is_realtime_serving(server_args: ServerArgs) -> bool:
|
def is_realtime_serving(server_args: ServerArgs) -> bool:
|
||||||
"""Synthetic warmup has no realtime session state."""
|
"""Synthetic warmup has no realtime session state."""
|
||||||
try:
|
return has_realtime_model_adapter(server_args)
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.openai.realtime.registry import (
|
|
||||||
get_realtime_model_adapter,
|
|
||||||
)
|
|
||||||
|
|
||||||
get_realtime_model_adapter(server_args)
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def should_run_synthetic_server_warmup(server_args: ServerArgs) -> bool:
|
def should_run_synthetic_server_warmup(server_args: ServerArgs) -> bool:
|
||||||
@@ -101,11 +100,69 @@ def should_run_explicit_client_warmup(server_args: ServerArgs) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def auto_residency_skip_reason(server_args: ServerArgs) -> str | None:
|
||||||
|
"""Final gate for warmup-calibrated residency placement.
|
||||||
|
|
||||||
|
Only rules out paths the planner was not designed for; the workers
|
||||||
|
re-check per component (explicit placement, FSDP modules, custom
|
||||||
|
strategies, missing sizes) and per measurement.
|
||||||
|
"""
|
||||||
|
args_reason = auto_residency_args_skip_reason(server_args)
|
||||||
|
if args_reason is not None:
|
||||||
|
return args_reason
|
||||||
|
if not should_run_synthetic_server_warmup(server_args):
|
||||||
|
return "no synthetic server warmup to calibrate from"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Enough to clear a probe that overshot the card, few enough that a failure
|
||||||
|
# which is not about probe size gives up quickly instead of walking the
|
||||||
|
# workload down to nothing.
|
||||||
|
MAX_WARMUP_DEGRADE_ATTEMPTS = 3
|
||||||
|
|
||||||
|
|
||||||
|
_OUT_OF_MEMORY_MARKERS = (
|
||||||
|
"out of memory",
|
||||||
|
"outofmemory",
|
||||||
|
"cudaerrormemoryallocation",
|
||||||
|
"cublas_status_alloc_failed",
|
||||||
|
"cannot allocate memory",
|
||||||
|
"unable to allocate",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_out_of_memory(error: Any) -> bool:
|
||||||
|
text = str(error).lower()
|
||||||
|
return any(marker in text for marker in _OUT_OF_MEMORY_MARKERS)
|
||||||
|
|
||||||
|
|
||||||
|
def _degrade_after_oom(server_args: ServerArgs, req: Req) -> Req | None:
|
||||||
|
"""Next warmup probe to try after `req` ran the card out of memory.
|
||||||
|
|
||||||
|
Only memory failures are worth retrying smaller; anything else fails the
|
||||||
|
same way at every size and should surface instead of being shrunk away.
|
||||||
|
"""
|
||||||
|
lighter = lighten_warmup_req(server_args, req)
|
||||||
|
if lighter is None:
|
||||||
|
return None
|
||||||
|
logger.warning(
|
||||||
|
"%s ran out of memory; retrying warmup at %s",
|
||||||
|
format_warmup_req(req),
|
||||||
|
format_warmup_req(lighter),
|
||||||
|
)
|
||||||
|
return lighter
|
||||||
|
|
||||||
|
|
||||||
def format_warmup_req(req_or_group: Any) -> str:
|
def format_warmup_req(req_or_group: Any) -> str:
|
||||||
req = get_first_generation_req(req_or_group)
|
req = get_first_generation_req(req_or_group)
|
||||||
prefix = (
|
if req is not None and req.extra.get("auto_residency_full_shape_probe"):
|
||||||
"server warmup req" if is_server_based_warmup(req_or_group) else "warmup req"
|
prefix = "auto residency probe"
|
||||||
)
|
else:
|
||||||
|
prefix = (
|
||||||
|
"server warmup req"
|
||||||
|
if is_server_based_warmup(req_or_group)
|
||||||
|
else "warmup req"
|
||||||
|
)
|
||||||
if req is None:
|
if req is None:
|
||||||
return prefix
|
return prefix
|
||||||
|
|
||||||
@@ -131,6 +188,8 @@ def build_client_warmup_reqs(
|
|||||||
server_args: ServerArgs,
|
server_args: ServerArgs,
|
||||||
*,
|
*,
|
||||||
warmup_input_path: str | None = None,
|
warmup_input_path: str | None = None,
|
||||||
|
rewarm: bool = False,
|
||||||
|
step_limit: int | None = None,
|
||||||
) -> list[Req]:
|
) -> list[Req]:
|
||||||
warmup_reqs = build_warmup_reqs(
|
warmup_reqs = build_warmup_reqs(
|
||||||
server_args,
|
server_args,
|
||||||
@@ -143,6 +202,12 @@ def build_client_warmup_reqs(
|
|||||||
for req in warmup_reqs:
|
for req in warmup_reqs:
|
||||||
if req.is_warmup:
|
if req.is_warmup:
|
||||||
req.extra["warmup_total"] = warmup_total
|
req.extra["warmup_total"] = warmup_total
|
||||||
|
if step_limit is not None:
|
||||||
|
req.num_inference_steps = min(req.num_inference_steps, step_limit)
|
||||||
|
if rewarm:
|
||||||
|
# a repeat pass after an auto-residency change: keep it out of
|
||||||
|
# the scheduler's warmup progress accounting (already at N/N)
|
||||||
|
req.extra["server_warmup_rewarm"] = True
|
||||||
return warmup_reqs
|
return warmup_reqs
|
||||||
|
|
||||||
|
|
||||||
@@ -151,6 +216,8 @@ async def run_async_client_warmup(
|
|||||||
forward: Callable[[Req], Awaitable[OutputBatch]],
|
forward: Callable[[Req], Awaitable[OutputBatch]],
|
||||||
*,
|
*,
|
||||||
fail_open: bool = False,
|
fail_open: bool = False,
|
||||||
|
rewarm: bool = False,
|
||||||
|
step_limit: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
warmup_input_path = None
|
warmup_input_path = None
|
||||||
@@ -158,9 +225,20 @@ async def run_async_client_warmup(
|
|||||||
warmup_input_path = prepare_warmup_image_path(server_args)
|
warmup_input_path = prepare_warmup_image_path(server_args)
|
||||||
|
|
||||||
for req in build_client_warmup_reqs(
|
for req in build_client_warmup_reqs(
|
||||||
server_args, warmup_input_path=warmup_input_path
|
server_args,
|
||||||
|
warmup_input_path=warmup_input_path,
|
||||||
|
rewarm=rewarm,
|
||||||
|
step_limit=step_limit,
|
||||||
):
|
):
|
||||||
response = await forward(req)
|
response = await forward(req)
|
||||||
|
for _ in range(MAX_WARMUP_DEGRADE_ATTEMPTS):
|
||||||
|
if response.error is None or not _is_out_of_memory(response.error):
|
||||||
|
break
|
||||||
|
lighter = _degrade_after_oom(server_args, req)
|
||||||
|
if lighter is None:
|
||||||
|
break
|
||||||
|
req = lighter
|
||||||
|
response = await forward(req)
|
||||||
if response.error is not None:
|
if response.error is not None:
|
||||||
raise RuntimeError(response.error)
|
raise RuntimeError(response.error)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -184,6 +262,14 @@ def run_sync_client_warmup(
|
|||||||
server_args, warmup_input_path=warmup_input_path
|
server_args, warmup_input_path=warmup_input_path
|
||||||
):
|
):
|
||||||
response = forward(req)
|
response = forward(req)
|
||||||
|
for _ in range(MAX_WARMUP_DEGRADE_ATTEMPTS):
|
||||||
|
if response.error is None or not _is_out_of_memory(response.error):
|
||||||
|
break
|
||||||
|
lighter = _degrade_after_oom(server_args, req)
|
||||||
|
if lighter is None:
|
||||||
|
break
|
||||||
|
req = lighter
|
||||||
|
response = forward(req)
|
||||||
if response.error is not None:
|
if response.error is not None:
|
||||||
raise RuntimeError(response.error)
|
raise RuntimeError(response.error)
|
||||||
|
|
||||||
@@ -275,6 +361,18 @@ class SchedulerWarmupMixin:
|
|||||||
if not is_warmup:
|
if not is_warmup:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
req = get_first_generation_req(req_or_group)
|
||||||
|
if req is not None and req.extra.get("server_warmup_rewarm"):
|
||||||
|
# auto-residency re-warm passes repeat already-counted requests;
|
||||||
|
# advancing the bar again would log N+1/N in CI
|
||||||
|
if output_batch.error is not None:
|
||||||
|
logger.warning(
|
||||||
|
"%s processing failed: %s",
|
||||||
|
self._format_warmup_req(req_or_group),
|
||||||
|
output_batch.error,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
server_based_warmup = is_server_based_warmup(req_or_group)
|
server_based_warmup = is_server_based_warmup(req_or_group)
|
||||||
self._warmup_processed += 1
|
self._warmup_processed += 1
|
||||||
self._advance_warmup_progress_bar(req_or_group, output_batch)
|
self._advance_warmup_progress_bar(req_or_group, output_batch)
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ class RequestMetrics:
|
|||||||
self.request_id = request_id
|
self.request_id = request_id
|
||||||
self.stages: Dict[str, float] = {}
|
self.stages: Dict[str, float] = {}
|
||||||
self.steps: list[float] = []
|
self.steps: list[float] = []
|
||||||
|
self.steps_by_stage: Dict[str, list[float]] = {}
|
||||||
|
self.stage_iterations: Dict[str, tuple[int, int]] = {}
|
||||||
|
self.active_stage_name: str | None = None
|
||||||
self.total_duration_ms: float = 0.0
|
self.total_duration_ms: float = 0.0
|
||||||
self.suppress_stage_breakdown: bool = False
|
self.suppress_stage_breakdown: bool = False
|
||||||
# memory tracking: {checkpoint_name: MemorySnapshot}
|
# memory tracking: {checkpoint_name: MemorySnapshot}
|
||||||
@@ -77,7 +80,26 @@ class RequestMetrics:
|
|||||||
"""Records the duration of a denoising step in execution order."""
|
"""Records the duration of a denoising step in execution order."""
|
||||||
if self.suppress_stage_breakdown:
|
if self.suppress_stage_breakdown:
|
||||||
return
|
return
|
||||||
self.steps.append(duration_s * 1000)
|
duration_ms = duration_s * 1000
|
||||||
|
self.steps.append(duration_ms)
|
||||||
|
if self.active_stage_name is not None:
|
||||||
|
self.steps_by_stage.setdefault(self.active_stage_name, []).append(
|
||||||
|
duration_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
def record_stage_iterations(
|
||||||
|
self, measured_iterations: int, target_iterations: int
|
||||||
|
) -> None:
|
||||||
|
"""Record calibration and default-workload iterations for this stage."""
|
||||||
|
if self.suppress_stage_breakdown or self.active_stage_name is None:
|
||||||
|
return
|
||||||
|
measured = max(1, int(measured_iterations))
|
||||||
|
target = max(1, int(target_iterations))
|
||||||
|
previous = self.stage_iterations.get(self.active_stage_name, (0, 0))
|
||||||
|
self.stage_iterations[self.active_stage_name] = (
|
||||||
|
previous[0] + measured,
|
||||||
|
previous[1] + target,
|
||||||
|
)
|
||||||
|
|
||||||
def record_memory_snapshot(self, checkpoint_name: str, snapshot: MemorySnapshot):
|
def record_memory_snapshot(self, checkpoint_name: str, snapshot: MemorySnapshot):
|
||||||
if self.suppress_stage_breakdown:
|
if self.suppress_stage_breakdown:
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ first real request, without copying user traffic. It starts from the model's
|
|||||||
sampling defaults, then keeps startup bounded by choosing common low-cost
|
sampling defaults, then keeps startup bounded by choosing common low-cost
|
||||||
resolution/frame buckets and trimming the denoising step count.
|
resolution/frame buckets and trimming the denoising step count.
|
||||||
|
|
||||||
|
When warmup-calibrated auto residency is active, warmup uses the full default
|
||||||
|
serving shape but keeps the trimmed step count. Memory depends on the
|
||||||
|
activation shape rather than the number of repeated denoising steps, so this
|
||||||
|
directly measures placement headroom without running a full generation or an
|
||||||
|
extra stateful pipeline request.
|
||||||
|
|
||||||
Image models may run a tiny second step because first/last step paths often
|
Image models may run a tiny second step because first/last step paths often
|
||||||
initialize different kernels or scheduler state. Video models cap frames and
|
initialize different kernels or scheduler state. Video models cap frames and
|
||||||
steps to keep startup bounded. Explicit warmup resolutions share this builder;
|
steps to keep startup bounded. Explicit warmup resolutions share this builder;
|
||||||
@@ -13,17 +19,26 @@ callers send them through the scheduler client so warmup exercises the same
|
|||||||
request transport path as real generation.
|
request transport path as real generation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
from copy import copy
|
from copy import copy
|
||||||
|
from dataclasses import fields, replace
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from sglang.multimodal_gen.configs.pipeline_configs.base import ModelTaskType
|
from sglang.multimodal_gen.configs.pipeline_configs.base import ModelTaskType
|
||||||
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
|
from sglang.multimodal_gen.configs.sample.sampling_params import (
|
||||||
|
SamplingParams,
|
||||||
|
align_num_frames_for_num_gpus,
|
||||||
|
resolve_sequence_shard,
|
||||||
|
)
|
||||||
from sglang.multimodal_gen.registry import get_pipeline_config_classes
|
from sglang.multimodal_gen.registry import get_pipeline_config_classes
|
||||||
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
|
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
|
||||||
from sglang.multimodal_gen.runtime.server_args import (
|
from sglang.multimodal_gen.runtime.server_args import (
|
||||||
ServerArgs,
|
ServerArgs,
|
||||||
is_ltx2_two_stage_pipeline_name,
|
is_ltx2_two_stage_pipeline_name,
|
||||||
)
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.server_args.auto_tune import (
|
||||||
|
auto_residency_args_skip_reason,
|
||||||
|
)
|
||||||
from sglang.multimodal_gen.runtime.utils.common import parse_size
|
from sglang.multimodal_gen.runtime.utils.common import parse_size
|
||||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||||
|
|
||||||
@@ -42,6 +57,11 @@ SERVER_WARMUP_MAX_VIDEO_FRAMES = 17
|
|||||||
SERVER_WARMUP_LTX2_TWO_STAGE_MAX_VIDEO_FRAMES = 25
|
SERVER_WARMUP_LTX2_TWO_STAGE_MAX_VIDEO_FRAMES = 25
|
||||||
SERVER_WARMUP_IMAGE_STEPS = 2
|
SERVER_WARMUP_IMAGE_STEPS = 2
|
||||||
SERVER_WARMUP_VIDEO_STEPS = 2
|
SERVER_WARMUP_VIDEO_STEPS = 2
|
||||||
|
# Two-step schedulers can have one compile-heavy step and one lower-order
|
||||||
|
# boundary step, leaving no representative steady-state timing sample. Auto
|
||||||
|
# residency extrapolates this timing to the default request, so collect four
|
||||||
|
# steps while retaining the shorter warmup for every non-planning path.
|
||||||
|
AUTO_RESIDENCY_TIMING_STEPS = 4
|
||||||
|
|
||||||
|
|
||||||
def get_model_sampling_defaults(server_args: ServerArgs) -> SamplingParams:
|
def get_model_sampling_defaults(server_args: ServerArgs) -> SamplingParams:
|
||||||
@@ -50,13 +70,44 @@ def get_model_sampling_defaults(server_args: ServerArgs) -> SamplingParams:
|
|||||||
config_classes = get_pipeline_config_classes(pipeline_class_name)
|
config_classes = get_pipeline_config_classes(pipeline_class_name)
|
||||||
if config_classes is not None:
|
if config_classes is not None:
|
||||||
_, sampling_params_cls = config_classes
|
_, sampling_params_cls = config_classes
|
||||||
return sampling_params_cls()
|
defaults = sampling_params_cls()
|
||||||
|
return _apply_warmup_sampling_overrides(server_args, defaults)
|
||||||
|
|
||||||
return SamplingParams.from_pretrained(
|
defaults = SamplingParams.from_pretrained(
|
||||||
server_args.model_path,
|
server_args.model_path,
|
||||||
backend=server_args.backend,
|
backend=server_args.backend,
|
||||||
model_id=server_args.model_id,
|
model_id=server_args.model_id,
|
||||||
)
|
)
|
||||||
|
return _apply_warmup_sampling_overrides(server_args, defaults)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_warmup_sampling_overrides(
|
||||||
|
server_args: ServerArgs, defaults: SamplingParams
|
||||||
|
) -> SamplingParams:
|
||||||
|
value = server_args.warmup_sampling_params
|
||||||
|
if value is None:
|
||||||
|
return defaults
|
||||||
|
if isinstance(value, str):
|
||||||
|
try:
|
||||||
|
value = json.loads(value)
|
||||||
|
except json.JSONDecodeError as error:
|
||||||
|
raise ValueError(
|
||||||
|
"--warmup-sampling-params must be a JSON object"
|
||||||
|
) from error
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise ValueError("--warmup-sampling-params must be a JSON object")
|
||||||
|
field_names = {item.name for item in fields(defaults)}
|
||||||
|
unknown = value.keys() - field_names
|
||||||
|
if unknown:
|
||||||
|
raise ValueError(
|
||||||
|
f"invalid --warmup-sampling-params fields: {', '.join(sorted(unknown))}"
|
||||||
|
)
|
||||||
|
updated = copy(defaults)
|
||||||
|
for name, field_value in value.items():
|
||||||
|
# Some model contracts intentionally expose fixed dataclass fields
|
||||||
|
# with init=False; a warmup workload still needs to mirror the request.
|
||||||
|
object.__setattr__(updated, name, field_value)
|
||||||
|
return updated
|
||||||
|
|
||||||
|
|
||||||
def _resolve_default_warmup_resolution(
|
def _resolve_default_warmup_resolution(
|
||||||
@@ -221,6 +272,72 @@ def _fit_resolution_to_area(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _halve_num_frames(server_args: ServerArgs, num_frames: int) -> int:
|
||||||
|
"""Halve the latent frame count, keeping the model's frame arithmetic."""
|
||||||
|
if num_frames <= 1:
|
||||||
|
return num_frames
|
||||||
|
ratio = (
|
||||||
|
server_args.pipeline_config.vae_config.arch_config.temporal_compression_ratio
|
||||||
|
)
|
||||||
|
if not ratio or ratio <= 1:
|
||||||
|
return max(1, num_frames // 2)
|
||||||
|
latent_frames = (num_frames - 1) // ratio + 1
|
||||||
|
# round up: halving 5 latent frames should land on 3 (9 frames), not 2 (5)
|
||||||
|
return ((latent_frames + 1) // 2 - 1) * ratio + 1
|
||||||
|
|
||||||
|
|
||||||
|
def _lighter_valid_num_frames(server_args: ServerArgs, num_frames: int) -> int:
|
||||||
|
"""Largest frame count at or below half that the model's frame contract accepts.
|
||||||
|
|
||||||
|
``adjust_num_frames`` rounds up (LongLive2 maps 17 frames to 29), so the
|
||||||
|
halved count is walked down until it is a fixed point of the contract.
|
||||||
|
"""
|
||||||
|
halved = _halve_num_frames(server_args, num_frames)
|
||||||
|
adjust = getattr(server_args.pipeline_config, "adjust_num_frames", None)
|
||||||
|
for candidate in range(halved, 0, -1):
|
||||||
|
adjusted = adjust(candidate) if callable(adjust) else candidate
|
||||||
|
if not isinstance(adjusted, int) or isinstance(adjusted, bool):
|
||||||
|
adjusted = candidate
|
||||||
|
if adjusted == candidate:
|
||||||
|
return candidate if candidate < num_frames else num_frames
|
||||||
|
return num_frames
|
||||||
|
|
||||||
|
|
||||||
|
def lighten_warmup_req(server_args: ServerArgs, req: Req) -> Req | None:
|
||||||
|
"""Roughly halve a warmup probe, or None once it cannot shrink further.
|
||||||
|
|
||||||
|
Warmup peak memory tracks width * height * num_frames, so a card that could
|
||||||
|
not hold the full probe usually holds half of it while still walking the
|
||||||
|
same code path. Frames go first: they drive video activation size, and
|
||||||
|
cutting them leaves the spatial kernels at their serving shape.
|
||||||
|
"""
|
||||||
|
params = req.sampling_params
|
||||||
|
if params is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
num_frames = params.num_frames or 1
|
||||||
|
lighter_frames = _lighter_valid_num_frames(server_args, num_frames)
|
||||||
|
if lighter_frames < num_frames:
|
||||||
|
return _replace_warmup_workload(req, num_frames=lighter_frames)
|
||||||
|
|
||||||
|
width = params.width
|
||||||
|
height = params.height
|
||||||
|
if not width or not height:
|
||||||
|
return None
|
||||||
|
alignment = _warmup_resolution_alignment(server_args)
|
||||||
|
lighter = _fit_resolution_to_area(width, height, width * height // 2, alignment)
|
||||||
|
# below the alignment floor the fit rounds back up; only take a real cut
|
||||||
|
if lighter[0] * lighter[1] >= width * height:
|
||||||
|
return None
|
||||||
|
return _replace_warmup_workload(req, width=lighter[0], height=lighter[1])
|
||||||
|
|
||||||
|
|
||||||
|
def _replace_warmup_workload(req: Req, **changes: int) -> Req:
|
||||||
|
lighter = copy(req)
|
||||||
|
lighter.sampling_params = replace(req.sampling_params, **changes)
|
||||||
|
return lighter
|
||||||
|
|
||||||
|
|
||||||
def _is_resolution_aligned(resolution: tuple[int, int], alignment: int) -> bool:
|
def _is_resolution_aligned(resolution: tuple[int, int], alignment: int) -> bool:
|
||||||
width, height = resolution
|
width, height = resolution
|
||||||
return width % alignment == 0 and height % alignment == 0
|
return width % alignment == 0 and height % alignment == 0
|
||||||
@@ -268,7 +385,87 @@ def _resolve_warmup_num_frames(
|
|||||||
)
|
)
|
||||||
warmup_num_frames = min(num_frames, frame_budget)
|
warmup_num_frames = min(num_frames, frame_budget)
|
||||||
|
|
||||||
return server_args.pipeline_config.adjust_num_frames(warmup_num_frames)
|
return _apply_warmup_frame_contract(
|
||||||
|
server_args, sampling_defaults, num_frames=warmup_num_frames
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_warmup_frame_contract(
|
||||||
|
server_args: ServerArgs, sampling_defaults: SamplingParams, *, num_frames: int
|
||||||
|
) -> int:
|
||||||
|
"""Re-apply the real-request frame contract to a capped warmup frame count.
|
||||||
|
|
||||||
|
Warmup requests skip ``SamplingParams._adjust``, so the cap must run the
|
||||||
|
model frame contract itself -- without this, e.g. LongLive2's capped 17
|
||||||
|
frames map to 5 latent frames (not divisible by its 8-frame causal block)
|
||||||
|
and every server warmup fails silently under fail-open. Pipelines that
|
||||||
|
align frames to ``num_gpus`` (rather than sharding the sequence dim) get
|
||||||
|
the same latent alignment real requests get.
|
||||||
|
"""
|
||||||
|
num_frames = server_args.pipeline_config.adjust_num_frames(num_frames)
|
||||||
|
if (
|
||||||
|
sampling_defaults.adjust_frames
|
||||||
|
and not resolve_sequence_shard(
|
||||||
|
server_args.pipeline_config, sampling_defaults.enable_sequence_shard
|
||||||
|
)
|
||||||
|
and server_args.num_gpus > 1
|
||||||
|
):
|
||||||
|
num_frames = align_num_frames_for_num_gpus(
|
||||||
|
num_frames,
|
||||||
|
num_gpus=server_args.num_gpus,
|
||||||
|
vae_config=server_args.pipeline_config.vae_config,
|
||||||
|
round_down=sampling_defaults.num_frames_round_down,
|
||||||
|
)
|
||||||
|
return num_frames
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_default_workload_shape(
|
||||||
|
server_args: ServerArgs,
|
||||||
|
sampling_defaults: SamplingParams,
|
||||||
|
) -> tuple[int | None, int | None, int]:
|
||||||
|
"""Resolve the serving shape used by warmup-based memory planning."""
|
||||||
|
width = sampling_defaults.width
|
||||||
|
height = sampling_defaults.height
|
||||||
|
if (width is None or height is None) and sampling_defaults.supported_resolutions:
|
||||||
|
width, height = max(
|
||||||
|
sampling_defaults.supported_resolutions,
|
||||||
|
key=lambda size: size[0] * size[1],
|
||||||
|
)
|
||||||
|
num_frames = sampling_defaults.num_frames or 1
|
||||||
|
if num_frames > 1:
|
||||||
|
num_frames = _apply_warmup_frame_contract(
|
||||||
|
server_args, sampling_defaults, num_frames=num_frames
|
||||||
|
)
|
||||||
|
return width, height, num_frames
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_auto_residency_warmup_shape(
|
||||||
|
server_args: ServerArgs,
|
||||||
|
sampling_defaults: SamplingParams,
|
||||||
|
*,
|
||||||
|
warmup_shape: tuple[int, int, int | None],
|
||||||
|
server_based_warmup: bool,
|
||||||
|
) -> tuple[int, int, int] | None:
|
||||||
|
"""Return the full serving shape when bounded warmup is smaller.
|
||||||
|
|
||||||
|
The probe still runs only the bounded warmup step count. Denoising steps
|
||||||
|
repeat the same activation shape, so one full-shape forward measures the
|
||||||
|
placement constraints directly without paying for a full generation or
|
||||||
|
extrapolating a small-shape allocator peak.
|
||||||
|
"""
|
||||||
|
if not server_based_warmup:
|
||||||
|
return None
|
||||||
|
if auto_residency_args_skip_reason(server_args) is not None:
|
||||||
|
return None
|
||||||
|
width, height, num_frames = resolve_default_workload_shape(
|
||||||
|
server_args, sampling_defaults
|
||||||
|
)
|
||||||
|
if width is None or height is None:
|
||||||
|
return None
|
||||||
|
target = (width, height, num_frames)
|
||||||
|
if target == warmup_shape:
|
||||||
|
return None
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
def _effective_cfg_scale(sampling_defaults: SamplingParams) -> float | None:
|
def _effective_cfg_scale(sampling_defaults: SamplingParams) -> float | None:
|
||||||
@@ -381,11 +578,42 @@ def build_warmup_reqs(
|
|||||||
sampling_defaults,
|
sampling_defaults,
|
||||||
server_based_warmup=server_based_warmup,
|
server_based_warmup=server_based_warmup,
|
||||||
)
|
)
|
||||||
|
auto_residency_warmup_shape = (
|
||||||
|
_resolve_auto_residency_warmup_shape(
|
||||||
|
server_args,
|
||||||
|
sampling_defaults,
|
||||||
|
warmup_shape=(width, height, warmup_num_frames),
|
||||||
|
server_based_warmup=server_based_warmup,
|
||||||
|
)
|
||||||
|
if warmup_resolutions is None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
collect_auto_residency_metrics = (
|
||||||
|
warmup_resolutions is None
|
||||||
|
and server_based_warmup
|
||||||
|
and auto_residency_args_skip_reason(server_args) is None
|
||||||
|
)
|
||||||
|
if collect_auto_residency_metrics and sampling_defaults.num_inference_steps:
|
||||||
|
warmup_steps = min(
|
||||||
|
int(sampling_defaults.num_inference_steps),
|
||||||
|
max(warmup_steps, AUTO_RESIDENCY_TIMING_STEPS),
|
||||||
|
)
|
||||||
|
shapes = [
|
||||||
|
(width, height, warmup_num_frames, False) for width, height in resolutions
|
||||||
|
]
|
||||||
|
if auto_residency_warmup_shape is not None:
|
||||||
|
# The bounded warmup runs first: its measurement lets the worker size
|
||||||
|
# the full-shape probe to the memory the card actually has left. The
|
||||||
|
# bounded shape then runs once more so the allocator pool and kernel
|
||||||
|
# caches serving starts from are shaped by a serving-sized request,
|
||||||
|
# not by the probe (the worker drops the probe's pool before it).
|
||||||
|
shapes.append((*auto_residency_warmup_shape, True))
|
||||||
|
shapes.append(shapes[0])
|
||||||
|
|
||||||
# build warmup reqs
|
# build warmup reqs
|
||||||
warmup_reqs = []
|
warmup_reqs = []
|
||||||
include_warmup_image = should_include_warmup_image(server_args, server_based_warmup)
|
include_warmup_image = should_include_warmup_image(server_args, server_based_warmup)
|
||||||
for width, height in resolutions:
|
for width, height, num_frames, is_probe in shapes:
|
||||||
req_kwargs = dict(
|
req_kwargs = dict(
|
||||||
data_type=task_type.data_type(),
|
data_type=task_type.data_type(),
|
||||||
width=width,
|
width=width,
|
||||||
@@ -399,7 +627,7 @@ def build_warmup_reqs(
|
|||||||
guidance_scale_2=sampling_defaults.guidance_scale_2,
|
guidance_scale_2=sampling_defaults.guidance_scale_2,
|
||||||
true_cfg_scale=sampling_defaults.true_cfg_scale,
|
true_cfg_scale=sampling_defaults.true_cfg_scale,
|
||||||
num_inference_steps=sampling_defaults.num_inference_steps,
|
num_inference_steps=sampling_defaults.num_inference_steps,
|
||||||
num_frames=warmup_num_frames,
|
num_frames=num_frames,
|
||||||
)
|
)
|
||||||
if include_warmup_image:
|
if include_warmup_image:
|
||||||
if warmup_input_path is None:
|
if warmup_input_path is None:
|
||||||
@@ -407,7 +635,13 @@ def build_warmup_reqs(
|
|||||||
"Warmup image path is required for image-input model"
|
"Warmup image path is required for image-input model"
|
||||||
)
|
)
|
||||||
req_kwargs["prompt"] = DEFAULT_PLACEHOLDER_PROMPT
|
req_kwargs["prompt"] = DEFAULT_PLACEHOLDER_PROMPT
|
||||||
req_kwargs["image_path"] = [warmup_input_path]
|
default_image_path = sampling_defaults.image_path
|
||||||
|
image_count = (
|
||||||
|
len(default_image_path)
|
||||||
|
if isinstance(default_image_path, (list, tuple))
|
||||||
|
else 1
|
||||||
|
)
|
||||||
|
req_kwargs["image_path"] = [warmup_input_path] * max(1, image_count)
|
||||||
if server_args.enable_cfg_parallel:
|
if server_args.enable_cfg_parallel:
|
||||||
if not req_kwargs.get("negative_prompt"):
|
if not req_kwargs.get("negative_prompt"):
|
||||||
req_kwargs["negative_prompt"] = DEFAULT_PLACEHOLDER_PROMPT
|
req_kwargs["negative_prompt"] = DEFAULT_PLACEHOLDER_PROMPT
|
||||||
@@ -441,6 +675,13 @@ def build_warmup_reqs(
|
|||||||
req.extra["return_warmup_result"] = True
|
req.extra["return_warmup_result"] = True
|
||||||
if server_based_warmup:
|
if server_based_warmup:
|
||||||
req.extra["server_based_warmup"] = True
|
req.extra["server_based_warmup"] = True
|
||||||
|
if collect_auto_residency_metrics:
|
||||||
|
# Stage timers already synchronize around warmup stages. Keep
|
||||||
|
# their values for residency planning instead of discarding
|
||||||
|
# the measurements after paying that cost.
|
||||||
|
req.metrics.suppress_stage_breakdown = False
|
||||||
|
if is_probe:
|
||||||
|
req.extra["auto_residency_full_shape_probe"] = True
|
||||||
warmup_reqs.append(req)
|
warmup_reqs.append(req)
|
||||||
|
|
||||||
return warmup_reqs
|
return warmup_reqs
|
||||||
|
|||||||
@@ -1568,6 +1568,7 @@
|
|||||||
"expected_median_denoise_ms": 1010.9,
|
"expected_median_denoise_ms": 1010.9,
|
||||||
"load_peak_vram_mb": 27320.0,
|
"load_peak_vram_mb": 27320.0,
|
||||||
"runtime_peak_vram_mb": 35926.0,
|
"runtime_peak_vram_mb": 35926.0,
|
||||||
|
"warmup_peak_vram_mb": 56748.0,
|
||||||
"load_peak_allocated_mb": 26241.0,
|
"load_peak_allocated_mb": 26241.0,
|
||||||
"runtime_peak_allocated_mb": 31922.0,
|
"runtime_peak_allocated_mb": 31922.0,
|
||||||
"estimated_full_test_time_s": 77.0
|
"estimated_full_test_time_s": 77.0
|
||||||
@@ -1701,6 +1702,7 @@
|
|||||||
"expected_median_denoise_ms": 656.68,
|
"expected_median_denoise_ms": 656.68,
|
||||||
"load_peak_vram_mb": 35350.0,
|
"load_peak_vram_mb": 35350.0,
|
||||||
"runtime_peak_vram_mb": 42674.0,
|
"runtime_peak_vram_mb": 42674.0,
|
||||||
|
"warmup_peak_vram_mb": 45782.0,
|
||||||
"load_peak_allocated_mb": 34033.0,
|
"load_peak_allocated_mb": 34033.0,
|
||||||
"runtime_peak_allocated_mb": 38569.0,
|
"runtime_peak_allocated_mb": 38569.0,
|
||||||
"estimated_full_test_time_s": 243.0
|
"estimated_full_test_time_s": 243.0
|
||||||
@@ -1833,6 +1835,7 @@
|
|||||||
"expected_median_denoise_ms": 2020.71,
|
"expected_median_denoise_ms": 2020.71,
|
||||||
"load_peak_vram_mb": 6440.0,
|
"load_peak_vram_mb": 6440.0,
|
||||||
"runtime_peak_vram_mb": 21524.0,
|
"runtime_peak_vram_mb": 21524.0,
|
||||||
|
"warmup_peak_vram_mb": 26902.0,
|
||||||
"load_peak_allocated_mb": 6175.0,
|
"load_peak_allocated_mb": 6175.0,
|
||||||
"runtime_peak_allocated_mb": 15472.0,
|
"runtime_peak_allocated_mb": 15472.0,
|
||||||
"estimated_full_test_time_s": 204.3
|
"estimated_full_test_time_s": 204.3
|
||||||
@@ -2812,7 +2815,7 @@
|
|||||||
"expected_avg_denoise_ms": 477.49,
|
"expected_avg_denoise_ms": 477.49,
|
||||||
"expected_median_denoise_ms": 56.74,
|
"expected_median_denoise_ms": 56.74,
|
||||||
"load_peak_vram_mb": 34022.0,
|
"load_peak_vram_mb": 34022.0,
|
||||||
"runtime_peak_vram_mb": 61190.0,
|
"runtime_peak_vram_mb": 62706.0,
|
||||||
"load_peak_allocated_mb": 33866.0,
|
"load_peak_allocated_mb": 33866.0,
|
||||||
"runtime_peak_allocated_mb": 57075.0,
|
"runtime_peak_allocated_mb": 57075.0,
|
||||||
"estimated_full_test_time_s": 153.1
|
"estimated_full_test_time_s": 153.1
|
||||||
|
|||||||
@@ -465,6 +465,7 @@ class DiffusionServerBase:
|
|||||||
summary,
|
summary,
|
||||||
expected_load_peak_vram_mb,
|
expected_load_peak_vram_mb,
|
||||||
expected_runtime_peak_vram_mb,
|
expected_runtime_peak_vram_mb,
|
||||||
|
scenario.warmup_peak_vram_mb,
|
||||||
expected_load_peak_allocated_mb=scenario.load_peak_allocated_mb,
|
expected_load_peak_allocated_mb=scenario.load_peak_allocated_mb,
|
||||||
expected_runtime_peak_allocated_mb=(
|
expected_runtime_peak_allocated_mb=(
|
||||||
scenario.runtime_peak_allocated_mb
|
scenario.runtime_peak_allocated_mb
|
||||||
@@ -539,10 +540,12 @@ class DiffusionServerBase:
|
|||||||
|
|
||||||
if os.environ.get("SGLANG_GEN_BASELINE", "0") == "1":
|
if os.environ.get("SGLANG_GEN_BASELINE", "0") == "1":
|
||||||
logger.info(
|
logger.info(
|
||||||
"%s realtime peak VRAM baseline: load=%.0fMiB, runtime=%.0fMiB",
|
"%s realtime peak VRAM baseline: load=%.0fMiB, runtime=%.0fMiB, "
|
||||||
|
"warmup=%.0fMiB",
|
||||||
case.id,
|
case.id,
|
||||||
summary.load_peak_vram_mb,
|
summary.load_peak_vram_mb,
|
||||||
summary.runtime_peak_vram_mb,
|
summary.runtime_peak_vram_mb,
|
||||||
|
summary.warmup_peak_vram_mb,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -559,6 +562,7 @@ class DiffusionServerBase:
|
|||||||
summary,
|
summary,
|
||||||
scenario.load_peak_vram_mb,
|
scenario.load_peak_vram_mb,
|
||||||
scenario.runtime_peak_vram_mb,
|
scenario.runtime_peak_vram_mb,
|
||||||
|
scenario.warmup_peak_vram_mb,
|
||||||
expected_load_peak_allocated_mb=scenario.load_peak_allocated_mb,
|
expected_load_peak_allocated_mb=scenario.load_peak_allocated_mb,
|
||||||
expected_runtime_peak_allocated_mb=scenario.runtime_peak_allocated_mb,
|
expected_runtime_peak_allocated_mb=scenario.runtime_peak_allocated_mb,
|
||||||
)
|
)
|
||||||
@@ -579,6 +583,7 @@ class DiffusionServerBase:
|
|||||||
"median_denoise_ms": summary.median_denoise_ms,
|
"median_denoise_ms": summary.median_denoise_ms,
|
||||||
"load_peak_vram_mb": summary.load_peak_vram_mb,
|
"load_peak_vram_mb": summary.load_peak_vram_mb,
|
||||||
"runtime_peak_vram_mb": summary.runtime_peak_vram_mb,
|
"runtime_peak_vram_mb": summary.runtime_peak_vram_mb,
|
||||||
|
"warmup_peak_vram_mb": summary.warmup_peak_vram_mb,
|
||||||
"load_peak_allocated_mb": summary.load_peak_allocated_mb,
|
"load_peak_allocated_mb": summary.load_peak_allocated_mb,
|
||||||
"runtime_peak_allocated_mb": summary.runtime_peak_allocated_mb,
|
"runtime_peak_allocated_mb": summary.runtime_peak_allocated_mb,
|
||||||
"stage_metrics": summary.stage_metrics,
|
"stage_metrics": summary.stage_metrics,
|
||||||
@@ -680,6 +685,7 @@ class DiffusionServerBase:
|
|||||||
{
|
{
|
||||||
"load_peak_vram_mb": round(summary.load_peak_vram_mb, 2),
|
"load_peak_vram_mb": round(summary.load_peak_vram_mb, 2),
|
||||||
"runtime_peak_vram_mb": round(summary.runtime_peak_vram_mb, 2),
|
"runtime_peak_vram_mb": round(summary.runtime_peak_vram_mb, 2),
|
||||||
|
"warmup_peak_vram_mb": round(summary.warmup_peak_vram_mb, 2),
|
||||||
"load_peak_allocated_mb": round(summary.load_peak_allocated_mb, 2),
|
"load_peak_allocated_mb": round(summary.load_peak_allocated_mb, 2),
|
||||||
"runtime_peak_allocated_mb": round(
|
"runtime_peak_allocated_mb": round(
|
||||||
summary.runtime_peak_allocated_mb, 2
|
summary.runtime_peak_allocated_mb, 2
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ class ServerManager:
|
|||||||
"--log-level=debug",
|
"--log-level=debug",
|
||||||
]
|
]
|
||||||
if self.extra_args.strip():
|
if self.extra_args.strip():
|
||||||
command.extend(self.extra_args.strip().split())
|
command.extend(shlex.split(self.extra_args))
|
||||||
access_log_exclude_flag = "--uvicorn-access-log-exclude-prefixes"
|
access_log_exclude_flag = "--uvicorn-access-log-exclude-prefixes"
|
||||||
if not any(arg.startswith(access_log_exclude_flag) for arg in command):
|
if not any(arg.startswith(access_log_exclude_flag) for arg in command):
|
||||||
command.extend(["--uvicorn-access-log-exclude-prefixes", "/health"])
|
command.extend(["--uvicorn-access-log-exclude-prefixes", "/health"])
|
||||||
@@ -590,6 +590,7 @@ class PerformanceValidator:
|
|||||||
summary: PerformanceSummary,
|
summary: PerformanceSummary,
|
||||||
expected_load_peak_vram_mb: float,
|
expected_load_peak_vram_mb: float,
|
||||||
expected_runtime_peak_vram_mb: float,
|
expected_runtime_peak_vram_mb: float,
|
||||||
|
expected_warmup_peak_vram_mb: float | None = None,
|
||||||
expected_load_peak_allocated_mb: float | None = None,
|
expected_load_peak_allocated_mb: float | None = None,
|
||||||
expected_runtime_peak_allocated_mb: float | None = None,
|
expected_runtime_peak_allocated_mb: float | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -611,6 +612,16 @@ class PerformanceValidator:
|
|||||||
expected_allocated=expected_runtime_peak_allocated_mb,
|
expected_allocated=expected_runtime_peak_allocated_mb,
|
||||||
tolerance=self.tolerances.runtime_peak_vram,
|
tolerance=self.tolerances.runtime_peak_vram,
|
||||||
)
|
)
|
||||||
|
# the full-shape warmup probe keeps its own budget, separate from serving
|
||||||
|
if expected_warmup_peak_vram_mb is not None and summary.warmup_peak_vram_mb > 0:
|
||||||
|
self._assert_le(
|
||||||
|
"Warmup Peak VRAM",
|
||||||
|
summary.warmup_peak_vram_mb,
|
||||||
|
expected_warmup_peak_vram_mb,
|
||||||
|
self.tolerances.runtime_peak_vram,
|
||||||
|
min_abs_tolerance=128.0,
|
||||||
|
unit=" MiB",
|
||||||
|
)
|
||||||
|
|
||||||
def _assert_peak_vram(
|
def _assert_peak_vram(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ class ScenarioConfig:
|
|||||||
estimated_full_test_time_s: float | None = None
|
estimated_full_test_time_s: float | None = None
|
||||||
load_peak_vram_mb: float | None = None
|
load_peak_vram_mb: float | None = None
|
||||||
runtime_peak_vram_mb: float | None = None
|
runtime_peak_vram_mb: float | None = None
|
||||||
|
# Peak of the warmup calibration probe (the default workload's full shape
|
||||||
|
# under the load-safe placement); None skips the check until a baseline exists.
|
||||||
|
warmup_peak_vram_mb: float | None = None
|
||||||
# Allocated peaks; when present they are the enforced VRAM figure and the
|
# Allocated peaks; when present they are the enforced VRAM figure and the
|
||||||
# reserved peaks above are reported only (reserved tracks pool history).
|
# reserved peaks above are reported only (reserved tracks pool history).
|
||||||
load_peak_allocated_mb: float | None = None
|
load_peak_allocated_mb: float | None = None
|
||||||
@@ -140,6 +143,7 @@ class ScenarioConfig:
|
|||||||
estimated_full_test_time_s=optional_float("estimated_full_test_time_s"),
|
estimated_full_test_time_s=optional_float("estimated_full_test_time_s"),
|
||||||
load_peak_vram_mb=optional_float("load_peak_vram_mb"),
|
load_peak_vram_mb=optional_float("load_peak_vram_mb"),
|
||||||
runtime_peak_vram_mb=optional_float("runtime_peak_vram_mb"),
|
runtime_peak_vram_mb=optional_float("runtime_peak_vram_mb"),
|
||||||
|
warmup_peak_vram_mb=optional_float("warmup_peak_vram_mb"),
|
||||||
load_peak_allocated_mb=optional_float("load_peak_allocated_mb"),
|
load_peak_allocated_mb=optional_float("load_peak_allocated_mb"),
|
||||||
runtime_peak_allocated_mb=optional_float("runtime_peak_allocated_mb"),
|
runtime_peak_allocated_mb=optional_float("runtime_peak_allocated_mb"),
|
||||||
load_peak_host_anon_mb=optional_float("load_peak_host_anon_mb"),
|
load_peak_host_anon_mb=optional_float("load_peak_host_anon_mb"),
|
||||||
@@ -451,6 +455,7 @@ class PerformanceSummary:
|
|||||||
all_denoise_steps: dict[int, float]
|
all_denoise_steps: dict[int, float]
|
||||||
load_peak_vram_mb: float = 0.0
|
load_peak_vram_mb: float = 0.0
|
||||||
runtime_peak_vram_mb: float = 0.0
|
runtime_peak_vram_mb: float = 0.0
|
||||||
|
warmup_peak_vram_mb: float = 0.0
|
||||||
load_peak_allocated_mb: float = 0.0
|
load_peak_allocated_mb: float = 0.0
|
||||||
runtime_peak_allocated_mb: float = 0.0
|
runtime_peak_allocated_mb: float = 0.0
|
||||||
load_peak_host_anon_mb: float = 0.0
|
load_peak_host_anon_mb: float = 0.0
|
||||||
@@ -490,6 +495,9 @@ class PerformanceSummary:
|
|||||||
runtime_peak_vram_mb = float(
|
runtime_peak_vram_mb = float(
|
||||||
record.memory_snapshots.get("runtime_peak", {}).get("peak_reserved_mb", 0.0)
|
record.memory_snapshots.get("runtime_peak", {}).get("peak_reserved_mb", 0.0)
|
||||||
)
|
)
|
||||||
|
warmup_peak_vram_mb = float(
|
||||||
|
record.memory_snapshots.get("warmup_peak", {}).get("peak_reserved_mb", 0.0)
|
||||||
|
)
|
||||||
load_peak_allocated_mb = float(
|
load_peak_allocated_mb = float(
|
||||||
record.memory_snapshots.get("load_peak", {}).get("peak_allocated_mb", 0.0)
|
record.memory_snapshots.get("load_peak", {}).get("peak_allocated_mb", 0.0)
|
||||||
)
|
)
|
||||||
@@ -517,6 +525,7 @@ class PerformanceSummary:
|
|||||||
all_denoise_steps=per_step,
|
all_denoise_steps=per_step,
|
||||||
load_peak_vram_mb=load_peak_vram_mb,
|
load_peak_vram_mb=load_peak_vram_mb,
|
||||||
runtime_peak_vram_mb=runtime_peak_vram_mb,
|
runtime_peak_vram_mb=runtime_peak_vram_mb,
|
||||||
|
warmup_peak_vram_mb=warmup_peak_vram_mb,
|
||||||
load_peak_allocated_mb=load_peak_allocated_mb,
|
load_peak_allocated_mb=load_peak_allocated_mb,
|
||||||
runtime_peak_allocated_mb=runtime_peak_allocated_mb,
|
runtime_peak_allocated_mb=runtime_peak_allocated_mb,
|
||||||
load_peak_host_anon_mb=load_peak_host_anon_mb,
|
load_peak_host_anon_mb=load_peak_host_anon_mb,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from sglang.multimodal_gen.runtime.entrypoints.openai.realtime.realtime_output_a
|
|||||||
)
|
)
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.openai.realtime.registry import (
|
from sglang.multimodal_gen.runtime.entrypoints.openai.realtime.registry import (
|
||||||
get_realtime_model_adapter,
|
get_realtime_model_adapter,
|
||||||
|
has_realtime_model_adapter,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch
|
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch
|
||||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.lingbot_world.lingbot_world_causal_denoising import (
|
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.lingbot_world.lingbot_world_causal_denoising import (
|
||||||
@@ -976,6 +977,7 @@ def test_realtime_input_validation_reuses_generator_across_chunks():
|
|||||||
def test_realtime_registry_resolves_lingbot_adapter():
|
def test_realtime_registry_resolves_lingbot_adapter():
|
||||||
server_args = SimpleNamespace(pipeline_config=LingBotWorldCausalDMDConfig())
|
server_args = SimpleNamespace(pipeline_config=LingBotWorldCausalDMDConfig())
|
||||||
|
|
||||||
|
assert has_realtime_model_adapter(server_args)
|
||||||
adapter = get_realtime_model_adapter(server_args)
|
adapter = get_realtime_model_adapter(server_args)
|
||||||
|
|
||||||
assert isinstance(adapter, lingbot_realtime.LingBotWorldRealtimeAdapter)
|
assert isinstance(adapter, lingbot_realtime.LingBotWorldRealtimeAdapter)
|
||||||
|
|||||||
@@ -0,0 +1,921 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""Unit tests for warmup-calibrated auto residency adjustment."""
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.configs.pipeline_configs.base import ModelTaskType
|
||||||
|
from sglang.multimodal_gen.configs.pipeline_configs.longlive2 import LongLive2T2VConfig
|
||||||
|
from sglang.multimodal_gen.runtime.managers.memory_managers.auto_residency import (
|
||||||
|
ACTIVATION_EXTRAPOLATION_MARGIN,
|
||||||
|
GIB_BYTES,
|
||||||
|
DefaultWorkload,
|
||||||
|
WarmupMemoryRecord,
|
||||||
|
estimate_default_workload_peak_bytes,
|
||||||
|
estimate_default_workload_timing,
|
||||||
|
estimate_layerwise_layer_uses,
|
||||||
|
estimate_workload_phase_peaks,
|
||||||
|
resolve_measured_default_workload,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.warmup_request_builder import (
|
||||||
|
SERVER_WARMUP_MAX_VIDEO_FRAMES,
|
||||||
|
_resolve_auto_residency_warmup_shape,
|
||||||
|
_resolve_warmup_num_frames,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _record(
|
||||||
|
*,
|
||||||
|
width=832,
|
||||||
|
height=480,
|
||||||
|
num_frames=17,
|
||||||
|
baseline_gib=10,
|
||||||
|
peak_gib=12,
|
||||||
|
peak_reserved_gib=0,
|
||||||
|
succeeded=True,
|
||||||
|
num_inference_steps=1,
|
||||||
|
total_duration_ms=0.0,
|
||||||
|
stage_duration_ms=None,
|
||||||
|
step_duration_ms=(),
|
||||||
|
step_duration_ms_by_stage=None,
|
||||||
|
stage_iterations=None,
|
||||||
|
phase_active_components=None,
|
||||||
|
phase_used_components=None,
|
||||||
|
phase_full_weight_transition_components=None,
|
||||||
|
layerwise_layer_uses=None,
|
||||||
|
layerwise_layer_uses_by_stage=None,
|
||||||
|
) -> WarmupMemoryRecord:
|
||||||
|
return WarmupMemoryRecord(
|
||||||
|
width=width,
|
||||||
|
height=height,
|
||||||
|
num_frames=num_frames,
|
||||||
|
baseline_allocated_bytes=baseline_gib * GIB_BYTES,
|
||||||
|
peak_allocated_bytes=peak_gib * GIB_BYTES,
|
||||||
|
succeeded=succeeded,
|
||||||
|
peak_reserved_bytes=peak_reserved_gib * GIB_BYTES,
|
||||||
|
num_inference_steps=num_inference_steps,
|
||||||
|
total_duration_ms=total_duration_ms,
|
||||||
|
stage_duration_ms=stage_duration_ms or {},
|
||||||
|
step_duration_ms=step_duration_ms,
|
||||||
|
step_duration_ms_by_stage=step_duration_ms_by_stage or {},
|
||||||
|
stage_iterations=stage_iterations or {},
|
||||||
|
phase_active_components=phase_active_components or {},
|
||||||
|
phase_used_components=phase_used_components or {},
|
||||||
|
phase_full_weight_transition_components=(
|
||||||
|
phase_full_weight_transition_components or {}
|
||||||
|
),
|
||||||
|
layerwise_layer_uses=layerwise_layer_uses or {},
|
||||||
|
layerwise_layer_uses_by_stage=layerwise_layer_uses_by_stage or {},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEstimateDefaultWorkloadTiming:
|
||||||
|
def test_scales_only_denoising_with_target_steps(self):
|
||||||
|
record = _record(
|
||||||
|
num_inference_steps=2,
|
||||||
|
total_duration_ms=1_200,
|
||||||
|
stage_duration_ms={
|
||||||
|
"TextEncodingStage": 100,
|
||||||
|
"DenoisingStage": 1_000,
|
||||||
|
},
|
||||||
|
phase_active_components={
|
||||||
|
"0:TextEncodingStage:use:text_encoder": ("text_encoder",),
|
||||||
|
"1:DenoisingStage:use:transformer": ("transformer",),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
request_ns, stage_ns, component_stages = estimate_default_workload_timing(
|
||||||
|
records=[record],
|
||||||
|
target_units=record.workload_units(),
|
||||||
|
target_num_inference_steps=40,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert stage_ns == {
|
||||||
|
"TextEncodingStage": 100_000_000,
|
||||||
|
"DenoisingStage": 20_000_000_000,
|
||||||
|
}
|
||||||
|
assert request_ns == 20_200_000_000
|
||||||
|
assert component_stages == {
|
||||||
|
"text_encoder": ("TextEncodingStage",),
|
||||||
|
"transformer": ("DenoisingStage",),
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_uses_steady_step_instead_of_scaling_first_step_setup(self):
|
||||||
|
record = _record(
|
||||||
|
num_inference_steps=2,
|
||||||
|
total_duration_ms=720,
|
||||||
|
stage_duration_ms={
|
||||||
|
"TextEncodingStage": 100,
|
||||||
|
"DenoisingStage": 620,
|
||||||
|
},
|
||||||
|
step_duration_ms=(500, 100),
|
||||||
|
phase_active_components={
|
||||||
|
"0:TextEncodingStage:use:text_encoder": ("text_encoder",),
|
||||||
|
"1:DenoisingStage:use:transformer": ("transformer",),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
request_ns, stage_ns, _ = estimate_default_workload_timing(
|
||||||
|
records=[record],
|
||||||
|
target_units=record.workload_units(),
|
||||||
|
target_num_inference_steps=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert stage_ns["DenoisingStage"] == 1_020_000_000
|
||||||
|
assert request_ns == 1_120_000_000
|
||||||
|
|
||||||
|
def test_scales_standard_and_nonstandard_denoising_stages_together(self):
|
||||||
|
record = _record(
|
||||||
|
num_inference_steps=2,
|
||||||
|
total_duration_ms=1_200,
|
||||||
|
stage_duration_ms={
|
||||||
|
"TextEncodingStage": 100,
|
||||||
|
"DenoisingStage": 500,
|
||||||
|
"CustomDenoisingStage": 600,
|
||||||
|
},
|
||||||
|
phase_active_components={
|
||||||
|
"0:TextEncodingStage:use:text_encoder": ("text_encoder",),
|
||||||
|
"1:DenoisingStage:use:transformer": ("transformer",),
|
||||||
|
"2:CustomDenoisingStage:use:custom_refiner": ("custom_refiner",),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
request_ns, stage_ns, _ = estimate_default_workload_timing(
|
||||||
|
records=[record],
|
||||||
|
target_units=record.workload_units(),
|
||||||
|
target_num_inference_steps=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert stage_ns == {
|
||||||
|
"TextEncodingStage": 100_000_000,
|
||||||
|
"DenoisingStage": 2_500_000_000,
|
||||||
|
"CustomDenoisingStage": 3_000_000_000,
|
||||||
|
}
|
||||||
|
assert request_ns == 5_600_000_000
|
||||||
|
|
||||||
|
def test_uses_each_stage_iteration_target(self):
|
||||||
|
record = _record(
|
||||||
|
num_inference_steps=4,
|
||||||
|
total_duration_ms=1_100,
|
||||||
|
stage_duration_ms={
|
||||||
|
"ShapeDenoisingStage": 500,
|
||||||
|
"PaintStage": 600,
|
||||||
|
},
|
||||||
|
step_duration_ms_by_stage={
|
||||||
|
"ShapeDenoisingStage": (200, 100, 100, 100),
|
||||||
|
},
|
||||||
|
stage_iterations={
|
||||||
|
"ShapeDenoisingStage": (4, 50),
|
||||||
|
"PaintStage": (4, 30),
|
||||||
|
},
|
||||||
|
phase_active_components={
|
||||||
|
"0:ShapeDenoisingStage:use:hy3dshape_model": ("hy3dshape_model",),
|
||||||
|
"1:PaintStage:use:paint_transformer": ("paint_transformer",),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
request_ns, stage_ns, _ = estimate_default_workload_timing(
|
||||||
|
records=[record],
|
||||||
|
target_units=record.workload_units(),
|
||||||
|
target_num_inference_steps=50,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert stage_ns == {
|
||||||
|
"ShapeDenoisingStage": 5_000_000_000,
|
||||||
|
"PaintStage": 4_500_000_000,
|
||||||
|
}
|
||||||
|
assert request_ns == 9_500_000_000
|
||||||
|
|
||||||
|
|
||||||
|
class TestEstimateLayerwiseLayerUses:
|
||||||
|
def test_scales_repeated_groups_but_not_one_shot_groups(self):
|
||||||
|
record = _record(
|
||||||
|
num_inference_steps=2,
|
||||||
|
layerwise_layer_uses={
|
||||||
|
"transformer": {
|
||||||
|
"token_refiner.blocks": (1, 1),
|
||||||
|
"blocks": (2, 2, 2),
|
||||||
|
},
|
||||||
|
"vae": {
|
||||||
|
"encoder.down_blocks": (0, 0),
|
||||||
|
"decoder.up_blocks": (1, 1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
uses = estimate_layerwise_layer_uses(
|
||||||
|
records=[record],
|
||||||
|
target_units=record.workload_units(),
|
||||||
|
target_num_inference_steps=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert uses["transformer"] == {
|
||||||
|
"token_refiner.blocks": (1, 1),
|
||||||
|
"blocks": (10, 10, 10),
|
||||||
|
}
|
||||||
|
assert uses["vae"] == {
|
||||||
|
"encoder.down_blocks": (0, 0),
|
||||||
|
"decoder.up_blocks": (1, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_stage_role_scales_nonstandard_denoiser_but_not_encoder_repeats(self):
|
||||||
|
record = _record(
|
||||||
|
num_inference_steps=2,
|
||||||
|
phase_used_components={
|
||||||
|
"0:TextEncodingStage:use:custom_encoder": ("custom_encoder",),
|
||||||
|
"1:CustomDenoisingStage:use:custom_refiner": ("custom_refiner",),
|
||||||
|
},
|
||||||
|
layerwise_layer_uses={
|
||||||
|
"custom_encoder": {"layers": (2, 2)},
|
||||||
|
"custom_refiner": {"blocks": (2, 2)},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
uses = estimate_layerwise_layer_uses(
|
||||||
|
records=[record],
|
||||||
|
target_units=record.workload_units(),
|
||||||
|
target_num_inference_steps=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert uses["custom_encoder"]["layers"] == (2, 2)
|
||||||
|
assert uses["custom_refiner"]["blocks"] == (10, 10)
|
||||||
|
|
||||||
|
def test_scales_each_stage_calls_with_its_own_iteration_target(self):
|
||||||
|
record = _record(
|
||||||
|
num_inference_steps=4,
|
||||||
|
phase_used_components={
|
||||||
|
"0:ShapeStage:use:transformer": ("transformer",),
|
||||||
|
"1:PaintStage:use:transformer": ("transformer",),
|
||||||
|
},
|
||||||
|
stage_iterations={
|
||||||
|
"ShapeStage": (4, 50),
|
||||||
|
"PaintStage": (4, 30),
|
||||||
|
},
|
||||||
|
layerwise_layer_uses={
|
||||||
|
"transformer": {"blocks": (9, 9), "one_shot": (2, 2)}
|
||||||
|
},
|
||||||
|
layerwise_layer_uses_by_stage={
|
||||||
|
"ShapeStage": {"transformer": {"blocks": (4, 4), "one_shot": (1, 1)}},
|
||||||
|
"PaintStage": {"transformer": {"blocks": (4, 4), "one_shot": (1, 1)}},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
uses = estimate_layerwise_layer_uses(
|
||||||
|
records=[record],
|
||||||
|
target_units=record.workload_units(),
|
||||||
|
target_num_inference_steps=50,
|
||||||
|
)
|
||||||
|
|
||||||
|
# one untracked one-shot call + 50 shape calls + 30 paint calls
|
||||||
|
assert uses["transformer"]["blocks"] == (81, 81)
|
||||||
|
assert uses["transformer"]["one_shot"] == (2, 2)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEstimateDefaultWorkloadPeak:
|
||||||
|
def test_same_shape_uses_measured_peak(self):
|
||||||
|
record = _record()
|
||||||
|
estimate = estimate_default_workload_peak_bytes(
|
||||||
|
records=[record], target_units=record.workload_units()
|
||||||
|
)
|
||||||
|
assert estimate == record.peak_allocated_bytes
|
||||||
|
|
||||||
|
def test_unknown_target_disables_estimation(self):
|
||||||
|
# An unknown target would silently equate the capped warmup peak with
|
||||||
|
# the serving peak and promote with no margin at all.
|
||||||
|
record = _record()
|
||||||
|
estimate = estimate_default_workload_peak_bytes(
|
||||||
|
records=[record], target_units=None
|
||||||
|
)
|
||||||
|
assert estimate is None
|
||||||
|
|
||||||
|
def test_single_capped_record_scales_only_the_activation_part(self):
|
||||||
|
# Warmup capped to 832x480x17; Wan-class default is 704x1280x121.
|
||||||
|
record = _record()
|
||||||
|
target_units = 704 * 1280 * 121
|
||||||
|
ratio = target_units / record.workload_units()
|
||||||
|
assert ratio > 10 # the cap ratio this formula exists for
|
||||||
|
|
||||||
|
estimate = estimate_default_workload_peak_bytes(
|
||||||
|
records=[record], target_units=target_units
|
||||||
|
)
|
||||||
|
activation = record.peak_allocated_bytes - record.baseline_allocated_bytes
|
||||||
|
expected = record.baseline_allocated_bytes + int(
|
||||||
|
activation * ratio * ACTIVATION_EXTRAPOLATION_MARGIN
|
||||||
|
)
|
||||||
|
assert estimate == expected
|
||||||
|
# Scaling the whole peak would inflate the estimate by the resident
|
||||||
|
# weights times the cap ratio and adjustment would never trigger.
|
||||||
|
naive = int(record.peak_allocated_bytes * ratio)
|
||||||
|
assert estimate < naive
|
||||||
|
|
||||||
|
def test_two_point_fit_separates_constant_from_linear(self):
|
||||||
|
# Two calibration sizes let the estimator measure the slope instead
|
||||||
|
# of assuming everything above the baseline scales. Under offload the
|
||||||
|
# baseline is nearly empty, so the single-point formula would scale
|
||||||
|
# the whole peak (~x13 here); the fit extrapolates only the measured
|
||||||
|
# linear part.
|
||||||
|
small = _record(num_frames=9, baseline_gib=1, peak_gib=14)
|
||||||
|
large = _record(num_frames=17, baseline_gib=1, peak_gib=16)
|
||||||
|
target_units = 1280 * 720 * 81
|
||||||
|
|
||||||
|
estimate = estimate_default_workload_peak_bytes(
|
||||||
|
records=[small, large], target_units=target_units
|
||||||
|
)
|
||||||
|
slope = (large.peak_allocated_bytes - small.peak_allocated_bytes) / (
|
||||||
|
large.workload_units() - small.workload_units()
|
||||||
|
)
|
||||||
|
constant = large.peak_allocated_bytes - slope * large.workload_units()
|
||||||
|
expected = int(
|
||||||
|
constant + slope * target_units * ACTIVATION_EXTRAPOLATION_MARGIN
|
||||||
|
)
|
||||||
|
assert estimate == expected
|
||||||
|
single_point = estimate_default_workload_peak_bytes(
|
||||||
|
records=[large], target_units=target_units
|
||||||
|
)
|
||||||
|
assert estimate < single_point
|
||||||
|
|
||||||
|
def test_negative_slope_falls_back_to_single_point_formula(self):
|
||||||
|
small = _record(num_frames=9, peak_gib=16)
|
||||||
|
large = _record(num_frames=17, peak_gib=14)
|
||||||
|
target_units = 1280 * 720 * 81
|
||||||
|
estimate = estimate_default_workload_peak_bytes(
|
||||||
|
records=[small, large], target_units=target_units
|
||||||
|
)
|
||||||
|
fallback = max(
|
||||||
|
estimate_default_workload_peak_bytes(
|
||||||
|
records=[record], target_units=target_units
|
||||||
|
)
|
||||||
|
for record in (small, large)
|
||||||
|
)
|
||||||
|
assert estimate == fallback
|
||||||
|
|
||||||
|
def test_weight_floor_prevents_scaling_constant_component_memory(self):
|
||||||
|
small = WarmupMemoryRecord(
|
||||||
|
width=832,
|
||||||
|
height=480,
|
||||||
|
num_frames=9,
|
||||||
|
baseline_allocated_bytes=GIB_BYTES,
|
||||||
|
peak_allocated_bytes=int(29.5 * GIB_BYTES),
|
||||||
|
succeeded=True,
|
||||||
|
)
|
||||||
|
large = WarmupMemoryRecord(
|
||||||
|
width=832,
|
||||||
|
height=480,
|
||||||
|
num_frames=17,
|
||||||
|
baseline_allocated_bytes=GIB_BYTES,
|
||||||
|
peak_allocated_bytes=int(29.4 * GIB_BYTES),
|
||||||
|
succeeded=True,
|
||||||
|
)
|
||||||
|
target_units = 1024 * 1024 * 81
|
||||||
|
|
||||||
|
estimate = estimate_default_workload_peak_bytes(
|
||||||
|
records=[small, large],
|
||||||
|
target_units=target_units,
|
||||||
|
constant_weight_bytes=int(29.4 * GIB_BYTES),
|
||||||
|
)
|
||||||
|
fallback_without_weights = estimate_default_workload_peak_bytes(
|
||||||
|
records=[small, large], target_units=target_units
|
||||||
|
)
|
||||||
|
|
||||||
|
assert estimate < 50 * GIB_BYTES
|
||||||
|
assert estimate < fallback_without_weights
|
||||||
|
|
||||||
|
def test_covering_measurement_bounds_the_target(self):
|
||||||
|
capped = _record(num_frames=17, peak_gib=12)
|
||||||
|
full = _record(width=1280, height=720, num_frames=81, peak_gib=30)
|
||||||
|
estimate = estimate_default_workload_peak_bytes(
|
||||||
|
records=[capped, full], target_units=1280 * 720 * 81
|
||||||
|
)
|
||||||
|
assert estimate == full.peak_allocated_bytes
|
||||||
|
|
||||||
|
def test_multiple_records_take_the_max(self):
|
||||||
|
low = _record(peak_gib=12)
|
||||||
|
high = _record(peak_gib=20)
|
||||||
|
estimate = estimate_default_workload_peak_bytes(
|
||||||
|
records=[low, high], target_units=low.workload_units()
|
||||||
|
)
|
||||||
|
assert estimate == high.peak_allocated_bytes
|
||||||
|
|
||||||
|
def test_failure_at_the_target_size_disables_estimation(self):
|
||||||
|
good = _record(num_frames=9, peak_gib=8)
|
||||||
|
failed = _record(num_frames=17, succeeded=False)
|
||||||
|
assert (
|
||||||
|
estimate_default_workload_peak_bytes(
|
||||||
|
records=[good, failed], target_units=failed.workload_units()
|
||||||
|
)
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_failure_below_the_target_size_disables_estimation(self):
|
||||||
|
good = _record(num_frames=9, peak_gib=8)
|
||||||
|
failed = _record(num_frames=17, succeeded=False)
|
||||||
|
assert (
|
||||||
|
estimate_default_workload_peak_bytes(
|
||||||
|
records=[good, failed], target_units=failed.workload_units() * 2
|
||||||
|
)
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_failure_above_the_target_size_is_dropped(self):
|
||||||
|
good = _record(num_frames=9, peak_gib=8)
|
||||||
|
failed = _record(num_frames=81, succeeded=False)
|
||||||
|
estimate = estimate_default_workload_peak_bytes(
|
||||||
|
records=[good, failed], target_units=good.workload_units()
|
||||||
|
)
|
||||||
|
assert estimate == good.peak_allocated_bytes
|
||||||
|
|
||||||
|
def test_no_records_disables_estimation(self):
|
||||||
|
assert (
|
||||||
|
estimate_default_workload_peak_bytes(records=[], target_units=None) is None
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_phase_estimation_preserves_active_component_membership(self):
|
||||||
|
small = _record(num_frames=9, peak_gib=30)
|
||||||
|
large = _record(num_frames=17, peak_gib=32)
|
||||||
|
small = WarmupMemoryRecord(
|
||||||
|
width=small.width,
|
||||||
|
height=small.height,
|
||||||
|
num_frames=small.num_frames,
|
||||||
|
baseline_allocated_bytes=small.baseline_allocated_bytes,
|
||||||
|
peak_allocated_bytes=small.peak_allocated_bytes,
|
||||||
|
succeeded=small.succeeded,
|
||||||
|
phase_peak_allocated_bytes={"denoise": 30 * GIB_BYTES},
|
||||||
|
phase_active_components={"denoise": ("transformer",)},
|
||||||
|
)
|
||||||
|
large = WarmupMemoryRecord(
|
||||||
|
width=large.width,
|
||||||
|
height=large.height,
|
||||||
|
num_frames=large.num_frames,
|
||||||
|
baseline_allocated_bytes=large.baseline_allocated_bytes,
|
||||||
|
peak_allocated_bytes=large.peak_allocated_bytes,
|
||||||
|
succeeded=large.succeeded,
|
||||||
|
phase_peak_allocated_bytes={"denoise": 32 * GIB_BYTES},
|
||||||
|
phase_active_components={"denoise": ("transformer",)},
|
||||||
|
)
|
||||||
|
|
||||||
|
peaks, active, used, _ = estimate_workload_phase_peaks(
|
||||||
|
records=[small, large],
|
||||||
|
target_units=832 * 480 * 81,
|
||||||
|
component_weight_bytes={"transformer": 28 * GIB_BYTES},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert peaks["denoise"] >= 32 * GIB_BYTES
|
||||||
|
assert active == {"denoise": ("transformer",)}
|
||||||
|
assert used == active
|
||||||
|
|
||||||
|
def test_phase_estimation_uses_allocated_peak(self):
|
||||||
|
record = WarmupMemoryRecord(
|
||||||
|
width=1024,
|
||||||
|
height=1024,
|
||||||
|
num_frames=1,
|
||||||
|
baseline_allocated_bytes=5 * GIB_BYTES,
|
||||||
|
peak_allocated_bytes=12 * GIB_BYTES,
|
||||||
|
succeeded=True,
|
||||||
|
phase_peak_allocated_bytes={"denoise": 11 * GIB_BYTES},
|
||||||
|
phase_active_components={"denoise": ("transformer",)},
|
||||||
|
)
|
||||||
|
|
||||||
|
peaks, _, _, _ = estimate_workload_phase_peaks(
|
||||||
|
records=[record],
|
||||||
|
target_units=record.workload_units(),
|
||||||
|
component_weight_bytes={"transformer": 10 * GIB_BYTES},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert peaks["denoise"] == 11 * GIB_BYTES
|
||||||
|
|
||||||
|
def test_phase_estimation_preserves_full_weight_transition_components(self):
|
||||||
|
record = WarmupMemoryRecord(
|
||||||
|
width=1024,
|
||||||
|
height=1024,
|
||||||
|
num_frames=1,
|
||||||
|
baseline_allocated_bytes=2 * GIB_BYTES,
|
||||||
|
peak_allocated_bytes=4 * GIB_BYTES,
|
||||||
|
succeeded=True,
|
||||||
|
phase_peak_allocated_bytes={"lora_switch": 4 * GIB_BYTES},
|
||||||
|
phase_full_weight_transition_components={"lora_switch": ("transformer",)},
|
||||||
|
)
|
||||||
|
|
||||||
|
peaks, _, _, transitions = estimate_workload_phase_peaks(
|
||||||
|
records=[record],
|
||||||
|
target_units=record.workload_units(),
|
||||||
|
component_weight_bytes={"transformer": 2 * GIB_BYTES},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert peaks == {"lora_switch": 4 * GIB_BYTES}
|
||||||
|
assert transitions == {"lora_switch": ("transformer",)}
|
||||||
|
|
||||||
|
def test_phase_estimation_prefers_target_layout_over_smaller_warmup(self):
|
||||||
|
small = WarmupMemoryRecord(
|
||||||
|
width=256,
|
||||||
|
height=256,
|
||||||
|
num_frames=9,
|
||||||
|
baseline_allocated_bytes=2 * GIB_BYTES,
|
||||||
|
peak_allocated_bytes=30 * GIB_BYTES,
|
||||||
|
succeeded=True,
|
||||||
|
phase_peak_allocated_bytes={"denoise": 30 * GIB_BYTES},
|
||||||
|
phase_active_components={"denoise": ()},
|
||||||
|
)
|
||||||
|
target = WarmupMemoryRecord(
|
||||||
|
width=768,
|
||||||
|
height=512,
|
||||||
|
num_frames=25,
|
||||||
|
baseline_allocated_bytes=2 * GIB_BYTES,
|
||||||
|
peak_allocated_bytes=48 * GIB_BYTES,
|
||||||
|
succeeded=True,
|
||||||
|
phase_peak_allocated_bytes={"denoise": 48 * GIB_BYTES},
|
||||||
|
phase_active_components={"denoise": ("transformer",)},
|
||||||
|
)
|
||||||
|
|
||||||
|
peaks, active, used, _ = estimate_workload_phase_peaks(
|
||||||
|
records=[small, target],
|
||||||
|
target_units=target.workload_units(),
|
||||||
|
component_weight_bytes={"transformer": 40 * GIB_BYTES},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert peaks == {"denoise": 48 * GIB_BYTES}
|
||||||
|
assert active == {"denoise": ("transformer",)}
|
||||||
|
assert used == active
|
||||||
|
|
||||||
|
def test_phase_estimation_keeps_distinct_active_layouts_separate(self):
|
||||||
|
transformer_phase = WarmupMemoryRecord(
|
||||||
|
width=768,
|
||||||
|
height=512,
|
||||||
|
num_frames=25,
|
||||||
|
baseline_allocated_bytes=2 * GIB_BYTES,
|
||||||
|
peak_allocated_bytes=48 * GIB_BYTES,
|
||||||
|
succeeded=True,
|
||||||
|
phase_peak_allocated_bytes={"denoise": 48 * GIB_BYTES},
|
||||||
|
phase_active_components={"denoise": ("transformer",)},
|
||||||
|
)
|
||||||
|
encoder_phase = WarmupMemoryRecord(
|
||||||
|
width=768,
|
||||||
|
height=512,
|
||||||
|
num_frames=25,
|
||||||
|
baseline_allocated_bytes=2 * GIB_BYTES,
|
||||||
|
peak_allocated_bytes=20 * GIB_BYTES,
|
||||||
|
succeeded=True,
|
||||||
|
phase_peak_allocated_bytes={"denoise": 20 * GIB_BYTES},
|
||||||
|
phase_active_components={"denoise": ("text_encoder",)},
|
||||||
|
)
|
||||||
|
|
||||||
|
peaks, active, used, _ = estimate_workload_phase_peaks(
|
||||||
|
records=[transformer_phase, encoder_phase],
|
||||||
|
target_units=transformer_phase.workload_units(),
|
||||||
|
component_weight_bytes={
|
||||||
|
"transformer": 40 * GIB_BYTES,
|
||||||
|
"text_encoder": 10 * GIB_BYTES,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert peaks == {
|
||||||
|
"denoise:layout:0": 20 * GIB_BYTES,
|
||||||
|
"denoise:layout:1": 48 * GIB_BYTES,
|
||||||
|
}
|
||||||
|
assert active == {
|
||||||
|
"denoise:layout:0": ("text_encoder",),
|
||||||
|
"denoise:layout:1": ("transformer",),
|
||||||
|
}
|
||||||
|
assert used == active
|
||||||
|
|
||||||
|
|
||||||
|
class TestResolveMeasuredDefaultWorkload:
|
||||||
|
def test_uses_effective_warmup_resolution_for_implicit_image_size(self):
|
||||||
|
workload = DefaultWorkload(
|
||||||
|
width=None,
|
||||||
|
height=None,
|
||||||
|
num_frames=1,
|
||||||
|
num_inference_steps=40,
|
||||||
|
)
|
||||||
|
|
||||||
|
resolved = resolve_measured_default_workload(
|
||||||
|
workload,
|
||||||
|
[
|
||||||
|
_record(width=512, height=512),
|
||||||
|
_record(width=1024, height=1024),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resolved == DefaultWorkload(
|
||||||
|
width=1024,
|
||||||
|
height=1024,
|
||||||
|
num_frames=1,
|
||||||
|
num_inference_steps=40,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_keeps_default_frames_when_warmup_caps_video(self):
|
||||||
|
workload = DefaultWorkload(
|
||||||
|
width=None,
|
||||||
|
height=None,
|
||||||
|
num_frames=81,
|
||||||
|
num_inference_steps=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
resolved = resolve_measured_default_workload(
|
||||||
|
workload, [_record(width=832, height=480, num_frames=17)]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resolved.num_frames == 81
|
||||||
|
|
||||||
|
def test_does_not_replace_explicit_default_shape(self):
|
||||||
|
workload = DefaultWorkload(
|
||||||
|
width=1280,
|
||||||
|
height=720,
|
||||||
|
num_frames=81,
|
||||||
|
num_inference_steps=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
resolve_measured_default_workload(
|
||||||
|
workload, [_record(width=512, height=512)]
|
||||||
|
)
|
||||||
|
is workload
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestWarmupFrameAdjustment:
|
||||||
|
def _server_args(self, *, bcg: bool = False, num_gpus: int = 1) -> SimpleNamespace:
|
||||||
|
return SimpleNamespace(
|
||||||
|
pipeline_config=LongLive2T2VConfig(),
|
||||||
|
pipeline_class_name=None,
|
||||||
|
enable_breakable_cuda_graph=bcg,
|
||||||
|
num_gpus=num_gpus,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _defaults(self) -> SimpleNamespace:
|
||||||
|
return SimpleNamespace(
|
||||||
|
num_frames=61,
|
||||||
|
adjust_frames=True,
|
||||||
|
enable_sequence_shard=None,
|
||||||
|
num_frames_round_down=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_capped_frames_keep_the_model_frame_contract(self):
|
||||||
|
# LongLive2 default 61 frames is capped to 17, whose 5 latent frames
|
||||||
|
# break the 8-frame causal block; the builder must re-align to 29.
|
||||||
|
assert SERVER_WARMUP_MAX_VIDEO_FRAMES == 17
|
||||||
|
num_frames = _resolve_warmup_num_frames(
|
||||||
|
self._server_args(), self._defaults(), server_based_warmup=True
|
||||||
|
)
|
||||||
|
assert num_frames == 29
|
||||||
|
|
||||||
|
def test_bcg_keeps_full_serving_frames(self):
|
||||||
|
num_frames = _resolve_warmup_num_frames(
|
||||||
|
self._server_args(bcg=True), self._defaults(), server_based_warmup=True
|
||||||
|
)
|
||||||
|
assert num_frames == 61
|
||||||
|
|
||||||
|
def test_non_server_warmup_keeps_default_frames(self):
|
||||||
|
num_frames = _resolve_warmup_num_frames(
|
||||||
|
self._server_args(), self._defaults(), server_based_warmup=False
|
||||||
|
)
|
||||||
|
assert num_frames == 61
|
||||||
|
|
||||||
|
def test_capped_frames_get_the_gpu_alignment_real_requests_get(self):
|
||||||
|
# a frame-aligning pipeline (no sequence shard) on multiple GPUs:
|
||||||
|
# 17 frames -> 5 latent frames -> ceil to 6 latents on 2 GPUs -> 21
|
||||||
|
args = SimpleNamespace(
|
||||||
|
pipeline_config=SimpleNamespace(
|
||||||
|
task_type=ModelTaskType.T2V,
|
||||||
|
adjust_num_frames=lambda n: n,
|
||||||
|
vae_config=SimpleNamespace(
|
||||||
|
use_temporal_scaling_frames=True,
|
||||||
|
arch_config=SimpleNamespace(temporal_compression_ratio=4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pipeline_class_name=None,
|
||||||
|
enable_breakable_cuda_graph=False,
|
||||||
|
num_gpus=2,
|
||||||
|
)
|
||||||
|
defaults = SimpleNamespace(
|
||||||
|
num_frames=81,
|
||||||
|
adjust_frames=True,
|
||||||
|
enable_sequence_shard=None,
|
||||||
|
num_frames_round_down=False,
|
||||||
|
)
|
||||||
|
num_frames = _resolve_warmup_num_frames(
|
||||||
|
args, defaults, server_based_warmup=True
|
||||||
|
)
|
||||||
|
assert num_frames == 21
|
||||||
|
|
||||||
|
|
||||||
|
class TestAutoResidencyWarmupShape:
|
||||||
|
def _patch_gate(self, monkeypatch, reason: str | None = None) -> None:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"sglang.multimodal_gen.runtime.warmup_request_builder.auto_residency_args_skip_reason",
|
||||||
|
lambda _args: reason,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _wan_like_args(self) -> SimpleNamespace:
|
||||||
|
return SimpleNamespace(
|
||||||
|
pipeline_config=SimpleNamespace(
|
||||||
|
task_type=ModelTaskType.T2V,
|
||||||
|
adjust_num_frames=lambda n: n,
|
||||||
|
),
|
||||||
|
num_gpus=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _defaults(
|
||||||
|
self,
|
||||||
|
num_frames: int,
|
||||||
|
*,
|
||||||
|
width: int | None = 1280,
|
||||||
|
height: int | None = 720,
|
||||||
|
supported_resolutions=None,
|
||||||
|
) -> SimpleNamespace:
|
||||||
|
return SimpleNamespace(
|
||||||
|
width=width,
|
||||||
|
height=height,
|
||||||
|
num_frames=num_frames,
|
||||||
|
supported_resolutions=supported_resolutions,
|
||||||
|
adjust_frames=True,
|
||||||
|
enable_sequence_shard=None,
|
||||||
|
num_frames_round_down=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_capped_video_gets_a_full_shape_probe(self, monkeypatch):
|
||||||
|
self._patch_gate(monkeypatch)
|
||||||
|
probe = _resolve_auto_residency_warmup_shape(
|
||||||
|
self._wan_like_args(),
|
||||||
|
self._defaults(81),
|
||||||
|
warmup_shape=(832, 480, 17),
|
||||||
|
server_based_warmup=True,
|
||||||
|
)
|
||||||
|
assert probe == (1280, 720, 81)
|
||||||
|
|
||||||
|
def test_matching_warmup_needs_no_probe(self, monkeypatch):
|
||||||
|
self._patch_gate(monkeypatch)
|
||||||
|
probe = _resolve_auto_residency_warmup_shape(
|
||||||
|
self._wan_like_args(),
|
||||||
|
self._defaults(17, width=832, height=480),
|
||||||
|
warmup_shape=(832, 480, 17),
|
||||||
|
server_based_warmup=True,
|
||||||
|
)
|
||||||
|
assert probe is None
|
||||||
|
|
||||||
|
def test_unknown_target_resolution_skips_probe(self, monkeypatch):
|
||||||
|
self._patch_gate(monkeypatch)
|
||||||
|
probe = _resolve_auto_residency_warmup_shape(
|
||||||
|
self._wan_like_args(),
|
||||||
|
self._defaults(81, width=None, height=None),
|
||||||
|
warmup_shape=(832, 480, 17),
|
||||||
|
server_based_warmup=True,
|
||||||
|
)
|
||||||
|
assert probe is None
|
||||||
|
|
||||||
|
def test_skip_gate_disables_probe(self, monkeypatch):
|
||||||
|
# Full-shape calibration must share the adjustment's own gate (kill
|
||||||
|
# switch, quantized, manual, ...).
|
||||||
|
self._patch_gate(monkeypatch, reason="performance_mode=manual")
|
||||||
|
probe = _resolve_auto_residency_warmup_shape(
|
||||||
|
self._wan_like_args(),
|
||||||
|
self._defaults(81),
|
||||||
|
warmup_shape=(832, 480, 17),
|
||||||
|
server_based_warmup=True,
|
||||||
|
)
|
||||||
|
assert probe is None
|
||||||
|
|
||||||
|
def test_supported_resolution_fills_missing_target_size(self, monkeypatch):
|
||||||
|
self._patch_gate(monkeypatch)
|
||||||
|
probe = _resolve_auto_residency_warmup_shape(
|
||||||
|
self._wan_like_args(),
|
||||||
|
self._defaults(
|
||||||
|
81,
|
||||||
|
width=None,
|
||||||
|
height=None,
|
||||||
|
supported_resolutions=[(832, 480), (1024, 1024)],
|
||||||
|
),
|
||||||
|
warmup_shape=(832, 480, 17),
|
||||||
|
server_based_warmup=True,
|
||||||
|
)
|
||||||
|
assert probe == (1024, 1024, 81)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAutoResidencySkipReason:
|
||||||
|
def _base_args(self, **overrides) -> SimpleNamespace:
|
||||||
|
args = SimpleNamespace(
|
||||||
|
performance_mode="auto",
|
||||||
|
warmup_mode="server",
|
||||||
|
warmup_resolutions=None,
|
||||||
|
disagg_role="monolithic",
|
||||||
|
backend="sglang",
|
||||||
|
enable_breakable_cuda_graph=False,
|
||||||
|
enable_torch_compile=False,
|
||||||
|
batching_max_size=1,
|
||||||
|
dp_size=1,
|
||||||
|
ulysses_degree=1,
|
||||||
|
use_fsdp_inference=False,
|
||||||
|
quantization=None,
|
||||||
|
component_quantizations={},
|
||||||
|
transformer_weights_path=None,
|
||||||
|
nunchaku_config=None,
|
||||||
|
direct_gpu_weight_loading=False,
|
||||||
|
ltx2_two_stage_device_mode=None,
|
||||||
|
pipeline_class_name=None,
|
||||||
|
pipeline_config=SimpleNamespace(
|
||||||
|
task_type=ModelTaskType.T2V,
|
||||||
|
supports_auto_residency=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for key, value in overrides.items():
|
||||||
|
setattr(args, key, value)
|
||||||
|
return args
|
||||||
|
|
||||||
|
def _skip_reason(self, args):
|
||||||
|
from sglang.multimodal_gen.runtime.server_warmup import (
|
||||||
|
auto_residency_skip_reason,
|
||||||
|
)
|
||||||
|
|
||||||
|
return auto_residency_skip_reason(args)
|
||||||
|
|
||||||
|
def test_env_kill_switch(self, monkeypatch):
|
||||||
|
monkeypatch.setenv("SGLANG_DIFFUSION_DISABLE_AUTO_RESIDENCY", "1")
|
||||||
|
reason = self._skip_reason(self._base_args())
|
||||||
|
assert (
|
||||||
|
reason is not None and "SGLANG_DIFFUSION_DISABLE_AUTO_RESIDENCY" in reason
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_manual_performance_mode(self, monkeypatch):
|
||||||
|
monkeypatch.delenv("SGLANG_DIFFUSION_DISABLE_AUTO_RESIDENCY", raising=False)
|
||||||
|
reason = self._skip_reason(self._base_args(performance_mode="manual"))
|
||||||
|
assert reason == "performance_mode=manual"
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"overrides, expected_fragment",
|
||||||
|
[
|
||||||
|
({"warmup_mode": "request"}, "server warmup"),
|
||||||
|
({"disagg_role": "denoiser"}, "server warmup"),
|
||||||
|
({"backend": "diffusers"}, "diffusers"),
|
||||||
|
(
|
||||||
|
{"ltx2_two_stage_device_mode": "original"},
|
||||||
|
"LTX-2 original two-stage placement",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"pipeline_class_name": "LTX2TwoStagePipeline"},
|
||||||
|
"legacy LTX-2 two-stage placement",
|
||||||
|
),
|
||||||
|
({"enable_breakable_cuda_graph": True}, "CUDA graph"),
|
||||||
|
# compile warmup strips the memory layout (layerwise DiT +
|
||||||
|
# resident aux components on CPU): its peaks are not serving peaks
|
||||||
|
({"enable_torch_compile": True}, "stripped memory layout"),
|
||||||
|
({"batching_max_size": 4}, "batching"),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"pipeline_config": SimpleNamespace(
|
||||||
|
task_type=ModelTaskType.T2V,
|
||||||
|
supports_auto_residency=False,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
"post-warmup residency changes",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_excluded_paths(self, monkeypatch, overrides, expected_fragment):
|
||||||
|
monkeypatch.delenv("SGLANG_DIFFUSION_DISABLE_AUTO_RESIDENCY", raising=False)
|
||||||
|
monkeypatch.delenv("SGLANG_CACHE_DIT_ENABLED", raising=False)
|
||||||
|
reason = self._skip_reason(self._base_args(**overrides))
|
||||||
|
assert reason is not None and expected_fragment in reason
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"overrides",
|
||||||
|
[
|
||||||
|
{"quantization": "fp8"},
|
||||||
|
{"component_quantizations": {"image_encoder": "fp8"}},
|
||||||
|
{"transformer_weights_path": "/x.safetensors"},
|
||||||
|
{"direct_gpu_weight_loading": True},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_fixed_loading_paths_still_calibrate_other_components(
|
||||||
|
self, monkeypatch, overrides
|
||||||
|
):
|
||||||
|
monkeypatch.delenv("SGLANG_DIFFUSION_DISABLE_AUTO_RESIDENCY", raising=False)
|
||||||
|
monkeypatch.delenv("SGLANG_CACHE_DIT_ENABLED", raising=False)
|
||||||
|
reason = self._skip_reason(self._base_args(**overrides))
|
||||||
|
assert reason is None or reason == "requires CUDA"
|
||||||
|
|
||||||
|
def test_cache_dit_excluded(self, monkeypatch):
|
||||||
|
monkeypatch.delenv("SGLANG_DIFFUSION_DISABLE_AUTO_RESIDENCY", raising=False)
|
||||||
|
monkeypatch.setenv("SGLANG_CACHE_DIT_ENABLED", "true")
|
||||||
|
reason = self._skip_reason(self._base_args())
|
||||||
|
assert reason is not None and "cache-dit" in reason
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"overrides",
|
||||||
|
[
|
||||||
|
{"dp_size": 2},
|
||||||
|
{"ulysses_degree": 2},
|
||||||
|
{"use_fsdp_inference": True},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_parallel_paths_reach_platform_gate(self, monkeypatch, overrides):
|
||||||
|
monkeypatch.delenv("SGLANG_DIFFUSION_DISABLE_AUTO_RESIDENCY", raising=False)
|
||||||
|
monkeypatch.delenv("SGLANG_CACHE_DIT_ENABLED", raising=False)
|
||||||
|
reason = self._skip_reason(self._base_args(**overrides))
|
||||||
|
assert reason is None or reason == "requires CUDA"
|
||||||
|
|
||||||
|
def test_eligible_path_reaches_platform_gate(self, monkeypatch):
|
||||||
|
monkeypatch.delenv("SGLANG_DIFFUSION_DISABLE_AUTO_RESIDENCY", raising=False)
|
||||||
|
monkeypatch.delenv("SGLANG_CACHE_DIT_ENABLED", raising=False)
|
||||||
|
reason = self._skip_reason(self._base_args())
|
||||||
|
# on a CUDA host everything passes; CPU CI stops at the platform gate
|
||||||
|
assert reason is None or reason == "requires CUDA"
|
||||||
@@ -28,12 +28,13 @@ from sglang.multimodal_gen.configs.pipeline_configs.longlive2 import (
|
|||||||
LongLive2T2VConfig,
|
LongLive2T2VConfig,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.configs.sample.longlive2 import LongLive2SamplingParams
|
from sglang.multimodal_gen.configs.sample.longlive2 import LongLive2SamplingParams
|
||||||
|
from sglang.multimodal_gen.configs.sample.minimax_h3 import MiniMaxH3SamplingParams
|
||||||
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
|
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.diffusion_generator import DiffGenerator
|
from sglang.multimodal_gen.runtime.entrypoints.control_requests import (
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.utils import (
|
|
||||||
SetLoraReq,
|
SetLoraReq,
|
||||||
UnmergeLoraWeightsReq,
|
UnmergeLoraWeightsReq,
|
||||||
)
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.entrypoints.diffusion_generator import DiffGenerator
|
||||||
from sglang.multimodal_gen.runtime.managers.scheduler import Scheduler
|
from sglang.multimodal_gen.runtime.managers.scheduler import Scheduler
|
||||||
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import (
|
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import (
|
||||||
OutputBatch,
|
OutputBatch,
|
||||||
@@ -53,6 +54,7 @@ from sglang.multimodal_gen.runtime.server_warmup import (
|
|||||||
from sglang.multimodal_gen.runtime.warmup_request_builder import (
|
from sglang.multimodal_gen.runtime.warmup_request_builder import (
|
||||||
DEFAULT_PLACEHOLDER_PROMPT,
|
DEFAULT_PLACEHOLDER_PROMPT,
|
||||||
SERVER_WARMUP_IMAGE_FALLBACK_RESOLUTION,
|
SERVER_WARMUP_IMAGE_FALLBACK_RESOLUTION,
|
||||||
|
_apply_warmup_sampling_overrides,
|
||||||
_resolve_warmup_num_frames,
|
_resolve_warmup_num_frames,
|
||||||
build_warmup_reqs,
|
build_warmup_reqs,
|
||||||
should_include_warmup_image,
|
should_include_warmup_image,
|
||||||
@@ -108,6 +110,51 @@ def _make_validation_server_args(enable_cfg_parallel: bool) -> MagicMock:
|
|||||||
class TestWarmupReqCfgParallel(unittest.TestCase):
|
class TestWarmupReqCfgParallel(unittest.TestCase):
|
||||||
"""Warmup request construction and req-based warmup guards."""
|
"""Warmup request construction and req-based warmup guards."""
|
||||||
|
|
||||||
|
def test_sampling_workload_override_accepts_json(self):
|
||||||
|
defaults = SamplingParams(
|
||||||
|
width=1024,
|
||||||
|
height=1024,
|
||||||
|
num_frames=81,
|
||||||
|
num_inference_steps=35,
|
||||||
|
)
|
||||||
|
server_args = SimpleNamespace(
|
||||||
|
warmup_sampling_params=(
|
||||||
|
'{"width":832,"height":480,"num_frames":9,"num_inference_steps":4}'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
overridden = _apply_warmup_sampling_overrides(server_args, defaults)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
(
|
||||||
|
overridden.width,
|
||||||
|
overridden.height,
|
||||||
|
overridden.num_frames,
|
||||||
|
overridden.num_inference_steps,
|
||||||
|
),
|
||||||
|
(832, 480, 9, 4),
|
||||||
|
)
|
||||||
|
self.assertEqual((defaults.width, defaults.height), (1024, 1024))
|
||||||
|
|
||||||
|
def test_sampling_workload_override_rejects_unknown_field(self):
|
||||||
|
server_args = SimpleNamespace(
|
||||||
|
warmup_sampling_params={"not_a_sampling_field": 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(ValueError, "invalid --warmup-sampling-params"):
|
||||||
|
_apply_warmup_sampling_overrides(server_args, SamplingParams())
|
||||||
|
|
||||||
|
def test_sampling_workload_override_supports_fixed_model_fields(self):
|
||||||
|
defaults = MiniMaxH3SamplingParams()
|
||||||
|
server_args = SimpleNamespace(
|
||||||
|
warmup_sampling_params={"num_frames": 49, "fps": 12}
|
||||||
|
)
|
||||||
|
|
||||||
|
overridden = _apply_warmup_sampling_overrides(server_args, defaults)
|
||||||
|
|
||||||
|
self.assertEqual((overridden.num_frames, overridden.fps), (49, 12))
|
||||||
|
self.assertEqual((defaults.num_frames, defaults.fps), (1, 24))
|
||||||
|
|
||||||
def test_warmup_req_cfg_parallel_sets_do_cfg(self):
|
def test_warmup_req_cfg_parallel_sets_do_cfg(self):
|
||||||
server_args = _make_bare_scheduler(enable_cfg_parallel=True).server_args
|
server_args = _make_bare_scheduler(enable_cfg_parallel=True).server_args
|
||||||
sampling_defaults = SamplingParams()
|
sampling_defaults = SamplingParams()
|
||||||
@@ -270,6 +317,7 @@ class TestWarmupReqCfgParallel(unittest.TestCase):
|
|||||||
server_args.warmup_steps = 1
|
server_args.warmup_steps = 1
|
||||||
server_args.enable_cfg_parallel = False
|
server_args.enable_cfg_parallel = False
|
||||||
server_args.enable_torch_compile = False
|
server_args.enable_torch_compile = False
|
||||||
|
server_args.num_gpus = 1
|
||||||
|
|
||||||
server_args.pipeline_config.task_type = ModelTaskType.T2V
|
server_args.pipeline_config.task_type = ModelTaskType.T2V
|
||||||
server_args.pipeline_config.adjust_num_frames.side_effect = lambda value: value
|
server_args.pipeline_config.adjust_num_frames.side_effect = lambda value: value
|
||||||
@@ -334,6 +382,68 @@ class TestWarmupReqCfgParallel(unittest.TestCase):
|
|||||||
self.assertTrue(req.extra["return_warmup_result"])
|
self.assertTrue(req.extra["return_warmup_result"])
|
||||||
self.assertTrue(req.extra["server_based_warmup"])
|
self.assertTrue(req.extra["server_based_warmup"])
|
||||||
|
|
||||||
|
def test_auto_residency_uses_one_full_serving_shape_probe(self):
|
||||||
|
server_args = SimpleNamespace(
|
||||||
|
warmup_steps=1,
|
||||||
|
enable_cfg_parallel=False,
|
||||||
|
enable_torch_compile=False,
|
||||||
|
enable_breakable_cuda_graph=False,
|
||||||
|
pipeline_class_name=None,
|
||||||
|
num_gpus=1,
|
||||||
|
pipeline_config=SimpleNamespace(
|
||||||
|
task_type=ModelTaskType.T2V,
|
||||||
|
adjust_num_frames=lambda value: value,
|
||||||
|
vae_stride=None,
|
||||||
|
vae_scale_factor=None,
|
||||||
|
vae_config=SimpleNamespace(arch_config=None),
|
||||||
|
),
|
||||||
|
is_arg_explicitly_set=lambda _name: False,
|
||||||
|
)
|
||||||
|
sampling_defaults = SamplingParams(
|
||||||
|
width=1280,
|
||||||
|
height=720,
|
||||||
|
num_frames=81,
|
||||||
|
num_inference_steps=35,
|
||||||
|
adjust_frames=False,
|
||||||
|
supported_resolutions=[(1280, 720), (832, 480)],
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"sglang.multimodal_gen.runtime.warmup_request_builder.get_model_sampling_defaults",
|
||||||
|
return_value=sampling_defaults,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"sglang.multimodal_gen.runtime.warmup_request_builder.auto_residency_args_skip_reason",
|
||||||
|
return_value=None,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
reqs = build_warmup_reqs(
|
||||||
|
server_args,
|
||||||
|
warmup_resolutions=None,
|
||||||
|
server_based_warmup=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# the bounded warmup runs first so the worker can size the probe, and
|
||||||
|
# once more after it so serving starts from a serving-shaped pool
|
||||||
|
self.assertEqual(len(reqs), 3)
|
||||||
|
self.assertFalse(reqs[0].extra.get("auto_residency_full_shape_probe"))
|
||||||
|
self.assertFalse(reqs[2].extra.get("auto_residency_full_shape_probe"))
|
||||||
|
self.assertEqual(
|
||||||
|
(reqs[2].width, reqs[2].height, reqs[2].num_frames),
|
||||||
|
(reqs[0].width, reqs[0].height, reqs[0].num_frames),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
(reqs[1].width, reqs[1].height, reqs[1].num_frames),
|
||||||
|
(1280, 720, 81),
|
||||||
|
)
|
||||||
|
self.assertTrue(reqs[1].extra["auto_residency_full_shape_probe"])
|
||||||
|
self.assertFalse(reqs[1].metrics.suppress_stage_breakdown)
|
||||||
|
self.assertEqual(reqs[1].num_inference_steps, 4)
|
||||||
|
self.assertIn(
|
||||||
|
"auto residency probe (1280x720x81f, 4/35 steps)",
|
||||||
|
format_warmup_req(reqs[1]),
|
||||||
|
)
|
||||||
|
|
||||||
def test_server_based_warmup_uses_model_default_resolution(self):
|
def test_server_based_warmup_uses_model_default_resolution(self):
|
||||||
server_args = MagicMock()
|
server_args = MagicMock()
|
||||||
server_args.warmup_steps = 1
|
server_args.warmup_steps = 1
|
||||||
@@ -362,6 +472,7 @@ class TestWarmupReqCfgParallel(unittest.TestCase):
|
|||||||
server_args.warmup_steps = 1
|
server_args.warmup_steps = 1
|
||||||
server_args.enable_cfg_parallel = False
|
server_args.enable_cfg_parallel = False
|
||||||
server_args.enable_torch_compile = False
|
server_args.enable_torch_compile = False
|
||||||
|
server_args.num_gpus = 1
|
||||||
|
|
||||||
server_args.pipeline_config.task_type = ModelTaskType.T2V
|
server_args.pipeline_config.task_type = ModelTaskType.T2V
|
||||||
server_args.pipeline_config.adjust_num_frames.side_effect = lambda value: value
|
server_args.pipeline_config.adjust_num_frames.side_effect = lambda value: value
|
||||||
@@ -474,6 +585,7 @@ class TestWarmupReqCfgParallel(unittest.TestCase):
|
|||||||
server_args.warmup_steps = 1
|
server_args.warmup_steps = 1
|
||||||
server_args.enable_cfg_parallel = False
|
server_args.enable_cfg_parallel = False
|
||||||
server_args.enable_torch_compile = False
|
server_args.enable_torch_compile = False
|
||||||
|
server_args.num_gpus = 1
|
||||||
|
|
||||||
server_args.pipeline_config.task_type = ModelTaskType.T2V
|
server_args.pipeline_config.task_type = ModelTaskType.T2V
|
||||||
server_args.pipeline_config.adjust_num_frames.side_effect = lambda value: value
|
server_args.pipeline_config.adjust_num_frames.side_effect = lambda value: value
|
||||||
@@ -503,6 +615,7 @@ class TestWarmupReqCfgParallel(unittest.TestCase):
|
|||||||
pipeline_config=pipeline_config,
|
pipeline_config=pipeline_config,
|
||||||
enable_breakable_cuda_graph=False,
|
enable_breakable_cuda_graph=False,
|
||||||
pipeline_class_name=None,
|
pipeline_class_name=None,
|
||||||
|
num_gpus=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
num_frames = _resolve_warmup_num_frames(
|
num_frames = _resolve_warmup_num_frames(
|
||||||
@@ -545,6 +658,7 @@ class TestWarmupReqCfgParallel(unittest.TestCase):
|
|||||||
server_args.warmup_steps = 1
|
server_args.warmup_steps = 1
|
||||||
server_args.enable_cfg_parallel = False
|
server_args.enable_cfg_parallel = False
|
||||||
server_args.enable_torch_compile = False
|
server_args.enable_torch_compile = False
|
||||||
|
server_args.num_gpus = 1
|
||||||
|
|
||||||
server_args.pipeline_config.task_type = ModelTaskType.T2V
|
server_args.pipeline_config.task_type = ModelTaskType.T2V
|
||||||
server_args.pipeline_config.adjust_num_frames.side_effect = lambda value: value
|
server_args.pipeline_config.adjust_num_frames.side_effect = lambda value: value
|
||||||
@@ -581,10 +695,15 @@ class TestWarmupReqCfgParallel(unittest.TestCase):
|
|||||||
server_args.warmup_steps = 1
|
server_args.warmup_steps = 1
|
||||||
server_args.enable_cfg_parallel = False
|
server_args.enable_cfg_parallel = False
|
||||||
server_args.enable_torch_compile = False
|
server_args.enable_torch_compile = False
|
||||||
|
server_args.num_gpus = 1
|
||||||
server_args.pipeline_class_name = "LTX2TwoStageHQPipeline"
|
server_args.pipeline_class_name = "LTX2TwoStageHQPipeline"
|
||||||
|
|
||||||
server_args.pipeline_config.task_type = ModelTaskType.T2V
|
server_args.pipeline_config.task_type = ModelTaskType.T2V
|
||||||
server_args.pipeline_config.vae_scale_factor = 32
|
server_args.pipeline_config.vae_scale_factor = 32
|
||||||
|
server_args.pipeline_config.vae_config = SimpleNamespace(
|
||||||
|
use_temporal_scaling_frames=True,
|
||||||
|
arch_config=SimpleNamespace(temporal_compression_ratio=8),
|
||||||
|
)
|
||||||
server_args.pipeline_config.adjust_num_frames.return_value = 25
|
server_args.pipeline_config.adjust_num_frames.return_value = 25
|
||||||
server_args.num_gpus = 2
|
server_args.num_gpus = 2
|
||||||
|
|
||||||
@@ -725,6 +844,7 @@ class TestWarmupReqCfgParallel(unittest.TestCase):
|
|||||||
server_args.is_arg_explicitly_set.return_value = False
|
server_args.is_arg_explicitly_set.return_value = False
|
||||||
server_args.pipeline_config = SimpleNamespace(
|
server_args.pipeline_config = SimpleNamespace(
|
||||||
task_type=ModelTaskType.I2M,
|
task_type=ModelTaskType.I2M,
|
||||||
|
supports_auto_residency=True,
|
||||||
vae_stride=None,
|
vae_stride=None,
|
||||||
vae_scale_factor=None,
|
vae_scale_factor=None,
|
||||||
vae_config=None,
|
vae_config=None,
|
||||||
@@ -772,6 +892,33 @@ class TestWarmupReqCfgParallel(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(reqs[0].image_path, ["/tmp/warmup.png"])
|
self.assertEqual(reqs[0].image_path, ["/tmp/warmup.png"])
|
||||||
|
|
||||||
|
def test_server_based_warmup_keeps_image_input_count(self):
|
||||||
|
server_args = MagicMock()
|
||||||
|
server_args.warmup_steps = 1
|
||||||
|
server_args.enable_cfg_parallel = False
|
||||||
|
server_args.enable_torch_compile = False
|
||||||
|
server_args.pipeline_config.task_type = ModelTaskType.TI2I
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"sglang.multimodal_gen.runtime.warmup_request_builder.get_model_sampling_defaults",
|
||||||
|
return_value=SamplingParams(
|
||||||
|
width=512,
|
||||||
|
height=512,
|
||||||
|
image_path=["first.png", "second.png"],
|
||||||
|
),
|
||||||
|
):
|
||||||
|
reqs = build_warmup_reqs(
|
||||||
|
server_args,
|
||||||
|
warmup_resolutions=None,
|
||||||
|
warmup_input_path="/tmp/warmup.png",
|
||||||
|
server_based_warmup=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
reqs[0].image_path,
|
||||||
|
["/tmp/warmup.png", "/tmp/warmup.png"],
|
||||||
|
)
|
||||||
|
|
||||||
def test_server_based_warmup_keeps_required_image_input(self):
|
def test_server_based_warmup_keeps_required_image_input(self):
|
||||||
server_args = MagicMock()
|
server_args = MagicMock()
|
||||||
server_args.warmup_steps = 1
|
server_args.warmup_steps = 1
|
||||||
@@ -797,6 +944,7 @@ class TestWarmupReqCfgParallel(unittest.TestCase):
|
|||||||
server_args.warmup_steps = 1
|
server_args.warmup_steps = 1
|
||||||
server_args.enable_cfg_parallel = False
|
server_args.enable_cfg_parallel = False
|
||||||
server_args.enable_torch_compile = False
|
server_args.enable_torch_compile = False
|
||||||
|
server_args.num_gpus = 1
|
||||||
server_args.pipeline_config.task_type = ModelTaskType.TI2V
|
server_args.pipeline_config.task_type = ModelTaskType.TI2V
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from sglang.multimodal_gen.runtime.managers.memory_managers.component_manager im
|
|||||||
ComponentResidencyManager,
|
ComponentResidencyManager,
|
||||||
ComponentUse,
|
ComponentUse,
|
||||||
ResidencyState,
|
ResidencyState,
|
||||||
|
WarmupPhasePeak,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.runtime.managers.memory_managers.component_residency import (
|
from sglang.multimodal_gen.runtime.managers.memory_managers.component_residency import (
|
||||||
ComponentResidencyError,
|
ComponentResidencyError,
|
||||||
@@ -22,9 +23,19 @@ from sglang.multimodal_gen.runtime.pipelines_core.stages.image_encoding import (
|
|||||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.realtime.text_encoding import (
|
from sglang.multimodal_gen.runtime.pipelines_core.stages.realtime.text_encoding import (
|
||||||
RealtimeTextEncodingStage,
|
RealtimeTextEncodingStage,
|
||||||
)
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.platforms import current_platform
|
||||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||||
|
|
||||||
|
|
||||||
|
def _server_args(*, supports_auto_residency=True):
|
||||||
|
return SimpleNamespace(
|
||||||
|
enable_layerwise_nvtx_marker=False,
|
||||||
|
pipeline_config=SimpleNamespace(
|
||||||
|
supports_auto_residency=supports_auto_residency,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_component_offload_releases_preferred_component_after_request():
|
def test_component_offload_releases_preferred_component_after_request():
|
||||||
strategy = ComponentOffloadStrategy()
|
strategy = ComponentOffloadStrategy()
|
||||||
strategy.finish_use = Mock()
|
strategy.finish_use = Mock()
|
||||||
@@ -143,7 +154,7 @@ def test_group_warmup_state_requires_every_batch_to_be_warmup():
|
|||||||
_stage_name_mapping={},
|
_stage_name_mapping={},
|
||||||
component_residency_strategies={},
|
component_residency_strategies={},
|
||||||
)
|
)
|
||||||
server_args = SimpleNamespace(enable_layerwise_nvtx_marker=False)
|
server_args = _server_args()
|
||||||
manager = ComponentResidencyManager(pipeline, server_args)
|
manager = ComponentResidencyManager(pipeline, server_args)
|
||||||
|
|
||||||
manager.begin_request(
|
manager.begin_request(
|
||||||
@@ -166,6 +177,246 @@ class _Stage:
|
|||||||
return self.uses
|
return self.uses
|
||||||
|
|
||||||
|
|
||||||
|
def test_warmup_records_use_and_transition_peaks(monkeypatch):
|
||||||
|
device_module = SimpleNamespace(
|
||||||
|
is_available=lambda: True,
|
||||||
|
reset_peak_memory_stats=Mock(),
|
||||||
|
max_memory_allocated=lambda: 7,
|
||||||
|
memory_allocated=lambda: 2,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(torch, "get_device_module", lambda: device_module)
|
||||||
|
monkeypatch.setattr(current_platform, "is_cuda", lambda: True)
|
||||||
|
|
||||||
|
use = ComponentUse("denoise", "transformer")
|
||||||
|
stage = _Stage(use)
|
||||||
|
module = torch.nn.Linear(2, 2)
|
||||||
|
pipeline = SimpleNamespace(
|
||||||
|
modules={"transformer": module},
|
||||||
|
_stage_name_mapping={"denoise": stage},
|
||||||
|
component_residency_strategies={},
|
||||||
|
)
|
||||||
|
server_args = _server_args()
|
||||||
|
manager = ComponentResidencyManager(pipeline, server_args)
|
||||||
|
manager.strategy_for = Mock(return_value=Mock())
|
||||||
|
manager.refresh_pipeline(pipeline)
|
||||||
|
manager.begin_request([stage], SimpleNamespace(is_warmup=True), server_args)
|
||||||
|
|
||||||
|
manager.before_stage(stage, 0, SimpleNamespace(is_warmup=True), server_args)
|
||||||
|
manager.begin_stage()
|
||||||
|
manager.end_stage()
|
||||||
|
manager.finish_request()
|
||||||
|
|
||||||
|
peaks = manager.take_warmup_phase_peaks()
|
||||||
|
inactive_peak = WarmupPhasePeak((), 7)
|
||||||
|
transformer_peak = WarmupPhasePeak(
|
||||||
|
("transformer",), 7, used_components=("transformer",)
|
||||||
|
)
|
||||||
|
assert peaks["request:before-stage"] == inactive_peak
|
||||||
|
assert peaks["0:denoise:setup"] == inactive_peak
|
||||||
|
assert peaks["0:denoise:transition:idle->transformer"] == transformer_peak
|
||||||
|
assert peaks["0:denoise:use:transformer"] == transformer_peak
|
||||||
|
assert peaks["0:denoise:transition:transformer->idle"] == transformer_peak
|
||||||
|
assert peaks["0:denoise:between"] == inactive_peak
|
||||||
|
# A non-preferred component is being released during cleanup, so it is no
|
||||||
|
# longer part of the placement that follows this transition.
|
||||||
|
assert peaks["request:cleanup:transformer"] == inactive_peak
|
||||||
|
assert peaks["idle"] == WarmupPhasePeak(
|
||||||
|
active_components=(),
|
||||||
|
allocated_bytes=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_warmup_skips_memory_tracking_for_unsupported_pipeline(monkeypatch):
|
||||||
|
device_module = SimpleNamespace(
|
||||||
|
is_available=lambda: True,
|
||||||
|
reset_peak_memory_stats=Mock(),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(torch, "get_device_module", lambda: device_module)
|
||||||
|
monkeypatch.setattr(current_platform, "is_cuda", lambda: True)
|
||||||
|
|
||||||
|
stage = _Stage()
|
||||||
|
pipeline = SimpleNamespace(
|
||||||
|
modules={},
|
||||||
|
_stage_name_mapping={"stage": stage},
|
||||||
|
component_residency_strategies={},
|
||||||
|
)
|
||||||
|
server_args = _server_args(supports_auto_residency=False)
|
||||||
|
manager = ComponentResidencyManager(pipeline, server_args)
|
||||||
|
|
||||||
|
manager.begin_request([stage], SimpleNamespace(is_warmup=True), server_args)
|
||||||
|
manager.before_stage(stage, 0, SimpleNamespace(is_warmup=True), server_args)
|
||||||
|
manager.finish_request()
|
||||||
|
|
||||||
|
assert manager._track_warmup_memory is False
|
||||||
|
assert manager.take_warmup_phase_peaks() == {}
|
||||||
|
device_module.reset_peak_memory_stats.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_warmup_records_full_weight_transition_without_preparing(monkeypatch):
|
||||||
|
device_module = SimpleNamespace(
|
||||||
|
is_available=lambda: True,
|
||||||
|
reset_peak_memory_stats=Mock(),
|
||||||
|
max_memory_allocated=lambda: 7,
|
||||||
|
memory_allocated=lambda: 2,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(torch, "get_device_module", lambda: device_module)
|
||||||
|
monkeypatch.setattr(current_platform, "is_cuda", lambda: True)
|
||||||
|
|
||||||
|
stage = _Stage()
|
||||||
|
module = torch.nn.Linear(2, 2)
|
||||||
|
pipeline = SimpleNamespace(
|
||||||
|
modules={"transformer": module},
|
||||||
|
_stage_name_mapping={"lora_switch": stage},
|
||||||
|
component_residency_strategies={},
|
||||||
|
)
|
||||||
|
server_args = _server_args()
|
||||||
|
manager = ComponentResidencyManager(pipeline, server_args)
|
||||||
|
manager.strategy_for = Mock()
|
||||||
|
manager.refresh_pipeline(pipeline)
|
||||||
|
manager.begin_request([stage], SimpleNamespace(is_warmup=True), server_args)
|
||||||
|
|
||||||
|
manager.before_stage(stage, 0, SimpleNamespace(is_warmup=True), server_args)
|
||||||
|
with manager.full_weight_transition(("transformer",)):
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert manager._warmup_phase_peaks[
|
||||||
|
"0:lora_switch:full-weight-transition:transformer"
|
||||||
|
] == WarmupPhasePeak(
|
||||||
|
(),
|
||||||
|
7,
|
||||||
|
full_weight_transition_components=("transformer",),
|
||||||
|
)
|
||||||
|
assert manager._warmup_phase_key == "0:lora_switch:setup"
|
||||||
|
manager.strategy_for.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_warmup_records_same_component_dtype_prepare_as_transition(monkeypatch):
|
||||||
|
device_module = SimpleNamespace(
|
||||||
|
is_available=lambda: True,
|
||||||
|
reset_peak_memory_stats=Mock(),
|
||||||
|
max_memory_allocated=lambda: 7,
|
||||||
|
memory_allocated=lambda: 2,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(torch, "get_device_module", lambda: device_module)
|
||||||
|
monkeypatch.setattr(current_platform, "is_cuda", lambda: True)
|
||||||
|
|
||||||
|
first = ComponentUse("stage", "transformer", target_dtype=torch.float16)
|
||||||
|
second = ComponentUse("stage", "transformer", target_dtype=torch.bfloat16)
|
||||||
|
stage = _Stage(first, second)
|
||||||
|
module = torch.nn.Linear(2, 2)
|
||||||
|
pipeline = SimpleNamespace(
|
||||||
|
modules={"transformer": module},
|
||||||
|
_stage_name_mapping={"stage": stage},
|
||||||
|
component_residency_strategies={},
|
||||||
|
)
|
||||||
|
server_args = _server_args()
|
||||||
|
manager = ComponentResidencyManager(pipeline, server_args)
|
||||||
|
strategy = Mock()
|
||||||
|
manager.strategy_for = Mock(return_value=strategy)
|
||||||
|
manager.refresh_pipeline(pipeline)
|
||||||
|
manager.begin_request([stage], SimpleNamespace(is_warmup=True), server_args)
|
||||||
|
manager.before_stage(stage, 0, SimpleNamespace(is_warmup=True), server_args)
|
||||||
|
|
||||||
|
manager.begin_use(first, module=module)
|
||||||
|
manager.begin_use(second, module=module)
|
||||||
|
|
||||||
|
manager._record_warmup_phase_peak()
|
||||||
|
assert manager._warmup_phase_peaks[
|
||||||
|
"0:stage:transition:transformer->transformer"
|
||||||
|
] == WarmupPhasePeak(("transformer",), 7, used_components=("transformer",))
|
||||||
|
assert strategy.prepare_for_use.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_warmup_attributes_prefetch_peak_to_prefetched_component(monkeypatch):
|
||||||
|
device_module = SimpleNamespace(
|
||||||
|
is_available=lambda: True,
|
||||||
|
reset_peak_memory_stats=Mock(),
|
||||||
|
max_memory_allocated=lambda: 7,
|
||||||
|
memory_allocated=lambda: 2,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(torch, "get_device_module", lambda: device_module)
|
||||||
|
monkeypatch.setattr(current_platform, "is_cuda", lambda: True)
|
||||||
|
|
||||||
|
encoder_use = ComponentUse("encode", "text_encoder")
|
||||||
|
transformer_use = ComponentUse("denoise", "transformer", memory_intensive=True)
|
||||||
|
encode_stage = _Stage(encoder_use)
|
||||||
|
denoise_stage = _Stage(transformer_use)
|
||||||
|
modules = {
|
||||||
|
"text_encoder": torch.nn.Linear(2, 2),
|
||||||
|
"transformer": torch.nn.Linear(2, 2),
|
||||||
|
}
|
||||||
|
pipeline = SimpleNamespace(
|
||||||
|
modules=modules,
|
||||||
|
_stage_name_mapping={
|
||||||
|
"encode": encode_stage,
|
||||||
|
"denoise": denoise_stage,
|
||||||
|
},
|
||||||
|
component_residency_strategies={},
|
||||||
|
)
|
||||||
|
server_args = _server_args()
|
||||||
|
manager = ComponentResidencyManager(pipeline, server_args)
|
||||||
|
strategy = Mock()
|
||||||
|
strategy.prefetch_for_use.return_value = True
|
||||||
|
manager.strategy_for = Mock(return_value=strategy)
|
||||||
|
manager.refresh_pipeline(pipeline)
|
||||||
|
manager.begin_request(
|
||||||
|
[encode_stage, denoise_stage],
|
||||||
|
SimpleNamespace(is_warmup=True),
|
||||||
|
server_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
manager.before_stage(encode_stage, 0, SimpleNamespace(is_warmup=True), server_args)
|
||||||
|
manager.begin_stage()
|
||||||
|
manager.end_stage()
|
||||||
|
manager.before_stage(denoise_stage, 1, SimpleNamespace(is_warmup=True), server_args)
|
||||||
|
|
||||||
|
assert manager._warmup_phase_peaks[
|
||||||
|
"0:encode:prefetch:transformer"
|
||||||
|
] == WarmupPhasePeak(("transformer",), 7, used_components=("transformer",))
|
||||||
|
assert manager._warmup_phase_peaks["0:encode:between"] == WarmupPhasePeak((), 7)
|
||||||
|
|
||||||
|
|
||||||
|
def test_warmup_splits_sequential_component_transition(monkeypatch):
|
||||||
|
device_module = SimpleNamespace(
|
||||||
|
is_available=lambda: True,
|
||||||
|
reset_peak_memory_stats=Mock(),
|
||||||
|
max_memory_allocated=lambda: 7,
|
||||||
|
memory_allocated=lambda: 2,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(torch, "get_device_module", lambda: device_module)
|
||||||
|
monkeypatch.setattr(current_platform, "is_cuda", lambda: True)
|
||||||
|
|
||||||
|
first = ComponentUse("stage", "text_encoder")
|
||||||
|
second = ComponentUse("stage", "transformer")
|
||||||
|
stage = _Stage(first, second)
|
||||||
|
modules = {
|
||||||
|
"text_encoder": torch.nn.Linear(2, 2),
|
||||||
|
"transformer": torch.nn.Linear(2, 2),
|
||||||
|
}
|
||||||
|
pipeline = SimpleNamespace(
|
||||||
|
modules=modules,
|
||||||
|
_stage_name_mapping={"stage": stage},
|
||||||
|
component_residency_strategies={},
|
||||||
|
)
|
||||||
|
server_args = _server_args()
|
||||||
|
manager = ComponentResidencyManager(pipeline, server_args)
|
||||||
|
manager.strategy_for = Mock(return_value=Mock())
|
||||||
|
manager.refresh_pipeline(pipeline)
|
||||||
|
manager.begin_request([stage], SimpleNamespace(is_warmup=True), server_args)
|
||||||
|
manager.before_stage(stage, 0, SimpleNamespace(is_warmup=True), server_args)
|
||||||
|
|
||||||
|
manager.begin_use(first)
|
||||||
|
manager.begin_use(second)
|
||||||
|
manager._record_warmup_phase_peak()
|
||||||
|
|
||||||
|
assert manager._warmup_phase_peaks["0:stage:transition:text_encoder->idle"] == (
|
||||||
|
WarmupPhasePeak(("text_encoder",), 7, used_components=("text_encoder",))
|
||||||
|
)
|
||||||
|
assert manager._warmup_phase_peaks["0:stage:transition:idle->transformer"] == (
|
||||||
|
WarmupPhasePeak(("transformer",), 7, used_components=("transformer",))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _manager_for_stage(stage, modules):
|
def _manager_for_stage(stage, modules):
|
||||||
pipeline = SimpleNamespace(
|
pipeline = SimpleNamespace(
|
||||||
modules=modules,
|
modules=modules,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ from types import SimpleNamespace
|
|||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
from sglang.multimodal_gen.runtime.entrypoints import diffusion_generator as dg
|
from sglang.multimodal_gen.runtime.entrypoints import diffusion_generator as dg
|
||||||
|
from sglang.multimodal_gen.runtime.entrypoints.control_requests import ShutdownReq
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.diffusion_generator import DiffGenerator
|
from sglang.multimodal_gen.runtime.entrypoints.diffusion_generator import DiffGenerator
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.utils import ShutdownReq
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeProcess:
|
class _FakeProcess:
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ and a realtime session always lands on the same replica it started on.
|
|||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.utils import SetLoraReq, ShutdownReq
|
from sglang.multimodal_gen.runtime.entrypoints.control_requests import (
|
||||||
|
SetLoraReq,
|
||||||
|
ShutdownReq,
|
||||||
|
)
|
||||||
from sglang.multimodal_gen.runtime.pipelines_core import Req
|
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.pipelines_core.schedule_batch import OutputBatch
|
||||||
from sglang.multimodal_gen.runtime.scheduler_client import (
|
from sglang.multimodal_gen.runtime.scheduler_client import (
|
||||||
|
|||||||
@@ -56,3 +56,38 @@ def test_peer_cuda_device_uses_the_ulysses_group_mapping():
|
|||||||
patch(f"{_IPC}.dist.all_gather_object", side_effect=gather),
|
patch(f"{_IPC}.dist.all_gather_object", side_effect=gather),
|
||||||
):
|
):
|
||||||
assert _peer_cuda_device(group, rank=1, device=3) == 2
|
assert _peer_cuda_device(group, rank=1, device=3) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_drop_staging_clears_cached_buffers_and_nothing_else():
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.runtime.distributed.device_communicators.ipc_a2a import (
|
||||||
|
IpcA2AState,
|
||||||
|
)
|
||||||
|
|
||||||
|
state = IpcA2AState()
|
||||||
|
state.inited = True
|
||||||
|
state.calls = 7
|
||||||
|
state.staging = OrderedDict(
|
||||||
|
{(4, 4, "bf16"): ("local", "peer"), (8, 8, "bf16"): ("l", "p")}
|
||||||
|
)
|
||||||
|
|
||||||
|
state.drop_staging()
|
||||||
|
|
||||||
|
assert state.staging == OrderedDict()
|
||||||
|
assert state.inited is True and state.calls == 7
|
||||||
|
state.drop_staging() # idempotent on an empty cache
|
||||||
|
|
||||||
|
|
||||||
|
def test_drop_a2a_staging_buffers_clears_the_ulysses_cache():
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.runtime.layers import usp
|
||||||
|
|
||||||
|
usp._A2A_STAGING_BUFFERS[("qkv", torch.float16, 0)] = torch.empty(
|
||||||
|
8, dtype=torch.float16
|
||||||
|
)
|
||||||
|
with patch.object(torch.cuda, "is_available", return_value=False):
|
||||||
|
usp.drop_a2a_staging_buffers()
|
||||||
|
usp.drop_a2a_staging_buffers() # idempotent on an empty cache
|
||||||
|
assert usp._A2A_STAGING_BUFFERS == {}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from unittest.mock import Mock, patch
|
|||||||
|
|
||||||
from sglang.multimodal_gen.runtime import launch_server as ls
|
from sglang.multimodal_gen.runtime import launch_server as ls
|
||||||
from sglang.multimodal_gen.runtime.disaggregation.roles import RoleType
|
from sglang.multimodal_gen.runtime.disaggregation.roles import RoleType
|
||||||
from sglang.multimodal_gen.runtime.entrypoints.utils import ShutdownReq
|
from sglang.multimodal_gen.runtime.entrypoints.control_requests import ShutdownReq
|
||||||
|
|
||||||
|
|
||||||
class _FakeProcess:
|
class _FakeProcess:
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import json
|
import json
|
||||||
|
from types import SimpleNamespace
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
import sglang.multimodal_gen.runtime.managers.gpu_worker as gpu_worker_module
|
import sglang.multimodal_gen.runtime.managers.gpu_worker as gpu_worker_module
|
||||||
|
import sglang.multimodal_gen.runtime.managers.memory_managers.component_manager as component_manager_module
|
||||||
from sglang.multimodal_gen.runtime.managers.gpu_worker import GPUWorker
|
from sglang.multimodal_gen.runtime.managers.gpu_worker import GPUWorker
|
||||||
|
from sglang.multimodal_gen.runtime.managers.memory_managers.component_manager import (
|
||||||
|
WarmupPhasePeak,
|
||||||
|
)
|
||||||
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch
|
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
|
||||||
from sglang.multimodal_gen.runtime.utils.perf_logger import (
|
from sglang.multimodal_gen.runtime.utils.perf_logger import (
|
||||||
@@ -23,6 +28,16 @@ from sglang.multimodal_gen.test.server.testcase_configs import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _isolate_global_component_residency_manager(monkeypatch):
|
||||||
|
# Worker paths exercised here create the process-global residency manager
|
||||||
|
# for their fake pipeline; leaving it behind hands its modules to later
|
||||||
|
# tests (the worker prefers the global manager's placement modules).
|
||||||
|
monkeypatch.setattr(
|
||||||
|
component_manager_module, "_GLOBAL_COMPONENT_RESIDENCY_MANAGER", None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _perf_record(memory_snapshots: dict[str, dict]) -> RequestPerfRecord:
|
def _perf_record(memory_snapshots: dict[str, dict]) -> RequestPerfRecord:
|
||||||
return RequestPerfRecord(
|
return RequestPerfRecord(
|
||||||
request_id="request",
|
request_id="request",
|
||||||
@@ -35,6 +50,27 @@ def _perf_record(memory_snapshots: dict[str, dict]) -> RequestPerfRecord:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_metrics_attributes_steps_and_iterations_to_active_stage():
|
||||||
|
metrics = RequestMetrics("request")
|
||||||
|
metrics.active_stage_name = "ShapeStage"
|
||||||
|
|
||||||
|
metrics.record_step(0.1)
|
||||||
|
metrics.record_stage_iterations(4, 50)
|
||||||
|
metrics.active_stage_name = "PaintStage"
|
||||||
|
metrics.record_step(0.2)
|
||||||
|
metrics.record_stage_iterations(4, 30)
|
||||||
|
|
||||||
|
assert metrics.steps == [100.0, 200.0]
|
||||||
|
assert metrics.steps_by_stage == {
|
||||||
|
"ShapeStage": [100.0],
|
||||||
|
"PaintStage": [200.0],
|
||||||
|
}
|
||||||
|
assert metrics.stage_iterations == {
|
||||||
|
"ShapeStage": (4, 50),
|
||||||
|
"PaintStage": (4, 30),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_performance_summary_separates_load_and_runtime_peaks():
|
def test_performance_summary_separates_load_and_runtime_peaks():
|
||||||
summary = PerformanceSummary.from_req_perf_record(
|
summary = PerformanceSummary.from_req_perf_record(
|
||||||
_perf_record(
|
_perf_record(
|
||||||
@@ -62,13 +98,14 @@ def test_worker_records_replica_load_and_runtime_peaks():
|
|||||||
worker.is_output_rank = True
|
worker.is_output_rank = True
|
||||||
worker._load_peak_reserved_mb = 4096.0
|
worker._load_peak_reserved_mb = 4096.0
|
||||||
worker._runtime_peak_reserved_mb = 0.0
|
worker._runtime_peak_reserved_mb = 0.0
|
||||||
|
worker._warmup_peak_reserved_mb = 0.0
|
||||||
worker._load_peak_allocated_mb = 3000.0
|
worker._load_peak_allocated_mb = 3000.0
|
||||||
worker._runtime_peak_allocated_mb = 0.0
|
worker._runtime_peak_allocated_mb = 0.0
|
||||||
output = OutputBatch()
|
output = OutputBatch()
|
||||||
metrics = RequestMetrics("request")
|
metrics = RequestMetrics("request")
|
||||||
replica_group = Mock()
|
replica_group = Mock()
|
||||||
replica_group.all_reduce.return_value = torch.tensor(
|
replica_group.all_reduce.return_value = torch.tensor(
|
||||||
[5120.0, 3584.0, 3500.0, 2560.0], dtype=torch.float64
|
[5120.0, 3584.0, 6144.0, 3500.0, 2560.0], dtype=torch.float64
|
||||||
)
|
)
|
||||||
snapshots = [
|
snapshots = [
|
||||||
MemorySnapshot(0.0, 0.0, 2048.0, 3072.0),
|
MemorySnapshot(0.0, 0.0, 2048.0, 3072.0),
|
||||||
@@ -97,6 +134,88 @@ def test_worker_records_replica_load_and_runtime_peaks():
|
|||||||
assert metrics.memory_snapshots["runtime_peak"].peak_reserved_mb == 3584.0
|
assert metrics.memory_snapshots["runtime_peak"].peak_reserved_mb == 3584.0
|
||||||
assert metrics.memory_snapshots["load_peak"].peak_allocated_mb == 3500.0
|
assert metrics.memory_snapshots["load_peak"].peak_allocated_mb == 3500.0
|
||||||
assert metrics.memory_snapshots["runtime_peak"].peak_allocated_mb == 2560.0
|
assert metrics.memory_snapshots["runtime_peak"].peak_allocated_mb == 2560.0
|
||||||
|
assert metrics.memory_snapshots["warmup_peak"].peak_reserved_mb == 6144.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_server_warmup_preserves_peak_after_managed_stage_timeline():
|
||||||
|
worker = GPUWorker.__new__(GPUWorker)
|
||||||
|
worker._auto_residency_warmup_records = []
|
||||||
|
residency_manager = Mock()
|
||||||
|
residency_manager.take_warmup_phase_peaks.return_value = {
|
||||||
|
"0:denoise:use:transformer": WarmupPhasePeak(("transformer",), 8)
|
||||||
|
}
|
||||||
|
residency_manager.current_device_components.return_value = ("transformer",)
|
||||||
|
device_module = Mock()
|
||||||
|
device_module.max_memory_allocated.return_value = 9
|
||||||
|
device_module.max_memory_reserved.return_value = 12
|
||||||
|
req = SimpleNamespace(
|
||||||
|
width=64,
|
||||||
|
height=64,
|
||||||
|
num_frames=1,
|
||||||
|
num_inference_steps=1,
|
||||||
|
metrics=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(torch, "get_device_module", return_value=device_module),
|
||||||
|
patch.object(
|
||||||
|
gpu_worker_module,
|
||||||
|
"peek_global_component_residency_manager",
|
||||||
|
return_value=residency_manager,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
worker._record_server_warmup_memory(
|
||||||
|
req=req,
|
||||||
|
workload=(128, 96, 9, 2),
|
||||||
|
baseline_allocated_bytes=3,
|
||||||
|
succeeded=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
record = worker._auto_residency_warmup_records[0]
|
||||||
|
assert record.peak_allocated_bytes == 9
|
||||||
|
assert record.peak_reserved_bytes == 12
|
||||||
|
assert (record.width, record.height, record.num_frames) == (128, 96, 9)
|
||||||
|
assert record.num_inference_steps == 2
|
||||||
|
assert record.phase_peak_allocated_bytes["request:untracked"] == 9
|
||||||
|
assert record.phase_active_components["request:untracked"] == ("transformer",)
|
||||||
|
|
||||||
|
|
||||||
|
def test_server_warmup_does_not_treat_allocator_cache_as_untracked_live_memory():
|
||||||
|
worker = GPUWorker.__new__(GPUWorker)
|
||||||
|
worker._auto_residency_warmup_records = []
|
||||||
|
residency_manager = Mock()
|
||||||
|
residency_manager.take_warmup_phase_peaks.return_value = {
|
||||||
|
"0:denoise:use:transformer": WarmupPhasePeak(("transformer",), 8)
|
||||||
|
}
|
||||||
|
residency_manager.current_device_components.return_value = ("transformer",)
|
||||||
|
device_module = Mock()
|
||||||
|
device_module.max_memory_allocated.return_value = 8
|
||||||
|
device_module.max_memory_reserved.return_value = 12
|
||||||
|
req = SimpleNamespace(
|
||||||
|
width=64,
|
||||||
|
height=64,
|
||||||
|
num_frames=1,
|
||||||
|
num_inference_steps=1,
|
||||||
|
metrics=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(torch, "get_device_module", return_value=device_module),
|
||||||
|
patch.object(
|
||||||
|
gpu_worker_module,
|
||||||
|
"peek_global_component_residency_manager",
|
||||||
|
return_value=residency_manager,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
worker._record_server_warmup_memory(
|
||||||
|
req=req,
|
||||||
|
workload=(64, 64, 1, 1),
|
||||||
|
baseline_allocated_bytes=3,
|
||||||
|
succeeded=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
record = worker._auto_residency_warmup_records[0]
|
||||||
|
assert "request:untracked" not in record.phase_peak_allocated_bytes
|
||||||
|
|
||||||
|
|
||||||
def test_baseline_config_loads_per_scenario_peak_vram(tmp_path):
|
def test_baseline_config_loads_per_scenario_peak_vram(tmp_path):
|
||||||
@@ -249,6 +368,16 @@ def test_peak_vram_validation_enforces_allocated_when_baselined():
|
|||||||
with pytest.raises(AssertionError, match="Runtime Peak VRAM"):
|
with pytest.raises(AssertionError, match="Runtime Peak VRAM"):
|
||||||
validator.validate_peak_vram(reserved_drift, 10_000.0, 10_000.0)
|
validator.validate_peak_vram(reserved_drift, 10_000.0, 10_000.0)
|
||||||
|
|
||||||
|
reserved_drift.warmup_peak_vram_mb = 12_000.0
|
||||||
|
with pytest.raises(AssertionError, match="Warmup Peak VRAM"):
|
||||||
|
validator.validate_peak_vram(
|
||||||
|
reserved_drift,
|
||||||
|
10_000.0,
|
||||||
|
10_000.0,
|
||||||
|
expected_warmup_peak_vram_mb=10_000.0,
|
||||||
|
expected_runtime_peak_allocated_mb=8_000.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("load_peak_vram_mb", "runtime_peak_vram_mb", "message"),
|
("load_peak_vram_mb", "runtime_peak_vram_mb", "message"),
|
||||||
@@ -308,3 +437,138 @@ def test_results_json_merges_retry_sessions(tmp_path):
|
|||||||
|
|
||||||
results = json.loads(path.read_text(encoding="utf-8"))
|
results = json.loads(path.read_text(encoding="utf-8"))
|
||||||
assert {item["test_name"] for item in results} == {"first", "second"}
|
assert {item["test_name"] for item in results} == {"first", "second"}
|
||||||
|
|
||||||
|
|
||||||
|
def _warmup_batch_for_iterations(steps: int, target_steps: int):
|
||||||
|
from sglang.multimodal_gen.runtime.utils.perf_logger import RequestMetrics
|
||||||
|
|
||||||
|
metrics = RequestMetrics(request_id="probe")
|
||||||
|
metrics.suppress_stage_breakdown = False
|
||||||
|
metrics.active_stage_name = "DenoisingStage"
|
||||||
|
return SimpleNamespace(
|
||||||
|
metrics=metrics,
|
||||||
|
num_inference_steps=steps,
|
||||||
|
extra={"warmup_target_num_inference_steps": target_steps},
|
||||||
|
is_warmup=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stage_formula_records_probe_and_default_iterations():
|
||||||
|
from sglang.multimodal_gen.runtime.pipelines_core.stages.base import (
|
||||||
|
record_default_workload_iterations,
|
||||||
|
)
|
||||||
|
|
||||||
|
batch = _warmup_batch_for_iterations(steps=4, target_steps=50)
|
||||||
|
stage = SimpleNamespace(default_workload_iterations=lambda batch, steps: steps - 1)
|
||||||
|
record_default_workload_iterations(stage, batch)
|
||||||
|
assert batch.metrics.stage_iterations == {"DenoisingStage": (3, 49)}
|
||||||
|
|
||||||
|
|
||||||
|
def test_fixed_schedule_formula_records_the_same_count_twice():
|
||||||
|
from sglang.multimodal_gen.runtime.pipelines_core.stages.base import (
|
||||||
|
record_default_workload_iterations,
|
||||||
|
)
|
||||||
|
|
||||||
|
batch = _warmup_batch_for_iterations(steps=4, target_steps=50)
|
||||||
|
stage = SimpleNamespace(default_workload_iterations=lambda batch, steps: 8)
|
||||||
|
record_default_workload_iterations(stage, batch)
|
||||||
|
assert batch.metrics.stage_iterations == {"DenoisingStage": (8, 8)}
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_loop_record_wins_over_the_formula():
|
||||||
|
from sglang.multimodal_gen.runtime.pipelines_core.stages.base import (
|
||||||
|
record_default_workload_iterations,
|
||||||
|
)
|
||||||
|
|
||||||
|
batch = _warmup_batch_for_iterations(steps=4, target_steps=50)
|
||||||
|
batch.metrics.record_stage_iterations(12, 12)
|
||||||
|
stage = SimpleNamespace(default_workload_iterations=lambda batch, steps: steps)
|
||||||
|
record_default_workload_iterations(stage, batch)
|
||||||
|
assert batch.metrics.stage_iterations == {"DenoisingStage": (12, 12)}
|
||||||
|
|
||||||
|
|
||||||
|
def test_stage_without_a_formula_records_nothing():
|
||||||
|
from sglang.multimodal_gen.runtime.pipelines_core.stages.base import (
|
||||||
|
record_default_workload_iterations,
|
||||||
|
)
|
||||||
|
|
||||||
|
batch = _warmup_batch_for_iterations(steps=4, target_steps=50)
|
||||||
|
stage = SimpleNamespace(default_workload_iterations=lambda batch, steps: None)
|
||||||
|
record_default_workload_iterations(stage, batch)
|
||||||
|
assert batch.metrics.stage_iterations == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_warmup_peak_stays_out_of_the_runtime_peak():
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
worker = gpu_worker_module.GPUWorker.__new__(gpu_worker_module.GPUWorker)
|
||||||
|
worker._runtime_peak_reserved_mb = 0.0
|
||||||
|
worker._runtime_peak_allocated_mb = 0.0
|
||||||
|
worker._warmup_peak_reserved_mb = 0.0
|
||||||
|
worker.is_output_rank = False
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
gpu_worker_module.current_platform, "is_cpu", return_value=False
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
gpu_worker_module,
|
||||||
|
"capture_memory_snapshot",
|
||||||
|
side_effect=[
|
||||||
|
MemorySnapshot(0.0, 0.0, 35000.0, 40000.0),
|
||||||
|
MemorySnapshot(0.0, 0.0, 15000.0, 20000.0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
):
|
||||||
|
worker._record_output_peak_memory(SimpleNamespace(), is_warmup=True)
|
||||||
|
worker._record_output_peak_memory(SimpleNamespace(), is_warmup=False)
|
||||||
|
assert worker._warmup_peak_reserved_mb == 40000.0
|
||||||
|
assert worker._runtime_peak_reserved_mb == 20000.0
|
||||||
|
assert worker._runtime_peak_allocated_mb == 15000.0
|
||||||
|
|
||||||
|
|
||||||
|
def _warmup_req(*, probe: bool = False) -> SimpleNamespace:
|
||||||
|
extra = {"auto_residency_full_shape_probe": True} if probe else {}
|
||||||
|
return SimpleNamespace(is_warmup=True, extra=extra)
|
||||||
|
|
||||||
|
|
||||||
|
def test_worker_releases_the_probe_pool_before_the_next_request(monkeypatch):
|
||||||
|
import sglang.multimodal_gen.runtime.distributed.device_communicators.ipc_a2a as ipc_a2a_module
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
fake_device = SimpleNamespace(empty_cache=lambda: calls.append("empty_cache"))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
gpu_worker_module.torch, "get_device_module", lambda: fake_device
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(type(current_platform), "is_cpu", lambda self: False)
|
||||||
|
monkeypatch.setattr(type(current_platform), "is_mps", lambda self: False)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
ipc_a2a_module.IPC_A2A, "drop_staging", lambda: calls.append("drop_staging")
|
||||||
|
)
|
||||||
|
worker = GPUWorker.__new__(GPUWorker)
|
||||||
|
worker._release_warmup_pool_before_serving = False
|
||||||
|
|
||||||
|
worker._release_warmup_pool(_warmup_req())
|
||||||
|
worker._release_warmup_pool(_warmup_req(probe=True))
|
||||||
|
assert calls == []
|
||||||
|
|
||||||
|
# the bounded re-warm after the probe regrows the pool from empty, and the
|
||||||
|
# IPC staging buffers sized for the probe's messages go with it
|
||||||
|
worker._release_warmup_pool(_warmup_req())
|
||||||
|
assert calls == ["drop_staging", "empty_cache"]
|
||||||
|
|
||||||
|
worker._release_warmup_pool(SimpleNamespace(is_warmup=False, extra={}))
|
||||||
|
assert calls == ["drop_staging", "empty_cache"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_worker_keeps_the_pool_when_no_probe_ran(monkeypatch):
|
||||||
|
calls = []
|
||||||
|
fake_device = SimpleNamespace(empty_cache=lambda: calls.append("empty_cache"))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
gpu_worker_module.torch, "get_device_module", lambda: fake_device
|
||||||
|
)
|
||||||
|
worker = GPUWorker.__new__(GPUWorker)
|
||||||
|
worker._release_warmup_pool_before_serving = False
|
||||||
|
|
||||||
|
worker._release_warmup_pool(_warmup_req())
|
||||||
|
worker._release_warmup_pool(SimpleNamespace(is_warmup=False, extra={}))
|
||||||
|
assert calls == []
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
"""A warmup probe that does not fit is retried smaller instead of abandoned."""
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
|
||||||
|
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
|
||||||
|
from sglang.multimodal_gen.runtime.warmup_request_builder import lighten_warmup_req
|
||||||
|
|
||||||
|
|
||||||
|
def _server_args(temporal_compression_ratio: int = 4) -> SimpleNamespace:
|
||||||
|
arch_config = SimpleNamespace(
|
||||||
|
temporal_compression_ratio=temporal_compression_ratio,
|
||||||
|
vae_scale_factor=8,
|
||||||
|
spatial_compression_ratio=8,
|
||||||
|
)
|
||||||
|
return SimpleNamespace(
|
||||||
|
pipeline_class_name=None,
|
||||||
|
pipeline_config=SimpleNamespace(
|
||||||
|
vae_config=SimpleNamespace(arch_config=arch_config),
|
||||||
|
vae_scale_factor=8,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _req(width: int, height: int, num_frames: int) -> Req:
|
||||||
|
return Req(
|
||||||
|
sampling_params=SamplingParams(
|
||||||
|
width=width, height=height, num_frames=num_frames
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLightenWarmupReq:
|
||||||
|
def test_video_probe_halves_latent_frames_first(self):
|
||||||
|
lighter = lighten_warmup_req(_server_args(), _req(832, 480, 17))
|
||||||
|
assert (lighter.width, lighter.height) == (832, 480)
|
||||||
|
assert lighter.num_frames == 9
|
||||||
|
|
||||||
|
def test_frames_step_down_to_a_single_frame(self):
|
||||||
|
server_args = _server_args()
|
||||||
|
frames = []
|
||||||
|
req = _req(832, 480, 17)
|
||||||
|
for _ in range(4):
|
||||||
|
req = lighten_warmup_req(server_args, req)
|
||||||
|
if req is None:
|
||||||
|
break
|
||||||
|
frames.append(req.num_frames)
|
||||||
|
assert frames[:3] == [9, 5, 1]
|
||||||
|
|
||||||
|
def test_image_probe_halves_the_area(self):
|
||||||
|
lighter = lighten_warmup_req(_server_args(), _req(1024, 1024, 1))
|
||||||
|
assert lighter.num_frames == 1
|
||||||
|
assert lighter.width * lighter.height <= 1024 * 1024 // 2
|
||||||
|
assert lighter.width % 16 == 0 and lighter.height % 16 == 0
|
||||||
|
|
||||||
|
def test_the_original_request_is_left_alone(self):
|
||||||
|
req = _req(832, 480, 17)
|
||||||
|
lighten_warmup_req(_server_args(), req)
|
||||||
|
assert req.num_frames == 17
|
||||||
|
|
||||||
|
def test_a_probe_at_the_floor_cannot_shrink(self):
|
||||||
|
assert lighten_warmup_req(_server_args(), _req(16, 16, 1)) is None
|
||||||
|
|
||||||
|
def test_frames_follow_the_model_frame_contract(self):
|
||||||
|
# LongLive2-style contract: latent frames come in causal blocks of 8,
|
||||||
|
# so with a temporal ratio of 4 only 29, 61, 93, ... frames are valid.
|
||||||
|
server_args = _server_args()
|
||||||
|
|
||||||
|
def adjust_num_frames(num_frames: int) -> int:
|
||||||
|
latent = (num_frames - 1) // 4 + 1
|
||||||
|
if latent % 8 == 0:
|
||||||
|
return num_frames
|
||||||
|
return (max(8, latent // 8 * 8) - 1) * 4 + 1
|
||||||
|
|
||||||
|
server_args.pipeline_config.adjust_num_frames = adjust_num_frames
|
||||||
|
|
||||||
|
lighter = lighten_warmup_req(server_args, _req(960, 928, 61))
|
||||||
|
assert lighter.num_frames == 29
|
||||||
|
assert (lighter.width, lighter.height) == (960, 928)
|
||||||
|
|
||||||
|
# At the smallest valid frame count the probe shrinks the area instead.
|
||||||
|
floor = lighten_warmup_req(server_args, lighter)
|
||||||
|
assert floor.num_frames == 29
|
||||||
|
assert floor.width * floor.height <= 960 * 928 // 2
|
||||||
|
|
||||||
|
|
||||||
|
def _record(width: int, height: int, num_frames: int, *, peak_gib: float):
|
||||||
|
from sglang.multimodal_gen.runtime.managers.memory_managers.auto_residency import (
|
||||||
|
WarmupMemoryRecord,
|
||||||
|
)
|
||||||
|
|
||||||
|
return WarmupMemoryRecord(
|
||||||
|
width=width,
|
||||||
|
height=height,
|
||||||
|
num_frames=num_frames,
|
||||||
|
baseline_allocated_bytes=2 << 30,
|
||||||
|
peak_allocated_bytes=int(peak_gib * (1 << 30)),
|
||||||
|
succeeded=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFitAutoResidencyProbe:
|
||||||
|
def test_probe_shrinks_until_its_extrapolated_peak_fits(self):
|
||||||
|
from sglang.multimodal_gen.runtime.managers.gpu_worker import (
|
||||||
|
fit_auto_residency_probe,
|
||||||
|
)
|
||||||
|
|
||||||
|
fitted, estimate, steps = fit_auto_residency_probe(
|
||||||
|
_req(1280, 720, 81),
|
||||||
|
records=[_record(832, 480, 81, peak_gib=20.0)],
|
||||||
|
free_bytes=40 << 30,
|
||||||
|
total_bytes=80 << 30,
|
||||||
|
server_args=_server_args(),
|
||||||
|
)
|
||||||
|
assert steps >= 1
|
||||||
|
assert (fitted.width, fitted.height) == (1280, 720)
|
||||||
|
assert fitted.num_frames < 81
|
||||||
|
assert estimate is not None and estimate <= 40 << 30
|
||||||
|
|
||||||
|
def test_probe_that_fits_runs_at_full_shape(self):
|
||||||
|
from sglang.multimodal_gen.runtime.managers.gpu_worker import (
|
||||||
|
fit_auto_residency_probe,
|
||||||
|
)
|
||||||
|
|
||||||
|
fitted, _, steps = fit_auto_residency_probe(
|
||||||
|
_req(1280, 720, 81),
|
||||||
|
records=[_record(832, 480, 81, peak_gib=20.0)],
|
||||||
|
free_bytes=79 << 30,
|
||||||
|
total_bytes=80 << 30,
|
||||||
|
server_args=_server_args(),
|
||||||
|
)
|
||||||
|
assert steps == 0
|
||||||
|
assert fitted.num_frames == 81
|
||||||
|
|
||||||
|
def test_probe_never_shrinks_below_the_bounded_warmup_shape(self):
|
||||||
|
from sglang.multimodal_gen.runtime.managers.gpu_worker import (
|
||||||
|
fit_auto_residency_probe,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Nothing fits the extrapolation, but the bounded 832x480x17f warmup
|
||||||
|
# already ran, so the ladder (81 -> 41 -> 21 -> 9 frames) stops at the
|
||||||
|
# first shape at or below it instead of reaching a 16x16x1f probe.
|
||||||
|
fitted, _, steps = fit_auto_residency_probe(
|
||||||
|
_req(832, 480, 81),
|
||||||
|
records=[_record(832, 480, 17, peak_gib=30.0)],
|
||||||
|
free_bytes=8 << 30,
|
||||||
|
total_bytes=80 << 30,
|
||||||
|
server_args=_server_args(),
|
||||||
|
)
|
||||||
|
assert steps >= 1
|
||||||
|
assert (fitted.width, fitted.height) == (832, 480)
|
||||||
|
assert fitted.num_frames == 9
|
||||||
|
|
||||||
|
def test_without_a_trusted_estimate_the_probe_runs_as_requested(self):
|
||||||
|
from sglang.multimodal_gen.runtime.managers.gpu_worker import (
|
||||||
|
fit_auto_residency_probe,
|
||||||
|
)
|
||||||
|
|
||||||
|
fitted, estimate, steps = fit_auto_residency_probe(
|
||||||
|
_req(1280, 720, 81),
|
||||||
|
records=[],
|
||||||
|
free_bytes=1 << 30,
|
||||||
|
total_bytes=80 << 30,
|
||||||
|
server_args=_server_args(),
|
||||||
|
)
|
||||||
|
assert (steps, estimate) == (0, None)
|
||||||
|
assert fitted.num_frames == 81
|
||||||
|
|
||||||
|
|
||||||
|
class TestOutOfMemoryClassification:
|
||||||
|
def test_allocation_failures_from_libraries_count_as_out_of_memory(self):
|
||||||
|
from sglang.multimodal_gen.runtime.server_warmup import _is_out_of_memory
|
||||||
|
|
||||||
|
assert _is_out_of_memory("CUDA error: out of memory")
|
||||||
|
assert _is_out_of_memory("cuBLAS error: CUBLAS_STATUS_ALLOC_FAILED")
|
||||||
|
assert _is_out_of_memory(
|
||||||
|
"RuntimeError: cudaErrorMemoryAllocation: out of memory"
|
||||||
|
)
|
||||||
|
assert not _is_out_of_memory("shape mismatch in attention")
|
||||||
Reference in New Issue
Block a user