Skip to content

vmm support for cuda graph offload - #3543

Closed
Xdydy wants to merge 13 commits into
NVIDIA:release_v2.13from
Xdydy:Xdydy/v2.13_vmm
Closed

Xdydy wants to merge 13 commits into
NVIDIA:release_v2.13from
Xdydy:Xdydy/v2.13_vmm

Conversation

@Xdydy

@Xdydy Xdydy commented Sep 18, 2026

Copy link
Copy Markdown

Description

Add a fixed-VA CUDA VMM activation slot so a Graph-captured tensor address can keep its virtual address while physical backing is released and remapped outside Graph replay. This is the TE primitive behind local whole-layer CUDA Graph + fine-grained CPU activation offload.

Fixes # (issue)

Local CUDA Graph and CPU offload currently conflict on pointer semantics:

Graph replay  requires: captured tensor addresses never change
CPU offload   requires: physical device memory is released after forward

A naive empty() reload allocates a new pointer. Capture recorded src.data_ptr(), so replay either reads stale storage or silently aliases an allocator reuse of that VA. Related: NVIDIA/Megatron-LM#3697.

This PR does not implement Megatron scheduling. It exposes the VMM slot, resident workers, and deferred H2D so a caller (Megatron fine_grained_activation_offload) can:

  1. Reserve a VA and map physical backing before capture.
  2. Capture / replay Graphs against slot.tensor.data_ptr() (stable for the run).
  3. After forward D2H, unmap/release physical pages without freeing the VA.
  4. Before backward, map new backing at the same VA and H2D.
  5. Optionally prefetch remap while the current Graph runs, then launch H2D at replay time.

CUDA driver names stay CUDA-shaped in source. On MUSA they map at compile time through musify.h (cuMemMapmuMemMap, cudaMemcpyAsyncmusaMemcpyAsync, …). Python keeps CUDA names (CUDAActivationVMMAllocation, cuda_stream); a MUSA runtime still works when torch.cuda is aliased, and device.type in ("cuda", "musa") covers TE tests that import this module without musa_patch.

Type of change

  • Documentation change (change only to the documentation, either a fix or a new content)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Infra/Build change
  • Code refactoring

Changes

  • Add transformer_engine/pytorch/csrc/extensions/vmm_activation.cpp and bind it from pybind.cpp / extensions.h.
  • Add Python wrapper transformer_engine/pytorch/vmm_activation.py.
  • Add tests/pytorch/test_vmm_activation.py (async reload / host-callback lifetime).
  • Add musify.h VMM aliases used by this file (cuMemAddressReserve, cuMemCreate, cuMemMap, cuMemSetAccess, cuMemUnmap, cuMemRelease, cuMemGetInfo, …).

Not in this PR’s intended review surface: GEMM / DPA / cpu_offload.py Megatron handler changes that happen to sit on the same working branch.


Why a TE primitive (not a Megatron allocator wrapper)

Graph capture records a device pointer. CPU offload must drop physical pages. Those two facts only compose if the Graph sees a VA that outlives the pages mapped under it:

Naive:  act = torch.empty(...)     # address comes from the caching allocator
VMM:    slot = VMMActivationSlot(bytes, device)
        act  = slot.tensor(shape, stride, dtype)   # act.data_ptr() == reserved VA
cuMemAddressReserve  →  fixed VA (kept for the run)
cuMemCreate          →  physical allocation handle
cuMemMap             →  map handle at that VA
cuMemSetAccess       →  RW access for the device

offload path (after D2H):
  cuMemUnmap + cuMemRelease     →  pages gone, VA still reserved

reload path (before backward):
  cuMemCreate + cuMemMap + cuMemSetAccess at the same VA
  cudaMemcpyAsync(host → VA)

The captured Graph never sees a new pointer. Physical handles may change; the VA must not.

CUDAActivationVMMAllocation constructs the slot, wraps a non-owning from_blob view, and asserts tensor.data_ptr() == info["address"] after construct / remap / adopt. Using that view while the slot is unmapped is a hard page fault — that is intentional; it turns the silent allocator-reuse bug into a crash.


Slot lifecycle

construct
  cuMemAddressReserve + first cuMemCreate/Map/SetAccess
  tensor view at the reserved VA

forward offload (caller D2H, then)
  release_hooks_after(slots, d2h_stream)
    host func on d2h_stream after the D2H burst
    ReleaseWorker: cuMemUnmap + cuMemRelease
    VA reservation kept

backward restore
  remap_only_slot_after(slot, pinned_host, h2d_stream)   # remap now, no H2D
  … later, at Graph replay …
  launch_remap_slot_h2d(context, slot, h2d_stream)       # H2D now
  wait_remap_slot_on_stream(context, slot, compute_stream)
  adopt_async_remap()                                    # slot owns the new handle

close
  drain in-flight remap/release, unmap, release, cuMemAddressFree

Synchronous unmap_and_release() / create_and_remap() remain for tests and fallback. Training uses the async path.

Address stability check after every remap / adopt:

info["address"] == original_va and tensor.data_ptr() == original_va

Python API

transformer_engine.pytorch.vmm_activation:

Call Role
CUDAActivationVMMAllocation(shape, stride, dtype, device) reserve VA, map first backing, non-owning tensor view
release_hooks_after(allocs, stream) one host func after D2H; worker unmaps the batch
remap_hooks_after(allocs, stream) remap only, no copy
remap_and_copy_after / remap_and_copy_slot_after worker remaps and submits H2D
remap_only_slot_after worker remaps; H2D deferred
launch_remap_slot_h2d(ctx, i, stream) wait remapped, submit H2D on stream
wait_remap_slot_on_stream(ctx, i, stream) wait event_recorded, stream waits on slot event
wait_until_remap_slot_submitted host wait until that slot’s H2D event is recorded
wait_remap_copy_on_stream / enqueue_remap_copy_wait whole-context copy event (non-deferred path)
vmm_driver_memory_info() cuMemGetInfo free/total (caching-allocator stats do not cover cuMemCreate pages)
vmm_enable_trace / vmm_initialize_workers / vmm_set_serial_driver_workers diagnostics and worker policy

Stream handles: stream.cuda_stream, with musa_stream fallback.

Safety checks on remap submit: unique slots, reserved VA, no in-flight remap, mapped slot must already have a pending release, host tensor pinned / offset 0 / large enough. Adopted mappings are never unmapped by context cleanup.

Debug: MEGATRON_VMM_REMAP_DEBUG=1 prints per-call remap/release stages (cuMemUnmap, cuMemSetAccess, remap wait, H2D launch wait).


Files

transformer_engine/pytorch/csrc/extensions/vmm_activation.cpp
transformer_engine/pytorch/csrc/extensions.h
transformer_engine/pytorch/csrc/extensions/pybind.cpp
transformer_engine/pytorch/vmm_activation.py
transformer_engine/common/include/transformer_engine/musify.h   # VMM aliases
tests/pytorch/test_vmm_activation.py

Checklist:

  • I have read and followed the contributing guidelines
  • The functionality is complete
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Suggested coverage:

  • Address stability: data_ptr() unchanged across unmap/remap and deferred H2D.
  • release_hooks_after + remap_and_copy_after repeated reload (test_repeated_async_reload_releases_context_off_host_callback): no host-callback deadlock, values match, context error==0.
  • Deferred path: remap_only_slot_after does not submit H2D; launch_remap_slot_h2d does; compute stream waits on the slot event.
  • Negative: using the tensor view after unmap and before remap must fault, not silently alias a caching-allocator reuse of the captured VA.
  • cuMemGetInfo (not PyTorch memory_allocated) when claiming physical pages returned.

@github-actions github-actions Bot added the community-contribution PRs from external contributor outside the core maintainers, representing community-driven work. label Sep 18, 2026
@greptile-apps

greptile-apps Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 0/5

This PR is not safe to merge because it breaks supported CUDA/core/JAX builds and imports, removes still-used native APIs, forces unsupported attention paths, and has a multi-device VMM synchronization defect.

Findings

  1. P1 MUSA Replaces Supported Builds
  2. P1 Root Import Requires MUSA
  3. P1 Import Globally Rewrites PyTorch
  4. P1 Active Extension Bindings Removed
  5. P1 FlashAttention Is Always Forced
  6. P1 FlashAttention Detection Is Fabricated
  7. P1 Events Use Ambient Device

Summary

This PR adds a fixed-address VMM activation allocation with asynchronous release, remapping, deferred H2D copies, Python wrappers, and one repeated-reload test. It also contains a repository-wide MUSA port and unrelated attention, offload, GEMM, permutation, build, and package-initialization changes that materially alter existing CUDA behavior.

  • Adds resident release/remap workers and fixed-VA activation slots.
  • Adds Python APIs for synchronous and asynchronous remap/reload sequencing.
  • Replaces the common CUDA build and runtime initialization with unconditional MUSA behavior.
  • Removes active attention and KV-cache extension bindings and overrides backend selection.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  C[Captured graph uses fixed VA] --> D[D2H activation copy]
  D --> R[Release host callback]
  R --> RW[Release worker]
  RW --> U[Unmap and release physical pages]
  U --> MW[Remap worker]
  MW --> M[Map new pages at same VA]
  M --> H[Deferred or immediate H2D]
  H --> E[Record completion event]
  E --> W[Compute stream waits]
  W --> G[Graph replay reads restored activation]
Loading

Reviews (1) · Last reviewed commit: "[pre-commit.ci] auto fixes from pre-comm..."

set(CMAKE_CUDA_ARCHITECTURES 70 80 89 90)
endif()
endif()
find_package(MUSA REQUIRED)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 MUSA Replaces Supported Builds

The common build now always requires MUSA, MUSAToolkit, and MCCL from a hard-coded /usr/local/musa installation. Because setup.py adds this common extension for core-only, CUDA PyTorch, and JAX installations, supported non-MUSA builds fail during CMake configuration instead of using the existing CUDA build path.

Knowledge Base Used: Build, extensions, and packaging

Comment on lines +12 to +15
import torch
import torch.utils
import torch.utils.data
import torch_musa

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Root Import Requires MUSA

Root initialization now imports torch_musa before the existing optional-framework error handling. In core-only, JAX-only, or ordinary CUDA installations without torch_musa, import transformer_engine therefore fails even though MUSA is not a common runtime dependency.

Knowledge Base Used:

Comment on lines +49 to +72
torch.cuda.is_available = torch.musa.is_available
torch.cuda.current_device = torch.musa.current_device
torch.cuda.device_count = torch.musa.device_count
torch.cuda.set_device = torch.musa.set_device
torch.cuda.DoubleTensor = torch.musa.DoubleTensor
torch.cuda.FloatTensor = torch.musa.FloatTensor
torch.cuda.LongTensor = torch.musa.LongTensor
torch.cuda.HalfTensor = torch.musa.HalfTensor
torch.cuda.BFloat16Tensor = torch.musa.BFloat16Tensor
torch.cuda.IntTensor = torch.musa.IntTensor
torch.cuda.synchronize = torch.musa.synchronize
torch.cuda.get_rng_state = torch.musa.get_rng_state
torch.cuda.set_rng_state = torch.musa.set_rng_state
torch.cuda.synchronize = torch.musa.synchronize
torch.cuda.empty_cache = torch.musa.empty_cache
torch.Tensor.cuda = torch.Tensor.musa
torch.cuda.manual_seed = torch.musa.manual_seed
torch.cuda.Event = torch.musa.Event
torch.cuda.Stream = torch.musa.Stream
torch.cuda.current_stream = torch.musa.current_stream
torch.cuda.set_stream = torch.musa.set_stream
torch.cuda.get_device_properties = torch.musa.get_device_properties
# add torch.musa.current_devce() to activate torch.musa.default_generators
d = torch.musa.current_device()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Import Globally Rewrites PyTorch

Importing Transformer Engine now replaces process-wide PyTorch CUDA, tensor-construction, distributed, device, and autocast APIs with MUSA implementations and immediately initializes a MUSA device. As a result, unrelated PyTorch code in the same process receives altered CUDA behavior merely because the root package was imported.

Knowledge Base Used: PyTorch runtime and public API

Comment on lines +310 to +324
// m.def("fa_prepare_fwd", &transformer_engine::pytorch::fa_prepare_fwd,
// "Prepare QKV for Flash Attention", py::call_guard<py::gil_scoped_release>());
// m.def("fa_prepare_bwd", &transformer_engine::pytorch::fa_prepare_bwd,
// "Backward of QKV preparation for Flash Attention",
// py::call_guard<py::gil_scoped_release>());
m.def("fused_attn_fwd", &transformer_engine::pytorch::fused_attn_fwd,
"Fused Attention FP8/BF16/FP16 FWD with separate Q, K and V");
m.def("fused_attn_bwd", &transformer_engine::pytorch::fused_attn_bwd,
"Fused Attention FP8/BF16/FP16 BWD with separate Q, K and V");
m.def("copy_to_kv_cache", &transformer_engine::pytorch::copy_to_kv_cache,
"Copy new KV tokens to KV cache", py::call_guard<py::gil_scoped_release>());
m.def("convert_thd_to_bshd", &transformer_engine::pytorch::convert_thd_to_bshd,
"Convert a tensor from THD to BSHD", py::call_guard<py::gil_scoped_release>());
m.def("convert_bshd_to_thd", &transformer_engine::pytorch::convert_bshd_to_thd,
"Convert a tesnor from BSHD to THD", py::call_guard<py::gil_scoped_release>());
// m.def("copy_to_kv_cache", &transformer_engine::pytorch::copy_to_kv_cache,
// "Copy new KV tokens to KV cache", py::call_guard<py::gil_scoped_release>());
// m.def("convert_thd_to_bshd", &transformer_engine::pytorch::convert_thd_to_bshd,
// "Convert a tensor from THD to BSHD", py::call_guard<py::gil_scoped_release>());
// m.def("convert_bshd_to_thd", &transformer_engine::pytorch::convert_bshd_to_thd,
// "Convert a tesnor from BSHD to THD", py::call_guard<py::gil_scoped_release>());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Active Extension Bindings Removed

These bindings are removed while active Python paths still call them. KV-cache inference calls tex.copy_to_kv_cache, THD attention calls the conversion functions, and interleaved FlashAttention calls the preparation functions. Those paths now raise AttributeError instead of running the required native operation.

Knowledge Base Used: PyTorch runtime and public API

fused_attention_backend = _attention_backends["fused_attention_backend"]
use_unfused_attention = _attention_backends["use_unfused_attention"]

use_flash_attention = True # TODO:huang.huang set fa manually now!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 FlashAttention Is Always Forced

Setting use_flash_attention after backend selection discards all capability and configuration checks. When FlashAttention is missing, disabled, unsupported for the inputs, or intentionally superseded by fused or unfused attention, execution still enters self.flash_attention instead of the selected fallback, causing unsupported execution or a runtime failure.

Knowledge Base Used: PyTorch runtime and public API

Comment on lines 92 to 101
@@ -100,7 +101,7 @@
fa_utils.is_installed = True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 FlashAttention Detection Is Fabricated

Hard-coding the detected FlashAttention version to 2.5.0 can mark it installed even when the package is absent or a different version is present. On a compatible device, initialization then imports flash_attn.flash_attn_interface unconditionally, so an environment that should fall back cleanly can fail during import or call APIs incompatible with its actual installation.

Knowledge Base Used: PyTorch runtime and public API

Comment on lines +1400 to +1405
check_cuda_runtime(cudaEventCreateWithFlags(&context->copy_done_event, cudaEventDisableTiming),
"cudaEventCreateWithFlags(remap-and-copy)");
context->slot_done_events.resize(slots.size(), nullptr);
for (auto &event : context->slot_done_events) {
check_cuda_runtime(cudaEventCreateWithFlags(&event, cudaEventDisableTiming),
"cudaEventCreateWithFlags(remap-slot)");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Events Use Ambient Device

In a multi-device process where the current device, slot device, or stream device differ, these completion events are created on the calling thread's ambient device before slot devices are examined. The worker later switches to each slot's device and records the events on the supplied stream, causing cross-context event recording or waiting to fail and leaving the activation reload unsynchronized. Events must be created for the slot device, and mixed-device batches or mismatched streams should be rejected.

@Xdydy
Xdydy marked this pull request as draft September 18, 2026 06:59
@Xdydy Xdydy closed this Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-contribution PRs from external contributor outside the core maintainers, representing community-driven work.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants