[diffusion] logging: log available gpu mem while loading and generating (#15936)

This commit is contained in:
Mick
2025-12-28 00:34:58 +08:00
committed by GitHub
parent 41addd2e08
commit 39d56196a0
8 changed files with 242 additions and 36 deletions
@@ -7,7 +7,6 @@ import glob
import importlib.util import importlib.util
import json import json
import os import os
import time
import traceback import traceback
from abc import ABC from abc import ABC
from collections.abc import Generator, Iterable from collections.abc import Generator, Iterable
@@ -17,6 +16,7 @@ from typing import Any, cast
import torch import torch
import torch.distributed as dist import torch.distributed as dist
import torch.nn as nn import torch.nn as nn
from diffusers import AutoModel
from safetensors.torch import load_file as safetensors_load_file from safetensors.torch import load_file as safetensors_load_file
from torch.distributed import init_device_mesh from torch.distributed import init_device_mesh
from transformers import AutoImageProcessor, AutoProcessor, AutoTokenizer from transformers import AutoImageProcessor, AutoProcessor, AutoTokenizer
@@ -90,6 +90,26 @@ def _list_safetensors_files(model_path: str) -> list[str]:
return sorted(glob.glob(os.path.join(str(model_path), "*.safetensors"))) return sorted(glob.glob(os.path.join(str(model_path), "*.safetensors")))
def get_memory_usage_of_component(module) -> float | None:
"""
returned value is in GB, rounded to 2 decimal digits
"""
if not isinstance(module, nn.Module):
return None
BYTES_PER_GB = 1024**3
if hasattr(module, "get_memory_footprint"):
usage = module.get_memory_footprint() / BYTES_PER_GB
else:
# manually
param_size = sum(p.numel() * p.element_size() for p in module.parameters())
buffer_size = sum(b.numel() * b.element_size() for b in module.buffers())
total_size_bytes = param_size + buffer_size
usage = total_size_bytes / (1024**3)
return round(usage, 2)
class ComponentLoader(ABC): class ComponentLoader(ABC):
"""Base class for loading a specific type of model component.""" """Base class for loading a specific type of model component."""
@@ -118,7 +138,7 @@ class ComponentLoader(ABC):
server_args: ServerArgs, server_args: ServerArgs,
module_name: str, module_name: str,
transformers_or_diffusers: str, transformers_or_diffusers: str,
): ) -> tuple[AutoModel, float]:
""" """
Template method that standardizes logging around the core load implementation. Template method that standardizes logging around the core load implementation.
The priority of loading method is: The priority of loading method is:
@@ -127,7 +147,13 @@ class ComponentLoader(ABC):
If all of the above methods failed, an error will be thrown If all of the above methods failed, an error will be thrown
""" """
logger.info("Loading %s from %s", module_name, component_model_path) gpu_mem_before_loading = current_platform.get_available_gpu_memory()
logger.info(
"Loading %s from %s. avail mem: %.2f GB",
module_name,
component_model_path,
gpu_mem_before_loading,
)
try: try:
component = self.load_customized( component = self.load_customized(
component_model_path, server_args, module_name component_model_path, server_args, module_name
@@ -159,20 +185,27 @@ class ComponentLoader(ABC):
if component is None: if component is None:
logger.warning("Loaded %s returned None", module_name) logger.warning("Loaded %s returned None", module_name)
consumed = 0.0
else: else:
current_gpu_mem = current_platform.get_available_gpu_memory()
consumed = get_memory_usage_of_component(component)
if consumed is None or consumed == 0.0:
consumed = gpu_mem_before_loading - current_gpu_mem
logger.info( logger.info(
f"Loaded %s: %s from: {source}", f"Loaded %s: %s from {source}. avail mem: %.2f GB, %.2f GB consumed",
module_name, module_name,
component.__class__.__name__, component.__class__.__name__,
current_gpu_mem,
consumed,
) )
return component return component, consumed
def load_native( def load_native(
self, self,
component_model_path: str, component_model_path: str,
server_args: ServerArgs, server_args: ServerArgs,
transformers_or_diffusers: str, transformers_or_diffusers: str,
): ) -> AutoModel:
""" """
Load the component using the native library (transformers/diffusers). Load the component using the native library (transformers/diffusers).
""" """
@@ -273,9 +306,6 @@ class TextEncoderLoader(ComponentLoader):
allow_patterns_overrides: list[str] | None = None allow_patterns_overrides: list[str] | None = None
"""If defined, weights will load exclusively using these patterns.""" """If defined, weights will load exclusively using these patterns."""
counter_before_loading_weights: float = 0.0
counter_after_loading_weights: float = 0.0
def should_offload(self, server_args, model_config: ModelConfig | None = None): def should_offload(self, server_args, model_config: ModelConfig | None = None):
should_offload = server_args.text_encoder_cpu_offload should_offload = server_args.text_encoder_cpu_offload
if not should_offload: if not should_offload:
@@ -355,8 +385,6 @@ class TextEncoderLoader(ComponentLoader):
else: else:
weights_iterator = pt_weights_iterator(hf_weights_files, to_cpu=to_cpu) weights_iterator = pt_weights_iterator(hf_weights_files, to_cpu=to_cpu)
if self.counter_before_loading_weights == 0.0:
self.counter_before_loading_weights = time.perf_counter()
# apply the prefix. # apply the prefix.
return ((source.prefix + name, tensor) for (name, tensor) in weights_iterator) return ((source.prefix + name, tensor) for (name, tensor) in weights_iterator)
@@ -443,12 +471,6 @@ class TextEncoderLoader(ComponentLoader):
loaded_weights = model.load_weights( loaded_weights = model.load_weights(
self._get_all_weights(model, model_path, to_cpu=should_offload) self._get_all_weights(model, model_path, to_cpu=should_offload)
) )
self.counter_after_loading_weights = time.perf_counter()
logger.info(
"Loading weights took %.2f seconds",
self.counter_after_loading_weights
- self.counter_before_loading_weights,
)
# Explicitly move model to target device after loading weights # Explicitly move model to target device after loading weights
model = model.to(local_torch_device) model = model.to(local_torch_device)
@@ -17,8 +17,13 @@ from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_cfg_group, get_cfg_group,
get_tp_group, get_tp_group,
) )
from sglang.multimodal_gen.runtime.pipelines_core import Req, build_pipeline from sglang.multimodal_gen.runtime.pipelines_core import (
ComposedPipelineBase,
Req,
build_pipeline,
)
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.server_args import PortArgs, ServerArgs from sglang.multimodal_gen.runtime.server_args import PortArgs, ServerArgs
from sglang.multimodal_gen.runtime.utils.common import set_cuda_arch from sglang.multimodal_gen.runtime.utils.common import set_cuda_arch
from sglang.multimodal_gen.runtime.utils.logging_utils import ( from sglang.multimodal_gen.runtime.utils.logging_utils import (
@@ -51,7 +56,7 @@ class GPUWorker:
self.master_port = master_port self.master_port = master_port
# FIXME: should we use tcp as distribute init method? # FIXME: should we use tcp as distribute init method?
self.server_args = server_args self.server_args = server_args
self.pipeline = None self.pipeline: ComposedPipelineBase = None
self.init_device_and_model() self.init_device_and_model()
self.sp_group = get_sp_group() self.sp_group = get_sp_group()
@@ -107,6 +112,19 @@ class GPUWorker:
if self.rank == 0: if self.rank == 0:
peak_memory_bytes = torch.cuda.max_memory_allocated() peak_memory_bytes = torch.cuda.max_memory_allocated()
output_batch.peak_memory_mb = peak_memory_bytes / (1024**2) output_batch.peak_memory_mb = peak_memory_bytes / (1024**2)
peak_memory_gb = peak_memory_bytes / (1024**3)
remaining_gpu_mem_gb = (
current_platform.get_device_total_memory() / (1024**3)
- peak_memory_gb
)
can_stay_resident = self.get_can_stay_resident_components(
remaining_gpu_mem_gb
)
logger.info(
f"Peak GPU memory: {peak_memory_gb:.2f} GB, "
f"Remaining GPU memory at peak: {remaining_gpu_mem_gb:.2f} GB. "
f"Components that can stay resident: {can_stay_resident}"
)
duration_ms = (time.monotonic() - start_time) * 1000 duration_ms = (time.monotonic() - start_time) * 1000
output_batch.timings.total_duration_ms = duration_ms output_batch.timings.total_duration_ms = duration_ms
@@ -127,6 +145,40 @@ class GPUWorker:
finally: finally:
return output_batch return output_batch
def get_can_stay_resident_components(
self, remaining_gpu_mem_gb: float
) -> List[str]:
"""
Calculate which components can stay resident on GPU without being offloaded.
"""
can_stay_resident = []
if not self.pipeline:
return can_stay_resident
# Map memory_usage keys to server_args offload flags
# If the flag is False, the component is ALREADY resident, so we don't suggest it.
# If the flag is True, it is currently offloaded, so it's a candidate to "stay resident".
offload_flags = {
"transformer": self.server_args.dit_cpu_offload
or self.server_args.dit_layerwise_offload,
"vae": self.server_args.vae_cpu_offload,
"text_encoder": self.server_args.text_encoder_cpu_offload,
"text_encoder_2": self.server_args.text_encoder_cpu_offload,
"image_encoder": self.server_args.image_encoder_cpu_offload,
}
for name, usage in self.pipeline.memory_usages.items():
# Only consider components that are currently configured to be offloaded
is_offload_configured = offload_flags.get(name, False)
if not is_offload_configured:
continue
if usage <= remaining_gpu_mem_gb:
can_stay_resident.append(name)
remaining_gpu_mem_gb -= usage
return can_stay_resident
def set_lora( def set_lora(
self, self,
lora_nickname: str, lora_nickname: str,
@@ -85,11 +85,9 @@ class ComposedPipelineBase(ABC):
if self._required_config_modules is None: if self._required_config_modules is None:
raise NotImplementedError("Subclass must set _required_config_modules") raise NotImplementedError("Subclass must set _required_config_modules")
# temp disable for duplicate initialing tp
# maybe_init_distributed_environment_and_model_parallel(
# server_args.tp_size, server_args.sp_size
# )
# [module_name, gpu memory usage]
self.memory_usages: dict[str, float] = {}
# Load modules directly in initialization # Load modules directly in initialization
logger.info("Loading pipeline modules...") logger.info("Loading pipeline modules...")
self.modules = self.load_modules(server_args, loaded_modules) self.modules = self.load_modules(server_args, loaded_modules)
@@ -313,13 +311,15 @@ class ComposedPipelineBase(ABC):
) )
else: else:
component_model_path = os.path.join(self.model_path, load_module_name) component_model_path = os.path.join(self.model_path, load_module_name)
module = PipelineComponentLoader.load_module( module, memory_usage = PipelineComponentLoader.load_module(
module_name=load_module_name, module_name=load_module_name,
component_model_path=component_model_path, component_model_path=component_model_path,
transformers_or_diffusers=transformers_or_diffusers, transformers_or_diffusers=transformers_or_diffusers,
server_args=server_args, server_args=server_args,
) )
self.memory_usages[load_module_name] = memory_usage
if module_name in components: if module_name in components:
logger.warning("Overwriting module %s", module_name) logger.warning("Overwriting module %s", module_name)
components[module_name] = module components[module_name] = module
@@ -331,6 +331,8 @@ class ComposedPipelineBase(ABC):
f"Required module key: {module_name} value: {components.get(module_name)} was not found in loaded modules {components.keys()}" f"Required module key: {module_name} value: {components.get(module_name)} was not found in loaded modules {components.keys()}"
) )
logger.debug("Memory usage of loaded modules: %s", self.memory_usages)
return components return components
def add_stage(self, stage_name: str, stage: PipelineStage): def add_stage(self, stage_name: str, stage: PipelineStage):
@@ -4,7 +4,10 @@
# Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/platforms/cpu.py # Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/platforms/cpu.py
import platform import platform
from functools import lru_cache
from typing import Any
import psutil
import torch import torch
from sglang.multimodal_gen.runtime.platforms.interface import ( from sglang.multimodal_gen.runtime.platforms.interface import (
@@ -40,10 +43,10 @@ class CpuPlatform(Platform):
return platform.machine() return platform.machine()
@classmethod @classmethod
@lru_cache(maxsize=1)
def get_device_total_memory(cls, device_id: int = 0) -> int: def get_device_total_memory(cls, device_id: int = 0) -> int:
# This is a rough estimate for CPU memory
# In practice, you might want to use psutil or similar return psutil.virtual_memory().total
return 0
@classmethod @classmethod
def is_async_output_supported(cls, enforce_eager: bool | None) -> bool: def is_async_output_supported(cls, enforce_eager: bool | None) -> bool:
@@ -56,6 +59,30 @@ class CpuPlatform(Platform):
# For CPU, we can't easily get memory usage without additional libraries # For CPU, we can't easily get memory usage without additional libraries
return 0.0 return 0.0
@classmethod
def get_available_gpu_memory(
cls,
device_id: int = 0,
distributed: bool = False,
empty_cache: bool = True,
cpu_group: Any = None,
) -> float:
total_free_memory = psutil.virtual_memory().available
# For simplicity, we assume 1 NUMA node for now in this platform abstraction
# as get_cpu_ids_by_node is not available in multimodal_gen.runtime.utils
n_numa_node = 1
free_memory = total_free_memory / n_numa_node
if distributed:
import torch.distributed as dist
tensor = torch.tensor(free_memory, dtype=torch.float32)
dist.all_reduce(tensor, op=dist.ReduceOp.MIN, group=cpu_group)
free_memory = float(tensor.item())
return free_memory / (1 << 30)
@classmethod @classmethod
def get_device_communicator_cls(cls) -> str: def get_device_communicator_cls(cls) -> str:
return "sglang.multimodal_gen.runtime.distributed.device_communicators.cpu_communicator.CpuCommunicator" return "sglang.multimodal_gen.runtime.distributed.device_communicators.cpu_communicator.CpuCommunicator"
@@ -5,12 +5,12 @@
"""Code inside this file can safely assume cuda platform, e.g. importing """Code inside this file can safely assume cuda platform, e.g. importing
pynvml. However, it should not initialize cuda context. pynvml. However, it should not initialize cuda context.
""" """
import os import os
from collections.abc import Callable from collections.abc import Callable
from functools import lru_cache, wraps from functools import lru_cache, wraps
from typing import TypeVar from typing import Any, TypeVar
import psutil
import torch import torch
from typing_extensions import ParamSpec from typing_extensions import ParamSpec
@@ -82,6 +82,7 @@ class CudaPlatformBase(Platform):
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod
@lru_cache(maxsize=1)
def get_device_total_memory(cls, device_id: int = 0) -> int: def get_device_total_memory(cls, device_id: int = 0) -> int:
raise NotImplementedError raise NotImplementedError
@@ -111,6 +112,38 @@ class CudaPlatformBase(Platform):
torch.cuda.reset_peak_memory_stats(device) torch.cuda.reset_peak_memory_stats(device)
return float(torch.cuda.max_memory_allocated(device)) return float(torch.cuda.max_memory_allocated(device))
@classmethod
def get_available_gpu_memory(
cls,
device_id: int = 0,
distributed: bool = False,
empty_cache: bool = True,
cpu_group: Any = None,
) -> float:
if empty_cache:
torch.cuda.empty_cache()
# Orin, Thor, Spark
# SM 8.7 is Orin, 11.0 is Thor, 12.1 is Spark
SHARED_SYSMEM_DEVICE_MEM_SMS = (87, 110, 121)
capability = cls.get_device_capability(device_id)
sm = capability.to_int() if capability else 0
if sm in SHARED_SYSMEM_DEVICE_MEM_SMS:
free_gpu_memory = psutil.virtual_memory().available
else:
free_gpu_memory, _ = torch.cuda.mem_get_info(device_id)
if distributed:
import torch.distributed as dist
tensor = torch.tensor(free_gpu_memory, dtype=torch.float32, device="cuda")
dist.all_reduce(tensor, op=dist.ReduceOp.MIN, group=cpu_group)
free_gpu_memory = float(tensor.item())
return free_gpu_memory / (1 << 30)
@classmethod @classmethod
def get_attn_backend_cls_str( def get_attn_backend_cls_str(
cls, cls,
@@ -409,6 +442,7 @@ class NonNvmlCudaPlatform(CudaPlatformBase):
return str(torch.cuda.get_device_name(device_id)) return str(torch.cuda.get_device_name(device_id))
@classmethod @classmethod
@lru_cache(maxsize=1)
def get_device_total_memory(cls, device_id: int = 0) -> int: def get_device_total_memory(cls, device_id: int = 0) -> int:
device_props = torch.cuda.get_device_properties(device_id) device_props = torch.cuda.get_device_properties(device_id)
return int(device_props.total_memory) return int(device_props.total_memory)
@@ -7,7 +7,7 @@ from __future__ import annotations
import enum import enum
import random import random
from functools import lru_cache from functools import lru_cache
from typing import TYPE_CHECKING, NamedTuple from typing import TYPE_CHECKING, Any, NamedTuple
import numpy as np import numpy as np
import torch import torch
@@ -216,6 +216,7 @@ class Platform:
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod
@lru_cache(maxsize=1)
def get_device_total_memory(cls, device_id: int = 0) -> int: def get_device_total_memory(cls, device_id: int = 0) -> int:
"""Get the total memory of a device in bytes.""" """Get the total memory of a device in bytes."""
raise NotImplementedError raise NotImplementedError
@@ -307,6 +308,19 @@ class Platform:
""" """
raise NotImplementedError raise NotImplementedError
@classmethod
def get_available_gpu_memory(
cls,
device_id: int = 0,
distributed: bool = False,
empty_cache: bool = True,
cpu_group: Any = None,
) -> float:
"""
Return the available memory in GiB.
"""
raise NotImplementedError
@classmethod @classmethod
def get_device_communicator_cls(cls) -> str: def get_device_communicator_cls(cls) -> str:
""" """
@@ -1,17 +1,21 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo # Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
from functools import lru_cache
from typing import Any
# SPDX-License-Identifier: Apache-2.0 import psutil
import torch import torch
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum from sglang.multimodal_gen.runtime.platforms import (
from sglang.multimodal_gen.runtime.platforms.interface import ( AttentionBackendEnum,
DeviceCapability,
Platform, Platform,
PlatformEnum, PlatformEnum,
) )
from sglang.multimodal_gen.runtime.platforms.interface import DeviceCapability
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
# SPDX-License-Identifier: Apache-2.0
logger = init_logger(__name__) logger = init_logger(__name__)
@@ -35,8 +39,10 @@ class MpsPlatform(Platform):
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod
@lru_cache(maxsize=1)
def get_device_total_memory(cls, device_id: int = 0) -> int: def get_device_total_memory(cls, device_id: int = 0) -> int:
raise NotImplementedError
return psutil.virtual_memory().total
@classmethod @classmethod
def is_async_output_supported(cls, enforce_eager: bool | None) -> bool: def is_async_output_supported(cls, enforce_eager: bool | None) -> bool:
@@ -55,6 +61,30 @@ class MpsPlatform(Platform):
) -> float: ) -> float:
return 0.0 return 0.0
@classmethod
def get_available_gpu_memory(
cls,
device_id: int = 0,
distributed: bool = False,
empty_cache: bool = True,
cpu_group: Any = None,
) -> float:
if empty_cache:
torch.mps.empty_cache()
# For MPS, available memory is essentially the system available memory
free_memory = psutil.virtual_memory().available
if distributed:
import torch.distributed as dist
tensor = torch.tensor(free_memory, dtype=torch.float32)
dist.all_reduce(tensor, op=dist.ReduceOp.MIN, group=cpu_group)
free_memory = float(tensor.item())
return free_memory / (1 << 30)
@classmethod @classmethod
def get_attn_backend_cls_str( def get_attn_backend_cls_str(
cls, cls,
@@ -6,6 +6,8 @@
This file is a platform abstraction for ROCm GPUs, This file is a platform abstraction for ROCm GPUs,
adjusted to match the structure and interface of `cuda.py`. adjusted to match the structure and interface of `cuda.py`.
""" """
from functools import lru_cache
from typing import Any
import torch import torch
@@ -39,6 +41,7 @@ class RocmPlatform(Platform):
return str(torch.cuda.get_device_name(device_id)) return str(torch.cuda.get_device_name(device_id))
@classmethod @classmethod
@lru_cache(maxsize=1)
def get_device_total_memory(cls, device_id: int = 0) -> int: def get_device_total_memory(cls, device_id: int = 0) -> int:
return torch.cuda.get_device_properties(device_id).total_memory return torch.cuda.get_device_properties(device_id).total_memory
@@ -61,6 +64,28 @@ class RocmPlatform(Platform):
torch.cuda.reset_peak_memory_stats(device) torch.cuda.reset_peak_memory_stats(device)
return float(torch.cuda.max_memory_allocated(device)) return float(torch.cuda.max_memory_allocated(device))
@classmethod
def get_available_gpu_memory(
cls,
device_id: int = 0,
distributed: bool = False,
empty_cache: bool = True,
cpu_group: Any = None,
) -> float:
if empty_cache:
torch.cuda.empty_cache()
free_gpu_memory, _ = torch.cuda.mem_get_info(device_id)
if distributed:
import torch.distributed as dist
tensor = torch.tensor(free_gpu_memory, dtype=torch.float32, device="cuda")
dist.all_reduce(tensor, op=dist.ReduceOp.MIN, group=cpu_group)
free_gpu_memory = float(tensor.item())
return free_gpu_memory / (1 << 30)
@classmethod @classmethod
def get_attn_backend_cls_str( def get_attn_backend_cls_str(
cls, cls,