Skip to content

[TransferEngine] Add NVLink VMM Fabric memory lifecycle#2966

Draft
neverhook wants to merge 2 commits into
kvcache-ai:mainfrom
neverhook:codex/nvlink-vmm-fabric-lifecycle
Draft

[TransferEngine] Add NVLink VMM Fabric memory lifecycle#2966
neverhook wants to merge 2 commits into
kvcache-ai:mainfrom
neverhook:codex/nvlink-vmm-fabric-lifecycle

Conversation

@neverhook

@neverhook neverhook commented Jul 16, 2026

Copy link
Copy Markdown

Description

Adds the Transfer Engine primitives required by RFC #2914 to own, publish,
import, and clean up CUDA VMM allocations over the existing NVLink transport.
This supersedes the withdrawn #2934 with a narrower review boundary.

  • Introduces NvlinkVmmAllocation for DEVICE and CUDA HOST_NUMA allocation
    locations, including Fabric capability checks, alignment, exact range
    provenance, and reverse-order ownership cleanup.
  • Publishes Provider Fabric descriptors while retaining the CUDA allocation
    handle. Ambiguous commit-before-error outcomes are compensated inside
    NvlinkTransport; no generic TransferMetadata transaction model is added.
  • Imports Consumer Fabric handles lazily and keeps failed map/unmap/release
    ownership quarantined until cleanup succeeds or process exit.
  • Handles an NVLink failure before the first Slice with an NVLink-local
    terminal sentinel. The shared TransferTask/completion model is unchanged.
  • Exposes NVLink Fabric support as a capability query. Store policy such as
    requiring an exact transport set is intentionally left to the Store PR.
  • Preserves the existing Transfer Engine wire schema.

Related RFC: #2914

Explicitly excluded from this PR

  • Separate prerequisite PRs for historical metadata/completion behavior.
  • Generic TransferMetadata publication transactions or new shared
    TransferTask state.
  • NVLink result/consumer metrics and any new metrics aggregation API.
  • finalizeTransferResult, repeated-result observation fields, and periodic
    event-driven polling state.
  • RDMA registration smoke tests or dual-protocol policy.
  • Rewrites of the existing batch registration transaction.
  • Persistent production *_for_testing callbacks.
  • Store pool integration; that will be submitted separately after this
    Transfer Engine contract lands.

Module

  • Transfer Engine (mooncake-transfer-engine)
  • CI/CD

Type of Change

  • New feature
  • Bug fix
  • Refactor

How Has This Been Tested?

Local checks:

git diff --check origin/main

Results:

  • Changed C/C++ files pass the repository clang-format 20 check.
  • GitHub Actions YAML, documentation, and spelling checks pass.
  • ARM64 CUDA/MNNVL targeted build and nvlink_vmm_unit tests pass in
    both Python 3.10 and 3.12 ARM64 jobs.
  • The complete Linux Build & Test matrix passes on the unchanged PR head,
    including all four Ubuntu/Python wheel integration combinations.
  • Current-revision GB200/NVL72 hardware rerun is pending.

The ARM64 job builds only:

nvlink_transport_test
nvlink_vmm_allocation_test

and requires CTest to discover at least one nvlink_vmm_unit test before
running the label.

The earlier development branch used by #2934 completed a dual-node GB200
Provider/Consumer data-plane run, but that evidence is not claimed as a
current-revision result after this scope reduction.

Checklist

AI Assistance Disclosure

  • AI tools were used.

Codex assisted with replaying the latest main, removing unrelated changes,
implementing the narrowed lifecycle semantics, and preparing tests and this
draft PR. The human submitter remains responsible for reviewing,
understanding, and defending every changed line.

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces NVLink Virtual Memory Management (VMM) allocation support and Fabric memory transport capabilities to the Mooncake transfer engine, refactoring memory lifecycle management in NvlinkTransport and adding comprehensive tests. The review feedback highlights critical safety and performance improvements, including addressing potential null pointer dereferences when retrieving the local segment descriptor in TransferMetadata, and optimizing driver overhead by batching cuMemSetAccess calls rather than executing them in a loop for each device.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 1267 to 1271
RWSpinlock::WriteGuard guard(segment_lock_);
auto new_segment_desc = std::make_shared<SegmentDesc>();
auto &segment_desc = segment_id_to_desc_map_[LOCAL_SEGMENT_ID];
auto& segment_desc = segment_id_to_desc_map_[LOCAL_SEGMENT_ID];
old_segment_desc = segment_desc;
new_segment_desc = std::make_shared<SegmentDesc>();
*new_segment_desc = *segment_desc;

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.

high

There is a risk of a null pointer dereference if segment_id_to_desc_map_[LOCAL_SEGMENT_ID] is not initialized or is null. We should add a defensive null check before dereferencing segment_desc.

        RWSpinlock::WriteGuard guard(segment_lock_);
        auto& segment_desc = segment_id_to_desc_map_[LOCAL_SEGMENT_ID];
        if (!segment_desc) {
            return ERR_METADATA;
        }
        old_segment_desc = segment_desc;
        new_segment_desc = std::make_shared<SegmentDesc>();
        *new_segment_desc = *segment_desc;

Comment on lines 1300 to 1304
RWSpinlock::WriteGuard guard(segment_lock_);
auto new_segment_desc = std::make_shared<SegmentDesc>();
auto &segment_desc = segment_id_to_desc_map_[LOCAL_SEGMENT_ID];
auto& segment_desc = segment_id_to_desc_map_[LOCAL_SEGMENT_ID];
old_segment_desc = segment_desc;
new_segment_desc = std::make_shared<SegmentDesc>();
*new_segment_desc = *segment_desc;

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.

high

There is a risk of a null pointer dereference if segment_id_to_desc_map_[LOCAL_SEGMENT_ID] is not initialized or is null. We should add a defensive null check before dereferencing segment_desc.

        RWSpinlock::WriteGuard guard(segment_lock_);
        auto& segment_desc = segment_id_to_desc_map_[LOCAL_SEGMENT_ID];
        if (!segment_desc) {
            return ERR_METADATA;
        }
        old_segment_desc = segment_desc;
        new_segment_desc = std::make_shared<SegmentDesc>();
        *new_segment_desc = *segment_desc;

Comment on lines +945 to +977
int device_count = 0;
result = fabric_driver_api_.device_get_count(&device_count);
if (result != CUDA_SUCCESS || device_count <= 0) {
LOG(ERROR) << "NvlinkTransport: cuDeviceGetCount "
"failed during lazy import: "
<< result;
return ERR_CONTEXT;
}

for (int ordinal = 0; ordinal < device_count; ++ordinal) {
CUdevice device;
result =
fabric_driver_api_.device_get(&device, ordinal);
if (result != CUDA_SUCCESS) {
LOG(ERROR) << "NvlinkTransport: cuDeviceGet failed "
"during lazy import: "
<< result;
return ERR_CONTEXT;
}
CUmemAccessDesc access = {};
access.location.type = CU_MEM_LOCATION_TYPE_DEVICE;
access.location.id = device;
access.flags = CU_MEM_ACCESS_FLAGS_PROT_READWRITE;
result = fabric_driver_api_.mem_set_access(
attempt.address(), entry.length, &access, 1);
if (result != CUDA_SUCCESS) {
LOG(ERROR)
<< "NvlinkTransport: cuMemSetAccess failed for "
"visible device "
<< ordinal << ": " << result;
return ERR_MEMORY;
}
}

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.

medium

Calling cuMemSetAccess individually for each device in a loop can introduce significant driver overhead and performance bottlenecks, especially on multi-GPU systems. Batching all access configurations into a single cuMemSetAccess call is much more efficient.

                    int device_count = 0;
                    result = fabric_driver_api_.device_get_count(&device_count);
                    if (result != CUDA_SUCCESS || device_count <= 0) {
                        LOG(ERROR) << "NvlinkTransport: cuDeviceGetCount "
                                      "failed during lazy import: "
                                   << result;
                        return ERR_CONTEXT;
                    }

                    std::vector<CUmemAccessDesc> access_descs;
                    access_descs.reserve(device_count);
                    for (int ordinal = 0; ordinal < device_count; ++ordinal) {
                        CUdevice device;
                        result =
                            fabric_driver_api_.device_get(&device, ordinal);
                        if (result != CUDA_SUCCESS) {
                            LOG(ERROR) << "NvlinkTransport: cuDeviceGet failed "
                                          "during lazy import: "
                                   << result;
                            return ERR_CONTEXT;
                        }
                        CUmemAccessDesc access = {};
                        access.location.type = CU_MEM_LOCATION_TYPE_DEVICE;
                        access.location.id = device;
                        access.flags = CU_MEM_ACCESS_FLAGS_PROT_READWRITE;
                        access_descs.push_back(access);
                    }

                    result = fabric_driver_api_.mem_set_access(
                        attempt.address(), entry.length, access_descs.data(), access_descs.size());
                    if (result != CUDA_SUCCESS) {
                        LOG(ERROR)
                            << "NvlinkTransport: cuMemSetAccess failed: " << result;
                        return ERR_MEMORY;
                    }

Comment on lines +636 to +678
auto grant_access = [&](CUmemLocationType location_type,
int location_id) -> Status {
CUmemAccessDesc access = {};
access.location.type = location_type;
access.location.id = location_id;
access.flags = CU_MEM_ACCESS_FLAGS_PROT_READWRITE;
CUresult access_result = api.mem_set_access(ptr, length, &access, 1);
if (access_result != CUDA_SUCCESS)
return cudaDriverFailure("cuMemSetAccess", access_result);
return Status::OK();
};

if (options.location_type == LocationType::HOST_NUMA) {
status =
grant_access(CU_MEM_LOCATION_TYPE_HOST_NUMA, options.location_id);
if (!status.ok()) {
return rollback(status);
}
}

int device_count = 0;
result = api.device_get_count(&device_count);
if (result != CUDA_SUCCESS) {
status = cudaDriverFailure("cuDeviceGetCount", result);
return rollback(status);
}
if (device_count <= 0) {
return rollback(Status::NotSupportedTransport(
"VMM allocation requires at least one visible CUDA device"));
}

for (int ordinal = 0; ordinal < device_count; ++ordinal) {
CUdevice device;
result = api.device_get(&device, ordinal);
if (result != CUDA_SUCCESS) {
status = cudaDriverFailure("cuDeviceGet", result);
return rollback(status);
}
status = grant_access(CU_MEM_LOCATION_TYPE_DEVICE, device);
if (!status.ok()) {
return rollback(status);
}
}

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.

medium

Calling cuMemSetAccess individually for each device in a loop can introduce significant driver overhead and performance bottlenecks, especially on multi-GPU systems. Batching all access configurations into a single cuMemSetAccess call is much more efficient.

    std::vector<CUmemAccessDesc> access_descs;
    if (options.location_type == LocationType::HOST_NUMA) {
        CUmemAccessDesc access = {};
        access.location.type = CU_MEM_LOCATION_TYPE_HOST_NUMA;
        access.location.id = options.location_id;
        access.flags = CU_MEM_ACCESS_FLAGS_PROT_READWRITE;
        access_descs.push_back(access);
    }

    int device_count = 0;
    result = api.device_get_count(&device_count);
    if (result != CUDA_SUCCESS) {
        status = cudaDriverFailure("cuDeviceGetCount", result);
        return rollback(status);
    }
    if (device_count <= 0) {
        return rollback(Status::NotSupportedTransport(
            "VMM allocation requires at least one visible CUDA device"));
    }

    for (int ordinal = 0; ordinal < device_count; ++ordinal) {
        CUdevice device;
        result = api.device_get(&device, ordinal);
        if (result != CUDA_SUCCESS) {
            status = cudaDriverFailure("cuDeviceGet", result);
            return rollback(status);
        }
        CUmemAccessDesc access = {};
        access.location.type = CU_MEM_LOCATION_TYPE_DEVICE;
        access.location.id = device;
        access.flags = CU_MEM_ACCESS_FLAGS_PROT_READWRITE;
        access_descs.push_back(access);
    }

    if (!access_descs.empty()) {
        CUresult access_result = api.mem_set_access(ptr, length, access_descs.data(), access_descs.size());
        if (access_result != CUDA_SUCCESS) {
            status = cudaDriverFailure("cuMemSetAccess", access_result);
            return rollback(status);
        }
    }

@codecov-commenter

codecov-commenter commented Jul 16, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 28.57143% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ake-transfer-engine/include/transfer_engine_impl.h 0.00% 3 Missing ⚠️
...oncake-transfer-engine/include/transfer_metadata.h 57.14% 3 Missing ⚠️
mooncake-transfer-engine/src/multi_transport.cpp 0.00% 2 Missing ⚠️
mooncake-transfer-engine/src/transfer_engine.cpp 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@neverhook
neverhook force-pushed the codex/nvlink-vmm-fabric-lifecycle branch from 4aef80a to ae700b3 Compare July 16, 2026 17:38
@neverhook neverhook closed this Jul 16, 2026
@neverhook neverhook reopened this Jul 16, 2026

@he-yufeng he-yufeng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for this — the VMM lifecycle handling here is careful work. A few things I want to call out and two points worth a closer look before merge. I focused the review on the new NvlinkVmmAllocation lifecycle in nvlink_vmm_allocation.cpp.

What's done well. The reverse-order Release() with per-stage commit and retained ownership on failure is the right shape for CUDA VMM teardown, and pre-allocating the intrusive CleanupPendingVmmOwnerNode before acquiring any CUDA resource (so recording a rollback can never itself fail under memory pressure) is a nice touch. Moving the owned-range provenance removal to the very start of Release(), before the first unmap, so a stuck teardown stops satisfying IsExactOwnedRange rather than staying eligible for a fresh remote registration, is exactly the safe direction.

1. A single stuck teardown can silently wedge the whole allocation path (availability). CreateWithDriverApi opens with:

if (!RetryCleanupPendingOwners()) {
    return Status::Memory("a previous VMM factory rollback is still pending cleanup");
}

If any quarantined owner keeps failing Release() (driver persistently rejecting a teardown stage), every subsequent Create fails from here on, process-wide, with no upper bound and no escape valve. The retention itself is defensible, but the failure is invisible to operators: CleanupPendingOwnerCount() exists but isn't wired to any metric or log-once, so the symptom presents as "all Fabric VMM allocations suddenly fail" with the root cause buried. Could we at least surface the pending count (a gauge, or a throttled LOG(ERROR) naming the retained range) so this degraded state is observable? Whether a bounded-retry-then-give-up policy is acceptable is a product call, but the observability gap seems worth closing regardless.

2. Rollback re-releases a handle whose first cuMemRelease returned an error (worth confirming). In the success path the handle is released once and the flags cleared:

result = api.mem_release(handle);
if (result != CUDA_SUCCESS) {
    status = cudaDriverFailure("cuMemRelease", result);
    return rollback(status);   // -> owner->Release() -> mem_release(handle) again
}
owner->handle_owned_ = false;

When that mem_release fails, handle_owned_ is still true, so the rollback path's Release() calls cuMemRelease on the same handle a second time. This is only safe if a failing cuMemRelease is guaranteed to be atomic (no state change on error), so the retry acts on a still-valid handle rather than double-releasing one that was partially torn down. I believe that's the driver's contract, but given this is the one spot where the same handle can be released twice, it'd be good to either confirm that assumption explicitly or clear handle_owned_ before entering rollback here and lean on the quarantine path if the handle truly leaked. Same reasoning applies to the mid-Create failures that reach rollback while mapped_/address_reserved_ are set — those are fine because each stage's flag gates its own teardown, but the handle stage is the asymmetric one.

Neither of these blocks the design, which I think is sound. #2 is a small correctness clarification; #1 is the one I'd most like addressed since a wedged allocation path is hard to diagnose in production. Happy to look again once you've had a chance to weigh in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants