Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
dd8b301
Scale native allreduce/allgather algos for MNNVL/MNNVLS
Binyang2014 Apr 27, 2026
893a08e
Enable MNNVL allreduce tuning
Binyang2014 Apr 28, 2026
dded5e0
Improve MNNVL allreduce tuning performance
Binyang2014 Apr 28, 2026
865c2bc
Optimize MNNVL allreduce without symmetric memory
Binyang2014 Apr 28, 2026
3bc00cb
Enable NVLS zero-copy without symmetric memory flag
Binyang2014 Apr 28, 2026
533f329
Tune no-sym MNNVL with RSAG zero-copy
Binyang2014 Apr 28, 2026
45a651b
Decouple IPC-domain hint from bootstrap nRanksPerNode
Binyang2014 May 1, 2026
2a2fca8
Rename collective ctx/kernel param nRanksPerNode to ipcDomainNranks
Binyang2014 May 1, 2026
2efda4d
Restore compile-time templated NRanksPerNode for rsag_zero_copy
Binyang2014 May 1, 2026
1c29817
Revert AllreduceRsAgZeroCopy non-symmetric ctx key tag back to ++tag
Binyang2014 May 1, 2026
7bc5e04
Reset GPU tokens before reuse
Binyang2014 May 2, 2026
9a36884
Rename gpuMemset wrapper and zero TokenPool slots in deleter
Binyang2014 May 2, 2026
987f800
Merge remote-tracking branch 'origin/main' into binyli/mnnvl
Binyang2014 May 4, 2026
6296803
Make NVLS non-zero-copy allreduce algorithms MNNVL-ready
Binyang2014 May 5, 2026
9aeeaf0
Simplify torch-integration tuning example for MPI-only multi-node tes…
Binyang2014 May 6, 2026
905b23d
Drop non-MNNVL multi_node regime from torch-integration example
Binyang2014 May 6, 2026
4a0d5b2
Simplify torch-integration tuning example
Binyang2014 May 6, 2026
307a471
Shorten verbose comments and use THROW in validateIpcDomainSpansWorld
Binyang2014 May 6, 2026
f0c6ac0
Fold validateIpcDomainSpansWorld into getIpcDomainNranks
Binyang2014 May 6, 2026
bde23ce
Revert verbose RSAG zero-copy comment; rename NRanksPerNode template …
Binyang2014 May 6, 2026
095cfff
Revert RSAG nBlocks default to 64
Binyang2014 May 6, 2026
639b80d
Tie AllreduceAllpairPacket maxBlockNum_ to MAX_IPC_DOMAIN_NRANKS - 1
Binyang2014 May 6, 2026
e8caab7
Strip preflight validation blocks from NVLS pipeline allreduce kernels
Binyang2014 May 6, 2026
7d80a33
Default torch example SYMMETRIC_MEMORY env to 1
Binyang2014 May 6, 2026
d1b04a3
NVLS zero-copy allreduce: support FP16 accumulator for FP8 inputs
Binyang2014 May 7, 2026
113d859
fix
Binyang2014 May 8, 2026
9ff7e1c
update
Binyang2014 May 8, 2026
654bcfa
update
Binyang2014 May 8, 2026
5516bdb
fix
Binyang2014 May 8, 2026
e208cc3
WIP
Binyang2014 May 8, 2026
825fc12
address hang issue
Binyang2014 May 9, 2026
224b3de
Clean up completed communicator receives
Binyang2014 May 13, 2026
0c09239
Merge branch 'main' into binyli/mnnvl
Binyang2014 May 13, 2026
7724e49
Fix lint and ROCm error alias
Copilot May 13, 2026
24850ef
Merge branch 'main' into binyli/mnnvl
Binyang2014 May 15, 2026
ee82cc4
Merge branch 'main' into binyli/mnnvl
Binyang2014 May 15, 2026
dbebde2
Configure IPC domain per communicator
Binyang2014 May 15, 2026
93b4354
temp solution
Binyang2014 May 15, 2026
0744e80
detect ipc domain automaticlly
Binyang2014 May 16, 2026
94af88d
Fix tuning example hang
Binyang2014 May 16, 2026
f32cfb1
update
Binyang2014 May 16, 2026
594dc79
Address NVLS review feedback
seagater May 16, 2026
18d3737
Tighten NVML IPC domain hash lookup
seagater May 16, 2026
4db71b9
Move barrier into setupNvlsChannels and clean up NVLS pipeline state
Binyang2014 May 18, 2026
35331cf
Fix collective topology sizing
Binyang2014 May 20, 2026
ac44e98
update
Binyang2014 May 20, 2026
7308c32
merge main
Binyang2014 May 22, 2026
42ece40
Fix memory leak
Binyang2014 May 24, 2026
641420d
increase nvls memory size to 64 GB
Binyang2014 May 26, 2026
ea73a1e
WIP
Binyang2014 May 26, 2026
ba0b3e3
merge main
Binyang2014 Jun 4, 2026
99f20cd
merge main
Binyang2014 Jun 4, 2026
ecc8c5c
move tuner to benchmark
Binyang2014 Jun 4, 2026
000cd5b
update algo
Binyang2014 Jun 5, 2026
cd9d504
add check for nvls fp8 support
Binyang2014 Jun 5, 2026
d754c35
lint and log
Binyang2014 Jun 6, 2026
2954d81
log
Binyang2014 Jun 6, 2026
d401f97
WIP
Binyang2014 Jun 6, 2026
ee3606e
WIP
Binyang2014 Jun 6, 2026
e1c2679
add more logs
Binyang2014 Jun 8, 2026
0a26c30
for buffer pool
Binyang2014 Jun 12, 2026
ddf8b14
update for buffer pool
Binyang2014 Jun 13, 2026
9c26eb4
add test
Binyang2014 Jun 15, 2026
b4030f1
Merge origin/main into binyli/mnnvl
Binyang2014 Jun 30, 2026
6924259
wip
caiocbr Jul 24, 2026
cd4c660
Merge main into binyli/mnnvl
Binyang2014 Jul 26, 2026
644fa6e
Merge origin/main into binyli/mnnvl
Binyang2014 Jul 28, 2026
5d79947
update macro
Binyang2014 Jul 28, 2026
7cc49ee
code clean-up
Binyang2014 Jul 28, 2026
695ce28
Merge branch 'main' into binyli/mnnvl
Binyang2014 Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions include/mscclpp/gpu_data_types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ using __bfloat162 = __nv_bfloat162;

#endif

// Defined when the current device compilation target supports FP8 (`e4m3`/`e5m2`) multimem
// instructions.
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 1000) && \
(defined(__CUDA_ARCH_SPECIFIC__) || defined(__CUDA_ARCH_FAMILY_SPECIFIC__))
#define MSCCLPP_DEVICE_FP8_MULTIMEM_SUPPORTED
#define MSCCLPP_DEVICE_FP8_MULTIMEM_SUPPORTED 1
#else
#define MSCCLPP_DEVICE_FP8_MULTIMEM_SUPPORTED 0
#endif

/// Software float8 with 4 exponent bits, 3 mantissa bits, exponent bias = 15.
Expand Down
59 changes: 57 additions & 2 deletions include/mscclpp/gpu_utils.hpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// Licensed under the MIT License.

#ifndef MSCCLPP_GPU_UTILS_HPP_
#define MSCCLPP_GPU_UTILS_HPP_

#include <memory>
#include <unordered_map>
#include <utility>

#include "env.hpp"
#include "errors.hpp"
Expand Down Expand Up @@ -143,6 +144,8 @@ std::shared_ptr<GpuStreamPool> gpuStreamPool();

namespace detail {

class GpuBufferPoolStorage;

void setReadWriteMemoryAccess(void* base, size_t size);
int gpuIdFromAddress(void* ptr);

Expand Down Expand Up @@ -332,7 +335,8 @@ enum class GpuBufferGranularity {
MultiCastRecommended,
};

/// Allocates a GPU memory space specialized for communication. The memory is zeroed out. Get the device pointer by
/// Owns a GPU memory space specialized for communication. Memory allocated by the public constructor is zeroed.
/// Memory returned by `GpuBufferPool::allocate()` may contain data from a previous use. Get the device pointer by
/// `GpuBuffer::data()`.
///
/// Use this function for communication buffers, i.e., only when other devices (CPU, GPU, NIC, etc.) may access this
Expand Down Expand Up @@ -402,12 +406,63 @@ class GpuBuffer {
int deviceId() const { return deviceId_; }

private:
friend class detail::GpuBufferPoolStorage;

GpuBuffer(size_t nelems, size_t bytes, int deviceId, std::shared_ptr<T> memory)
: nelems_(nelems), bytes_(bytes), deviceId_(deviceId), memory_(std::move(memory)) {}

size_t nelems_;
size_t bytes_;
int deviceId_;
std::shared_ptr<T> memory_;
};

/// A deterministic sub-allocation pool for GPU communication buffers.
///
/// The pool allocates one `GpuBuffer<char>` slab and returns reference-counted buffers for sub-ranges in that slab.
/// If all ranks create pools with the same size and make the same allocation calls, returned buffers have identical
/// offsets from the slab base pointer. Python bindings use this property to build symmetric memory buffers while
/// relying on normal Python reference counting to return buffers to the local pool.
/// Callers must keep each returned buffer alive until all GPU work using it has completed.
///
class GpuBufferPool {
public:
/// Constructs a pool backed by a single `GpuBuffer<char>`.
/// @param bytes Number of bytes to reserve in the pool.
/// @param granularity Granularity used to size the underlying `GpuBuffer`.
explicit GpuBufferPool(size_t bytes, GpuBufferGranularity granularity = GpuBufferGranularity::MultiCastMinimum);

/// Allocates a sub-range from the pool.
/// The returned memory has unspecified contents and may contain data from a previous allocation.
/// @param bytes Number of bytes to allocate.
/// @param alignment Alignment in bytes for the returned offset.
/// @return A reference-counted `GpuBuffer<char>` backed by the pool.
std::shared_ptr<GpuBuffer<char>> allocate(size_t bytes, size_t alignment = 256);

/// Returns the number of bytes in the underlying pool allocation.
/// @return Number of bytes in the pool.
size_t bytes() const;

/// Returns the number of bytes that are available for new buffers.
/// @return Number of free bytes.
size_t freeBytes() const;

/// Returns the number of bytes currently held by active pooled buffers.
/// @return Number of active bytes.
size_t activeBytes() const;

/// Returns the device pointer to the pool base.
/// @return Device pointer to the pool base.
char* data();

/// Returns the device id of the underlying pool allocation.
/// @return Device id of the underlying pool allocation.
int deviceId() const;

private:
std::shared_ptr<detail::GpuBufferPoolStorage> storage_;
};

} // namespace mscclpp

#endif // MSCCLPP_GPU_UTILS_HPP_
4 changes: 2 additions & 2 deletions include/mscclpp/switch_channel_device.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ struct SwitchChannelDeviceHandle {
: "l"(ptr)
: "memory");
}
#if defined(MSCCLPP_DEVICE_FP8_MULTIMEM_SUPPORTED)
#if MSCCLPP_DEVICE_FP8_MULTIMEM_SUPPORTED
else if constexpr (std::is_same_v<VectorType, f8_e4m3x4>) {
if constexpr (std::is_same_v<AccumT, __half>) {
asm("multimem.ld_reduce.relaxed.sys.global.add.acc::f16.e4m3x4 %0, [%1];"
Expand Down Expand Up @@ -224,7 +224,7 @@ struct SwitchChannelDeviceHandle {
"r"(val.words[1]), "r"(val.words[2]), "r"(val.words[3])
: "memory");
}
#if defined(MSCCLPP_DEVICE_FP8_MULTIMEM_SUPPORTED)
#if MSCCLPP_DEVICE_FP8_MULTIMEM_SUPPORTED
else if constexpr (std::is_same_v<VectorType, f8_e4m3x4>) {
asm volatile("multimem.st.relaxed.sys.global.e4m3x4 [%0], %1;" ::"l"(ptr), "r"(val.words[0]) : "memory");
} else if constexpr (std::is_same_v<VectorType, f8_e4m3x8>) {
Expand Down
117 changes: 69 additions & 48 deletions python/csrc/gpu_utils_py.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// Licensed under the MIT License.

#include <dlpack/dlpack.h>
#include <nanobind/nanobind.h>
#include <nanobind/stl/optional.h>
#include <nanobind/stl/shared_ptr.h>
#include <nanobind/stl/string.h>
#include <nanobind/stl/vector.h>

#include <memory>
#include <mscclpp/gpu_data_types.hpp>
#include <mscclpp/gpu_utils.hpp>

Expand All @@ -15,6 +17,13 @@ using namespace mscclpp;

constexpr int BYTE_BITS = 8;

struct DlpackContext {

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.

Is there value in creating this struct just for makeDlpack instead of just temp variables within the function?

DLManagedTensor managedTensor{};
std::vector<int64_t> shape;
std::vector<int64_t> strides;
std::shared_ptr<void> owner;
};

static DLDeviceType getDeviceType() {
#if defined(MSCCLPP_USE_ROCM)
return kDLROCM;
Expand Down Expand Up @@ -52,65 +61,67 @@ static DLDataType getDlType(std::string type) {
}
}

static nb::capsule toDlpack(GpuBuffer<char> buffer, std::string dataType, std::vector<int64_t>& shape,
std::vector<int64_t>& strides) {
static void dlpackCapsuleDestructor(PyObject* capsule) {
if (PyCapsule_IsValid(capsule, "used_dltensor")) {
return;
}
if (!PyCapsule_IsValid(capsule, "dltensor")) {
return;
}
DLManagedTensor* managedTensor = static_cast<DLManagedTensor*>(PyCapsule_GetPointer(capsule, "dltensor"));
if (managedTensor == nullptr) {
return;
}
if (managedTensor->deleter) {
managedTensor->deleter(managedTensor);
}
}

static nb::capsule makeDlpack(void* data, size_t bytes, int deviceId, std::shared_ptr<void> owner, std::string dataType,
std::vector<int64_t> shape, std::vector<int64_t> strides) {
DLDataType dtype = getDlType(dataType);
int64_t* tensorShape = shape.size() > 0 ? new int64_t[shape.size()] : new int64_t[1];
int64_t* tensorStrides = strides.size() > 0 ? new int64_t[strides.size()] : nullptr;
if (shape.size() == 0) {
tensorShape[0] = (int64_t)(buffer.nelems() / ((dtype.bits * dtype.lanes + 7) / BYTE_BITS));
} else {
for (size_t i = 0; i < shape.size(); ++i) {
tensorShape[i] = shape[i];
auto ctx = std::make_unique<DlpackContext>();
size_t elementBytes = (dtype.bits * dtype.lanes + 7) / BYTE_BITS;
if (shape.empty()) {
if (bytes % elementBytes != 0) {
throw Error("DLPack buffer size must be divisible by the element size.", ErrorCode::InvalidUsage);
}
ctx->shape.push_back((int64_t)(bytes / elementBytes));
} else {
ctx->shape = std::move(shape);
}
for (size_t i = 0; i < strides.size(); ++i) {
tensorStrides[i] = strides[i];
ctx->strides = std::move(strides);
if (!ctx->strides.empty() && ctx->strides.size() != ctx->shape.size()) {
throw Error("DLPack strides must have the same length as shape.", ErrorCode::InvalidUsage);
}
ctx->owner = std::move(owner);

DLManagedTensor* dlManagedTensor = new DLManagedTensor();
dlManagedTensor->dl_tensor.data = buffer.data();
DLManagedTensor* dlManagedTensor = &ctx->managedTensor;
dlManagedTensor->dl_tensor.data = data;
dlManagedTensor->dl_tensor.device.device_type = getDeviceType();
dlManagedTensor->dl_tensor.device.device_id = buffer.deviceId();
dlManagedTensor->dl_tensor.ndim = shape.size() == 0 ? 1 : shape.size();
dlManagedTensor->dl_tensor.strides = tensorStrides;
dlManagedTensor->dl_tensor.shape = tensorShape;
dlManagedTensor->dl_tensor.device.device_id = deviceId;
dlManagedTensor->dl_tensor.ndim = static_cast<int>(ctx->shape.size());
dlManagedTensor->dl_tensor.strides = ctx->strides.empty() ? nullptr : ctx->strides.data();
dlManagedTensor->dl_tensor.shape = ctx->shape.data();
dlManagedTensor->dl_tensor.byte_offset = 0;
dlManagedTensor->dl_tensor.dtype = dtype;
dlManagedTensor->manager_ctx = new GpuBuffer<char>(buffer);
dlManagedTensor->deleter = [](DLManagedTensor* self) {
delete static_cast<GpuBuffer<char>*>(self->manager_ctx);
self->manager_ctx = nullptr;
self->dl_tensor.data = nullptr;
if (self->dl_tensor.shape != nullptr) {
delete[] self->dl_tensor.shape;
self->dl_tensor.shape = nullptr;
if (self->dl_tensor.strides) {
delete[] self->dl_tensor.strides;
self->dl_tensor.strides = nullptr;
}
}
delete self;
};
dlManagedTensor->manager_ctx = ctx.get();
dlManagedTensor->deleter = [](DLManagedTensor* self) { delete static_cast<DlpackContext*>(self->manager_ctx); };

PyObject* dlCapsule = PyCapsule_New(static_cast<void*>(dlManagedTensor), "dltensor", [](PyObject* capsule) {
if (PyCapsule_IsValid(capsule, "used_dltensor")) {
return;
}
if (!PyCapsule_IsValid(capsule, "dltensor")) {
return;
}
DLManagedTensor* managedTensor = static_cast<DLManagedTensor*>(PyCapsule_GetPointer(capsule, "dltensor"));
if (managedTensor == nullptr) {
return;
}
if (managedTensor->deleter) {
managedTensor->deleter(managedTensor);
}
});
PyObject* dlCapsule = PyCapsule_New(static_cast<void*>(dlManagedTensor), "dltensor", dlpackCapsuleDestructor);
if (dlCapsule == nullptr) {
throw Error("Failed to create DLPack capsule.", ErrorCode::InvalidUsage);
}
ctx.release();
return nb::steal<nb::capsule>(dlCapsule);
}

static nb::capsule toDlpack(GpuBuffer<char> buffer, std::string dataType, std::vector<int64_t>& shape,
std::vector<int64_t>& strides) {
auto owner = std::make_shared<GpuBuffer<char>>(buffer);
return makeDlpack(buffer.data(), buffer.nelems(), buffer.deviceId(), std::move(owner), dataType, shape, strides);
}

void register_gpu_utils(nb::module_& m) {
m.def("is_nvls_supported", &isNvlsSupported);
m.def("is_bulk_supported", &isBulkSupported);
Expand All @@ -132,4 +143,14 @@ void register_gpu_utils(nb::module_& m) {
return toDlpack(self, dataType, shape, strides);
},
nb::arg("data_type"), nb::arg("shape") = std::vector<int64_t>(), nb::arg("strides") = std::vector<int64_t>());

nb::class_<GpuBufferPool>(m, "CppRawGpuBufferPool")
.def(nb::init<size_t, GpuBufferGranularity>(), nb::arg("bytes"),
nb::arg("granularity") = GpuBufferGranularity::MultiCastMinimum)
.def("bytes", &GpuBufferPool::bytes)
.def("free_bytes", &GpuBufferPool::freeBytes)
.def("active_bytes", &GpuBufferPool::activeBytes)
.def("data", [](GpuBufferPool& self) { return reinterpret_cast<uintptr_t>(self.data()); })
.def("device_id", &GpuBufferPool::deviceId)
.def("allocate", &GpuBufferPool::allocate, nb::arg("bytes"), nb::arg("alignment") = 256);
}
1 change: 1 addition & 0 deletions python/mscclpp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"AlgorithmCollection",
"CommGroup",
"GpuBuffer",
"GpuBufferPool",
"GpuBufferGranularity",
]

Expand Down
70 changes: 68 additions & 2 deletions python/mscclpp/_core/buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@

import cupy as cp
import numpy as np
from mscclpp._mscclpp import CppRawGpuBuffer, CppGpuBufferGranularity
from mscclpp._mscclpp import (
CppRawGpuBuffer,
CppRawGpuBufferPool,
CppGpuBufferGranularity,
)

__all__ = ["GpuBuffer", "GpuBufferGranularity"]
__all__ = ["GpuBuffer", "GpuBufferPool", "GpuBufferGranularity"]

GpuBufferGranularity = CppGpuBufferGranularity

Expand All @@ -35,3 +39,65 @@ def __new__(
buffer = CppRawGpuBuffer(np.prod(shape) * np.dtype(dtype).itemsize, granularity)
memptr = cp.cuda.MemoryPointer(cp.cuda.UnownedMemory(buffer.data(), buffer.bytes(), buffer), 0)
return cp.ndarray(shape, dtype=dtype, strides=strides, order=order, memptr=memptr)


class GpuBufferPool:
"""A GPU buffer pool that returns raw buffers backed by one communication-friendly allocation.

All ranks should create the same-sized pool and call :meth:`allocate` in the same order to get matching offsets.
Keep each returned buffer alive until all GPU work using it has completed.
"""

def __init__(
self,
nbytes: int,
granularity: CppGpuBufferGranularity = CppGpuBufferGranularity.MultiCastMinimum,
):
if nbytes <= 0:
raise ValueError("Pool size must be positive.")
self._pool = CppRawGpuBufferPool(int(nbytes), granularity)

@property
def bytes(self) -> int:
"""Number of bytes in the underlying pool allocation."""
return self._pool.bytes()

@property
def free_bytes(self) -> int:
"""Number of bytes available for new buffers."""
return self._pool.free_bytes()

@property
def active_bytes(self) -> int:
"""Number of bytes currently held by live raw buffers."""
return self._pool.active_bytes()

@property
def data(self) -> int:
"""Device pointer to the pool base allocation."""
return self._pool.data()

@property
def device_id(self) -> int:
"""CUDA/HIP device id of the pool allocation."""
return self._pool.device_id()

def allocate(
self,
nbytes: int,
alignment: int = 256,
Comment thread
Binyang2014 marked this conversation as resolved.
) -> CppRawGpuBuffer:
"""Allocate a raw buffer from the pool.

Args:
nbytes: Number of bytes to allocate.
alignment: Required byte alignment of the allocation offset from the pool base.

The returned memory has unspecified contents and may contain data from a previous allocation.
"""
if nbytes <= 0:
raise ValueError("Buffer size must be positive.")
if alignment <= 0:
raise ValueError("Alignment must be positive.")

return self._pool.allocate(int(nbytes), int(alignment))
Loading
Loading