-
Notifications
You must be signed in to change notification settings - Fork 4.9k
fix(zero3): async grad offload + pinned offload buffers by default #8207
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 6 commits
d6f43c5
d537ae3
d4bc2fc
12bdbc5
fbb0277
0b8426f
ef6a75d
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 |
|---|---|---|
|
|
@@ -42,10 +42,13 @@ class DeepSpeedZeroOffloadParamConfig(DeepSpeedConfigModel): | |
| NVMe is enabled. | ||
| """ | ||
|
|
||
| pin_memory: bool = False | ||
| pin_memory: bool = True | ||
| """ | ||
| Offload to page-locked CPU memory. This could boost throughput at the cost | ||
| of extra memory overhead. | ||
| Offload to page-locked (pinned) CPU memory. Required for asynchronous, | ||
| full-bandwidth GPU<->CPU transfers and for overlap of grad/param offload | ||
| with compute. Defaults to True. Disable only on hosts with tight memlock | ||
| limits (ulimit -l) or very limited resident RAM, since pinned memory | ||
| cannot be paged out. | ||
| """ | ||
|
|
||
|
|
||
|
|
@@ -69,10 +72,13 @@ class DeepSpeedZeroOffloadOptimizerConfig(DeepSpeedConfigModel): | |
| gradient, momentum, and variance). | ||
| """ | ||
|
|
||
| pin_memory: bool = False | ||
| pin_memory: bool = True | ||
|
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.
When a config still uses the supported deprecated form Useful? React with 👍 / 👎. |
||
| """ | ||
| Offload to page-locked CPU memory. This could boost throughput at the cost | ||
| of extra memory overhead. | ||
| Offload to page-locked (pinned) CPU memory. Required for asynchronous, | ||
| full-bandwidth GPU<->CPU transfers and for overlap of grad/param offload | ||
| with compute. Defaults to True. Disable only on hosts with tight memlock | ||
| limits (ulimit -l) or very limited resident RAM, since pinned memory | ||
| cannot be paged out. | ||
| """ | ||
|
|
||
| pipeline_read: bool = False | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # DeepSpeed Team | ||
|
|
||
| from deepspeed.utils.logging import logger | ||
|
|
||
| _GB = 1024**3 | ||
| # Emit an INFO checkpoint each time cumulative pinned memory reaches the next | ||
| # power-of-two multiple of this base. Doubling keeps the number of checkpoints | ||
| # logarithmic in the total, so large offload paths (e.g. per-parameter shards | ||
| # pinned at init) produce a few readable milestones instead of flooding the log. | ||
| _CHECKPOINT_BASE_GB = 32 | ||
|
|
||
|
|
||
| def _fmt_bytes(num_bytes: int) -> str: | ||
| kb = 1024 | ||
| mb = kb * 1024 | ||
| gb = mb * 1024 | ||
| if num_bytes >= gb: | ||
| return f"{num_bytes / gb:.3f} GB" | ||
| if num_bytes >= mb: | ||
| return f"{num_bytes / mb:.2f} MB" | ||
| if num_bytes >= kb: | ||
| return f"{num_bytes / kb:.1f} KB" | ||
| return f"{num_bytes} B" | ||
|
|
||
|
|
||
| class _PinnedMemoryTracker: | ||
| """Process-wide total of host memory pinned through the accelerator's | ||
| ``pin_memory``. Pinned memory is page-locked: it cannot be swapped out and | ||
| counts against the host memlock limit (``ulimit -l``). The running total is | ||
| a useful hint when diagnosing host out-of-memory errors, which often surface | ||
| far from the call site that consumed the resident-RAM budget. | ||
| """ | ||
|
|
||
| def __init__(self): | ||
| self.reset() | ||
|
|
||
| def reset(self) -> None: | ||
| self._bytes = 0 | ||
| self._calls = 0 | ||
| self._next_checkpoint = _CHECKPOINT_BASE_GB * _GB | ||
|
|
||
| def track(self, num_bytes: int) -> None: | ||
| self._bytes += num_bytes | ||
| self._calls += 1 | ||
| logger.debug(f"pin_memory: +{_fmt_bytes(num_bytes)} " | ||
| f"(call #{self._calls}, running total: {_fmt_bytes(self._bytes)})") | ||
| while self._bytes >= self._next_checkpoint: | ||
| msg = (f"[pinned-memory checkpoint] crossed {_fmt_bytes(self._next_checkpoint)}: " | ||
| f"{_fmt_bytes(self._bytes)} pinned across {self._calls} allocations") | ||
| logger.info(msg) | ||
| self._next_checkpoint *= 2 | ||
|
|
||
| def log_summary(self, tag: str = "") -> None: | ||
| prefix = f"[pinned-memory {tag}] " if tag else "[pinned-memory] " | ||
| logger.info(f"{prefix}{_fmt_bytes(self._bytes)} pinned across {self._calls} allocations") | ||
|
|
||
|
|
||
| _tracker = _PinnedMemoryTracker() | ||
|
|
||
|
|
||
| def track_pinned_memory(num_bytes: int) -> None: | ||
| _tracker.track(num_bytes) | ||
|
|
||
|
|
||
| def pinned_memory_summary(tag: str = "") -> None: | ||
| _tracker.log_summary(tag) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # DeepSpeed Team | ||
|
|
||
| import logging | ||
|
|
||
| import torch | ||
|
|
||
| from deepspeed.utils.pin_memory_tracker import ( | ||
| _fmt_bytes, | ||
| _tracker, | ||
| pinned_memory_summary, | ||
| track_pinned_memory, | ||
| ) | ||
|
|
||
|
|
||
| def test_track_accumulates_and_resets(): | ||
| _tracker.reset() | ||
| track_pinned_memory(100) | ||
| track_pinned_memory(2**30) | ||
| assert _tracker._bytes == 100 + 2**30 | ||
| assert _tracker._calls == 2 | ||
| _tracker.reset() | ||
| assert _tracker._bytes == 0 and _tracker._calls == 0 | ||
|
|
||
|
|
||
| def test_summary_does_not_raise(): | ||
| _tracker.reset() | ||
| track_pinned_memory(2**30) | ||
| pinned_memory_summary("unit-test") | ||
| _tracker.reset() | ||
|
|
||
|
|
||
| def test_fmt_bytes(): | ||
| assert _fmt_bytes(512) == "512 B" | ||
| assert _fmt_bytes(2048) == "2.0 KB" | ||
| assert _fmt_bytes(2**20) == "1.00 MB" | ||
| assert _fmt_bytes(2**30).endswith("GB") | ||
|
|
||
|
|
||
| def test_torch_tensor_nbytes_is_consistent(): | ||
| t = torch.zeros(1024, dtype=torch.float32) | ||
| track_pinned_memory(t.nbytes) | ||
| assert _tracker._bytes == 4096 | ||
| _tracker.reset() | ||
|
|
||
|
|
||
| def test_checkpoint_thresholds_double_from_32gb(): | ||
| _tracker.reset() | ||
| gb = 1024**3 | ||
| assert _tracker._next_checkpoint == 32 * gb | ||
| track_pinned_memory(30 * gb) # below 32 GB -> no crossing | ||
| assert _tracker._next_checkpoint == 32 * gb | ||
| track_pinned_memory(10 * gb) # 40 GB -> crosses 32 | ||
| assert _tracker._next_checkpoint == 64 * gb | ||
| track_pinned_memory(100 * gb) # 140 GB -> crosses 64 and 128 in one call | ||
| assert _tracker._next_checkpoint == 256 * gb | ||
| _tracker.reset() | ||
|
|
||
|
|
||
| def test_checkpoint_emits_info(caplog): | ||
| # The DeepSpeed logger does not propagate, so flip propagation so caplog | ||
| # (root-based) can observe the checkpoint INFO records. | ||
| _tracker.reset() | ||
| ds_logger = logging.getLogger("DeepSpeed") | ||
| old_prop = ds_logger.propagate | ||
| ds_logger.propagate = True | ||
| try: | ||
| caplog.clear() | ||
| with caplog.at_level(logging.INFO, logger="DeepSpeed"): | ||
| track_pinned_memory(33 * (1024**3)) # crosses the 32 GB checkpoint | ||
| track_pinned_memory(5 * (1024**3)) # 38 GB, no new checkpoint | ||
| checkpoints = [r.message for r in caplog.records if "checkpoint" in r.message] | ||
| assert len(checkpoints) == 1 | ||
| assert "32" in checkpoints[0] | ||
| finally: | ||
| ds_logger.propagate = old_prop | ||
| _tracker.reset() |
Uh oh!
There was an error while loading. Please reload this page.