chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# Copyright (c) DeepSpeed Team.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# DeepSpeed Team
|
||||
@@ -0,0 +1,375 @@
|
||||
# Copyright (c) DeepSpeed Team.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# DeepSpeed Team
|
||||
|
||||
import time
|
||||
import torch
|
||||
from typing import List
|
||||
|
||||
from deepspeed.runtime.superoffload.superoffload_utils import SuperOffloadCPUOptimizer, TaskKeys, ResultKeys, EventTypes
|
||||
from deepspeed.runtime.zero.partition_parameters import Parameter, Tensor
|
||||
from deepspeed.runtime.zero.stage3 import DeepSpeedZeroOptimizer_Stage3
|
||||
from deepspeed.utils.nvtx import instrument_w_nvtx
|
||||
from deepspeed.utils import logger
|
||||
from deepspeed.accelerator import get_accelerator
|
||||
|
||||
OPTIMIZER_STEP_TIMER = 'optimizer_step'
|
||||
|
||||
|
||||
def _validate_superoffload_accelerator():
|
||||
"""Validate that the current accelerator is compatible with SuperOffload."""
|
||||
accelerator = get_accelerator()
|
||||
assert accelerator.device_name() == 'cuda', (
|
||||
f"SuperOffload only supports NVIDIA CUDA GPUs, but found accelerator '{accelerator.device_name()}'.")
|
||||
|
||||
|
||||
class SuperOffloadOptimizer_Stage3(DeepSpeedZeroOptimizer_Stage3):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
module,
|
||||
init_optimizer,
|
||||
param_names,
|
||||
timers,
|
||||
ds_config,
|
||||
**kwargs,
|
||||
):
|
||||
_validate_superoffload_accelerator()
|
||||
|
||||
self.sub_group_to_param_num = {}
|
||||
self.sub_group_grad_partition_counts = {}
|
||||
self.async_cpuadam_num = 0
|
||||
self.max_grad_numel = 0
|
||||
|
||||
super().__init__(module, init_optimizer, param_names, timers, ds_config, **kwargs)
|
||||
|
||||
optimizer_configs = []
|
||||
for pg in self.optimizer.param_groups:
|
||||
optimizer_configs.append({
|
||||
"lr": pg["lr"],
|
||||
"betas": pg["betas"],
|
||||
"eps": pg["eps"],
|
||||
"weight_decay": pg["weight_decay"],
|
||||
"amsgrad": pg["amsgrad"],
|
||||
})
|
||||
cpuadam_cores_perc = kwargs.get("cpuadam_cores_perc", 0.8)
|
||||
self.superoffload_cpu_optimizer = SuperOffloadCPUOptimizer(optimizer_config=optimizer_configs,
|
||||
cpuadam_cores_perc=cpuadam_cores_perc,
|
||||
max_grad_numel=self.max_grad_numel)
|
||||
|
||||
def _create_fp16_sub_groups(self, params_group):
|
||||
|
||||
params_group_numel = sum([param.partition_numel() for param in params_group])
|
||||
sub_group_size = self.sub_group_size
|
||||
|
||||
if sub_group_size is None or sub_group_size >= params_group_numel:
|
||||
global_idx = len(self.sub_group_to_param_num)
|
||||
self.sub_group_to_param_num[global_idx] = len(params_group)
|
||||
self.max_grad_numel = max(self.max_grad_numel, params_group_numel)
|
||||
return [params_group]
|
||||
|
||||
sub_groups = []
|
||||
sub_group = []
|
||||
local_sub_group_size = 0
|
||||
|
||||
for param in params_group:
|
||||
sub_group.append(param)
|
||||
local_sub_group_size += param.partition_numel()
|
||||
|
||||
if local_sub_group_size >= sub_group_size or id(param) == id(params_group[-1]):
|
||||
self.max_grad_numel = max(self.max_grad_numel, local_sub_group_size)
|
||||
sub_groups.append(sub_group)
|
||||
global_idx = len(self.sub_group_to_param_num)
|
||||
self.sub_group_to_param_num[global_idx] = len(sub_group)
|
||||
|
||||
sub_group = []
|
||||
local_sub_group_size = 0
|
||||
|
||||
return sub_groups
|
||||
|
||||
def _optimizer_step(self, sub_group_id):
|
||||
param_group_id = self.sub_group_to_group_id[sub_group_id]
|
||||
fp32_param = self.fp32_partitioned_groups_flat[sub_group_id]
|
||||
|
||||
def step_with_gradscaler(optimizer):
|
||||
if self.torch_autocast_gradscaler:
|
||||
self.torch_autocast_gradscaler.step(optimizer)
|
||||
self.torch_autocast_gradscaler.update()
|
||||
else:
|
||||
optimizer.step()
|
||||
|
||||
cur_device = self.subgroup_to_device[sub_group_id]
|
||||
if cur_device != 'cpu':
|
||||
self.backup_optimizer.param_groups[param_group_id]['params'] = [fp32_param]
|
||||
step_with_gradscaler(self.backup_optimizer)
|
||||
self.backup_optimizer.param_groups[param_group_id]['params'] = []
|
||||
|
||||
@instrument_w_nvtx
|
||||
def independent_gradient_partition_epilogue(self):
|
||||
super().independent_gradient_partition_epilogue()
|
||||
self.sub_group_grad_partition_counts.clear()
|
||||
|
||||
@instrument_w_nvtx
|
||||
def _reassign_or_swap_out_partitioned_parameters(self, sub_group_id):
|
||||
if self.subgroup_to_device[sub_group_id] == 'cpu':
|
||||
self.fp16_partitioned_groups_flat[sub_group_id].data.copy_(
|
||||
self.fp32_partitioned_groups_flat[sub_group_id].data)
|
||||
self._unflatten_partitioned_parameters(sub_group_id)
|
||||
return
|
||||
|
||||
if self.fp16_partitioned_groups_flat[sub_group_id] is not None:
|
||||
self.fp16_partitioned_groups_flat[sub_group_id].data.copy_(
|
||||
self.fp32_partitioned_groups_flat[sub_group_id].data)
|
||||
self._unflatten_partitioned_parameters(sub_group_id)
|
||||
else:
|
||||
self._partitioned_params_swap_out(sub_group_id)
|
||||
|
||||
@instrument_w_nvtx
|
||||
def _reassign_or_swap_out_partitioned_parameters_async(self, sub_group_id, updated_param):
|
||||
"""Asynchronously update partitioned parameters with optimized values."""
|
||||
self.fp32_partitioned_groups_flat[sub_group_id].data.copy_(updated_param, non_blocking=True)
|
||||
|
||||
@instrument_w_nvtx
|
||||
def partition_grads(self, params_to_release: List[Parameter], grad_partitions: List[Tensor]) -> None:
|
||||
completed_sub_groups = []
|
||||
|
||||
for param, grad_partition in zip(params_to_release, grad_partitions):
|
||||
i, dest_offset, _ = self.grad_position[self.get_param_id(param)]
|
||||
|
||||
# Accumulate gradient into the grad_buffer, mirroring base class logic
|
||||
grad_buffer = self._DeepSpeedZeroOptimizer_Stage3__param_id_to_grad_partition[param.ds_id].narrow(
|
||||
0, 0, grad_partition.numel())
|
||||
if self.micro_step_id == 0:
|
||||
grad_buffer.copy_(grad_partition, non_blocking=True)
|
||||
grad_buffer = grad_buffer.to(grad_partition.device, non_blocking=True)
|
||||
elif get_accelerator().on_accelerator(grad_buffer):
|
||||
grad_buffer.add_(grad_partition.to(self.gradient_accumulation_dtype).view(grad_buffer.shape))
|
||||
else:
|
||||
cuda_grad_buffer = grad_buffer.to(grad_partition.device, non_blocking=True)
|
||||
cuda_grad_buffer.add_(grad_partition.to(self.gradient_accumulation_dtype).view(cuda_grad_buffer.shape))
|
||||
grad_buffer.copy_(cuda_grad_buffer, non_blocking=True)
|
||||
grad_buffer = cuda_grad_buffer
|
||||
|
||||
if self.is_gradient_accumulation_boundary:
|
||||
self.norm_for_param_grads[self.get_param_id(param)] = self._constant_buffered_norm2(grad_buffer)
|
||||
|
||||
fp32_grad_tensor = self.fp32_partitioned_groups_flat[i].grad.narrow(
|
||||
0, dest_offset, grad_buffer.numel())
|
||||
fp32_grad_tensor.copy_(grad_buffer.to(dtype=self.master_weights_and_grads_dtype), non_blocking=True)
|
||||
|
||||
self.sub_group_grad_partition_counts[i] = self.sub_group_grad_partition_counts.get(i, 0) + 1
|
||||
if self.sub_group_grad_partition_counts[i] == self.sub_group_to_param_num[i]:
|
||||
completed_sub_groups.append(i)
|
||||
|
||||
if self.is_gradient_accumulation_boundary and completed_sub_groups:
|
||||
get_accelerator().current_stream().synchronize()
|
||||
for i in completed_sub_groups:
|
||||
if self.subgroup_to_device[i] == 'cpu' and not self.clip_grad:
|
||||
param_group_id = self.sub_group_to_group_id[i]
|
||||
fp32_param = self.fp32_partitioned_groups_flat[i]
|
||||
current_lr = self.optimizer.param_groups[param_group_id]['lr']
|
||||
|
||||
self.superoffload_cpu_optimizer.async_step(param_group_id,
|
||||
i,
|
||||
fp32_param.data,
|
||||
fp32_param.grad.data,
|
||||
lr=current_lr)
|
||||
self.async_cpuadam_num += 1
|
||||
|
||||
result = self.superoffload_cpu_optimizer.get_result()
|
||||
if result is not None:
|
||||
self._reassign_or_swap_out_partitioned_parameters_async(result[TaskKeys.SUB_GROUP_ID],
|
||||
result[ResultKeys.UPDATED_PARAM])
|
||||
self.async_cpuadam_num -= 1
|
||||
|
||||
for param in params_to_release:
|
||||
if not get_accelerator().is_synchronized_device():
|
||||
if param.grad is not None:
|
||||
param.grad.record_stream(get_accelerator().current_stream())
|
||||
param.grad = None
|
||||
|
||||
@instrument_w_nvtx
|
||||
def step(self, closure=None):
|
||||
"""
|
||||
Not supporting closure.
|
||||
"""
|
||||
self._wait_for_async_operations()
|
||||
|
||||
self._pre_step()
|
||||
self._partition_all_parameters()
|
||||
|
||||
if self._overflow_check_and_loss_scale_update():
|
||||
if not self.clip_grad:
|
||||
self._handle_overflow_rollback()
|
||||
return
|
||||
|
||||
norm_groups = self._get_norm_groups()
|
||||
scaled_global_grad_norm = torch.linalg.vector_norm(torch.stack(norm_groups))
|
||||
self._global_grad_norm = scaled_global_grad_norm / self.loss_scale
|
||||
|
||||
timer_names = set()
|
||||
timer_names.add(OPTIMIZER_STEP_TIMER)
|
||||
self.timers(OPTIMIZER_STEP_TIMER).start()
|
||||
|
||||
if self.clip_grad:
|
||||
self._step_with_clipping(scaled_global_grad_norm, timer_names)
|
||||
else:
|
||||
self._step_without_clipping(scaled_global_grad_norm, timer_names)
|
||||
|
||||
self.timers(OPTIMIZER_STEP_TIMER).stop()
|
||||
self._post_step(timer_names)
|
||||
|
||||
def _step_without_clipping(self, scaled_global_grad_norm, timer_names):
|
||||
"""Fast path: async CPU steps already completed during backward."""
|
||||
for sub_group_id, group in enumerate(self.fp16_groups):
|
||||
self._prepare_sub_group(sub_group_id, timer_names)
|
||||
self.unscale_and_clip_grads(sub_group_id, scaled_global_grad_norm)
|
||||
self._optimizer_step(sub_group_id)
|
||||
self._reassign_or_swap_out_partitioned_parameters(sub_group_id)
|
||||
self._release_sub_group(sub_group_id, timer_names)
|
||||
|
||||
def _step_with_clipping(self, scaled_global_grad_norm, timer_names):
|
||||
"""Clipping path: no async steps were done during backward,
|
||||
so we unscale+clip first, then step all sub-groups."""
|
||||
for sub_group_id, group in enumerate(self.fp16_groups):
|
||||
self._prepare_sub_group(sub_group_id, timer_names)
|
||||
self.unscale_and_clip_grads(sub_group_id, scaled_global_grad_norm)
|
||||
|
||||
if self.subgroup_to_device[sub_group_id] == 'cpu':
|
||||
param_group_id = self.sub_group_to_group_id[sub_group_id]
|
||||
fp32_param = self.fp32_partitioned_groups_flat[sub_group_id]
|
||||
current_lr = self.optimizer.param_groups[param_group_id]['lr']
|
||||
self._sync_cpu_optimizer_step(param_group_id,
|
||||
sub_group_id,
|
||||
fp32_param.data,
|
||||
fp32_param.grad.data,
|
||||
lr=current_lr)
|
||||
else:
|
||||
self._optimizer_step(sub_group_id)
|
||||
|
||||
self._reassign_or_swap_out_partitioned_parameters(sub_group_id)
|
||||
self._release_sub_group(sub_group_id, timer_names)
|
||||
|
||||
def _wait_for_async_operations(self, timeout_seconds=60):
|
||||
"""Wait for all pending asynchronous CPU optimizer operations to complete with timeout error.
|
||||
|
||||
Args:
|
||||
timeout_seconds (int): Maximum time to wait before throwing an error. Default is 60 seconds.
|
||||
"""
|
||||
if self.async_cpuadam_num > 0:
|
||||
logger.info(f"[INFO] {self.async_cpuadam_num} asynchronous CPU optimizer operations pending...")
|
||||
if self.async_cpuadam_num == 0:
|
||||
return
|
||||
|
||||
start_time = time.time()
|
||||
initial_pending_ops = self.async_cpuadam_num
|
||||
|
||||
while self.async_cpuadam_num > 0:
|
||||
result = self.superoffload_cpu_optimizer.get_result()
|
||||
if result is None:
|
||||
current_time = time.time()
|
||||
elapsed_time = current_time - start_time
|
||||
|
||||
# Throw error if we've been waiting longer than the timeout
|
||||
if elapsed_time >= timeout_seconds:
|
||||
raise RuntimeError(
|
||||
f"SuperOffload CPU optimizer timeout after {elapsed_time:.1f} seconds. "
|
||||
f"Still waiting for {self.async_cpuadam_num}/{initial_pending_ops} async operations to complete. "
|
||||
f"This indicates a deadlock or critical performance issue in the CPU optimizer.")
|
||||
|
||||
time.sleep(0.001) # 1ms sleep
|
||||
continue
|
||||
|
||||
self._reassign_or_swap_out_partitioned_parameters_async(result[TaskKeys.SUB_GROUP_ID],
|
||||
result[ResultKeys.UPDATED_PARAM])
|
||||
self.async_cpuadam_num -= 1
|
||||
|
||||
def _wait_for_single_async_result(self, event_type: str, timeout_seconds=60):
|
||||
"""Wait for a single asynchronous CPU-Adam optimizer operation with timeout.
|
||||
|
||||
Args:
|
||||
event_type (str): Type of operation expected ('adam_step' or 'rollback').
|
||||
timeout_seconds (int): Maximum time to wait before throwing an error. Default is 60 seconds.
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
while True:
|
||||
result = self.superoffload_cpu_optimizer.get_result(expected_event_type=event_type)
|
||||
if result is not None:
|
||||
self._reassign_or_swap_out_partitioned_parameters_async(result[TaskKeys.SUB_GROUP_ID],
|
||||
result[ResultKeys.UPDATED_PARAM])
|
||||
break
|
||||
|
||||
current_time = time.time()
|
||||
elapsed_time = current_time - start_time
|
||||
|
||||
# Throw error if we've been waiting longer than the timeout
|
||||
if elapsed_time >= timeout_seconds:
|
||||
raise RuntimeError(f"SuperOffload CPU optimizer timeout after {elapsed_time:.1f} seconds. "
|
||||
f"This indicates a deadlock or critical performance issue in the CPU optimizer.")
|
||||
|
||||
time.sleep(0.001) # 1ms sleep
|
||||
|
||||
def _sync_cpu_optimizer_step(self,
|
||||
param_group_id: int,
|
||||
sub_group_id: int,
|
||||
fp32_param_data,
|
||||
fp32_grad_data,
|
||||
rollback: bool = False,
|
||||
lr: float = None,
|
||||
timeout_seconds: int = 60):
|
||||
event_type = EventTypes.ROLLBACK if rollback else EventTypes.ADAM_STEP
|
||||
self.superoffload_cpu_optimizer.async_step(param_group_id,
|
||||
sub_group_id,
|
||||
fp32_param_data,
|
||||
fp32_grad_data,
|
||||
rollback=rollback,
|
||||
lr=lr)
|
||||
# Wait for completion
|
||||
self._wait_for_single_async_result(event_type, timeout_seconds)
|
||||
|
||||
def _handle_overflow_rollback(self):
|
||||
"""Handle gradient overflow by rolling back CPU optimizer states."""
|
||||
for sub_group_id, _ in enumerate(self.fp16_groups):
|
||||
if self.subgroup_to_device[sub_group_id] == 'cpu':
|
||||
param_group_id = self.sub_group_to_group_id[sub_group_id]
|
||||
fp32_param = self.fp32_partitioned_groups_flat[sub_group_id]
|
||||
|
||||
# Trigger rollback
|
||||
self._sync_cpu_optimizer_step(param_group_id,
|
||||
sub_group_id,
|
||||
fp32_param.data,
|
||||
fp32_param.grad.data,
|
||||
rollback=True)
|
||||
|
||||
def _handle_gradient_clipping(self, scaled_global_grad_norm):
|
||||
"""Handle gradient clipping with CPU optimizer rollback and re-optimization."""
|
||||
for sub_group_id, _ in enumerate(self.fp16_groups):
|
||||
if self.subgroup_to_device[sub_group_id] == 'cpu':
|
||||
param_group_id = self.sub_group_to_group_id[sub_group_id]
|
||||
fp32_param = self.fp32_partitioned_groups_flat[sub_group_id]
|
||||
|
||||
# Rollback CPU optimizer states
|
||||
self._sync_cpu_optimizer_step(param_group_id,
|
||||
sub_group_id,
|
||||
fp32_param.data,
|
||||
fp32_param.grad.data,
|
||||
rollback=True)
|
||||
|
||||
# Clip gradients and re-optimize
|
||||
self.unscale_and_clip_grads(sub_group_id, scaled_global_grad_norm)
|
||||
|
||||
current_lr = self.optimizer.param_groups[param_group_id]['lr']
|
||||
self._sync_cpu_optimizer_step(param_group_id,
|
||||
sub_group_id,
|
||||
fp32_param.data,
|
||||
fp32_param.grad.data,
|
||||
rollback=False,
|
||||
lr=current_lr)
|
||||
|
||||
@instrument_w_nvtx
|
||||
def check_clip_grads(self, total_norm):
|
||||
"""Check if gradients need to be clipped based on the global norm."""
|
||||
unscaled_norm = total_norm / self.loss_scale
|
||||
return self.clip_grad and unscaled_norm > self.clip_grad
|
||||
@@ -0,0 +1,297 @@
|
||||
# Copyright (c) DeepSpeed Team.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# DeepSpeed Team
|
||||
"""
|
||||
SuperOffload utilities for 1) running CPU optimizers in separate processes.
|
||||
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional, Any
|
||||
import torch
|
||||
import torch.multiprocessing as mp
|
||||
import psutil
|
||||
|
||||
from deepspeed.ops.adam import DeepSpeedCPUAdam
|
||||
from deepspeed.utils import logger
|
||||
|
||||
|
||||
class TaskKeys:
|
||||
PARAM_DATA = "param_data"
|
||||
PARAM_GRAD = "param_grad"
|
||||
PARAM_GROUP_ID = "param_group_id"
|
||||
SUB_GROUP_ID = "sub_group_id"
|
||||
ROLLBACK = "rollback"
|
||||
LR = "lr"
|
||||
|
||||
|
||||
class ResultKeys:
|
||||
UPDATED_PARAM = "updated_param"
|
||||
EVENT_TYPE = "event_type"
|
||||
|
||||
|
||||
class EventTypes:
|
||||
ADAM_STEP = "adam_step"
|
||||
ROLLBACK = "rollback"
|
||||
|
||||
|
||||
def superoffload_optimizer_worker(param_queue: mp.SimpleQueue, result_queue: mp.SimpleQueue,
|
||||
optimizer_config: Dict[str, Any], max_grad_numel: int) -> None:
|
||||
"""
|
||||
This function runs in a separate process and continuously processes optimization
|
||||
tasks from the parameter queue. It creates a DeepSpeedCPUAdam optimizer and
|
||||
applies optimization steps to parameters received from the main process.
|
||||
|
||||
Args:
|
||||
param_queue: Queue for receiving optimization tasks
|
||||
result_queue: Queue for sending back optimization results
|
||||
optimizer_config: Configuration dictionary for the optimizer containing
|
||||
lr, betas, eps, weight_decay, and amsgrad parameters
|
||||
max_grad_numel: Maximum number of elements expected in gradient tensors
|
||||
"""
|
||||
cpu_tensor = torch.randn(1, device="cpu")
|
||||
cpu_param = torch.nn.Parameter(cpu_tensor)
|
||||
|
||||
try:
|
||||
if isinstance(optimizer_config, list):
|
||||
pg_configs = optimizer_config
|
||||
else:
|
||||
pg_configs = [optimizer_config]
|
||||
|
||||
first_cfg = pg_configs[0]
|
||||
optimizer = DeepSpeedCPUAdam([cpu_param],
|
||||
lr=first_cfg["lr"],
|
||||
betas=first_cfg["betas"],
|
||||
eps=first_cfg["eps"],
|
||||
weight_decay=first_cfg["weight_decay"],
|
||||
amsgrad=first_cfg["amsgrad"])
|
||||
for cfg in pg_configs[1:]:
|
||||
dummy = torch.nn.Parameter(torch.randn(1, device="cpu"))
|
||||
optimizer.add_param_group({
|
||||
"params": [dummy],
|
||||
"lr": cfg["lr"],
|
||||
"betas": cfg["betas"],
|
||||
"eps": cfg["eps"],
|
||||
"weight_decay": cfg["weight_decay"],
|
||||
"amsgrad": cfg["amsgrad"],
|
||||
})
|
||||
except KeyError as e:
|
||||
error_msg = f"Missing required optimizer config key: {e}"
|
||||
logger.error(error_msg)
|
||||
result_queue.put({"error": error_msg})
|
||||
return
|
||||
|
||||
# Pre-allocate reusable pinned memory buffer for gradients
|
||||
pinned_grad_buffer = torch.empty(max_grad_numel, dtype=torch.float32, device='cpu', pin_memory=True)
|
||||
|
||||
while True:
|
||||
try:
|
||||
task = param_queue.get()
|
||||
|
||||
if task is None:
|
||||
logger.debug("Received termination signal, shutting down worker")
|
||||
break
|
||||
|
||||
param_data = task[TaskKeys.PARAM_DATA]
|
||||
param_grad = task[TaskKeys.PARAM_GRAD]
|
||||
param_group_id = task[TaskKeys.PARAM_GROUP_ID]
|
||||
sub_group_id = task[TaskKeys.SUB_GROUP_ID]
|
||||
rollback = task.get(TaskKeys.ROLLBACK, False)
|
||||
task_lr = task.get(TaskKeys.LR, None)
|
||||
|
||||
logger.debug(f"Processing param_group_id: {param_group_id}, sub_group_id: {sub_group_id}")
|
||||
|
||||
del task[TaskKeys.PARAM_DATA]
|
||||
del task[TaskKeys.PARAM_GRAD]
|
||||
task.clear()
|
||||
|
||||
if task_lr is not None:
|
||||
optimizer.param_groups[param_group_id]['lr'] = task_lr
|
||||
|
||||
grad_numel = param_grad.numel()
|
||||
if grad_numel > max_grad_numel:
|
||||
error_msg = (
|
||||
f"Gradient size {grad_numel} exceeds pre-allocated buffer size {max_grad_numel}. "
|
||||
f"This indicates insufficient buffer allocation. Please increase max_grad_numel parameter.")
|
||||
result_queue.put({"error": error_msg})
|
||||
break
|
||||
|
||||
param_grad_cpu = pinned_grad_buffer[:grad_numel].view_as(param_grad)
|
||||
param_grad_cpu.copy_(param_grad, non_blocking=False)
|
||||
|
||||
fp32_param = torch.nn.Parameter(param_data)
|
||||
fp32_param.grad = param_grad_cpu
|
||||
|
||||
optimizer.param_groups[param_group_id]['params'] = [fp32_param]
|
||||
|
||||
if rollback:
|
||||
logger.debug(f"Rolling back optimizer state for sub_group_id: {sub_group_id}")
|
||||
optimizer.rollback_subgroup(sub_group_id)
|
||||
else:
|
||||
optimizer.step_subgroup(sub_group_id)
|
||||
|
||||
# Send result back to main process
|
||||
event_type = EventTypes.ROLLBACK if rollback else EventTypes.ADAM_STEP
|
||||
result_queue.put({
|
||||
TaskKeys.PARAM_GROUP_ID: param_group_id,
|
||||
TaskKeys.SUB_GROUP_ID: sub_group_id,
|
||||
ResultKeys.UPDATED_PARAM: fp32_param.data,
|
||||
ResultKeys.EVENT_TYPE: event_type,
|
||||
})
|
||||
|
||||
# Clean up references to free memory
|
||||
optimizer.param_groups[param_group_id]['params'] = []
|
||||
del param_grad_cpu, fp32_param.grad, fp32_param, param_grad, param_data
|
||||
|
||||
except KeyError as e:
|
||||
error_msg = f"Missing required task key: {e}"
|
||||
logger.error(error_msg)
|
||||
result_queue.put({"error": error_msg})
|
||||
break
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error in worker process: {e}"
|
||||
logger.error(error_msg)
|
||||
result_queue.put({"error": error_msg})
|
||||
break
|
||||
|
||||
# Clean up pinned memory buffer
|
||||
if 'pinned_grad_buffer' in locals():
|
||||
del pinned_grad_buffer
|
||||
logger.debug("Cleaned up pinned memory buffer")
|
||||
|
||||
logger.debug("Worker process terminated")
|
||||
|
||||
|
||||
class SuperOffloadCPUOptimizer:
|
||||
|
||||
def __init__(self,
|
||||
optimizer_config: Dict[str, Any],
|
||||
cpuadam_cores_perc: float = 0.8,
|
||||
max_grad_numel: int = 1000000) -> None:
|
||||
if not 0 < cpuadam_cores_perc <= 1:
|
||||
raise ValueError("cpuadam_cores_perc must be between 0 and 1")
|
||||
|
||||
self.max_grad_numel = max_grad_numel
|
||||
self.mp_context = mp.get_context('spawn')
|
||||
self.param_queue = self.mp_context.SimpleQueue()
|
||||
self.result_queue = self.mp_context.SimpleQueue()
|
||||
|
||||
self.cpuadam_process = self.mp_context.Process(
|
||||
target=superoffload_optimizer_worker,
|
||||
args=(self.param_queue, self.result_queue, optimizer_config, max_grad_numel),
|
||||
daemon=True,
|
||||
)
|
||||
self.cpuadam_process.start()
|
||||
|
||||
# Set CPU affinity for better performance isolation
|
||||
self._set_cpu_affinity(cpuadam_cores_perc)
|
||||
|
||||
def _set_cpu_affinity(self, cpuadam_cores_perc: float) -> None:
|
||||
"""
|
||||
Set CPU affinity for the main (Pytorch) process and worker (CPU Adam) process.
|
||||
|
||||
Args:
|
||||
cpuadam_cores_perc: Percentage of cores to allocate to the worker (CPU Adam) process
|
||||
"""
|
||||
try:
|
||||
current_process = psutil.Process()
|
||||
all_cores = current_process.cpu_affinity()
|
||||
num_cores = len(all_cores)
|
||||
|
||||
split_idx = int((1 - cpuadam_cores_perc) * num_cores)
|
||||
pt_cores = all_cores[:split_idx]
|
||||
cpuadam_cores = all_cores[split_idx:]
|
||||
|
||||
# Set affinity for main process (PyTorch)
|
||||
current_process.cpu_affinity(pt_cores)
|
||||
|
||||
# Set affinity for optimizer process (CPU Adam)
|
||||
optimizer_process = psutil.Process(self.cpuadam_process.pid)
|
||||
optimizer_process.cpu_affinity(cpuadam_cores)
|
||||
|
||||
logger.debug(f"Set CPU affinity - PyTorch cores: {pt_cores}, "
|
||||
f"Optimizer cores: {cpuadam_cores}")
|
||||
|
||||
except (psutil.AccessDenied, psutil.NoSuchProcess, AttributeError) as e:
|
||||
logger.debug(f"Could not set CPU affinities for superoffload optimizer process: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Unexpected error setting CPU affinity: {e}")
|
||||
|
||||
def async_step(self,
|
||||
param_group_id: int,
|
||||
sub_group_id: int,
|
||||
fp32_param: torch.Tensor,
|
||||
fp32_grad: torch.Tensor,
|
||||
rollback: bool = False,
|
||||
lr: float = None) -> None:
|
||||
"""
|
||||
Queue parameter for optimization in the worker process.
|
||||
"""
|
||||
if not self.cpuadam_process.is_alive():
|
||||
raise RuntimeError("Worker process is not alive")
|
||||
|
||||
task = {
|
||||
TaskKeys.PARAM_DATA: fp32_param,
|
||||
TaskKeys.PARAM_GRAD: fp32_grad,
|
||||
TaskKeys.PARAM_GROUP_ID: param_group_id,
|
||||
TaskKeys.SUB_GROUP_ID: sub_group_id,
|
||||
TaskKeys.ROLLBACK: rollback,
|
||||
}
|
||||
if lr is not None:
|
||||
task[TaskKeys.LR] = lr
|
||||
self.param_queue.put(task)
|
||||
|
||||
def get_result(self, expected_event_type: str = None) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get result from worker process with optional event type validation.
|
||||
|
||||
Args:
|
||||
expected_event_type (str, optional): Expected event type ('adam_step' or 'rollback').
|
||||
If provided, validates that the result matches.
|
||||
"""
|
||||
if self.result_queue.empty():
|
||||
return None
|
||||
|
||||
result = self.result_queue.get()
|
||||
|
||||
if "error" in result:
|
||||
raise RuntimeError(f"Error in worker process: {result['error']}")
|
||||
|
||||
# Validate event type if expected_event_type is provided
|
||||
if expected_event_type is not None:
|
||||
result_event_type = result.get(ResultKeys.EVENT_TYPE)
|
||||
if result_event_type != expected_event_type:
|
||||
raise RuntimeError(f"Event type mismatch: expected '{expected_event_type}', got '{result_event_type}'")
|
||||
|
||||
return result
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Shutdown the worker process gracefully.
|
||||
|
||||
Sends termination signal to worker and waits for clean shutdown.
|
||||
If the process doesn't terminate within the timeout, it will be forcefully killed.
|
||||
"""
|
||||
if not self.cpuadam_process.is_alive():
|
||||
logger.debug("Worker process already terminated")
|
||||
return
|
||||
|
||||
# Send termination signal
|
||||
self.param_queue.put(None)
|
||||
|
||||
# Wait for graceful shutdown
|
||||
self.cpuadam_process.join(timeout=5)
|
||||
|
||||
if self.cpuadam_process.is_alive():
|
||||
logger.warning("Optimizer process did not terminate cleanly within timeout, "
|
||||
"forcefully terminating")
|
||||
self.cpuadam_process.terminate()
|
||||
self.cpuadam_process.join(timeout=2)
|
||||
|
||||
# Last resort: kill the process
|
||||
if self.cpuadam_process.is_alive():
|
||||
logger.error("Failed to terminate optimizer process, killing it")
|
||||
self.cpuadam_process.kill()
|
||||
self.cpuadam_process.join()
|
||||
|
||||
logger.debug("SuperOffload CPU optimizer closed successfully")
|
||||
Reference in New Issue
Block a user