Skip to content

Add native (DeepNVMe) host-memory pinning backend for accelerators - #8211

Open
sfc-gh-truwase wants to merge 7 commits into
masterfrom
tjruwase/native-pin-memory
Open

Add native (DeepNVMe) host-memory pinning backend for accelerators#8211
sfc-gh-truwase wants to merge 7 commits into
masterfrom
tjruwase/native-pin-memory

Conversation

@sfc-gh-truwase

@sfc-gh-truwase sfc-gh-truwase commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a native host-memory pinning backend, selectable via the DS_PIN_MEMORY_BACKEND environment variable (defaults to torch). When set to native, CPU memory is page-locked through the DeepNVMe async-io (mlock) allocator instead of torch.pin_memory().

Depends on / builds atop #8212 (shared DeepNVMe pinned-tensor manager). Native allocations go through new_cpu_locked_tensor, so I/O handles recognize them via the process-wide manager and skip bounce buffers.

  • New deepspeed/utils/pin_memory.py: a process-wide shared NativePinnedMemory manager that pins CPU memory, tracks pinned pointer ranges (so slices/views report as pinned), tags buffers with .ds_pinned, supports make_copy/match_shape, and frees on unpin. It fails early with a clear error if the async-io op cannot be built (no silent torch fallback). Native pins also use a weakref finalizer so GC releases mlocked pages when tensors are dropped without an explicit unpin.
  • Accelerator owns dispatch: pin_memory drops align_bytes and gains make_copy/match_shape; is_pinned is FakeTensor/meta-tensor safe; new unpin_memory (native frees, torch no-op). Subclasses retain only the device-specific _torch_pin_memory/_torch_is_pinned primitives.
  • Consolidation: XPU's bespoke native-pinning path is folded into the shared backend.
  • Callers: compile paths route through get_accelerator(). Swap-tensor buffers continue to allocate via I/O handles (from Share DeepNVMe pinned-tensor manager and route swap buffers through I/O handles #8212); with the shared manager they interoperate with native-pinned tensors. ZeRO / ZenFlow destroy() explicitly unpins optimizer-owned CPU-offload buffers under the native backend.
  • Docs: Host Memory Pinning section under RTD Memory Usage (docs/code-docs/source/memory.rst); also pushed to rtd-staging.
  • Tests: unit tests for the native manager, accelerator pinning APIs, and destroy-path unpin; accelerator tests moved under tests/unit/v1.

Test plan

  • pre-commit (yapf, flake8, check-license, check-torchdist, codespell) passes on all changed files.
  • Rebased onto master after Share DeepNVMe pinned-tensor manager and route swap buffers through I/O handles #8212 merge; shared-manager overlap resolved by keeping Share DeepNVMe pinned-tensor manager and route swap buffers through I/O handles #8212's swap/I/O paths.
  • Focused pin UTs on a GPU node: tests/unit/v1/pin_memory/test_pin_memory.py + tests/unit/v1/accelerator/test_accelerator.py + tests/unit/v1/nvme/test_pinned_manager.py (incl. GC finalizer coverage) — 21 passed.
  • Destroy-path unpin UTs: tests/unit/v1/pin_memory/test_destroy_unpin.py — 5/5 passed (native ZeRO-2/3 free all optimizer-owned ranges; native ZeRO-3+param-offload decreases ranges; torch backend destroy is a clean no-op).
  • Full tests/unit/v1/ suite on a GPU node — 1134 passed, 63 skipped, exit 0 (~2.5h).
  • Bounce-buffer check: under DS_PIN_MEMORY_BACKEND=native, a buffer from get_accelerator().pin_memory (and a narrow of it) is is_pinned on a different AIO handle than the allocator.
  • RTD staging render of Host Memory Pinning: https://deepspeed.readthedocs.io/en/rtd-staging/memory.html#host-memory-pinning
  • CPU-node sanity check on tunji-cpu-ds-0 (DS_ACCELERATOR=cpu): torch backend 11 passed; native backend 11 passed (test_pin_memory.py, native accelerator APIs, test_pinned_manager.py). XPU/HPU still pending reviewer hardware.

Made with Cursor

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cee4d7771e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

# surface the build/load failure here instead of silently degrading.
try:
from deepspeed.ops.op_builder import AsyncIOBuilder
self._handle = AsyncIOBuilder().load().aio_handle(128 * 1024, 8, False, False, 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Share the native allocation manager with AIO handles

When DS_PIN_MEMORY_BACKEND=native is used for non-GDS DeepNVMe swapping, this constructs a private AIO handle just for allocation, but the actual read/write handles each create their own _pinned_tensor_mgr and cpu_op_desc_t only skips the bounce buffer for tensors managed by that same handle (csrc/aio/py_lib/deepspeed_py_io_handle.cpp:49,253 and csrc/aio/py_lib/deepspeed_cpu_op.cpp:26-27). As a result, buffers returned by get_accelerator().pin_memory(..., make_copy=False) satisfy the Python range check but are not recognized by the handle performing I/O, so every native-pinned swap still gets copied through a bounce buffer; share the pinned manager or allocate through the I/O handle that will use the buffer.

Useful? React with 👍 / 👎.

@sfc-gh-truwase
sfc-gh-truwase marked this pull request as draft August 4, 2026 15:21
@delock

delock commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

I think host memory pinning accouting is needed. I made an attempt in the following PR (in order to justfy pin_memory as default offload option).
https://github.com/deepspeedai/DeepSpeed/pull/8207/changes

Introduce a native host-memory pinning backend selectable via the
DS_PIN_MEMORY_BACKEND env var (defaults to "torch"). When set to "native",
CPU memory is page-locked through the DeepNVMe async-io (mlock) allocator
instead of torch.pin_memory().

- deepspeed/utils/pin_memory.py: new NativePinnedMemory manager (process-wide
  shared singleton) that pins CPU memory, tracks pinned pointer ranges so
  slices/views report as pinned, tags buffers with .ds_pinned, supports
  make_copy/match_shape, and frees on unpin. Construction fails early with a
  clear error if the async-io op cannot be built.
- Accelerator owns the native-vs-torch dispatch. pin_memory drops align_bytes
  and gains make_copy/match_shape; is_pinned is FakeTensor/meta-tensor safe;
  add unpin_memory (native frees, torch is a no-op). Subclasses keep only the
  device-specific _torch_pin_memory/_torch_is_pinned primitives.
- Consolidate XPU's bespoke native pinning path into the shared backend.
- Route swap_tensor and compile callers through the accelerator.
- Add unit tests for the native manager and the accelerator pinning APIs, and
  move the accelerator tests under tests/unit/v1.

Signed-off-by: Olatunji Ruwase <tunji.ruwase@snowflake.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@sfc-gh-truwase
sfc-gh-truwase force-pushed the tjruwase/native-pin-memory branch from cee4d77 to f7934bc Compare August 5, 2026 15:36
@sfc-gh-truwase
sfc-gh-truwase marked this pull request as ready for review August 5, 2026 15:36

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f7934bc8d3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread deepspeed/utils/pin_memory.py Outdated

def pin(self, tensor, make_copy=True, match_shape=True):
numel = tensor.numel()
locked = self._handle.new_cpu_locked_tensor(numel, tensor)[:numel]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Release native pin allocations on tensor GC

When DS_PIN_MEMORY_BACKEND=native is enabled, existing get_accelerator().pin_memory(...) call sites still rely on the old torch.pin_memory() lifetime semantics where dropping the tensor releases the pinned allocation. This allocation goes through the AIO pinned manager and is only freed via the new unpin_memory() path, which those existing call sites do not call, so repeatedly creating/destroying engines or offload buffers in one process can accumulate mlocked memory until process exit and eventually hit the memlock limit. Attach a deleter/finalizer or update the owning call sites to free these native allocations explicitly.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 780f786. NativePinnedMemory.pin() now attaches a weakref.finalize to the returned tensor that frees the page-locked allocation (free_cpu_locked_tensor) when the tensor is garbage-collected — matching torch.pin_memory()'s lifetime, so callers that never call unpin_memory() no longer accumulate mlocked host memory. An explicit unpin() cancels the finalizer (.detach()) to avoid a redundant free. Added test_pin_freed_on_gc (drops the tensor, forces GC, asserts the range is released); the full pin-memory suite is 22/22 on a GPU node.

# None so the accelerator uses its (torch) pinning path. When native is
# selected but the async-io ops cannot be built, constructing the manager
# raises rather than silently falling back.
if os.environ.get("DS_PIN_MEMORY_BACKEND", "torch").strip().lower() != "native":

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document the native pinning backend

This introduces the user-visible DS_PIN_MEMORY_BACKEND=native mode, but the commit does not add any documentation for enabling it, the AIO build requirement, or the new explicit unpin behavior. The project rules require documentation updates for new features, so users otherwise have to infer these operational requirements from code and tests.

AGENTS.md reference: AGENTS.md:L26-L26

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented under RTD Memory Usage as Host Memory Pinning (docs/code-docs/source/memory.rst):

  • DS_PIN_MEMORY_BACKEND=torch|native selection and defaults
  • async-io build / memlock requirements for native
  • accelerator pin_memory / is_pinned / unpin_memory APIs
  • native lifetime (weakref finalizer + explicit unpin in ZeRO/destroy())

Also refreshed the older Pinned Memory accounting notes to the current offload_*.pin_memory config knobs and cross-linked them to this section.

@sfc-gh-truwase

Copy link
Copy Markdown
Collaborator Author

Addressed Codex review (r3713658448) by landing #8212 first, then rebasing this PR onto updated master (f7934bc8).

With the process-wide shared deepspeed_pin_tensor_t manager, buffers allocated via NativePinnedMemory (new_cpu_locked_tensor) are registered in the same manager every I/O handle consults. Verified on a GPU node: a native-pinned buffer (and a narrow of it) is reported is_pinned by a different AIO handle than the allocator, so DeepNVMe will skip the bounce-buffer path.

sfc-gh-truwase and others added 2 commits August 5, 2026 15:56
Native pin_memory() allocations are page-locked through the AIO manager and were
only released by an explicit unpin_memory(), which existing callers relying on
torch.pin_memory()'s GC-based lifetime never call -- so repeatedly creating and
dropping pinned buffers leaked mlocked host memory until process exit.

Attach a weakref.finalize to the returned tensor that frees the allocation when
it is garbage-collected, and cancel that finalizer on explicit unpin() to avoid
a redundant free. Addresses Codex review r3721965881.

Signed-off-by: Olatunji Ruwase <tunji.ruwase@snowflake.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Release optimizer-owned page-locked host buffers when engines tear down
under DS_PIN_MEMORY_BACKEND=native, so mlocked memory does not wait on GC.

Signed-off-by: Olatunji Ruwase <tunji.ruwase@snowflake.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@sfc-gh-truwase

Copy link
Copy Markdown
Collaborator Author

Addressed the native pin lifetime concern with two complementary paths:

  1. weakref.finalize on NativePinnedMemory.pin() (already landed) so GC frees mlocked allocations when tensors are dropped without an explicit unpin.
  2. Explicit accelerator.unpin_memory() in ZeRO/ZenFlow destroy() for optimizer-owned CPU-offload pins (this commit), so teardown does not wait on GC.

Coverage:

  • ZeRO-1/2, ZeRO-3, and ZenFlow overrides unpin their owned offload buffers in destroy().
  • Param-offload partition pins that are rebound to flattened views are left to the finalizer (unpinning those views is ineffective / unsafe).
  • New UTs in tests/unit/v1/pin_memory/test_destroy_unpin.py (5/5 passed on GPU): native ZeRO-2/3 free all optimizer pins; native ZeRO-3+param-offload decreases ranges; torch backend destroy remains a clean no-op.

sfc-gh-truwase and others added 2 commits August 5, 2026 18:36
Document DS_PIN_MEMORY_BACKEND (torch vs native), the async-io build
requirement, and native unpin/lifetime semantics for the new pinning backend.

Signed-off-by: Olatunji Ruwase <tunji.ruwase@snowflake.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Make Host Memory Pinning a peer of Memory Requirements, with Backend
Selection, Requirements for native, and Lifetime and unpinning nested under it.

Signed-off-by: Olatunji Ruwase <tunji.ruwase@snowflake.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@sfc-gh-truwase

Copy link
Copy Markdown
Collaborator Author

@delock , @stas00, @tohtana see this page for docs preview of this feature:
https://deepspeed.readthedocs.io/en/rtd-staging/memory.html#host-memory-pinning

sfc-gh-truwase and others added 2 commits August 6, 2026 15:34
Colocate NativePinnedMemory unit tests with the destroy-path pin_memory
suite under the v1 tree.

Signed-off-by: Olatunji Ruwase <tunji.ruwase@snowflake.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants