Introduce WeightUpdater and WeightExporter components (#31148)

This commit is contained in:
fzyzcjy
2026-07-14 15:53:33 +08:00
committed by GitHub
parent 45dfa318fb
commit 6507d4a090
11 changed files with 640 additions and 519 deletions
@@ -300,17 +300,17 @@ class SchedulerWeightUpdaterManager:
def save_remote_model(self, params):
url = params["url"]
self.tp_worker.model_runner.save_remote_model(url)
self.tp_worker.model_runner.weight_exporter.save_remote_model(url)
if self.draft_worker is not None:
draft_url = params.get("draft_url", None)
assert (
draft_url is not None
), "draft_url must be provided when draft model is enabled"
self.draft_worker.model_runner.save_remote_model(draft_url)
self.draft_worker.model_runner.weight_exporter.save_remote_model(draft_url)
def save_sharded_model(self, params):
self.tp_worker.model_runner.save_sharded_model(
self.tp_worker.model_runner.weight_exporter.save_sharded_model(
path=params["path"],
pattern=params["pattern"],
max_size=params["max_size"],
+26 -18
View File
@@ -101,7 +101,7 @@ class BaseTpWorker(ABC):
)
def update_weights_from_disk(self, recv_req: UpdateWeightFromDiskReqInput):
success, message = self.model_runner.update_weights_from_disk(
success, message = self.model_runner.weight_updater.update_weights_from_disk(
recv_req.model_path,
recv_req.load_format,
recapture_cuda_graph=recv_req.recapture_cuda_graph,
@@ -109,7 +109,7 @@ class BaseTpWorker(ABC):
return success, message
def init_weights_update_group(self, recv_req: InitWeightsUpdateGroupReqInput):
success, message = self.model_runner.init_weights_update_group(
success, message = self.model_runner.weight_updater.init_weights_update_group(
recv_req.master_address,
recv_req.master_port,
recv_req.rank_offset,
@@ -120,8 +120,10 @@ class BaseTpWorker(ABC):
return success, message
def destroy_weights_update_group(self, recv_req: DestroyWeightsUpdateGroupReqInput):
success, message = self.model_runner.destroy_weights_update_group(
recv_req.group_name,
success, message = (
self.model_runner.weight_updater.destroy_weights_update_group(
recv_req.group_name,
)
)
return success, message
@@ -129,7 +131,7 @@ class BaseTpWorker(ABC):
self, recv_req: InitWeightsSendGroupForRemoteInstanceReqInput
):
success, message = (
self.model_runner.init_weights_send_group_for_remote_instance(
self.model_runner.weight_exporter.init_weights_send_group_for_remote_instance(
recv_req.master_address,
recv_req.ports,
recv_req.group_rank,
@@ -143,29 +145,33 @@ class BaseTpWorker(ABC):
def send_weights_to_remote_instance(
self, recv_req: SendWeightsToRemoteInstanceReqInput
):
success, message = self.model_runner.send_weights_to_remote_instance(
recv_req.master_address,
recv_req.ports,
recv_req.group_name,
success, message = (
self.model_runner.weight_exporter.send_weights_to_remote_instance(
recv_req.master_address,
recv_req.ports,
recv_req.group_name,
)
)
return success, message
def update_weights_from_distributed(
self, recv_req: UpdateWeightsFromDistributedReqInput
):
success, message = self.model_runner.update_weights_from_distributed(
recv_req.names,
recv_req.dtypes,
recv_req.shapes,
recv_req.group_name,
recv_req.load_format,
success, message = (
self.model_runner.weight_updater.update_weights_from_distributed(
recv_req.names,
recv_req.dtypes,
recv_req.shapes,
recv_req.group_name,
recv_req.load_format,
)
)
return success, message
def update_weights_from_tensor(self, recv_req: UpdateWeightsFromTensorReqInput):
monkey_patch_torch_reductions()
success, message = self.model_runner.update_weights_from_tensor(
success, message = self.model_runner.weight_updater.update_weights_from_tensor(
named_tensors=MultiprocessingSerializer.deserialize(
recv_req.serialized_named_tensors[self.tp_rank]
),
@@ -175,11 +181,13 @@ class BaseTpWorker(ABC):
def update_weights_from_ipc(self, recv_req: UpdateWeightsFromIPCReqInput):
"""Update weights from IPC for checkpoint-engine integration."""
success, message = self.model_runner.update_weights_from_ipc(recv_req)
success, message = self.model_runner.weight_updater.update_weights_from_ipc(
recv_req
)
return success, message
def get_weights_by_name(self, recv_req: GetWeightsByNameReqInput):
parameter = self.model_runner.get_weights_by_name(
parameter = self.model_runner.weight_exporter.get_weights_by_name(
recv_req.name, recv_req.truncate_size
)
return parameter
+50 -480
View File
@@ -17,7 +17,6 @@ from __future__ import annotations
import contextlib
import datetime
import gc
import inspect
import logging
import os
@@ -26,7 +25,7 @@ import threading
import time
from collections import defaultdict
from dataclasses import dataclass
from typing import Any, Callable, List, Optional, Tuple, Union
from typing import Any, List, Optional, Union
import torch
import torch.distributed as dist
@@ -159,6 +158,12 @@ from sglang.srt.model_executor.forward_context import (
)
from sglang.srt.model_executor.graph_shared_output import GraphSharedOutput
from sglang.srt.model_executor.hook_manager import register_forward_hooks
from sglang.srt.model_executor.model_runner_components.weight_exporter import (
WeightExporter,
)
from sglang.srt.model_executor.model_runner_components.weight_updater import (
WeightUpdater,
)
from sglang.srt.model_executor.model_runner_kv_cache_mixin import (
ModelRunnerKVCacheMixin,
)
@@ -171,17 +176,13 @@ from sglang.srt.model_executor.runner import (
PrefillCudaGraphRunner,
get_batch_sizes_to_capture,
)
from sglang.srt.model_loader.loader import DefaultModelLoader, get_model_loader
from sglang.srt.model_loader.loader import get_model_loader
from sglang.srt.model_loader.remote_instance_weight_loader_utils import (
RemoteInstanceWeightLoaderBackend,
register_memory_region,
trigger_init_weights_send_group_for_remote_instance_request,
)
from sglang.srt.model_loader.utils import (
resolve_language_model,
set_default_torch_dtype,
)
from sglang.srt.model_loader.weight_utils import default_weight_loader
from sglang.srt.model_loader.utils import resolve_language_model
from sglang.srt.platforms import current_platform
from sglang.srt.runtime_context import get_flags, get_parallel, get_server_args
from sglang.srt.sampling.sampling_batch_info import SamplingBatchInfo
@@ -206,15 +207,12 @@ from sglang.srt.state_capturer.routed_experts import (
set_global_experts_capturer,
)
from sglang.srt.utils import (
MultiprocessingSerializer,
broadcast_pyobj,
cpu_has_amx_support,
dynamic_import,
enable_show_time_cost,
get_available_gpu_memory,
get_bool_env_var,
init_cublas,
init_custom_process_group,
is_hip,
is_host_cpu_arm64,
is_npu,
@@ -234,17 +232,10 @@ from sglang.srt.utils.offloader import (
get_offloader,
set_offloader,
)
from sglang.srt.utils.patch_torch import (
monkey_patch_torch_reductions,
register_sgl_tp_rank,
)
from sglang.srt.utils.patch_torch import register_sgl_tp_rank
from sglang.srt.utils.profile_utils import build_step_span_name
from sglang.srt.utils.torch_memory_saver_adapter import TorchMemorySaverAdapter
from sglang.srt.utils.weight_checker import WeightChecker
from sglang.srt.weight_sync.tensor_bucket import (
FlattenedTensorBucket,
FlattenedTensorMetadata,
)
_is_hip = is_hip()
_is_npu = is_npu()
@@ -577,8 +568,30 @@ class ModelRunner(ModelRunnerKVCacheMixin):
), "Pipeline Parallel is not compatible with this model."
# For weight updates
self._model_update_group = {}
self._weights_send_group = {}
self.init_weight_updater()
self.init_weight_exporter()
def init_weight_updater(self):
self.weight_updater = WeightUpdater(
tp_rank=self.tp_rank,
device=self.device,
gpu_id=self.gpu_id,
model_config=self.model_config,
custom_weight_loaders=self.server_args.custom_weight_loader,
get_model=lambda: self.model,
update_model_fields=self.update_model_fields,
recapture_cuda_graph=self.init_decode_cuda_graph,
get_model_runner=lambda: self,
)
def init_weight_exporter(self):
self.weight_exporter = WeightExporter(
tp_rank=self.tp_rank,
tp_size=self.tp_size,
gpu_id=self.gpu_id,
get_model_path=lambda: self.model_config.model_path,
get_model=lambda: self.model,
)
def init_msprobe(self):
# Init the msprobe
@@ -1672,7 +1685,7 @@ class ModelRunner(ModelRunnerKVCacheMixin):
self.expert_backup_client.update_weights(weight_name_filter)
else:
# Load the missing weights from disk
self.update_weights_from_disk(
self.weight_updater.update_weights_from_disk(
get_server_args().model_path,
get_server_args().load_format,
weight_name_filter=weight_name_filter,
@@ -1742,409 +1755,6 @@ class ModelRunner(ModelRunnerKVCacheMixin):
"All ranks are marked as elastic_ep_rejoin."
)
def update_weights_from_disk(
self,
model_path: str,
load_format: str,
weight_name_filter: Optional[Callable[[str], bool]] = None,
recapture_cuda_graph: bool = False,
) -> tuple[bool, str]:
"""Update engine weights in-place from the disk."""
logger.info(
f"Update engine weights online from disk begin. "
f"avail mem={get_available_gpu_memory(self.device, self.gpu_id, empty_cache=False):.2f} GB"
)
target_device = torch.device(self.device)
self.model_config.model_path = model_path
load_config = LoadConfig(load_format=load_format)
# Only support DefaultModelLoader for now
loader = get_model_loader(load_config, self.model_config)
if not isinstance(loader, DefaultModelLoader):
message = f"Failed to get model loader: {loader}."
return False, message
def get_weight_iter(config):
iter = loader._get_weights_iterator(
DefaultModelLoader.Source.init_new(config, self.model)
)
if weight_name_filter is not None:
iter = (
(name, weight) for name, weight in iter if weight_name_filter(name)
)
return iter
def model_load_weights(model, iter):
loader.load_weights_and_postprocess(model, iter, target_device)
return model
with set_default_torch_dtype(self.model_config.dtype):
try:
iter = get_weight_iter(self.model_config)
except Exception as e:
message = f"Failed to get weights iterator: {e}."
return False, message
try:
model = model_load_weights(self.model, iter)
except Exception as e:
message = (
f"Failed to update weights: {e}.\nRolling back to original weights."
)
del iter
gc.collect()
iter = get_weight_iter(self.model_config)
self.model = model_load_weights(self.model, iter)
return False, message
self.model = model
self.server_args.override(
"model_runner.update_weights",
model_path=model_path,
load_format=load_format,
)
self.load_config = load_config
if recapture_cuda_graph and (
self.device == "cuda"
or self.device == "musa"
or (
current_platform.is_out_of_tree()
and current_platform.support_cuda_graph()
)
):
self.init_decode_cuda_graph()
logger.info("Update weights end.")
return True, "Succeeded to update model weights."
def init_weights_send_group_for_remote_instance(
self,
master_address,
ports,
group_rank,
world_size,
group_name,
backend="nccl",
):
assert (
torch.distributed.is_initialized()
), "Default torch process group must be initialized"
assert group_name != "", "Group name cannot be empty"
ports_list = ports.split(",")
assert (
len(ports_list) == self.tp_size
), f"Expected {self.tp_size} ports, but got {len(ports_list)} ports."
group_port = ports_list[self.tp_rank]
group_name = f"{group_name}_{group_port}_{self.tp_rank}"
logger.info(
f"init custom process group: tp_rank={self.tp_rank}, gpu_id={self.gpu_id}, master_address={master_address}, master_port={group_port}, "
f"group_rank={group_rank}, world_size={world_size}, group_name={group_name}, backend={backend}"
)
current_platform.empty_cache()
success = False
message = ""
try:
na = NetworkAddress(master_address, group_port)
self._weights_send_group[group_name] = init_custom_process_group(
backend=backend,
init_method=na.to_tcp(),
world_size=world_size,
rank=group_rank,
group_name=group_name,
device_id=torch.device("cuda", self.gpu_id),
)
dist.barrier(group=self._weights_send_group[group_name])
success = True
message = f"Succeeded to init group through {na.to_host_port_str()} group."
except Exception as e:
message = f"Failed to init group: {e}."
logger.error(message)
current_platform.empty_cache()
return success, message
def send_weights_to_remote_instance(
self,
master_address,
ports,
group_name,
):
assert (
torch.distributed.is_initialized()
), "Default torch process group must be initialized"
assert group_name != "", "Group name cannot be empty"
ports_list = ports.split(",")
assert (
len(ports_list) == self.tp_size
), f"Expected {self.tp_size} ports, but got {len(ports_list)} ports."
group_port = ports_list[self.tp_rank]
group_name = f"{group_name}_{group_port}_{self.tp_rank}"
if self._weights_send_group[group_name] is not None:
send_group = self._weights_send_group[group_name]
else:
message = f"Group {group_name} not in _weights_send_group list. Please call `init_weights_send_group_for_remote_instance` first."
logger.error(message)
return False, message
current_platform.empty_cache()
success = False
na = NetworkAddress(master_address, group_port)
message = ""
try:
for _, weights in self.model.named_parameters():
torch.distributed.broadcast(
weights,
src=0,
group=send_group,
)
success = True
message = f"Succeeded to send weights through {na.to_host_port_str()} {group_name}."
except Exception as e:
message = f"Failed to send weights: {e}."
logger.error(message)
# destroy the process group after sending weights
del self._weights_send_group[group_name]
torch.distributed.distributed_c10d.destroy_process_group(send_group)
current_platform.empty_cache()
return success, message
def init_weights_update_group(
self,
master_address,
master_port,
rank_offset,
world_size,
group_name,
backend="nccl",
):
"""Initialize the Torch process group for model parameter updates.
`_model_update_group` is used in the RLHF workflow, where rank
0 is the actor model in the training engine, and the other ranks are
the inference engine, which is used for rollout.
In the RLHF workflow, the training engine updates the model
weights/parameters online, and broadcasts them to the inference
engine through the `_model_update_group` process group.
"""
assert (
torch.distributed.is_initialized()
), "Default torch process group must be initialized"
assert group_name != "", "Group name cannot be empty"
rank = rank_offset + self.tp_rank
logger.info(
f"init custom process group: master_address={master_address}, master_port={master_port}, "
f"rank_offset={rank_offset}, rank={rank}, world_size={world_size}, group_name={group_name}, backend={backend}"
)
try:
na = NetworkAddress(master_address, master_port)
self._model_update_group[group_name] = init_custom_process_group(
backend=backend,
init_method=na.to_tcp(),
world_size=world_size,
rank=rank,
group_name=group_name,
)
return True, "Succeeded to initialize custom process group."
except Exception as e:
message = f"Failed to initialize custom process group: {e}."
logger.error(message)
return False, message
def destroy_weights_update_group(self, group_name):
try:
if group_name in self._model_update_group:
pg = self._model_update_group.pop(group_name)
torch.distributed.destroy_process_group(pg)
return True, "Succeeded to destroy custom process group."
else:
return False, "The group to be destroyed does not exist."
except Exception as e:
message = f"Failed to destroy custom process group: {e}."
logger.error(message)
return False, message
def update_weights_from_distributed(
self,
names,
dtypes,
shapes,
group_name,
load_format: Optional[str] = None,
):
"""
Update specific parameter in the model weights online
through `_model_update_group` process group.
Args:
name: the name of the parameter to be updated.
dtype: the data type of the parameter to be updated.
shape: the shape of the parameter to be updated.
"""
assert group_name in self._model_update_group, (
f"Group {group_name} not in {list(self._model_update_group.keys())}. "
"Please call `init_weights_update_group` first."
)
if load_format == "flattened_bucket":
return self._update_bucketed_weights_from_distributed(
names, dtypes, shapes, group_name
)
try:
weights = []
handles = []
for name, dtype, shape in zip(names, dtypes, shapes):
target_dtype = (
dtype if isinstance(dtype, torch.dtype) else getattr(torch, dtype)
)
weight = torch.empty(shape, dtype=target_dtype, device=self.device)
handles.append(
torch.distributed.broadcast(
weight,
src=0,
group=self._model_update_group[group_name],
async_op=True,
)
)
weights.append((name, weight))
for handle in handles:
handle.wait()
self.model.load_weights(weights)
return True, "Succeeded to update parameter online."
except Exception as e:
error_msg = (
f"Failed to update parameter online: {e}. "
f"The full weights of the ModelRunner are partially updated. "
f"Please discard the whole weights."
)
logger.error(error_msg)
return False, error_msg
def _update_bucketed_weights_from_distributed(
self, names, dtypes, shapes, group_name
):
try:
named_tensors = []
for name, dtype, shape in zip(names, dtypes, shapes):
target_dtype = (
dtype if isinstance(dtype, torch.dtype) else getattr(torch, dtype)
)
named_tensors.append(
(name, torch.empty(shape, dtype=target_dtype, device=self.device))
)
bucket = FlattenedTensorBucket(named_tensors=named_tensors)
flattened_tensor = bucket.get_flattened_tensor()
torch.distributed.broadcast(
flattened_tensor,
src=0,
group=self._model_update_group[group_name],
)
reconstructed_tensors = bucket.reconstruct_tensors()
self.model.load_weights(reconstructed_tensors)
return True, f"Succeeded to update parameter online."
except Exception as e:
error_msg = (
f"Failed to update parameter online: {e}. "
f"The full weights of the ModelRunner are partially updated. "
f"Please discard the whole weights."
)
logger.error(error_msg)
return False, error_msg
def update_weights_from_tensor(
self,
named_tensors: List[Tuple[str, Union[torch.Tensor, LocalSerializedTensor]]],
load_format: Optional[str] = None,
):
monkey_patch_torch_reductions()
if load_format == "flattened_bucket":
# Handle flattened bucket format
return self._update_weights_from_flattened_bucket(
flattened_tensor_bucket_dict=named_tensors
)
# We need to get device after patch otherwise the device would be wrong
device_module = torch.get_device_module(self.device)
infered_device = device_module.current_device()
named_tensors = [
(name, _unwrap_tensor(tensor, tp_rank=self.tp_rank, device=infered_device))
for name, tensor in named_tensors
]
if load_format == "direct":
_model_load_weights_direct(self.model, named_tensors)
elif load_format in self.server_args.custom_weight_loader:
custom_loader = dynamic_import(load_format)
custom_loader(self.model, named_tensors)
elif load_format is None:
self.model.load_weights(named_tensors)
else:
raise NotImplementedError(f"Unknown load_format={load_format}")
return True, "Success"
def _update_weights_from_flattened_bucket(
self,
flattened_tensor_bucket_dict,
):
"""Handle flattened bucket format for weight updates"""
flattened_tensor = flattened_tensor_bucket_dict["flattened_tensor"]
metadata = flattened_tensor_bucket_dict["metadata"]
# Convert metadata dict to our format
converted_metadata = []
for meta in metadata:
converted_meta = FlattenedTensorMetadata(
name=meta.name,
shape=meta.shape,
dtype=meta.dtype,
start_idx=meta.start_idx,
end_idx=meta.end_idx,
numel=meta.numel,
)
converted_metadata.append(converted_meta)
# Create bucket and reconstruct tensors
bucket = FlattenedTensorBucket(
flattened_tensor=flattened_tensor, metadata=converted_metadata
)
reconstructed_tensors = bucket.reconstruct_tensors()
# Load the reconstructed tensors using the standard method
self.model.load_weights(reconstructed_tensors)
return True, "Success"
def get_weights_by_name(
self, name: str, truncate_size: int = 100
) -> Optional[torch.Tensor]:
"""Get the weights of the parameter by its name. Similar to `get_parameter` in Hugging Face.
Only used for unit test with an unoptimized performance.
For optimized performance, please use torch.save and torch.load.
"""
# TODO: (chenyang) Add support for Qwen models.
try:
return self.model.get_weights_by_name(
name, truncate_size, tp_size=self.tp_size
)
except Exception as e:
logger.error(f"Error when getting parameter {name}: {e}")
return None
def init_lora_manager(self):
self.lora_manager = LoRAManager(
base_model=self.model,
@@ -3176,44 +2786,11 @@ class ModelRunner(ModelRunnerKVCacheMixin):
forward_batch.token_ids_logprobs,
)
def save_remote_model(self, url: str):
from sglang.srt.model_loader.loader import RemoteModelLoader
logger.info(f"Saving model to {url}")
RemoteModelLoader.save_model(self.model, self.model_config.model_path, url)
def save_sharded_model(
self, path: str, pattern: Optional[str] = None, max_size: Optional[int] = None
):
from sglang.srt.model_loader.loader import ShardedStateLoader
logger.info(
f"Save sharded model to {path} with pattern {pattern} and max_size {max_size}"
)
ShardedStateLoader.save_model(self.model, path, pattern, max_size)
def check_weights(self, action: str, allow_quant_error: bool = False):
return self._weight_checker.handle(
action=action, allow_quant_error=allow_quant_error
)
def update_weights_from_ipc(self, recv_req):
"""Update weights from IPC for checkpoint-engine integration."""
try:
from sglang.srt.checkpoint_engine.checkpoint_engine_worker import (
SGLangCheckpointEngineWorkerExtensionImpl,
)
# Create a worker extension that integrates with SGLang's model
worker = SGLangCheckpointEngineWorkerExtensionImpl(self)
worker.update_weights_from_ipc(recv_req.zmq_handles)
return True, "IPC weight update completed successfully"
except ImportError as e:
return False, f"IPC weight update failed: ImportError {e}"
except Exception as e:
logger.error(f"IPC weight update failed: {e}")
return False, str(e)
def _maybe_rebalance_after_rank_fault(
self,
output: ModelRunnerOutput,
@@ -3241,25 +2818,18 @@ class ModelRunner(ModelRunnerKVCacheMixin):
)
return output
def _model_load_weights_direct(model, named_tensors: List[Tuple[str, torch.Tensor]]):
params_dict = dict(model.named_parameters())
for name, tensor in named_tensors:
default_weight_loader(params_dict[name], tensor)
def _unwrap_tensor(tensor, tp_rank, device):
if isinstance(tensor, LocalSerializedTensor):
tensor = tensor.get(tp_rank)
return tensor.to(device)
@dataclass
class LocalSerializedTensor:
"""torch.Tensor that gets serialized by MultiprocessingSerializer (which only serializes a pointer and not the data).
The i-th element in the list corresponds to i-th rank's GPU."""
values: List[bytes]
def get(self, rank: int):
return MultiprocessingSerializer.deserialize(self.values[rank])
def update_model_fields(
self,
new_model: torch.nn.Module,
*,
model_path: str,
load_format: str,
load_config: LoadConfig,
) -> None:
self.model = new_model
self.server_args.override(
"model_runner.update_model_fields",
model_path=model_path,
load_format=load_format,
)
self.load_config = load_config
@@ -0,0 +1,157 @@
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from typing import Any, Callable, Optional
import torch
import torch.distributed as dist
from sglang.srt.platforms import current_platform
from sglang.srt.utils import init_custom_process_group
from sglang.srt.utils.network import NetworkAddress
logger = logging.getLogger(__name__)
@dataclass(slots=True, kw_only=True)
class WeightExporter:
tp_rank: int
tp_size: int
gpu_id: int
get_model_path: Callable[[], str]
get_model: Callable[[], Any]
_weights_send_group: dict = field(default_factory=dict)
def init_weights_send_group_for_remote_instance(
self,
master_address,
ports,
group_rank,
world_size,
group_name,
backend="nccl",
):
assert (
torch.distributed.is_initialized()
), "Default torch process group must be initialized"
assert group_name != "", "Group name cannot be empty"
ports_list = ports.split(",")
assert (
len(ports_list) == self.tp_size
), f"Expected {self.tp_size} ports, but got {len(ports_list)} ports."
group_port = ports_list[self.tp_rank]
group_name = f"{group_name}_{group_port}_{self.tp_rank}"
logger.info(
f"init custom process group: tp_rank={self.tp_rank}, gpu_id={self.gpu_id}, master_address={master_address}, master_port={group_port}, "
f"group_rank={group_rank}, world_size={world_size}, group_name={group_name}, backend={backend}"
)
current_platform.empty_cache()
success = False
message = ""
try:
na = NetworkAddress(master_address, group_port)
self._weights_send_group[group_name] = init_custom_process_group(
backend=backend,
init_method=na.to_tcp(),
world_size=world_size,
rank=group_rank,
group_name=group_name,
device_id=torch.device("cuda", self.gpu_id),
)
dist.barrier(group=self._weights_send_group[group_name])
success = True
message = f"Succeeded to init group through {na.to_host_port_str()} group."
except Exception as e:
message = f"Failed to init group: {e}."
logger.error(message)
current_platform.empty_cache()
return success, message
def send_weights_to_remote_instance(
self,
master_address,
ports,
group_name,
):
assert (
torch.distributed.is_initialized()
), "Default torch process group must be initialized"
assert group_name != "", "Group name cannot be empty"
ports_list = ports.split(",")
assert (
len(ports_list) == self.tp_size
), f"Expected {self.tp_size} ports, but got {len(ports_list)} ports."
group_port = ports_list[self.tp_rank]
group_name = f"{group_name}_{group_port}_{self.tp_rank}"
if self._weights_send_group[group_name] is not None:
send_group = self._weights_send_group[group_name]
else:
message = f"Group {group_name} not in _weights_send_group list. Please call `init_weights_send_group_for_remote_instance` first."
logger.error(message)
return False, message
current_platform.empty_cache()
success = False
na = NetworkAddress(master_address, group_port)
message = ""
try:
for _, weights in self.get_model().named_parameters():
torch.distributed.broadcast(
weights,
src=0,
group=send_group,
)
success = True
message = f"Succeeded to send weights through {na.to_host_port_str()} {group_name}."
except Exception as e:
message = f"Failed to send weights: {e}."
logger.error(message)
# destroy the process group after sending weights
del self._weights_send_group[group_name]
torch.distributed.distributed_c10d.destroy_process_group(send_group)
current_platform.empty_cache()
return success, message
def save_remote_model(self: WeightExporter, url: str):
from sglang.srt.model_loader.loader import RemoteModelLoader
logger.info(f"Saving model to {url}")
RemoteModelLoader.save_model(self.get_model(), self.get_model_path(), url)
def save_sharded_model(
self: WeightExporter,
path: str,
pattern: Optional[str] = None,
max_size: Optional[int] = None,
):
from sglang.srt.model_loader.loader import ShardedStateLoader
logger.info(
f"Save sharded model to {path} with pattern {pattern} and max_size {max_size}"
)
ShardedStateLoader.save_model(self.get_model(), path, pattern, max_size)
def get_weights_by_name(
self: WeightExporter, name: str, truncate_size: int = 100
) -> Optional[torch.Tensor]:
"""Get the weights of the parameter by its name. Similar to `get_parameter` in Hugging Face.
Only used for unit test with an unoptimized performance.
For optimized performance, please use torch.save and torch.load.
"""
# TODO: (chenyang) Add support for Qwen models.
try:
return self.get_model().get_weights_by_name(
name, truncate_size, tp_size=self.tp_size
)
except Exception as e:
logger.error(f"Error when getting parameter {name}: {e}")
return None
@@ -0,0 +1,377 @@
from __future__ import annotations
import gc
import logging
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Union
import torch
from sglang.srt.configs.load_config import LoadConfig
from sglang.srt.model_loader.loader import DefaultModelLoader, get_model_loader
from sglang.srt.model_loader.utils import set_default_torch_dtype
from sglang.srt.model_loader.weight_utils import default_weight_loader
from sglang.srt.platforms import current_platform
from sglang.srt.utils import (
MultiprocessingSerializer,
dynamic_import,
get_available_gpu_memory,
init_custom_process_group,
)
from sglang.srt.utils.network import NetworkAddress
from sglang.srt.utils.patch_torch import monkey_patch_torch_reductions
from sglang.srt.weight_sync.tensor_bucket import (
FlattenedTensorBucket,
FlattenedTensorMetadata,
)
if TYPE_CHECKING:
from sglang.srt.configs.model_config import ModelConfig
from sglang.srt.model_executor.model_runner import ModelRunner
logger = logging.getLogger(__name__)
@dataclass(frozen=True, slots=True, kw_only=True)
class WeightUpdater:
tp_rank: int
device: str
gpu_id: int
model_config: ModelConfig
custom_weight_loaders: dict
get_model: Callable[[], Any]
update_model_fields: Callable[..., None]
recapture_cuda_graph: Callable[[], None]
get_model_runner: Callable[[], ModelRunner]
_model_update_group: dict = field(default_factory=dict)
def init_weights_update_group(
self,
master_address,
master_port,
rank_offset,
world_size,
group_name,
backend="nccl",
):
"""Initialize the Torch process group for model parameter updates.
`_model_update_group` is used in the RLHF workflow, where rank
0 is the actor model in the training engine, and the other ranks are
the inference engine, which is used for rollout.
In the RLHF workflow, the training engine updates the model
weights/parameters online, and broadcasts them to the inference
engine through the `_model_update_group` process group.
"""
assert (
torch.distributed.is_initialized()
), "Default torch process group must be initialized"
assert group_name != "", "Group name cannot be empty"
rank = rank_offset + self.tp_rank
logger.info(
f"init custom process group: master_address={master_address}, master_port={master_port}, "
f"rank_offset={rank_offset}, rank={rank}, world_size={world_size}, group_name={group_name}, backend={backend}"
)
try:
na = NetworkAddress(master_address, master_port)
self._model_update_group[group_name] = init_custom_process_group(
backend=backend,
init_method=na.to_tcp(),
world_size=world_size,
rank=rank,
group_name=group_name,
)
return True, "Succeeded to initialize custom process group."
except Exception as e:
message = f"Failed to initialize custom process group: {e}."
logger.error(message)
return False, message
def destroy_weights_update_group(self, group_name):
try:
if group_name in self._model_update_group:
pg = self._model_update_group.pop(group_name)
torch.distributed.destroy_process_group(pg)
return True, "Succeeded to destroy custom process group."
else:
return False, "The group to be destroyed does not exist."
except Exception as e:
message = f"Failed to destroy custom process group: {e}."
logger.error(message)
return False, message
def update_weights_from_disk(
self: WeightUpdater,
model_path: str,
load_format: str,
weight_name_filter: Optional[Callable[[str], bool]] = None,
recapture_cuda_graph: bool = False,
) -> tuple[bool, str]:
"""Update engine weights in-place from the disk."""
logger.info(
f"Update engine weights online from disk begin. "
f"avail mem={get_available_gpu_memory(self.device, self.gpu_id, empty_cache=False):.2f} GB"
)
target_device = torch.device(self.device)
self.model_config.model_path = model_path
load_config = LoadConfig(load_format=load_format)
# Only support DefaultModelLoader for now
loader = get_model_loader(load_config, self.model_config)
if not isinstance(loader, DefaultModelLoader):
message = f"Failed to get model loader: {loader}."
return False, message
def get_weight_iter(config):
iter = loader._get_weights_iterator(
DefaultModelLoader.Source.init_new(config, self.get_model())
)
if weight_name_filter is not None:
iter = (
(name, weight) for name, weight in iter if weight_name_filter(name)
)
return iter
def model_load_weights(model, iter):
loader.load_weights_and_postprocess(model, iter, target_device)
return model
with set_default_torch_dtype(self.model_config.dtype):
try:
iter = get_weight_iter(self.model_config)
except Exception as e:
message = f"Failed to get weights iterator: {e}."
return False, message
try:
model = model_load_weights(self.get_model(), iter)
except Exception as e:
message = (
f"Failed to update weights: {e}.\nRolling back to original weights."
)
del iter
gc.collect()
iter = get_weight_iter(self.model_config)
model_load_weights(self.get_model(), iter)
return False, message
self.update_model_fields(
model,
model_path=model_path,
load_format=load_format,
load_config=load_config,
)
if recapture_cuda_graph and (
self.device == "cuda"
or self.device == "musa"
or (
current_platform.is_out_of_tree()
and current_platform.support_cuda_graph()
)
):
self.recapture_cuda_graph()
logger.info("Update weights end.")
return True, "Succeeded to update model weights."
def update_weights_from_distributed(
self: WeightUpdater,
names,
dtypes,
shapes,
group_name,
load_format: Optional[str] = None,
):
"""
Update specific parameter in the model weights online
through `_model_update_group` process group.
Args:
name: the name of the parameter to be updated.
dtype: the data type of the parameter to be updated.
shape: the shape of the parameter to be updated.
"""
assert group_name in self._model_update_group, (
f"Group {group_name} not in {list(self._model_update_group.keys())}. "
"Please call `init_weights_update_group` first."
)
if load_format == "flattened_bucket":
return self._update_bucketed_weights_from_distributed(
names, dtypes, shapes, group_name
)
try:
weights = []
handles = []
for name, dtype, shape in zip(names, dtypes, shapes):
target_dtype = (
dtype if isinstance(dtype, torch.dtype) else getattr(torch, dtype)
)
weight = torch.empty(shape, dtype=target_dtype, device=self.device)
handles.append(
torch.distributed.broadcast(
weight,
src=0,
group=self._model_update_group[group_name],
async_op=True,
)
)
weights.append((name, weight))
for handle in handles:
handle.wait()
self.get_model().load_weights(weights)
return True, "Succeeded to update parameter online."
except Exception as e:
error_msg = (
f"Failed to update parameter online: {e}. "
f"The full weights of the ModelRunner are partially updated. "
f"Please discard the whole weights."
)
logger.error(error_msg)
return False, error_msg
def _update_bucketed_weights_from_distributed(
self: WeightUpdater, names, dtypes, shapes, group_name
):
try:
named_tensors = []
for name, dtype, shape in zip(names, dtypes, shapes):
target_dtype = (
dtype if isinstance(dtype, torch.dtype) else getattr(torch, dtype)
)
named_tensors.append(
(
name,
torch.empty(shape, dtype=target_dtype, device=self.device),
)
)
bucket = FlattenedTensorBucket(named_tensors=named_tensors)
flattened_tensor = bucket.get_flattened_tensor()
torch.distributed.broadcast(
flattened_tensor,
src=0,
group=self._model_update_group[group_name],
)
reconstructed_tensors = bucket.reconstruct_tensors()
self.get_model().load_weights(reconstructed_tensors)
return True, f"Succeeded to update parameter online."
except Exception as e:
error_msg = (
f"Failed to update parameter online: {e}. "
f"The full weights of the ModelRunner are partially updated. "
f"Please discard the whole weights."
)
logger.error(error_msg)
return False, error_msg
def update_weights_from_tensor(
self: WeightUpdater,
named_tensors: List[Tuple[str, Union[torch.Tensor, LocalSerializedTensor]]],
load_format: Optional[str] = None,
):
monkey_patch_torch_reductions()
if load_format == "flattened_bucket":
# Handle flattened bucket format
return self._update_weights_from_flattened_bucket(
flattened_tensor_bucket_dict=named_tensors
)
# We need to get device after patch otherwise the device would be wrong
device_module = torch.get_device_module(self.device)
infered_device = device_module.current_device()
named_tensors = [
(name, _unwrap_tensor(tensor, tp_rank=self.tp_rank, device=infered_device))
for name, tensor in named_tensors
]
if load_format == "direct":
_model_load_weights_direct(self.get_model(), named_tensors)
elif load_format in self.custom_weight_loaders:
custom_loader = dynamic_import(load_format)
custom_loader(self.get_model(), named_tensors)
elif load_format is None:
self.get_model().load_weights(named_tensors)
else:
raise NotImplementedError(f"Unknown load_format={load_format}")
return True, "Success"
def _update_weights_from_flattened_bucket(
self: WeightUpdater,
flattened_tensor_bucket_dict,
):
"""Handle flattened bucket format for weight updates"""
flattened_tensor = flattened_tensor_bucket_dict["flattened_tensor"]
metadata = flattened_tensor_bucket_dict["metadata"]
# Convert metadata dict to our format
converted_metadata = []
for meta in metadata:
converted_meta = FlattenedTensorMetadata(
name=meta.name,
shape=meta.shape,
dtype=meta.dtype,
start_idx=meta.start_idx,
end_idx=meta.end_idx,
numel=meta.numel,
)
converted_metadata.append(converted_meta)
# Create bucket and reconstruct tensors
bucket = FlattenedTensorBucket(
flattened_tensor=flattened_tensor, metadata=converted_metadata
)
reconstructed_tensors = bucket.reconstruct_tensors()
# Load the reconstructed tensors using the standard method
self.get_model().load_weights(reconstructed_tensors)
return True, "Success"
def update_weights_from_ipc(self: WeightUpdater, recv_req):
"""Update weights from IPC for checkpoint-engine integration."""
try:
from sglang.srt.checkpoint_engine.checkpoint_engine_worker import (
SGLangCheckpointEngineWorkerExtensionImpl,
)
# Create a worker extension that integrates with SGLang's model
worker = SGLangCheckpointEngineWorkerExtensionImpl(self.get_model_runner())
worker.update_weights_from_ipc(recv_req.zmq_handles)
return True, "IPC weight update completed successfully"
except ImportError as e:
return False, f"IPC weight update failed: ImportError {e}"
except Exception as e:
logger.error(f"IPC weight update failed: {e}")
return False, str(e)
def _model_load_weights_direct(model, named_tensors: List[Tuple[str, torch.Tensor]]):
params_dict = dict(model.named_parameters())
for name, tensor in named_tensors:
default_weight_loader(params_dict[name], tensor)
def _unwrap_tensor(tensor, tp_rank, device):
if isinstance(tensor, LocalSerializedTensor):
tensor = tensor.get(tp_rank)
return tensor.to(device)
@dataclass
class LocalSerializedTensor:
"""torch.Tensor that gets serialized by MultiprocessingSerializer (which only serializes a pointer and not the data).
The i-th element in the list corresponds to i-th rank's GPU."""
values: List[bytes]
def get(self, rank: int):
return MultiprocessingSerializer.deserialize(self.values[rank])
@@ -1738,18 +1738,22 @@ class EAGLEWorkerV2(BaseSpecWorker):
return out
def update_weights_from_disk(self, recv_req: UpdateWeightFromDiskReqInput):
success, message = self._draft_worker.draft_runner.update_weights_from_disk(
recv_req.model_path,
recv_req.load_format,
recapture_cuda_graph=recv_req.recapture_cuda_graph,
success, message = (
self._draft_worker.draft_runner.weight_updater.update_weights_from_disk(
recv_req.model_path,
recv_req.load_format,
recapture_cuda_graph=recv_req.recapture_cuda_graph,
)
)
if not success:
return success, message
return True, "Succeeded to update model weights."
def update_weights_from_ipc(self, recv_req: UpdateWeightsFromIPCReqInput):
success, message = self._draft_worker.draft_runner.update_weights_from_ipc(
recv_req
success, message = (
self._draft_worker.draft_runner.weight_updater.update_weights_from_ipc(
recv_req
)
)
if not success:
return success, message
@@ -1760,15 +1764,19 @@ class EAGLEWorkerV2(BaseSpecWorker):
named_tensors = MultiprocessingSerializer.deserialize(
recv_req.serialized_named_tensors[self.tp_rank]
)
success, message = self.draft_worker.draft_runner.update_weights_from_tensor(
named_tensors=named_tensors,
load_format=recv_req.load_format,
success, message = (
self.draft_worker.draft_runner.weight_updater.update_weights_from_tensor(
named_tensors=named_tensors,
load_format=recv_req.load_format,
)
)
if not success:
return success, message
success, message = self.target_worker.model_runner.update_weights_from_tensor(
named_tensors=named_tensors,
load_format=recv_req.load_format,
success, message = (
self.target_worker.model_runner.weight_updater.update_weights_from_tensor(
named_tensors=named_tensors,
load_format=recv_req.load_format,
)
)
return success, message
@@ -884,7 +884,7 @@ class MultiLayerEagleWorkerV2(BaseSpecWorker):
for i in range(self.speculative_num_steps):
success, message = self._draft_worker.draft_runner_list[
i
].update_weights_from_disk(
].weight_updater.update_weights_from_disk(
recv_req.model_path,
recv_req.load_format,
recapture_cuda_graph=recv_req.recapture_cuda_graph,
@@ -897,7 +897,7 @@ class MultiLayerEagleWorkerV2(BaseSpecWorker):
for i in range(self.speculative_num_steps):
success, message = self._draft_worker.draft_runner_list[
i
].update_weights_from_ipc(recv_req)
].weight_updater.update_weights_from_ipc(recv_req)
if not success:
return success, message
return True, "Succeeded to update model weights."
+1
View File
@@ -2748,6 +2748,7 @@ class SafeUnpickler(pickle.Unpickler):
# --- SGLang & Unitest ---
"sglang.srt.weight_sync.tensor_bucket.",
"sglang.srt.model_executor.model_runner.",
"sglang.srt.model_executor.model_runner_components.weight_updater.",
"sglang.srt.layers.",
"sglang.srt.utils.",
"sglang.srt.disaggregation.",
+3 -1
View File
@@ -7,7 +7,9 @@ from torch.distributed.tensor import DTensor
from sglang.srt.entrypoints.engine import Engine
from sglang.srt.managers.io_struct import UpdateWeightsFromTensorReqInput
from sglang.srt.model_executor.model_runner import LocalSerializedTensor
from sglang.srt.model_executor.model_runner_components.weight_updater import (
LocalSerializedTensor,
)
from sglang.srt.utils import MultiprocessingSerializer