chore: import upstream snapshot with attribution
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
@@ -0,0 +1,172 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from sglang.multimodal_gen.runtime.distributed import get_tp_rank, get_tp_world_size
from sglang.multimodal_gen.runtime.loader.weight_utils import compute_weights_checksum
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
iter_materialized_weights,
)
from sglang.multimodal_gen.runtime.post_training.tensor_update_checker import (
TensorUpdateChecker,
)
from sglang.multimodal_gen.runtime.post_training.weights_updater import (
WeightsUpdater,
get_updatable_modules,
)
from sglang.srt.utils import MultiprocessingSerializer
from sglang.srt.utils.patch_torch import monkey_patch_torch_reductions
if TYPE_CHECKING:
from sglang.multimodal_gen.runtime.entrypoints.post_training.io_struct import (
UpdateWeightFromTensorCheckerReqInput,
UpdateWeightFromTensorReqInput,
)
class GPUWorkerPostTrainingMixin:
def update_weights_from_disk(
self,
model_path: str,
flush_cache: bool = True,
target_modules: list[str] | None = None,
) -> tuple[bool, str]:
if not self.pipeline:
return False, "Pipeline is not initialized"
updater = WeightsUpdater(self.pipeline)
success, message = updater.update_weights_from_disk(
model_path,
flush_cache=flush_cache,
target_modules=target_modules,
)
if success:
self.server_args.model_path = model_path
self.pipeline.model_path = model_path
return success, message
def update_weights_from_tensor(
self,
req: UpdateWeightFromTensorReqInput,
) -> tuple[bool, str]:
if not self.pipeline:
return False, "Pipeline is not initialized"
payload, error = self._select_rank_scoped_payload(
payloads=req.serialized_named_tensors,
field_name="serialized_named_tensors",
)
if error is not None:
return False, error
monkey_patch_torch_reductions()
try:
named_tensors = MultiprocessingSerializer.deserialize(payload)
except Exception as e:
return False, f"Failed to deserialize serialized_named_tensors: {e}"
updater = WeightsUpdater(self.pipeline)
return updater.update_weights_from_tensor(
named_tensors=named_tensors,
load_format=req.load_format,
target_modules=req.target_modules,
)
def update_weights_from_tensor_checker(
self,
req: UpdateWeightFromTensorCheckerReqInput,
) -> tuple[bool, str]:
if not self.pipeline:
return False, "Pipeline is not initialized"
checker = TensorUpdateChecker(self.pipeline)
result = checker.verify_across_tp(
target_module=req.target_module,
expected_named_tensors_sha256=req.expected_named_tensors_sha256,
tp_rank=get_tp_rank(),
tp_world_size=get_tp_world_size(),
tp_cpu_group=self.tp_cpu_group,
tp_root_rank=self.tp_group.first_rank,
)
if self.sp_group.world_size == 1:
return result
import torch
is_sp_root = self.sp_group.rank_in_group == 0
gathered_results = [None] * self.sp_group.world_size if is_sp_root else None
torch.distributed.gather_object(
result,
gathered_results,
dst=self.sp_group.first_rank,
group=self.sp_cpu_group,
)
final_result = None
if is_sp_root:
failures = [
(rank, message)
for rank, (success, message) in enumerate(gathered_results)
if not success
]
if failures:
rank, message = failures[0]
if len(failures) == 1:
final_result = (False, f"SP rank {rank}: {message}")
else:
final_result = (
False,
f"{len(failures)} SP ranks failed update_weight_from_tensor_checker; "
f"first failure on rank {rank}: {message}",
)
else:
final_result = result
final_result_holder = [final_result]
torch.distributed.broadcast_object_list(
final_result_holder,
src=self.sp_group.first_rank,
group=self.sp_cpu_group,
)
return final_result_holder[0]
def get_weights_checksum(
self, module_names: list[str] | None = None
) -> dict[str, str]:
if not self.pipeline:
return {"error": "Pipeline is not initialized"}
all_modules = get_updatable_modules(self.pipeline)
names = module_names if module_names is not None else list(all_modules.keys())
checksums: dict[str, str] = {}
for name in names:
module = all_modules.get(name)
if module is None:
checksums[name] = "not_found"
continue
checksums[name] = compute_weights_checksum(
iter_materialized_weights(module)
)
return checksums
def _select_rank_scoped_payload(
self,
payloads: list,
field_name: str,
) -> tuple[object | None, str | None]:
if not isinstance(payloads, list):
return None, f"{field_name} must be a list"
if not payloads:
return None, f"{field_name} is required"
tp_world_size = get_tp_world_size()
if len(payloads) not in (1, tp_world_size):
return (
None,
f"{field_name} size must be 1 or tp_size ({tp_world_size}), "
f"got {len(payloads)}",
)
payload_idx = get_tp_rank() if len(payloads) == tp_world_size else 0
return payloads[payload_idx], None
@@ -0,0 +1,62 @@
# SPDX-License-Identifier: Apache-2.0
"""RL-specific dataclasses used by post-training and rollout paths."""
from dataclasses import dataclass, field
from typing import Any
import torch
@dataclass
class RolloutSessionData:
"""Per-batch rollout state created by prepare_rollout(), lives on the batch object.
Cleared by setting ``batch._rollout_session_data = None``.
"""
pipeline_config: Any = None
sigma_max: float = 0.0
latents_shape: tuple | None = None
noise_buffer: torch.Tensor | None = None
local_log_prob_sum: list[torch.Tensor] = field(default_factory=list)
local_log_prob_count: list[torch.Tensor] = field(default_factory=list)
local_variance_noises: list[torch.Tensor] = field(default_factory=list)
local_prev_sample_means: list[torch.Tensor] = field(default_factory=list)
local_noise_std_devs: list[torch.Tensor] = field(default_factory=list)
local_model_outputs: list[torch.Tensor] = field(default_factory=list)
@dataclass
class RolloutDebugTensors:
"""Container for rollout debug tensors collected during denoising."""
rollout_variance_noises: torch.Tensor | None = None
rollout_prev_sample_means: torch.Tensor | None = None
rollout_noise_std_devs: torch.Tensor | None = None
rollout_model_outputs: torch.Tensor | None = None
@dataclass
class RolloutDenoisingEnv:
image_kwargs: dict[str, Any] | None = None
pos_cond_kwargs: dict[str, Any] | None = None
neg_cond_kwargs: dict[str, Any] | None = None
guidance: torch.Tensor | None = None
@dataclass
class RolloutDitTrajectory:
# [B, T+1, ...]: per-step noisy latents x_{t_0..t_{T-1}} followed by the
# final denoised latent x_{t_T} (last scheduler.step output).
latents: torch.Tensor | None = None
timesteps: torch.Tensor | None = None # [T]
@dataclass
class RolloutTrajectoryData:
rollout_log_probs: torch.Tensor | None = None
rollout_debug_tensors: RolloutDebugTensors | None = None
denoising_env: RolloutDenoisingEnv | None = None
dit_trajectory: RolloutDitTrajectory | None = None
@@ -0,0 +1,206 @@
"""Mixin for rollout-related denoising hooks.
Moved out of DenoisingStage to keep the core stage lean.
"""
from __future__ import annotations
from typing import Any
import torch
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
from sglang.multimodal_gen.runtime.post_training.rl_dataclasses import (
RolloutDenoisingEnv,
RolloutDitTrajectory,
RolloutTrajectoryData,
)
from sglang.multimodal_gen.runtime.post_training.scheduler_rl_mixin import (
SchedulerRLMixin,
)
from sglang.multimodal_gen.runtime.post_training.sp_utils import (
gather_stacked_latents_for_sp,
)
from sglang.multimodal_gen.runtime.server_args import ServerArgs
def _kwargs_to_cpu(d: Any) -> Any:
if isinstance(d, torch.Tensor):
return d.detach().cpu()
if isinstance(d, dict):
return {k: _kwargs_to_cpu(v) for k, v in d.items()}
if isinstance(d, list):
return [_kwargs_to_cpu(v) for v in d]
if isinstance(d, tuple):
return tuple(_kwargs_to_cpu(v) for v in d)
return d
class RolloutDenoisingMixin:
def _maybe_prepare_rollout(self, batch: Req):
"""Prepare denoising loop for rollout."""
if not isinstance(self.scheduler, SchedulerRLMixin):
if batch.rollout:
raise ValueError(
f"Scheduler {type(self.scheduler)} does not support rollout"
)
return
self.scheduler.release_rollout_resources(batch)
if batch.rollout:
self.scheduler.prepare_rollout(
batch=batch,
pipeline_config=self.server_args.pipeline_config,
)
def _maybe_collect_rollout_log_probs(self, batch: Req):
if not isinstance(self.scheduler, SchedulerRLMixin):
if batch.rollout:
raise ValueError(
f"Scheduler {type(self.scheduler)} does not support rollout"
)
return
if batch.rollout:
if batch.rollout_trajectory_data is None:
batch.rollout_trajectory_data = RolloutTrajectoryData()
batch.rollout_trajectory_data.rollout_log_probs = (
self.scheduler.collect_rollout_log_probs(batch)
)
if batch.rollout_debug_mode:
batch.rollout_trajectory_data.rollout_debug_tensors = (
self.scheduler.collect_rollout_debug_tensors(batch)
)
self.scheduler.release_rollout_resources(batch)
def _postprocess_rollout_outputs(
self,
batch: Req,
latents: torch.Tensor,
num_inference_steps: int,
final_timestep: torch.Tensor,
server_args: ServerArgs,
) -> None:
"""Finalize rollout-only outputs.
Must be called before ``_post_denoising_loop`` so that ``latents`` (the
last ``scheduler.step`` output) is still SP-sharded and can be gathered
uniformly with the per-step trajectory latents.
"""
self._maybe_collect_rollout_log_probs(batch)
# Append final denoised latent as the (T+1)-th entry (step_index=T),
# routed through the same filter so rollout_return_step_indices can
# include/exclude it.
self._maybe_append_dit_trajectory_step(
batch=batch,
latents=latents,
timestep_value=final_timestep,
step_index=num_inference_steps,
)
self._maybe_finalize_denoising_env_collection(
batch=batch,
pipeline_config=server_args.pipeline_config,
)
def _maybe_init_denoising_env_collection(
self,
batch,
pipeline_config,
image_kwargs: dict[str, Any],
pos_cond_kwargs: dict[str, Any],
neg_cond_kwargs: dict[str, Any],
guidance: torch.Tensor | None,
) -> None:
collect_env = batch.rollout_return_denoising_env
collect_traj = batch.rollout_return_dit_trajectory
if not (collect_env or collect_traj):
batch._rollout_denoising_env_state = None
return
if collect_env:
env = RolloutDenoisingEnv(
image_kwargs=_kwargs_to_cpu(image_kwargs),
pos_cond_kwargs=_kwargs_to_cpu(pos_cond_kwargs),
neg_cond_kwargs=(
_kwargs_to_cpu(neg_cond_kwargs) if neg_cond_kwargs else None
),
guidance=guidance.detach().cpu() if guidance is not None else None,
)
pos_src = pos_cond_kwargs
neg_src = neg_cond_kwargs
else:
env = None
pos_src = None
neg_src = None
batch._rollout_denoising_env_state = {
"env": env,
"step_latents": [],
"step_timesteps": [],
"pos_cond_kwargs_src": pos_src,
"neg_cond_kwargs_src": neg_src,
}
def _maybe_append_dit_trajectory_step(
self,
batch,
latents: torch.Tensor,
timestep_value: torch.Tensor,
step_index: int,
) -> None:
if not batch.rollout or not batch.rollout_return_dit_trajectory:
return
state = getattr(batch, "_rollout_denoising_env_state", None)
if state is None:
return
return_step_indices = getattr(batch, "rollout_return_step_indices", None)
if return_step_indices is not None and step_index not in return_step_indices:
return
state["step_latents"].append(latents.detach())
state["step_timesteps"].append(timestep_value.detach().cpu())
def _maybe_finalize_denoising_env_collection(self, batch, pipeline_config) -> None:
state = getattr(batch, "_rollout_denoising_env_state", None)
if state is None:
return
env: RolloutDenoisingEnv | None = state["env"]
step_latents: list[torch.Tensor] = state["step_latents"]
step_timesteps: list[torch.Tensor] = state["step_timesteps"]
if batch.rollout_trajectory_data is None:
batch.rollout_trajectory_data = RolloutTrajectoryData()
if step_latents and batch.rollout_return_dit_trajectory:
step_latents_tensor = torch.stack(step_latents, dim=1)
step_latents_tensor = gather_stacked_latents_for_sp(
pipeline_config=pipeline_config,
batch=batch,
stacked_latents=step_latents_tensor,
)
batch.rollout_trajectory_data.dit_trajectory = RolloutDitTrajectory(
latents=step_latents_tensor.cpu(),
timesteps=torch.stack(step_timesteps, dim=0).cpu(),
)
if env is not None and batch.rollout_return_denoising_env:
gather_fn = getattr(
pipeline_config, "gather_denoising_env_static_for_sp", None
)
pos_src = state.get("pos_cond_kwargs_src")
if pos_src is not None and env.pos_cond_kwargs is not None:
gathered_pos = gather_fn(batch, pos_src) if gather_fn else pos_src
env.pos_cond_kwargs = _kwargs_to_cpu(gathered_pos)
neg_src = state.get("neg_cond_kwargs_src")
if neg_src is not None and env.neg_cond_kwargs is not None:
gathered_neg = gather_fn(batch, neg_src) if gather_fn else neg_src
env.neg_cond_kwargs = _kwargs_to_cpu(gathered_neg)
batch.rollout_trajectory_data.denoising_env = env
batch._rollout_denoising_env_state = None
@@ -0,0 +1,46 @@
from __future__ import annotations
from typing import Any, List
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch
class SchedulerPostTrainingMixin:
def _handle_update_weights_from_disk(self, reqs: List[Any]) -> OutputBatch:
req = reqs[0]
success, message = self.worker.update_weights_from_disk(
model_path=req.model_path,
flush_cache=req.flush_cache,
target_modules=req.target_modules,
)
return OutputBatch(
output={"success": success, "message": message},
error=None if success else message,
)
def _handle_update_weights_from_tensor(self, reqs: List[Any]) -> OutputBatch:
req = reqs[0]
success, message = self.worker.update_weights_from_tensor(req)
if self.server_args.tp_size > 1:
import torch
torch.distributed.barrier(group=self.worker.tp_cpu_group)
return OutputBatch(
output={"success": success, "message": message},
error=None if success else message,
)
def _handle_update_weights_from_tensor_checker(
self, reqs: List[Any]
) -> OutputBatch:
req = reqs[0]
success, message = self.worker.update_weights_from_tensor_checker(req)
return OutputBatch(
output={"success": success, "message": message},
error=None if success else message,
)
def _handle_get_weights_checksum(self, reqs: List[Any]) -> OutputBatch:
req = reqs[0]
checksums = self.worker.get_weights_checksum(module_names=req.module_names)
return OutputBatch(output=checksums)
@@ -0,0 +1,116 @@
# SPDX-License-Identifier: Apache-2.0
"""Debug tensor helpers for rollout-enabled schedulers."""
import torch
from sglang.multimodal_gen.runtime.distributed import (
get_local_torch_device,
get_sp_world_size,
)
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
from sglang.multimodal_gen.runtime.post_training.rl_dataclasses import (
RolloutDebugTensors,
RolloutSessionData,
)
class SchedulerRLDebugMixin:
@staticmethod
def _reset_rollout_debug_tensors(rollout_session_data: RolloutSessionData) -> None:
rollout_session_data.local_variance_noises = []
rollout_session_data.local_prev_sample_means = []
rollout_session_data.local_noise_std_devs = []
rollout_session_data.local_model_outputs = []
def append_local_rollout_debug_tensors(
self,
batch,
*,
variance_noise: torch.Tensor,
prev_sample_mean: torch.Tensor,
noise_std_dev: torch.Tensor,
model_output: torch.Tensor,
) -> None:
rollout_session_data = batch._rollout_session_data
batch_size = variance_noise.shape[0]
# the underlying noise buffer in batch._rollout_session_data.noise_buffer is reused.
rollout_session_data.local_variance_noises.append(variance_noise.clone())
rollout_session_data.local_prev_sample_means.append(prev_sample_mean)
rollout_session_data.local_noise_std_devs.append(
noise_std_dev.expand((batch_size, 1))
)
rollout_session_data.local_model_outputs.append(model_output)
def consume_local_rollout_debug_tensors(
self,
batch,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
rollout_session_data = batch._rollout_session_data
variance_noises = torch.stack(rollout_session_data.local_variance_noises, dim=1)
prev_sample_means = torch.stack(
rollout_session_data.local_prev_sample_means, dim=1
)
noise_std_devs = torch.stack(rollout_session_data.local_noise_std_devs, dim=1)
model_outputs = torch.stack(rollout_session_data.local_model_outputs, dim=1)
self._reset_rollout_debug_tensors(rollout_session_data)
return variance_noises, prev_sample_means, noise_std_devs, model_outputs
def collect_rollout_debug_tensors(self, batch: Req) -> RolloutDebugTensors:
"""
Consume rollout debug tensors and merge for all SP ranks.
Returns rollout debug tensors with shape [B, T, ...].
"""
rollout_session_data = batch._rollout_session_data
variance_noises, prev_sample_means, noise_std_devs, model_outputs = (
self.consume_local_rollout_debug_tensors(batch)
)
if get_sp_world_size() > 1 and getattr(batch, "did_sp_shard_latents", False):
variance_noises = variance_noises.to(get_local_torch_device())
prev_sample_means = prev_sample_means.to(get_local_torch_device())
noise_std_devs = noise_std_devs.to(get_local_torch_device())
model_outputs = model_outputs.to(get_local_torch_device())
pipeline_config = rollout_session_data.pipeline_config
bsz, num_steps = variance_noises.shape[0], variance_noises.shape[1]
# [B, T, ...] -> [B*T, ...]
variance_noises_packed = variance_noises.contiguous().reshape(
bsz * num_steps, *variance_noises.shape[2:]
)
prev_sample_means_packed = prev_sample_means.contiguous().reshape(
bsz * num_steps, *prev_sample_means.shape[2:]
)
model_outputs_packed = model_outputs.contiguous().reshape(
bsz * num_steps, *model_outputs.shape[2:]
)
# Gather on packed tensors first.
variance_noises_packed = pipeline_config.gather_latents_for_sp(
variance_noises_packed, batch=batch
)
prev_sample_means_packed = pipeline_config.gather_latents_for_sp(
prev_sample_means_packed, batch=batch
)
model_outputs_packed = pipeline_config.gather_latents_for_sp(
model_outputs_packed, batch=batch
)
# Unpack back to [B, T, ...].
variance_noises = variance_noises_packed.reshape(
bsz, num_steps, *variance_noises_packed.shape[1:]
)
prev_sample_means = prev_sample_means_packed.reshape(
bsz, num_steps, *prev_sample_means_packed.shape[1:]
)
model_outputs = model_outputs_packed.reshape(
bsz, num_steps, *model_outputs_packed.shape[1:]
)
# noise_std_devs is same on every device, not a sharded latent tensor.
return RolloutDebugTensors(
rollout_variance_noises=variance_noises.cpu(),
rollout_prev_sample_means=prev_sample_means.cpu(),
rollout_noise_std_devs=noise_std_devs.cpu(),
rollout_model_outputs=model_outputs.cpu(),
)
@@ -0,0 +1,295 @@
# SPDX-License-Identifier: Apache-2.0
"""Flow-matching rollout step utilities for log-prob computation."""
import math
from typing import Any, Union
import torch
from sglang.multimodal_gen.runtime.distributed import (
get_sp_world_size,
)
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
from sglang.multimodal_gen.runtime.post_training.rl_dataclasses import (
RolloutSessionData,
)
from sglang.multimodal_gen.runtime.post_training.scheduler_rl_debug_mixin import (
SchedulerRLDebugMixin,
)
_LOG_SQRT_2PI = math.log(math.sqrt(2 * math.pi))
class SchedulerRLMixin(SchedulerRLDebugMixin):
@staticmethod
def _get_rollout_session_data(batch) -> RolloutSessionData:
"""Return the RolloutSessionData attached to *batch*, or raise if not prepared."""
rollout_session_data = getattr(batch, "_rollout_session_data", None)
if rollout_session_data is None:
raise RuntimeError("prepare_rollout() not called before rollout")
return rollout_session_data
def release_rollout_resources(self, batch) -> None:
"""Release rollout-owned resources. Call when denoising ends or before a new rollout."""
batch._rollout_session_data = None
def prepare_rollout(self, batch: Req, pipeline_config: Any = None) -> None:
"""Enable rollout and set SDE/CPS params. Call once before the denoising loop."""
if get_sp_world_size() > 1 and pipeline_config is None:
raise RuntimeError(
"SP rollout requires pipeline_config to be passed to prepare_rollout()."
)
batch._rollout_session_data = RolloutSessionData(
pipeline_config=pipeline_config,
sigma_max=self.sigmas[min(1, len(self.sigmas) - 1)].item(),
latents_shape=(
tuple(batch.latents.shape) if batch.latents is not None else None
),
)
def already_prepared_rollout(self, batch) -> bool:
return getattr(batch, "_rollout_session_data", None) is not None
def _get_or_create_rollout_noise_buffer(
self,
rollout_session_data: RolloutSessionData,
full_shape: tuple,
device: torch.device,
dtype: torch.dtype,
) -> torch.Tensor:
"""Get or create the reusable noise buffer (local or full shape) for rollout."""
buffer = rollout_session_data.noise_buffer
if (
buffer is None
or buffer.shape != full_shape
or buffer.dtype != dtype
or buffer.device != device
):
buffer = torch.empty(full_shape, device=device, dtype=dtype)
rollout_session_data.noise_buffer = buffer
return buffer
def _rollout_variance_noise(
self,
batch,
model_output: torch.FloatTensor,
generator: Union[torch.Generator, list[torch.Generator]],
) -> torch.FloatTensor:
"""Generate variance noise for rollout. If generator is a list, use generator[i] for the i-th batch item."""
assert generator is not None, "Generator must be provided"
rollout_session_data = self._get_rollout_session_data(batch)
device = model_output.device
dtype = model_output.dtype
local_shape = tuple(model_output.shape)
B = local_shape[0]
if isinstance(generator, torch.Generator):
assert B == 1, "Generator must be a list if batch size is not 1"
generator = [generator]
else:
assert (
len(generator) == B
), "Generator list must have the same length as batch size"
buffer = self._get_or_create_rollout_noise_buffer(
rollout_session_data, rollout_session_data.latents_shape, device, dtype
)
for i in range(B):
torch.randn(
rollout_session_data.latents_shape,
out=buffer[i : i + 1],
generator=generator[i],
)
sharded_noise, _ = rollout_session_data.pipeline_config.shard_latents_for_sp(
batch=batch, latents=buffer
)
if tuple(sharded_noise.shape) != local_shape:
raise ValueError(
"Rollout SP noise shape mismatch after shard. "
f"Expected local_shape={local_shape}, got {tuple(sharded_noise.shape)}."
)
return sharded_noise
def flow_sde_sampling(
self,
batch,
model_output: torch.FloatTensor,
sample: torch.FloatTensor,
current_sigma: torch.FloatTensor,
next_sigma: torch.FloatTensor,
generator: torch.Generator,
) -> torch.Tensor:
"""Flow rollout step for log-prob / sampling (see FlowGRPO-style references).
``rollout_sde_type`` (from batch SamplingParams):
1. ``"sde"``: Standard stochastic differential equation transition (Gaussian).
2. ``"cps"``: Coupled Particle Sampling.
3. ``"ode"``: Deterministic ODE step (no diffusion noise).
"""
rollout_session_data = self._get_rollout_session_data(batch)
sde_type = batch.rollout_sde_type
noise_level = float(batch.rollout_noise_level)
log_prob_no_const = batch.rollout_log_prob_no_const
debug_mode = bool(getattr(batch, "rollout_debug_mode", False))
if not log_prob_no_const and sde_type != "ode":
assert (
noise_level > 0
), "True log-probability computation requires a non-zero noise level."
dt = next_sigma - current_sigma
# step_index comes from the denoising-loop counter stashed by
# DenoisingStage — scheduler._step_index would differ when
# _begin_index != 0 (e.g. partial denoising).
sde_step_indices = getattr(batch, "rollout_sde_step_indices", None)
loop_step_index = getattr(batch, "_rollout_loop_step_index", None)
if (
sde_type != "ode"
and sde_step_indices is not None
and loop_step_index is not None
and loop_step_index not in sde_step_indices
):
effective_sde_type = "ode"
else:
effective_sde_type = sde_type
# sde/cps: cast to fp32 to match flowGRPO semantics and avoid the
# 0-dim-fp32 wrapped-scalar promotion demoting log-prob to bf16.
# ode: keep dtypes unchanged so rollout(ode) stays bit-exact with
# rollout=False (scheduling_flow_match_euler_discrete.step()).
# log_prob is computed on the full pre-shard noise buffer so SP ranks
# produce identical sums — see collect_rollout_log_probs().
if effective_sde_type == "sde":
model_output = model_output.float()
sample = sample.float()
variance_noise = self._rollout_variance_noise(
batch, model_output, generator
)
full_variance_noise = rollout_session_data.noise_buffer
std_dev_t = (
torch.sqrt(
current_sigma
/ (
1
- torch.where(
torch.isclose(current_sigma, current_sigma.new_tensor(1.0)),
rollout_session_data.sigma_max,
current_sigma,
)
)
)
* noise_level
)
noise_std_dev = std_dev_t * torch.sqrt(-1 * dt)
prev_sample_mean = (
sample * (1 + std_dev_t**2 / (2 * current_sigma) * dt)
+ model_output
* (1 + std_dev_t**2 * (1 - current_sigma) / (2 * current_sigma))
* dt
)
weighted_variance_noise = variance_noise * noise_std_dev
prev_sample = prev_sample_mean + weighted_variance_noise
log_prob_no_const_val = -((full_variance_noise * noise_std_dev) ** 2)
elif effective_sde_type == "cps":
model_output = model_output.float()
sample = sample.float()
variance_noise = self._rollout_variance_noise(
batch, model_output, generator
)
full_variance_noise = rollout_session_data.noise_buffer
std_dev_t = next_sigma * math.sin(noise_level * math.pi / 2)
noise_std_dev = std_dev_t
pred_original_sample = sample - current_sigma * model_output
noise_estimate = sample + model_output * (1 - current_sigma)
prev_sample_mean = pred_original_sample * (
1 - next_sigma
) + noise_estimate * torch.sqrt(next_sigma**2 - std_dev_t**2)
weighted_variance_noise = variance_noise * noise_std_dev
prev_sample = prev_sample_mean + weighted_variance_noise
log_prob_no_const_val = -((full_variance_noise * noise_std_dev) ** 2)
elif effective_sde_type == "ode":
prev_sample = sample + dt * model_output
prev_sample_mean = prev_sample
variance_noise = torch.zeros_like(model_output)
noise_std_dev = torch.zeros(
(), device=model_output.device, dtype=model_output.dtype
)
log_prob_no_const_val = torch.zeros(
rollout_session_data.latents_shape,
device=model_output.device,
dtype=torch.float32,
)
# Only enforce the "no full log-prob with ODE" constraint when the
# user explicitly chose ODE globally.
if sde_type == "ode":
assert (
log_prob_no_const
), "p_ode is always 0, true log_prob is meaningless, set rollout_log_prob_no_const to True to enable log_prob computation"
else:
raise ValueError(f"Unsupported sde_type: {sde_type}")
reduce_dims = list(range(1, len(log_prob_no_const_val.shape)))
local_elem_count = log_prob_no_const_val.new_full(
(log_prob_no_const_val.shape[0],),
float(math.prod(log_prob_no_const_val.shape[1:])),
)
if log_prob_no_const or effective_sde_type == "ode":
log_prob_local_sum = log_prob_no_const_val.sum(dim=reduce_dims)
else:
log_prob_local_sum = (
log_prob_no_const_val / (2 * (noise_std_dev**2))
- torch.log(noise_std_dev)
- _LOG_SQRT_2PI
).sum(dim=list(range(1, len(log_prob_no_const_val.shape))))
if debug_mode:
self.append_local_rollout_debug_tensors(
batch,
variance_noise=variance_noise,
prev_sample_mean=prev_sample_mean,
noise_std_dev=noise_std_dev,
model_output=model_output,
)
self.append_local_rollout_log_probs(batch, log_prob_local_sum, local_elem_count)
return prev_sample
def append_local_rollout_log_probs(
self, batch, log_prob_sum: torch.Tensor, log_prob_count: torch.Tensor
) -> None:
rollout_session_data = self._get_rollout_session_data(batch)
rollout_session_data.local_log_prob_sum.append(log_prob_sum)
rollout_session_data.local_log_prob_count.append(log_prob_count)
def consume_local_rollout_log_probs(
self, batch
) -> tuple[torch.Tensor, torch.Tensor]:
rollout_session_data = self._get_rollout_session_data(batch)
# [B, T]: batch dim 0, denoising step dim 1
values_sum = torch.stack(rollout_session_data.local_log_prob_sum, dim=1)
values_count = torch.stack(rollout_session_data.local_log_prob_count, dim=1)
rollout_session_data.local_log_prob_sum = []
rollout_session_data.local_log_prob_count = []
return values_sum, values_count
def collect_rollout_log_probs(self, batch: Req) -> torch.Tensor | None:
"""Per-step sums are already computed on the full pre-shard noise
buffer inside flow_sde_sampling, so every SP rank holds identical
values here and no all-reduce is needed."""
trajectory_log_prob_sum, trajectory_log_prob_count = (
self.consume_local_rollout_log_probs(batch)
)
rollout_log_probs_tensor = trajectory_log_prob_sum / trajectory_log_prob_count
return rollout_log_probs_tensor.cpu()
@@ -0,0 +1,61 @@
# SPDX-License-Identifier: Apache-2.0
"""Sequence Parallel helpers for post-training rollout code."""
from __future__ import annotations
import torch
from sglang.multimodal_gen.runtime.distributed import (
get_local_torch_device,
get_sp_world_size,
)
from sglang.multimodal_gen.runtime.distributed.communication_op import (
sequence_model_parallel_all_gather,
sequence_model_parallel_all_reduce,
)
def should_do_sp_collective(batch) -> bool:
return get_sp_world_size() > 1 and getattr(batch, "did_sp_shard_latents", False)
def gather_stacked_latents_for_sp(
pipeline_config,
batch,
stacked_latents: torch.Tensor,
) -> torch.Tensor:
if not should_do_sp_collective(batch):
return stacked_latents
if stacked_latents.dim() < 2:
return stacked_latents
bsz, t_steps = stacked_latents.shape[0], stacked_latents.shape[1]
flat_inputs = stacked_latents.flatten(0, 1).contiguous()
gathered_flat_inputs = pipeline_config.gather_latents_for_sp(
flat_inputs, batch=batch
)
return gathered_flat_inputs.unflatten(0, (bsz, t_steps))
def all_reduce_if_sp_sharded(batch, tensor: torch.Tensor) -> torch.Tensor:
if not should_do_sp_collective(batch):
return tensor
tensor = tensor.to(get_local_torch_device())
sequence_model_parallel_all_reduce(tensor)
return tensor
def all_gather_if_sp_sharded(batch, x: torch.Tensor, dim: int = 0) -> torch.Tensor:
if not should_do_sp_collective(batch):
return x
x = x.to(get_local_torch_device()).contiguous()
return sequence_model_parallel_all_gather(x, dim=dim)
def maybe_trim_sp_rope_seq_for_batch(batch, rope: torch.Tensor) -> torch.Tensor:
raw = getattr(batch, "raw_latent_shape", None)
if raw is None or len(raw) < 2:
return rope
target = int(raw[1])
if rope.shape[0] > target:
return rope[:target]
return rope
@@ -0,0 +1,263 @@
"""Verification helpers for diffusion update_weights_from_tensor workflows."""
from __future__ import annotations
import hashlib
from collections.abc import Iterable
import torch
from torch.distributed.tensor import DTensor
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
iter_materialized_weights,
)
_MAX_DISPLAY_TENSORS = 5
def _materialize_local_tensor(tensor: torch.Tensor) -> torch.Tensor:
if isinstance(tensor, DTensor):
tensor = tensor._local_tensor
return tensor.detach().cpu().contiguous()
def compute_tensor_sha256(tensor: torch.Tensor) -> str:
tensor = _materialize_local_tensor(tensor)
hasher = hashlib.sha256()
hasher.update(str(tensor.dtype).encode("utf-8"))
hasher.update(repr(tuple(tensor.shape)).encode("utf-8"))
hasher.update(tensor.view(torch.uint8).numpy().tobytes())
return hasher.hexdigest()
def build_named_tensor_sha256(
named_tensors: Iterable[tuple[str, torch.Tensor]],
) -> dict[str, str]:
sha256_by_name: dict[str, str] = {}
for name, tensor in named_tensors:
sha256_by_name[name] = compute_tensor_sha256(tensor)
return sha256_by_name
class TensorUpdateChecker:
def __init__(self, pipeline):
self.pipeline = pipeline
def verify_across_tp(
self,
target_module: str,
expected_named_tensors_sha256: dict[str, str],
tp_rank: int,
tp_world_size: int,
tp_cpu_group,
tp_root_rank: int,
) -> tuple[bool, str]:
if tp_world_size == 1:
return self.verify(
target_module=target_module,
expected_named_tensors_sha256=expected_named_tensors_sha256,
)
module = self.pipeline.get_module(target_module)
if module is None:
return False, f"Module '{target_module}' is not initialized"
local_named_tensors = dict(
self._iter_module_named_tensors(
module, expected_named_tensors_sha256.keys()
)
)
reference_tensors = dict(module.named_parameters())
reference_tensors.update(dict(module.named_buffers()))
actual_named_tensors_sha256: dict[str, str] | None = (
{} if tp_rank == 0 else None
)
for name, expected_sha256 in expected_named_tensors_sha256.items():
gathered_tensors: list[torch.Tensor | None] | None = (
[None] * tp_world_size if tp_rank == 0 else None
)
torch.distributed.gather_object(
(
_materialize_local_tensor(local_named_tensors[name])
if name in local_named_tensors
else None
),
gathered_tensors,
dst=tp_root_rank,
group=tp_cpu_group,
)
if tp_rank != 0:
continue
valid_tensors = [
tensor for tensor in gathered_tensors if tensor is not None
]
if len(valid_tensors) != len(gathered_tensors):
continue
local_sha256s = [compute_tensor_sha256(tensor) for tensor in valid_tensors]
if all(local_sha256 == expected_sha256 for local_sha256 in local_sha256s):
actual_named_tensors_sha256[name] = expected_sha256
continue
reference_tensor = reference_tensors.get(name)
candidate_dims: list[int] = []
if isinstance(reference_tensor, DTensor):
for placement in reference_tensor.placements:
shard_dim = getattr(placement, "dim", None)
if isinstance(shard_dim, int) and shard_dim not in candidate_dims:
candidate_dims.append(shard_dim)
for attr in ("input_dim", "output_dim"):
shard_dim = getattr(reference_tensor, attr, None)
if isinstance(shard_dim, int) and shard_dim not in candidate_dims:
candidate_dims.append(shard_dim)
reconstructed_sha256 = None
first_tensor = valid_tensors[0]
for shard_dim in candidate_dims:
if first_tensor.ndim == 0:
break
shard_dim %= first_tensor.ndim
compatible = True
for tensor in valid_tensors[1:]:
if (
tensor.ndim != first_tensor.ndim
or tensor.dtype != first_tensor.dtype
):
compatible = False
break
if any(
lhs != rhs
for dim, (lhs, rhs) in enumerate(
zip(first_tensor.shape, tensor.shape)
)
if dim != shard_dim
):
compatible = False
break
if not compatible:
continue
reconstructed = torch.cat(valid_tensors, dim=shard_dim).contiguous()
if compute_tensor_sha256(reconstructed) == expected_sha256:
reconstructed_sha256 = expected_sha256
break
actual_named_tensors_sha256[name] = reconstructed_sha256 or local_sha256s[0]
final_result: tuple[bool, str] | None = None
if tp_rank == 0:
final_result = self._compare_manifests(
target_module=target_module,
expected_named_tensors_sha256=expected_named_tensors_sha256,
actual_named_tensors_sha256=actual_named_tensors_sha256,
)
if final_result[0]:
final_result = (
True,
f"Verified module '{target_module}' update across {tp_world_size} TP ranks.",
)
final_result_holder = [final_result]
torch.distributed.broadcast_object_list(
final_result_holder,
src=tp_root_rank,
group=tp_cpu_group,
)
final_result = final_result_holder[0]
assert final_result is not None
return final_result
def verify(
self,
target_module: str,
expected_named_tensors_sha256: dict[str, str],
) -> tuple[bool, str]:
module = self.pipeline.get_module(target_module)
if module is None:
return False, f"Module '{target_module}' is not initialized"
actual_named_tensors_sha256 = build_named_tensor_sha256(
self._iter_module_named_tensors(
module, expected_named_tensors_sha256.keys()
)
)
return self._compare_manifests(
target_module=target_module,
expected_named_tensors_sha256=expected_named_tensors_sha256,
actual_named_tensors_sha256=actual_named_tensors_sha256,
)
def _iter_module_named_tensors(
self,
module: torch.nn.Module,
expected_names: Iterable[str],
):
expected_name_set = set(expected_names)
seen_names: set[str] = set()
for name, tensor in iter_materialized_weights(module):
if name not in expected_name_set:
continue
seen_names.add(name)
yield name, tensor
for name, tensor in module.named_buffers():
if name in seen_names or name not in expected_name_set:
continue
seen_names.add(name)
yield name, tensor
def _compare_manifests(
self,
*,
target_module: str,
expected_named_tensors_sha256: dict[str, str],
actual_named_tensors_sha256: dict[str, str],
) -> tuple[bool, str]:
missing_names = sorted(
name
for name in expected_named_tensors_sha256
if name not in actual_named_tensors_sha256
)
mismatched_names = sorted(
name
for name, expected_sha256 in expected_named_tensors_sha256.items()
if name in actual_named_tensors_sha256
and actual_named_tensors_sha256[name] != expected_sha256
)
if missing_names or mismatched_names:
parts: list[str] = []
if missing_names:
parts.append(
"missing "
f"{len(missing_names)} tensor(s): "
f"{self._format_tensor_names(missing_names)}"
)
if mismatched_names:
parts.append(
"checksum mismatch for "
f"{len(mismatched_names)} tensor(s): "
f"{self._format_tensor_names(mismatched_names)}"
)
return (
False,
f"Module '{target_module}' update weight check failed: "
+ "; ".join(parts),
)
return (
True,
f"Verified module '{target_module}' update for "
f"{len(expected_named_tensors_sha256)} tensor(s).",
)
def _format_tensor_names(self, names: list[str]) -> str:
displayed = names[:_MAX_DISPLAY_TENSORS]
formatted = ", ".join(displayed)
if len(names) > _MAX_DISPLAY_TENSORS:
formatted += f", ... (+{len(names) - _MAX_DISPLAY_TENSORS} more)"
return formatted
@@ -0,0 +1,520 @@
"""
In-place weight updates for diffusion pipeline modules.
This module provides WeightsUpdater, which swaps model weights at runtime
without restarting the server. It is the diffusion-engine counterpart of the
LLM engine's ModelRunner.update_weights_from_disk.
Detailed usage of higher level API can be found in
/python/sglang/multimodal_gen/test/single_test_file/test_update_weights_from_disk.py
Key design decisions:
- All-or-nothing with rollback: modules are updated sequentially. If
any module fails (shape mismatch, corrupted file, etc.), every module
that was already updated is rolled back by reloading its weights from
that module's last successfully-loaded weights directory. On a full
successful update, pipeline.model_path is updated to the new model_path;
target_modules updates keep per-module rollback state for hybrid models.
- Rollback failures propagate: if rollback itself fails, the exception is
not caught so the caller knows the model is in an inconsistent state.
This matches the LLM engine behaviour.
- Offload-aware: the diffusion LayerwiseOffloadManager replaces GPU
parameters with torch.empty((1,)) placeholders while real weights live
in consolidated pinned CPU buffers. A naive param.data.copy_() would
fail with a shape mismatch. Instead, the updater dynamically detects
active offload managers and writes new weights directly into their CPU
buffers via update_cpu_weights(), bypassing the placeholders entirely.
For any layer that happens to be prefetched on GPU at update time, the
live GPU tensor is also updated so the change takes effect immediately.
This requires no extra GPU memory and does not disturb the offload state.
- DTensor-aware: parameters that have been distributed via
torch.distributed.tensor are updated through distribute_tensor
so that each shard is correctly placed on the right device mesh.
"""
from __future__ import annotations
import gc
from pathlib import Path
from typing import Any
import torch
from torch.distributed.tensor import DTensor, distribute_tensor
from sglang.multimodal_gen.runtime.cache.teacache import TeaCacheMixin
from sglang.multimodal_gen.runtime.loader.utils import (
_list_safetensors_files,
get_param_names_mapping,
)
from sglang.multimodal_gen.runtime.loader.weight_utils import (
safetensors_weights_iterator,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
is_layerwise_offloaded_module,
)
from sglang.multimodal_gen.runtime.pipelines.diffusers_pipeline import DiffusersPipeline
from sglang.multimodal_gen.runtime.utils.hf_diffusers_utils import maybe_download_model
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.srt.weight_sync.tensor_bucket import (
FlattenedTensorBucket,
FlattenedTensorMetadata,
)
logger = init_logger(__name__)
_DEFAULT_TENSOR_TARGET_MODULE = "transformer"
def get_updatable_modules(pipeline) -> dict[str, torch.nn.Module]:
"""Return updatable nn.Module components for the given pipeline.
Works with both the native ComposedPipelineBase backend and the
DiffusersPipeline wrapper.
"""
if isinstance(pipeline, DiffusersPipeline):
diffusers_pipe = pipeline.get_module("diffusers_pipeline")
if diffusers_pipe is not None and diffusers_pipe.components is not None:
raw = diffusers_pipe.components
else:
raw = {}
else:
raw = pipeline.modules
return {n: m for n, m in raw.items() if isinstance(m, torch.nn.Module)}
def _get_weights_iter(weights_dir: str):
"""Return a (name, tensor) iterator over safetensors in weights_dir."""
safetensors_files = _list_safetensors_files(weights_dir)
if not safetensors_files:
raise FileNotFoundError(f"No safetensors files found in {weights_dir}")
return safetensors_weights_iterator(safetensors_files)
def _validate_weight_files(
local_model_path: str,
modules_to_update: list[tuple[str, torch.nn.Module]],
) -> tuple[dict[str, str], list[str]]:
"""Check that every module has a weights directory with safetensors files.
Returns:
(weights_map, missing) where weights_map maps module name to its
weights directory and missing lists modules without weight files.
"""
weights_map: dict[str, str] = {}
missing: list[str] = []
for module_name, _ in modules_to_update:
weights_dir = Path(local_model_path) / module_name
if weights_dir.exists() and _list_safetensors_files(str(weights_dir)):
weights_map[module_name] = str(weights_dir)
else:
missing.append(module_name)
return weights_map, missing
def _load_weights_into_module(module: torch.nn.Module, weights_iter) -> None:
"""Load weights into a module, handling offload-managed parameters.
For offloaded modules, updates CPU buffers directly via
update_cpu_weights(); non-offloaded parameters use in-place copy.
"""
model_params = dict(module.named_parameters())
weights_iter = _iter_module_weight_updates(module, weights_iter, model_params)
offload_managers: list = []
if is_layerwise_offloaded_module(module):
offload_managers = [m for m in module.layerwise_offload_managers if m.enabled]
if offload_managers:
weight_dict = dict(weights_iter)
offloaded_names: set[str] = set()
for manager in offload_managers:
offloaded_names.update(manager.update_cpu_weights(weight_dict))
remaining = ((n, w) for n, w in weight_dict.items() if n not in offloaded_names)
load_weights_into_model(remaining, model_params)
else:
load_weights_into_model(weights_iter, model_params)
def _build_module_weight_name_mapper(module: torch.nn.Module):
"""Build a chained regex mapper from mapping dicts exposed by the module."""
mapping_fns = []
for attr in ("lora_param_names_mapping", "param_names_mapping"):
mapping = getattr(module, attr, None)
if not mapping:
continue
mapping_fns.append(get_param_names_mapping(mapping))
if not mapping_fns:
return None
def map_name(name: str) -> str:
mapped_name = name
for mapping_fn in mapping_fns:
mapped_name = mapping_fn(mapped_name)[0]
return mapped_name
return map_name
def _iter_module_weight_updates(
module: torch.nn.Module,
weights_iter,
model_params: dict,
):
map_name = _build_module_weight_name_mapper(module)
module_name = type(module).__name__
for name, loaded_weight in weights_iter:
if name in model_params:
yield name, loaded_weight
continue
mapped_name = map_name(name) if map_name is not None else name
if mapped_name in model_params:
yield mapped_name, loaded_weight
continue
logger.warning(
"Skipping weight update for %s: parameter %r not found after mapping to %r",
module_name,
name,
mapped_name,
)
def load_weights_into_model(
weights_iter, model_params: dict, module_name: str | None = None
) -> None:
"""Copy weights from weights_iter into model_params in-place."""
for name, loaded_weight in weights_iter:
if name not in model_params:
logger.warning("Skipping weight update: parameter %r not found", name)
continue
param = model_params[name]
weight_loader = getattr(param, "weight_loader", None)
if callable(weight_loader):
weight_loader(param, loaded_weight.to(param.dtype))
else:
dtensor_param = param if isinstance(param, DTensor) else None
if dtensor_param is None and isinstance(
getattr(param, "data", None), DTensor
):
dtensor_param = param.data
if dtensor_param is not None:
distributed_weight = distribute_tensor(
loaded_weight.to(param.dtype),
dtensor_param.device_mesh,
dtensor_param.placements,
)
dtensor_param._local_tensor.copy_(distributed_weight._local_tensor)
else:
if param.shape != loaded_weight.shape:
module_prefix = f"{module_name}." if module_name else ""
raise ValueError(
f"Shape mismatch for {module_prefix}{name}: "
f"model={param.shape}, loaded={loaded_weight.shape}"
)
param.data.copy_(loaded_weight.to(param.dtype))
class WeightsUpdater:
"""In-place weight updates for diffusion pipeline modules.
Args:
pipeline: A ComposedPipelineBase (or DiffusersPipeline) instance
whose modules will be updated.
"""
def __init__(self, pipeline):
self.pipeline = pipeline
try:
self._module_weight_dirs = pipeline._weights_updater_module_weight_dirs
except AttributeError:
self._module_weight_dirs = {}
pipeline._weights_updater_module_weight_dirs = self._module_weight_dirs
def update_weights_from_disk(
self,
model_path: str,
flush_cache: bool = True,
target_modules: list[str] | None = None,
) -> tuple[bool, str]:
"""Update model weights from disk without restarting the server."""
logger.info(f"Updating weights from disk: {model_path}")
try:
modules_to_update = self._collect_modules(target_modules)
except ValueError as e:
logger.error(str(e))
return False, str(e)
if not modules_to_update:
error_msg = (
f"No matching modules found for update. "
f"Requested: {target_modules}. "
f"Available nn.Module(s): {list(get_updatable_modules(self.pipeline).keys())}"
)
logger.error(error_msg)
return False, error_msg
try:
local_model_path = maybe_download_model(model_path)
except Exception as e:
return False, f"Failed to download model: {e}"
weights_map, missing = _validate_weight_files(
local_model_path, modules_to_update
)
if missing:
error_msg = (
f"Cannot update weights: missing weight files for modules: {missing}. "
f"No partial updates allowed."
)
logger.error(error_msg)
return False, error_msg
logger.info(
f"Updating {len(weights_map)} modules: "
+ ", ".join(f"{n} <- {p}" for n, p in weights_map.items())
)
success, message = self._apply_weights(modules_to_update, weights_map)
if success:
for module_name, _ in modules_to_update:
self._module_weight_dirs[module_name] = weights_map[module_name]
if target_modules is None:
self.pipeline.model_path = local_model_path
gc.collect()
torch.cuda.empty_cache()
if success and flush_cache:
for _, module in modules_to_update:
if isinstance(module, TeaCacheMixin):
module.reset_teacache_state()
logger.info(message)
return success, message
def _collect_modules(
self, target_modules: list[str] | None
) -> list[tuple[str, torch.nn.Module]]:
"""Resolve target_modules to (name, module) pairs.
Raises:
ValueError: If target_modules contains names not found in the pipeline.
"""
components = get_updatable_modules(self.pipeline)
if target_modules is None:
names = list(components.keys())
else:
unknown = [n for n in target_modules if n not in components]
if unknown:
raise ValueError(
f"Module(s) requested for update not found in pipeline: {unknown}. "
f"Available Module(s): {list(components.keys())}"
)
names = target_modules
return [(name, components[name]) for name in names]
def _apply_weights(
self,
modules_to_update: list[tuple[str, torch.nn.Module]],
weights_map: dict[str, str],
) -> tuple[bool, str]:
"""Load weights into each module; rollback on first failure."""
updated_modules: list[str] = []
for module_name, module in modules_to_update:
try:
weights_iter = _get_weights_iter(weights_map[module_name])
_load_weights_into_module(module, weights_iter)
updated_modules.append(module_name)
except Exception as e:
rollback_list = updated_modules + [module_name]
logger.error(
f"Weight update failed for module '{module_name}': {e}. "
f"Rolling back {len(rollback_list)} module(s) "
f"(including partially-loaded '{module_name}'): "
f"{rollback_list}.",
exc_info=True,
)
self._rollback(rollback_list)
return False, (
f"Failed to update module '{module_name}': {e}. "
f"All modules rolled back to original weights."
)
names = ", ".join(updated_modules)
return True, f"Updated {len(updated_modules)} modules ({names})."
def _rollback(self, updated_modules: list[str]) -> None:
"""Restore updated_modules to original weights.
If rollback itself fails the exception propagates so the caller
knows the model is in an inconsistent state.
"""
if not updated_modules:
return
original_path: str | None = None
for name in updated_modules:
module = self.pipeline.get_module(name)
if module is None:
continue
weights_dir = self._module_weight_dirs.get(name)
if weights_dir is None:
if original_path is None:
original_path = maybe_download_model(self.pipeline.model_path)
weights_dir = str(Path(original_path) / name)
weights_dir = Path(weights_dir)
if not weights_dir.exists():
continue
weights_iter = _get_weights_iter(str(weights_dir))
_load_weights_into_module(module, weights_iter)
def update_weights_from_tensor(
self,
named_tensors: Any,
load_format: str | None = None,
target_modules: list[str] | None = None,
) -> tuple[bool, str]:
if target_modules is None:
target_modules = [_DEFAULT_TENSOR_TARGET_MODULE]
try:
modules_to_update = self._collect_modules(target_modules)
except ValueError as e:
logger.error(str(e))
return False, str(e)
if not modules_to_update:
error_msg = (
f"No matching modules found for update. "
f"Requested: {target_modules}. "
f"Available nn.Module(s): {list(get_updatable_modules(self.pipeline).keys())}"
)
logger.error(error_msg)
return False, error_msg
try:
module_payloads = self._resolve_module_payloads(
named_tensors=named_tensors,
modules_to_update=modules_to_update,
)
except ValueError as e:
logger.error(str(e))
return False, str(e)
updated_modules: list[str] = []
for module_name, module in modules_to_update:
try:
payload = module_payloads[module_name]
weights_iter = self._materialize_weights_iter(payload, load_format)
_load_weights_into_module(module, weights_iter)
updated_modules.append(module_name)
except Exception as e:
error_msg = (
f"Failed to update module '{module_name}' from tensor: {e}. "
f"The pipeline may be partially updated. "
f"Please discard the whole weights and reload from a known-good checkpoint."
)
logger.error(error_msg, exc_info=True)
return False, error_msg
gc.collect()
torch.cuda.empty_cache()
names = ", ".join(updated_modules)
message = f"Updated {len(updated_modules)} modules from tensor ({names})."
logger.info(message)
return True, message
def _resolve_module_payloads(
self,
named_tensors: Any,
modules_to_update: list[tuple[str, torch.nn.Module]],
) -> dict[str, Any]:
module_names = [name for name, _ in modules_to_update]
if isinstance(named_tensors, dict):
missing = [name for name in module_names if name not in named_tensors]
if missing:
raise ValueError(
f"Missing tensor payload for module(s): {missing}. "
f"Provided modules: {list(named_tensors.keys())}"
)
return {name: named_tensors[name] for name in module_names}
if len(module_names) == 1:
return {module_names[0]: named_tensors}
raise ValueError(
"Ambiguous tensor payload for multi-module update. "
"Provide a dict mapping module_name -> module payload, "
f"requested modules: {module_names}."
)
def _materialize_weights_iter(self, module_payload: Any, load_format: str | None):
if load_format == "flattened_bucket":
if not isinstance(module_payload, dict):
raise ValueError(
"flattened_bucket payload must be a dict with "
"'flattened_tensor' and 'metadata'."
)
flattened_tensor = module_payload.get("flattened_tensor")
metadata = module_payload.get("metadata")
if flattened_tensor is None or metadata is None:
raise ValueError(
"flattened_bucket payload missing 'flattened_tensor' or 'metadata'."
)
return self._reconstruct_from_flattened_bucket(flattened_tensor, metadata)
if isinstance(module_payload, (list, tuple)):
return iter(module_payload)
raise ValueError(
f"Unsupported module payload type for load_format={load_format}: "
f"{type(module_payload).__name__}"
)
def _reconstruct_from_flattened_bucket(self, flattened_tensor: Any, metadata: Any):
if not isinstance(flattened_tensor, torch.Tensor):
raise ValueError(
"flattened_bucket 'flattened_tensor' must be a torch.Tensor."
)
if not isinstance(metadata, list):
raise ValueError("flattened_bucket 'metadata' must be a list.")
converted_metadata: list[FlattenedTensorMetadata] = []
for meta in metadata:
converted_metadata.append(
FlattenedTensorMetadata(
name=meta.name,
shape=torch.Size(meta.shape),
dtype=self._normalize_torch_dtype(meta.dtype),
start_idx=int(meta.start_idx),
end_idx=int(meta.end_idx),
numel=int(meta.numel),
)
)
bucket = FlattenedTensorBucket(
flattened_tensor=flattened_tensor,
metadata=converted_metadata,
)
return bucket.reconstruct_tensors()
def _normalize_torch_dtype(self, dtype: Any) -> torch.dtype:
if isinstance(dtype, torch.dtype):
return dtype
if isinstance(dtype, str):
name = dtype.split(".")[-1]
normalized = getattr(torch, name, None)
if isinstance(normalized, torch.dtype):
return normalized
raise ValueError(f"Unsupported dtype in flattened_bucket metadata: {dtype!r}")