[diffusion] feat: add an arg for controlling the number of prefetched layers in layerwise-offload (#17693)
This commit is contained in:
@@ -742,7 +742,10 @@ class DenoisingStage(PipelineStage):
|
|||||||
# reset offload managers with prefetching first layer for next forward
|
# reset offload managers with prefetching first layer for next forward
|
||||||
for dit in filter(None, [self.transformer, self.transformer_2]):
|
for dit in filter(None, [self.transformer, self.transformer_2]):
|
||||||
if isinstance(dit, OffloadableDiTMixin):
|
if isinstance(dit, OffloadableDiTMixin):
|
||||||
dit.prepare_for_next_denoise()
|
# release all DiT weights to avoid peak VRAM usage, which may increasing the latency for next req
|
||||||
|
# TODO: should be make this an option?
|
||||||
|
for manager in dit.layerwise_offload_managers:
|
||||||
|
manager.release_all()
|
||||||
|
|
||||||
def _preprocess_sp_latents(self, batch: Req, server_args: ServerArgs):
|
def _preprocess_sp_latents(self, batch: Req, server_args: ServerArgs):
|
||||||
"""Shard latents for Sequence Parallelism if applicable."""
|
"""Shard latents for Sequence Parallelism if applicable."""
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import argparse
|
|||||||
import dataclasses
|
import dataclasses
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
@@ -285,6 +286,7 @@ class ServerArgs:
|
|||||||
# CPU offload parameters
|
# CPU offload parameters
|
||||||
dit_cpu_offload: bool | None = None
|
dit_cpu_offload: bool | None = None
|
||||||
dit_layerwise_offload: bool | None = None
|
dit_layerwise_offload: bool | None = None
|
||||||
|
dit_offload_prefetch_size: float = 0.0
|
||||||
text_encoder_cpu_offload: bool | None = None
|
text_encoder_cpu_offload: bool | None = None
|
||||||
image_encoder_cpu_offload: bool | None = None
|
image_encoder_cpu_offload: bool | None = None
|
||||||
vae_cpu_offload: bool | None = None
|
vae_cpu_offload: bool | None = None
|
||||||
@@ -617,6 +619,12 @@ class ServerArgs:
|
|||||||
help="Enable layerwise CPU offload with async H2D prefetch overlap for supported DiT models (e.g., Wan). "
|
help="Enable layerwise CPU offload with async H2D prefetch overlap for supported DiT models (e.g., Wan). "
|
||||||
"Cannot be used together with cache-dit (SGLANG_CACHE_DIT_ENABLED), dit_cpu_offload, or use_fsdp_inference.",
|
"Cannot be used together with cache-dit (SGLANG_CACHE_DIT_ENABLED), dit_cpu_offload, or use_fsdp_inference.",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dit-offload-prefetch-size",
|
||||||
|
type=float,
|
||||||
|
default=ServerArgs.dit_offload_prefetch_size,
|
||||||
|
help="The size of prefetch for dit-layerwise-offload. If the value is between 0.0 and 1.0, it is treated as a ratio of the total number of layers. If the value is >= 1, it is treated as the absolute number of layers. 0.0 means prefetch 1 layer (lowest memory). Values above 0.5 might have peak memory close to no offload but worse performance.",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--use-fsdp-inference",
|
"--use-fsdp-inference",
|
||||||
action=StoreBoolean,
|
action=StoreBoolean,
|
||||||
@@ -949,6 +957,21 @@ class ServerArgs:
|
|||||||
self.use_fsdp_inference = False
|
self.use_fsdp_inference = False
|
||||||
self.dit_layerwise_offload = False
|
self.dit_layerwise_offload = False
|
||||||
|
|
||||||
|
if self.dit_offload_prefetch_size > 1 and (
|
||||||
|
isinstance(self.dit_offload_prefetch_size, float)
|
||||||
|
and not self.dit_offload_prefetch_size.is_integer()
|
||||||
|
):
|
||||||
|
self.dit_offload_prefetch_size = int(
|
||||||
|
math.floor(self.dit_offload_prefetch_size)
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"Invalid --dit-offload-prefetch-size value passed, truncated to: {self.dit_offload_prefetch_size}"
|
||||||
|
)
|
||||||
|
if 0.5 <= self.dit_offload_prefetch_size < 1.0:
|
||||||
|
logger.info(
|
||||||
|
f"We do not recommend --dit-offload-prefetch-size to be between 0.5 and 1.0"
|
||||||
|
)
|
||||||
|
|
||||||
if not envs.SGLANG_CACHE_DIT_ENABLED:
|
if not envs.SGLANG_CACHE_DIT_ENABLED:
|
||||||
# TODO: need a better way to tell this
|
# TODO: need a better way to tell this
|
||||||
if (
|
if (
|
||||||
@@ -962,6 +985,9 @@ class ServerArgs:
|
|||||||
self.dit_layerwise_offload = True
|
self.dit_layerwise_offload = True
|
||||||
|
|
||||||
if self.dit_layerwise_offload:
|
if self.dit_layerwise_offload:
|
||||||
|
assert (
|
||||||
|
self.dit_offload_prefetch_size >= 0.0
|
||||||
|
), "dit_offload_prefetch_size must be non-negative"
|
||||||
if self.use_fsdp_inference:
|
if self.use_fsdp_inference:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"dit_layerwise_offload is enabled, automatically disabling use_fsdp_inference."
|
"dit_layerwise_offload is enabled, automatically disabling use_fsdp_inference."
|
||||||
|
|||||||
@@ -33,12 +33,13 @@ class LayerwiseOffloadManager:
|
|||||||
num_layers: int,
|
num_layers: int,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
pin_cpu_memory: bool = True,
|
pin_cpu_memory: bool = True,
|
||||||
|
prefetch_size: int = 1,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.model = model
|
self.model = model
|
||||||
self.layers_attr_str = layers_attr_str
|
self.layers_attr_str = layers_attr_str
|
||||||
self.num_layers = num_layers
|
self.num_layers = num_layers
|
||||||
self.pin_cpu_memory = pin_cpu_memory
|
self.pin_cpu_memory = pin_cpu_memory
|
||||||
|
self.prefetch_size = min(max(1, prefetch_size), self.num_layers)
|
||||||
self.enabled = bool(enabled and torch.cuda.is_available())
|
self.enabled = bool(enabled and torch.cuda.is_available())
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
return
|
return
|
||||||
@@ -57,6 +58,8 @@ class LayerwiseOffloadManager:
|
|||||||
self._weight_metadata: Dict[int, Dict[str, Dict[str, Any]]] = {}
|
self._weight_metadata: Dict[int, Dict[str, Dict[str, Any]]] = {}
|
||||||
# layer indices that are already in gpu
|
# layer indices that are already in gpu
|
||||||
self._gpu_layers: Set[int] = set()
|
self._gpu_layers: Set[int] = set()
|
||||||
|
# layer_idx -> torch.cuda.Event for fine-grained sync, to make sure the weight is resident in pre-hook
|
||||||
|
self._prefetch_events: Dict[int, torch.cuda.Event] = {}
|
||||||
|
|
||||||
self._named_parameters: Dict[str, torch.nn.Parameter] = {}
|
self._named_parameters: Dict[str, torch.nn.Parameter] = {}
|
||||||
self._named_buffers: Dict[str, torch.Tensor] = {}
|
self._named_buffers: Dict[str, torch.Tensor] = {}
|
||||||
@@ -127,13 +130,19 @@ class LayerwiseOffloadManager:
|
|||||||
self._consolidated_cpu_weights[layer_idx][dtype] = cpu_buffer
|
self._consolidated_cpu_weights[layer_idx][dtype] = cpu_buffer
|
||||||
|
|
||||||
# prefetch the first layer for warm-up
|
# prefetch the first layer for warm-up
|
||||||
self.prepare_for_next_denoise(non_blocking=False)
|
self.prepare_for_next_req(non_blocking=False)
|
||||||
|
|
||||||
self.register_forward_hooks()
|
self.register_forward_hooks()
|
||||||
logger.info("LayerwiseOffloadManager initialized")
|
logger.info(
|
||||||
|
f"LayerwiseOffloadManager initialized with num prefetched layer: {self.prefetch_size}, total num layers: {self.num_layers}"
|
||||||
|
)
|
||||||
|
|
||||||
def prepare_for_next_denoise(self, non_blocking=True):
|
def prepare_for_next_req(self, non_blocking=True):
|
||||||
self.prefetch_layer(0, non_blocking=non_blocking)
|
"""
|
||||||
|
Prepare for the next round of denoising loop with prefetching the necessary layers
|
||||||
|
"""
|
||||||
|
for i in range(self.prefetch_size):
|
||||||
|
self.prefetch_layer(i, non_blocking=non_blocking)
|
||||||
if not non_blocking and self.copy_stream is not None:
|
if not non_blocking and self.copy_stream is not None:
|
||||||
torch.cuda.current_stream().wait_stream(self.copy_stream)
|
torch.cuda.current_stream().wait_stream(self.copy_stream)
|
||||||
|
|
||||||
@@ -147,6 +156,9 @@ class LayerwiseOffloadManager:
|
|||||||
|
|
||||||
@torch.compiler.disable
|
@torch.compiler.disable
|
||||||
def prefetch_layer(self, layer_idx: int, non_blocking: bool = True) -> None:
|
def prefetch_layer(self, layer_idx: int, non_blocking: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
idempotent
|
||||||
|
"""
|
||||||
if not self.enabled or self.device is None or self.copy_stream is None:
|
if not self.enabled or self.device is None or self.copy_stream is None:
|
||||||
return
|
return
|
||||||
if layer_idx < 0 or layer_idx >= self.num_layers:
|
if layer_idx < 0 or layer_idx >= self.num_layers:
|
||||||
@@ -167,6 +179,11 @@ class LayerwiseOffloadManager:
|
|||||||
gpu_buffer.copy_(cpu_buffer, non_blocking=non_blocking)
|
gpu_buffer.copy_(cpu_buffer, non_blocking=non_blocking)
|
||||||
gpu_buffers[dtype] = gpu_buffer
|
gpu_buffers[dtype] = gpu_buffer
|
||||||
|
|
||||||
|
# record the prefetch event of this layer
|
||||||
|
event = torch.cuda.Event()
|
||||||
|
event.record(self.copy_stream)
|
||||||
|
self._prefetch_events[layer_idx] = event
|
||||||
|
|
||||||
# restore model's weights by their metadata using gpu buffer
|
# restore model's weights by their metadata using gpu buffer
|
||||||
for name, meta in self._weight_metadata[layer_idx].items():
|
for name, meta in self._weight_metadata[layer_idx].items():
|
||||||
dtype = meta["dtype"]
|
dtype = meta["dtype"]
|
||||||
@@ -182,8 +199,16 @@ class LayerwiseOffloadManager:
|
|||||||
|
|
||||||
@torch.compiler.disable
|
@torch.compiler.disable
|
||||||
def release_layer(self, layer_idx: int) -> None:
|
def release_layer(self, layer_idx: int) -> None:
|
||||||
|
"""
|
||||||
|
lightweight release layer weights
|
||||||
|
Basically set the reference count to the gpu weight tensor to zero. The weights on cpu is untouched
|
||||||
|
"""
|
||||||
if not self.enabled or self.device is None:
|
if not self.enabled or self.device is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# clear prefetch event, since it's useless and needs to be reset
|
||||||
|
self._prefetch_events.pop(layer_idx, None)
|
||||||
|
|
||||||
if layer_idx <= 0:
|
if layer_idx <= 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -259,14 +284,24 @@ class LayerwiseOffloadManager:
|
|||||||
|
|
||||||
def make_pre_hook(i):
|
def make_pre_hook(i):
|
||||||
def hook(module, input):
|
def hook(module, input):
|
||||||
self.prefetch_layer(i + 1, non_blocking=True)
|
# wait only for the current layer if it's being prefetched
|
||||||
|
if i == 0:
|
||||||
|
self.prepare_for_next_req(non_blocking=False)
|
||||||
|
if i in self._prefetch_events:
|
||||||
|
torch.cuda.current_stream().wait_event(self._prefetch_events[i])
|
||||||
|
|
||||||
|
# trigger batch prefetch (i + prefetch_size ~ i + 2 * prefetch_size) if needed
|
||||||
|
if i % self.prefetch_size == 0:
|
||||||
|
for j in range(i + self.prefetch_size, i + 2 * self.prefetch_size):
|
||||||
|
layer_to_prefetch = j % self.num_layers
|
||||||
|
self.prefetch_layer(layer_to_prefetch, non_blocking=True)
|
||||||
|
|
||||||
return hook
|
return hook
|
||||||
|
|
||||||
def make_post_hook(i):
|
def make_post_hook(i):
|
||||||
def hook(module, input, output):
|
def hook(module, input, output):
|
||||||
if self.copy_stream is not None:
|
# previous, we wait here, until the copy stream for next layer is finished,
|
||||||
torch.cuda.current_stream().wait_stream(self.copy_stream)
|
# now with any prefetch_size, only wait for the copy stream, when the copy stream is for the next layer
|
||||||
self.release_layer(i)
|
self.release_layer(i)
|
||||||
|
|
||||||
return hook
|
return hook
|
||||||
@@ -292,7 +327,7 @@ class OffloadableDiTMixin:
|
|||||||
|
|
||||||
# the list of names of a DiT's layers/blocks
|
# the list of names of a DiT's layers/blocks
|
||||||
layer_names: List[str]
|
layer_names: List[str]
|
||||||
layerwise_offload_managers: list[LayerwiseOffloadManager] | None = None
|
layerwise_offload_managers: list[LayerwiseOffloadManager] = []
|
||||||
|
|
||||||
def configure_layerwise_offload(self, server_args: ServerArgs):
|
def configure_layerwise_offload(self, server_args: ServerArgs):
|
||||||
self.layerwise_offload_managers = []
|
self.layerwise_offload_managers = []
|
||||||
@@ -303,12 +338,20 @@ class OffloadableDiTMixin:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
num_layers = len(module_list)
|
num_layers = len(module_list)
|
||||||
|
if server_args.dit_offload_prefetch_size < 1.0:
|
||||||
|
prefetch_size = 1 + int(
|
||||||
|
round(server_args.dit_offload_prefetch_size * (num_layers - 1))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
prefetch_size = int(server_args.dit_offload_prefetch_size)
|
||||||
|
|
||||||
manager = LayerwiseOffloadManager(
|
manager = LayerwiseOffloadManager(
|
||||||
model=self,
|
model=self,
|
||||||
layers_attr_str=layer_name,
|
layers_attr_str=layer_name,
|
||||||
num_layers=num_layers,
|
num_layers=num_layers,
|
||||||
enabled=True,
|
enabled=True,
|
||||||
pin_cpu_memory=server_args.pin_cpu_memory,
|
pin_cpu_memory=server_args.pin_cpu_memory,
|
||||||
|
prefetch_size=prefetch_size,
|
||||||
)
|
)
|
||||||
self.layerwise_offload_managers.append(manager)
|
self.layerwise_offload_managers.append(manager)
|
||||||
|
|
||||||
@@ -316,11 +359,11 @@ class OffloadableDiTMixin:
|
|||||||
f"Enabled layerwise offload for {self.__class__.__name__} on modules: {self.layer_names}"
|
f"Enabled layerwise offload for {self.__class__.__name__} on modules: {self.layer_names}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def prepare_for_next_denoise(self):
|
def prepare_for_next_req(self):
|
||||||
if self.layerwise_offload_managers is None:
|
if self.layerwise_offload_managers is None:
|
||||||
return
|
return
|
||||||
for manager in self.layerwise_offload_managers:
|
for manager in self.layerwise_offload_managers:
|
||||||
manager.prepare_for_next_denoise(non_blocking=True)
|
manager.prepare_for_next_req(non_blocking=True)
|
||||||
|
|
||||||
def disable_offload(self) -> None:
|
def disable_offload(self) -> None:
|
||||||
"""Disable layerwise offload: load all layers to GPU and remove hooks."""
|
"""Disable layerwise offload: load all layers to GPU and remove hooks."""
|
||||||
@@ -338,7 +381,5 @@ class OffloadableDiTMixin:
|
|||||||
for manager in self.layerwise_offload_managers:
|
for manager in self.layerwise_offload_managers:
|
||||||
if manager.enabled:
|
if manager.enabled:
|
||||||
manager.sync_all_layers_to_cpu()
|
manager.sync_all_layers_to_cpu()
|
||||||
for layer_idx in list(manager._gpu_layers):
|
manager.release_all()
|
||||||
if layer_idx > 0:
|
|
||||||
manager.release_layer(layer_idx)
|
|
||||||
manager.register_forward_hooks()
|
manager.register_forward_hooks()
|
||||||
|
|||||||
@@ -53,19 +53,22 @@ def _openai_client(port: int) -> OpenAI:
|
|||||||
|
|
||||||
|
|
||||||
def _build_server_extra_args(case: DiffusionTestCase) -> str:
|
def _build_server_extra_args(case: DiffusionTestCase) -> str:
|
||||||
|
server_args = case.server_args
|
||||||
a = os.environ.get("SGLANG_TEST_SERVE_ARGS", "")
|
a = os.environ.get("SGLANG_TEST_SERVE_ARGS", "")
|
||||||
a += f" --num-gpus {case.server_args.num_gpus}"
|
a += f" --num-gpus {server_args.num_gpus}"
|
||||||
if case.server_args.tp_size is not None:
|
if server_args.tp_size is not None:
|
||||||
a += f" --tp-size {case.server_args.tp_size}"
|
a += f" --tp-size {server_args.tp_size}"
|
||||||
if case.server_args.ulysses_degree is not None:
|
if server_args.ulysses_degree is not None:
|
||||||
a += f" --ulysses-degree {case.server_args.ulysses_degree}"
|
a += f" --ulysses-degree {server_args.ulysses_degree}"
|
||||||
if case.server_args.dit_layerwise_offload:
|
if server_args.dit_layerwise_offload:
|
||||||
a += " --dit-layerwise-offload true"
|
a += " --dit-layerwise-offload true"
|
||||||
if case.server_args.ring_degree is not None:
|
if server_args.dit_offload_prefetch_size:
|
||||||
a += f" --ring-degree {case.server_args.ring_degree}"
|
a += f" --dit-offload-prefetch-size {server_args.dit_offload_prefetch_size}"
|
||||||
if case.server_args.lora_path:
|
if server_args.ring_degree is not None:
|
||||||
a += f" --lora-path {case.server_args.lora_path}"
|
a += f" --ring-degree {server_args.ring_degree}"
|
||||||
if case.server_args.enable_warmup:
|
if server_args.lora_path:
|
||||||
|
a += f" --lora-path {server_args.lora_path}"
|
||||||
|
if server_args.enable_warmup:
|
||||||
a += " --enable-warmup"
|
a += " --enable-warmup"
|
||||||
return a
|
return a
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ def diffusion_server(case: DiffusionTestCase) -> ServerContext:
|
|||||||
if server_args.dit_layerwise_offload:
|
if server_args.dit_layerwise_offload:
|
||||||
extra_args += f" --dit-layerwise-offload true"
|
extra_args += f" --dit-layerwise-offload true"
|
||||||
|
|
||||||
|
if server_args.dit_offload_prefetch_size:
|
||||||
|
extra_args += (
|
||||||
|
f" --dit-offload-prefetch-size {server_args.dit_offload_prefetch_size}"
|
||||||
|
)
|
||||||
|
|
||||||
if server_args.text_encoder_cpu_offload:
|
if server_args.text_encoder_cpu_offload:
|
||||||
extra_args += f" --text-encoder-cpu-offload"
|
extra_args += f" --text-encoder-cpu-offload"
|
||||||
|
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ class DiffusionServerArgs:
|
|||||||
enable_warmup: bool = False
|
enable_warmup: bool = False
|
||||||
|
|
||||||
dit_layerwise_offload: bool = False
|
dit_layerwise_offload: bool = False
|
||||||
|
dit_offload_prefetch_size: int | float | None = None
|
||||||
enable_cache_dit: bool = False
|
enable_cache_dit: bool = False
|
||||||
text_encoder_cpu_offload: bool = False
|
text_encoder_cpu_offload: bool = False
|
||||||
|
|
||||||
@@ -369,6 +370,7 @@ ONE_GPU_CASES_A: list[DiffusionTestCase] = [
|
|||||||
model_path="black-forest-labs/FLUX.2-dev",
|
model_path="black-forest-labs/FLUX.2-dev",
|
||||||
modality="image",
|
modality="image",
|
||||||
dit_layerwise_offload=True,
|
dit_layerwise_offload=True,
|
||||||
|
dit_offload_prefetch_size=2,
|
||||||
),
|
),
|
||||||
T2I_sampling_params,
|
T2I_sampling_params,
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user