-
Notifications
You must be signed in to change notification settings - Fork 341
feat(weight_sync): Raiden weight synchronization with progressive memory reclamation #1990
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,8 @@ | |
| from __future__ import annotations | ||
|
|
||
| import collections | ||
| import gc | ||
| import resource | ||
| import socket | ||
| from typing import Any, List, Optional, Tuple | ||
|
|
||
|
|
@@ -25,6 +27,14 @@ | |
| import jax.numpy as jnp | ||
| from tunix.experimental.weight_sync import weight_sync | ||
|
|
||
|
|
||
| def _log_rss(tag: str) -> None: | ||
| """Logs process peak RSS (GB) -- pinpoints which bind() stage spikes host | ||
| memory, since ru_maxrss is a high-water mark that only grows. | ||
| """ | ||
| rss_gb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1e6 | ||
| logging.info("raiden bind rss checkpoint [%s]: %.1f GB (peak)", tag, rss_gb) | ||
|
|
||
| _ws_lib: Any = None | ||
| try: | ||
| from tpu_sync.api.jax import weight_synchronizer as _ws_lib # pytype: disable=import-error pylint: disable=g-import-not-at-top | ||
|
SurbhiJainUSC marked this conversation as resolved.
|
||
|
|
@@ -53,14 +63,24 @@ def local_ip() -> str: | |
| def to_host_cpu_state(state: Any) -> Any: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you have any test results on this ? like some before/after screenshots to show the improvement.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @khatwanimohit - do you have any results on this? |
||
| """Pulls arrays to client host memory; proxy arrays cannot bind directly.""" | ||
| cpu = jax.local_devices(backend="cpu")[0] | ||
|
|
||
| def pull(leaf): | ||
| leaves, treedef = jax.tree_util.tree_flatten(state) | ||
| del state | ||
| new_leaves = [] | ||
| for i in range(len(leaves)): | ||
| leaf = leaves[i] | ||
| leaves[i] = None | ||
| arr = getattr(leaf, "value", leaf) | ||
| if hasattr(arr, "shape") and hasattr(arr, "dtype"): | ||
| return jax.device_put(jax.device_get(arr), cpu) | ||
| return leaf | ||
|
|
||
| return jax.tree_util.tree_map(pull, state) | ||
| new_leaves.append(jax.device_put(jax.device_get(arr), cpu)) | ||
| else: | ||
| new_leaves.append(leaf) | ||
| del leaf, arr | ||
| # Periodically run GC to release Pathways proxy transit buffers incrementally | ||
| if i % 4 == 3: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what much performance impact this has? every 4 leaves seem like a very aggressive gc schedule.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| gc.collect() | ||
| del leaves | ||
| gc.collect() | ||
| return jax.tree_util.tree_unflatten(treedef, new_leaves) | ||
|
|
||
|
|
||
| def flatten_weights(state: Any) -> Tuple[List[str], List[Any]]: | ||
|
|
@@ -75,22 +95,33 @@ def flatten_weights(state: Any) -> Tuple[List[str], List[Any]]: | |
|
|
||
|
|
||
| def _bindable(arr: Any) -> bool: | ||
| """True if the native layer can bind this leaf.""" | ||
| """True if the native layer can bind this leaf. | ||
|
|
||
| Binding an unsupported leaf (e.g. RNG keys) can SIGSEGV, so only | ||
| floating-point, rank>=1, fully TPU- or CPU-resident arrays qualify. CPU is | ||
| allowed because host_stage deliberately copies proxy-backed (Pathways) | ||
| arrays to host CPU memory before bind() gets here -- rejecting "not TPU" | ||
| would drop every leaf it just staged. | ||
| """ | ||
| try: | ||
| if not hasattr(arr, "shape") or not hasattr(arr, "dtype"): | ||
| return False | ||
| if arr.ndim < 1: | ||
| return False | ||
| if not jnp.issubdtype(arr.dtype, jnp.floating): | ||
| return False | ||
| devices = arr.devices() | ||
| except AttributeError: | ||
| if not devices: | ||
| return False | ||
| return all(getattr(d, "platform", "?") in ("tpu", "cpu") for d in devices) | ||
| except Exception: | ||
| return False | ||
| on_local_hw = all( | ||
| getattr(d, "platform", "?") in ("cpu", "tpu") for d in devices | ||
| ) | ||
| return on_local_hw and jnp.issubdtype(arr.dtype, jnp.number) | ||
|
|
||
|
|
||
| def _filter_bindable( | ||
| names: List[str], arrays: List[Any] | ||
| ) -> Tuple[List[str], List[Any]]: | ||
| """Drops leaves the native layer cannot bind; binding them is undefined | ||
| behavior (observed: random RuntimeError or SIGSEGV on RNG-key arrays).""" | ||
| """Drops leaves _bindable rejects.""" | ||
| logging.vlog( | ||
| 1, | ||
| "raiden bind census: %s", | ||
|
|
@@ -103,6 +134,12 @@ def _filter_bindable( | |
| dropped = [] | ||
| for name, arr in zip(names, arrays): | ||
| if _bindable(arr): | ||
| if hasattr(arr, "block_until_ready"): | ||
| try: | ||
| # binding an in-flight buffer is part of what SIGSEGVs | ||
| arr.block_until_ready() | ||
| except Exception: | ||
| pass | ||
| keep_names.append(name) | ||
| keep_arrays.append(arr) | ||
| else: | ||
|
|
@@ -184,14 +221,18 @@ def active(self) -> bool: | |
| return self._sync is not None | ||
|
|
||
| def bind(self, state: Any) -> None: | ||
| """Binds this host's weights, or rebinds them after a training step. | ||
|
|
||
| With host_stage the arrays are copied to local CPU memory first; arrays | ||
| backed by the pathways proxy cannot bind in place. | ||
| """ | ||
| """Binds or rebinds weights to the Raiden transport.""" | ||
| _log_rss("bind:start") | ||
| # Clear previous buffers before staging to avoid holding duplicate weight | ||
| # copies in host memory during rebinds. | ||
| self.names = [] | ||
| self.arrays = [] | ||
| if self._host_stage: | ||
| state = to_host_cpu_state(state) | ||
| _log_rss("bind:after_host_stage") | ||
| self.names, self.arrays = _filter_bindable(*flatten_weights(state)) | ||
| del state | ||
| _log_rss("bind:after_flatten") | ||
| if _ws_lib is None: | ||
| return | ||
| if self._sync is None: | ||
|
|
@@ -206,8 +247,10 @@ def bind(self, state: Any) -> None: | |
| bind_ip=None, | ||
| auto_h2d=self._auto_h2d, | ||
| ) | ||
| _log_rss("bind:after_native_construct") | ||
| else: | ||
| self._sync.bind_weights(self.arrays) | ||
| _log_rss("bind:after_native_rebind") | ||
|
|
||
| def _require_sync(self, op: str) -> Any: | ||
| if self._sync is None: | ||
|
|
@@ -219,6 +262,11 @@ def d2h(self) -> None: | |
|
|
||
| def h2d(self) -> None: | ||
| self._require_sync("h2d()").h2d() | ||
| jax.block_until_ready(self.arrays) | ||
|
|
||
| def release_host_arrays(self) -> None: | ||
| """Drops host-staged array references to reclaim memory between sync rounds.""" | ||
| self.arrays = [] | ||
|
|
||
| def metrics(self) -> dict: | ||
| return self._sync.get_metrics() if self._sync else {} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -889,8 +889,16 @@ async def record_workers(final_error: str = "") -> None: | |
| " quiesced; no rollback needed" | ||
| ) from e | ||
|
|
||
| src_metadata = [m for per_source in src_meta_lists for m in per_source] | ||
| dst_metadata = [m for per_dest in dst_meta_lists for m in per_dest] | ||
| src_metadata = [ | ||
| weight_sync.dict_to_metadata(m) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are we changing the obj type? how come we need to do explicit dict to metadata conversion now but not before? also is current unit test covering this? |
||
| for per_source in src_meta_lists | ||
| for m in per_source | ||
| ] | ||
| dst_metadata = [ | ||
| weight_sync.dict_to_metadata(m) | ||
| for per_dest in dst_meta_lists | ||
| for m in per_dest | ||
| ] | ||
| if not src_metadata or not dst_metadata: | ||
| failures.append( | ||
| f"metadata: {len(src_metadata)} source, {len(dst_metadata)}" | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.