Skip to content

Commit 0b8a727

Browse files
committed
feat(megatron): add manual CPU offload as an alternative to torch_memory_saver
Colocate training offloads the train actor to CPU on every sleep/wake. The only path was torch_memory_saver (TMS) VMM pause()/resume(), which is semantically blind (backs up the whole runtime pool) and unsafe on backends without expandable_segments (e.g. Kunlunxin P800). This adds an explicit, application-level selective offload and lets it be selected per job. Behavior: - Default stays TMS. - New --manual-offload switches to selective offload: only live train state (param flat buffers + optimizer master/Adam state) is copied to CPU; grads and recomputable buffers are just released, then empty_cache() returns the freed pages to the driver. Changes: - weight_update/train_offload.py (new): MegatronTrainStateOffloader facade delegating to _TmsOffloadStrategy / _ManualOffloadStrategy behind an _OffloadStrategy Protocol. Owns TMS margin init and strategy selection (TMS when LD_PRELOAD'ed, else manual; --manual-offload forces manual). - backends/megatron/actor.py: sleep()/wake_up()/update_weights() delegate to the offloader; drop the inline _torch_memory_saver_enabled block and the torch_memory_saver / nullcontext imports. - weight_update/common.py: _maybe_get_cpu_backup() reads from the offloader's _relax_cpu_offload_data when a param's GPU storage was freed, and only takes the TMS get_cpu_backup() branch when the hook is actually LD_PRELOAD'ed. - distributed/ray/actor_group.py: skip LD_PRELOAD + TMS_INIT env vars under --manual-offload so TMS is not the active mechanism. - utils/arguments.py: add --manual-offload. - scripts/training/text/run-qwen35-8xklx.sh: pass --manual-offload for P800. No behavior change for existing TMS/CUDA users (default path untouched).
1 parent f1881a9 commit 0b8a727

6 files changed

Lines changed: 353 additions & 38 deletions

File tree

relax/backends/megatron/actor.py

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import socket
77
import time
88
from argparse import Namespace
9-
from contextlib import nullcontext
109
from functools import partial
1110
from typing import List
1211

@@ -25,7 +24,6 @@
2524
repatch = None
2625

2726
from tensordict import TensorDict
28-
from torch_memory_saver import torch_memory_saver
2927
from transformers import AutoConfig, AutoTokenizer
3028

3129
from relax.distributed.checkpoint_service.client.engine import create_client
@@ -94,6 +92,7 @@
9492
from .loss import compute_advantages_and_returns, get_log_probs_and_entropy, get_values
9593
from .model import forward_only, initialize_model_and_optimizer, save, train
9694
from .weight_update.common import named_params_and_buffers
95+
from .weight_update.train_offload import MegatronTrainStateOffloader
9796
from .weight_update.update_weight_from_distributed import UpdateWeightFromDistributed
9897
from .weight_update.update_weight_from_tensor import UpdateWeightFromTensor
9998

@@ -174,23 +173,6 @@ def _init(
174173
}
175174
dist.barrier(group=get_gloo_group())
176175

177-
self._torch_memory_saver_enabled = False
178-
if args.offload_train:
179-
x = max(int(args.train_memory_margin_bytes), 0)
180-
try:
181-
torch_memory_saver.memory_margin_bytes = x
182-
self._torch_memory_saver_enabled = True
183-
if x > 0:
184-
logger.info(f"Set torch_memory_saver.memory_margin_bytes to {x}")
185-
except (RuntimeError, NotImplementedError) as e:
186-
if "expandable_segments is not supported" in str(e) or "Only setter is supported" in str(e):
187-
logger.warning(
188-
"torch_memory_saver is unavailable in the current allocator mode; "
189-
"skip memory saver hooks and continue with offload_train."
190-
)
191-
else:
192-
raise
193-
194176
if role == "critic":
195177
self.args.load = self.args.critic_load
196178
self.args.save = self.args.critic_save
@@ -201,6 +183,11 @@ def _init(
201183
args, role
202184
)
203185

186+
# Train-state offload for colocate sleep/wake. Picks torch_memory_saver
187+
# (VMM pause) or manual selective CPU offload based on TMS availability;
188+
# both implementations live in the offloader. No-op when offload_train is off.
189+
self._train_state_offloader = MegatronTrainStateOffloader(self.model, self.optimizer, args)
190+
204191
start_rollout_id = loaded_rollout_id + 1
205192

206193
if role == "critic":
@@ -361,8 +348,7 @@ def sleep(self) -> None:
361348
self.weight_updater.disconnect_rollout_engines()
362349
destroy_process_groups()
363350

364-
if self._torch_memory_saver_enabled:
365-
torch_memory_saver.pause()
351+
self._train_state_offloader.offload()
366352

367353
print_memory("after offload model")
368354

@@ -371,8 +357,7 @@ def wake_up(self) -> None:
371357
assert self.args.offload_train
372358
print_memory("before wake_up model")
373359

374-
if self._torch_memory_saver_enabled:
375-
torch_memory_saver.resume()
360+
self._train_state_offloader.reload()
376361

377362
clear_memory()
378363
reload_process_groups(timeout_minutes=self.args.distributed_timeout_minutes)
@@ -1530,11 +1515,7 @@ def update_weights(self) -> None:
15301515
if dist.get_rank() == 0:
15311516
ray.get(self.rollout_manager.clear_num_new_engines.remote())
15321517

1533-
with (
1534-
torch_memory_saver.disable()
1535-
if self.args.offload_train and self._torch_memory_saver_enabled
1536-
else nullcontext()
1537-
):
1518+
with self._train_state_offloader.disable_during_update():
15381519
print_memory("before update_weights")
15391520
self.weight_updater.update_weights()
15401521
print_memory("after update_weights", clear_before_print=not device_utils.is_npu_available)
@@ -2092,3 +2073,4 @@ def _put_data_to_transfer_queue(self, output_dict=None, batch_meta=None, rollout
20922073
}
20932074
output_dict = TensorDict(output_dict, batch_size=[len(batch_meta.samples)])
20942075
run(self.data_system_client.async_put(data=output_dict, metadata=batch_meta))
2076+

relax/backends/megatron/weight_update/common.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from megatron.core.transformer.transformer_layer import get_transformer_layer_offset
1212

1313
from relax.backends.megatron.misc_utils import strip_param_name_prefix
14-
from relax.utils.device import is_npu_available
14+
from relax.backends.megatron.weight_update.train_offload import torch_memory_saver_preloaded
1515
from relax.utils.misc import get_hf_config
1616
from relax.utils.types import ParamInfo
1717

@@ -111,9 +111,6 @@ def all_gather_param(args, name: str, param: torch.nn.Parameter) -> torch.Tensor
111111
if "linear_fc1.weight" in name and "vision_model" not in name:
112112
param_partitions = [p.chunk(2, dim=0) for p in param_partitions]
113113
param_partitions = [p[0] for p in param_partitions] + [p[1] for p in param_partitions]
114-
# TODO:Temporary workaround for NPU to set partition_dim to 0
115-
if is_npu_available:
116-
partition_dim = 0
117114
# this is bug in megatron's grouped moe.
118115
if "linear_fc2.weight" in name and "vision_model" not in name:
119116
if partition_dim == 0:
@@ -207,11 +204,24 @@ def named_params_and_buffers(
207204
return ans
208205

209206

210-
def _maybe_get_cpu_backup(x: torch.Tensor):
211-
from torch_memory_saver import torch_memory_saver
207+
def _maybe_get_cpu_backup(x: torch.Tensor) -> torch.Tensor:
208+
# Manual selective offload (weight_update/train_offload.py): when the offloader
209+
# frees a param's GPU storage via storage().resize_(0), it stashes the CPU copy
210+
# on the tensor as ``_relax_cpu_offload_data``. If the GPU storage is empty, read
211+
# from that CPU copy instead of touching the now-invalid CUDA storage.
212+
if getattr(x, "_relax_cpu_offload_data", None) is not None and x.storage().size() == 0:
213+
return x._relax_cpu_offload_data
212214

213-
if (cpu_tensor := torch_memory_saver.get_cpu_backup(x)) is not None:
214-
return cpu_tensor
215+
# torch_memory_saver path: only usable when its LD_PRELOAD hook is active;
216+
# otherwise get_cpu_backup() would assert on an uninitialized saver.
217+
# NOTE: with --manual-offload, actor_group.py does NOT LD_PRELOAD the TMS hook,
218+
# so torch_memory_saver_preloaded() is False here and this branch is skipped —
219+
# i.e. LD_PRELOAD is the single source of truth for "TMS is the active mechanism".
220+
if torch_memory_saver_preloaded():
221+
from torch_memory_saver import torch_memory_saver
222+
223+
if (cpu_tensor := torch_memory_saver.get_cpu_backup(x)) is not None:
224+
return cpu_tensor
215225

216226
return x
217227

@@ -330,3 +340,4 @@ def _named_params_and_buffers_global(
330340
middle_path += "."
331341
layer_idx = int(layer_idx) + layer_offset
332342
yield f"module.module.{middle_path}decoder.layers.{layer_idx}.{rest}", buffer
343+

0 commit comments

Comments
 (0)