From 841fa3a5191c3ee395210ec56aa1767233ea364a Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Thu, 3 Sep 2026 11:02:34 -0400 Subject: [PATCH 01/56] wip: gpu-direct --- include/common.h | 18 +++++ include/ddstore.hpp | 18 ++++- src/common.cxx | 83 ++++++++++++++++++--- src/pyddstore.pyx | 171 ++++++++++++++++++++++++++++++++------------ 4 files changed, 230 insertions(+), 60 deletions(-) diff --git a/include/common.h b/include/common.h index 18318ef..745fbef 100644 --- a/include/common.h +++ b/include/common.h @@ -48,6 +48,12 @@ extern "C" size_t send_data_len; char *recv_data; size_t recv_data_len; + /* FI_HMEM_SYSTEM (0) if recv_data is host memory; otherwise the + * fi_hmem_iface value (FI_HMEM_CUDA, FI_HMEM_ROCR, ...) identifying + * what kind of GPU memory it is. Set by the caller (see + * ddstore.hpp's get()); the value is opaque here, just forwarded + * into fi_mr_regattr()'s attr.iface in read_from_remote(). */ + int recv_hmem_iface; struct fid_mr *mr; struct fid_mr *recv_mr; uint64_t key; @@ -97,6 +103,18 @@ extern "C" strcmp(f->info->fabric_attr->prov_name, "cxi") == 0); } + /* True only for the cxi provider (the real Slingshot/HW path). GPU + * (ROCr HMEM) buffer registration is only attempted when this is true — + * verified empirically that cxi's NULL-hints fi_getinfo() already + * reports FI_HMEM in caps by default on Frontier; hsn (tcp;ofi_rxm) + * has no such support and would otherwise fail with a confusing + * low-level libfabric error instead of a clear one. */ + static bool is_hmem_capable(struct fabric_state *f) + { + return f->info && f->info->fabric_attr->prov_name && + strcmp(f->info->fabric_attr->prov_name, "cxi") == 0; + } + void init_fabric(struct fabric_state *fabric); int handshake(struct fabric_state *fabric_state, MPI_Comm comm); int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset); diff --git a/include/ddstore.hpp b/include/ddstore.hpp index 2c582ee..94f30c6 100644 --- a/include/ddstore.hpp +++ b/include/ddstore.hpp @@ -352,8 +352,13 @@ class DDStore memcpy((char*)base + offset * disp * itemsize, buffer, nrows * disp * itemsize); } + /* hmem_iface: 0 (FI_HMEM_SYSTEM) for a host buffer, or an fi_hmem_iface + * value (FI_HMEM_CUDA, FI_HMEM_ROCR, ...) identifying what kind of GPU + * memory `buffer` is. Left as a plain int (not the enum) so the Cython + * binding (pyddstore.pyx) can pass it without cimporting the enum; + * read_from_remote() in common.cxx casts it back before use. */ template - void get(std::string name, long start, long count, T *buffer) + void get(std::string name, long start, long count, T *buffer, int hmem_iface = 0) { const VarInfo_t& varinfo = this->varlist.at(name); @@ -374,7 +379,11 @@ class DDStore // std::cout << "target,offset,start,count: " << target << "," << offset << "," << start << "," << count << // std::endl; - if (this->method == 0) + if (this->method == 0 && hmem_iface != 0) + { + throw std::runtime_error("GPU destination buffer is not supported with method=0 (MPI_Win)"); + } + else if (this->method == 0) { MPI_Win win = varinfo.win; MPI_Win_lock(MPI_LOCK_SHARED, target, 0, win); @@ -397,8 +406,13 @@ class DDStore else if (this->method == 1 || this->method == 2) { /* Methods 1 and 2 both use libfabric fi_read — same path. */ + if (hmem_iface != 0 && !is_hmem_capable(varinfo.fabric_state)) + throw std::runtime_error( + "GPU destination buffer requires DDSTORE_FABRIC=cxi " + "(current fabric does not support FI_HMEM)"); varinfo.fabric_state->recv_data = (char *)buffer; varinfo.fabric_state->recv_data_len = varinfo.disp * varinfo.itemsize * count; + varinfo.fabric_state->recv_hmem_iface = hmem_iface; int rc = read_from_remote(varinfo.fabric_state, target, (start - offset) * varinfo.disp * varinfo.itemsize); if (rc != 0) throw std::runtime_error( diff --git a/src/common.cxx b/src/common.cxx index 3885c3e..d4eeb7a 100644 --- a/src/common.cxx +++ b/src/common.cxx @@ -634,16 +634,51 @@ int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset // register dest buffer; close previous recv MR first to avoid leaking it if (fabric_state->recv_mr) fi_close(&fabric_state->recv_mr->fid); - fi_mr_reg( - fabric_state->domain, - fabric_state->recv_data, - fabric_state->recv_data_len, - FI_READ, - 0, - 0, - 0, - &fabric_state->recv_mr, - NULL); + + bool recv_is_hmem = fabric_state->recv_hmem_iface != FI_HMEM_SYSTEM; + if (recv_is_hmem && !is_hmem_capable(fabric_state)) + { + fprintf(stderr, "GPU (HMEM) recv buffer requested but fabric is not cxi\n"); + return 1; + } + + int mr_rc; + if (recv_is_hmem) + { + /* GPU destination buffer (ROCr on AMD, CUDA on NVIDIA -- whichever + * iface the caller set). No host-staged fallback exists: either + * fi_mr_regattr succeeds and fi_read() below DMAs straight into + * device memory, or it fails loudly here (checked below). */ + struct iovec iov = {fabric_state->recv_data, fabric_state->recv_data_len}; + struct fi_mr_attr attr; + memset(&attr, 0, sizeof(attr)); + attr.mr_iov = &iov; + attr.iov_count = 1; + attr.access = FI_READ; + attr.iface = (enum fi_hmem_iface)fabric_state->recv_hmem_iface; + attr.device.reserved = 0; /* ROCr/CUDA both resolve the device from the pointer */ + mr_rc = fi_mr_regattr(fabric_state->domain, &attr, 0, &fabric_state->recv_mr); + } + else + { + mr_rc = fi_mr_reg( + fabric_state->domain, + fabric_state->recv_data, + fabric_state->recv_data_len, + FI_READ, + 0, + 0, + 0, + &fabric_state->recv_mr, + NULL); + } + if (mr_rc != FI_SUCCESS) + { + fprintf(stderr, "%s failed: %s\n", + recv_is_hmem ? "fi_mr_regattr" : "fi_mr_reg", + fi_strerror(mr_rc)); + return 1; + } /* CXI (FI_MR_ENDPOINT): bind and enable recv MR before use. No-op for * hsn/verbs/gni/psm2 (is_mr_endpoint() is false for those). */ @@ -664,7 +699,13 @@ int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset } void *memory_descriptor = NULL; - if (is_local_mr_req(fabric_state)) + /* HMEM (device) buffers need their local descriptor passed to fi_read() + * regardless of FI_LOCAL_MR: CXI uses it to route the transfer into + * device memory. FI_LOCAL_MR is deprecated and unset on this libfabric + * build (is_local_mr_req() is always false here), so without this the + * descriptor stayed NULL for HMEM too and fi_read() silently no-op'd + * instead of DMAing into the GPU buffer. */ + if (is_local_mr_req(fabric_state) || recv_is_hmem) { memory_descriptor = fi_mr_desc(fabric_state->recv_mr); } @@ -698,12 +739,32 @@ int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset // return 1; // } + /* This loop blocks until the transfer completes — read_from_remote() does + * not return until it does. For a GPU (recv_hmem_iface != FI_HMEM_SYSTEM) + * destination, this is load-bearing: it's what keeps the caller's device + * buffer alive (still + * referenced on the Python stack, so PyTorch's caching allocator cannot + * reuse its storage) for the entire in-flight RDMA window. If this call + * is ever made asynchronous, GPU buffer safety must be re-examined. */ for (;;) { struct fi_cq_data_entry CQEntry = {0}; rc = fi_cq_read(fabric_state->cq_signal, &CQEntry, 1); if (rc == 1) + { + if (recv_is_hmem) + fprintf(stderr, + "[hmem debug] cq entry: len=%zu flags=0x%llx " + "recv_data=%p recv_data_len=%zu remote_addr=%llu " + "remote_key=%llu src_offset=%llu\n", + CQEntry.len, (unsigned long long)CQEntry.flags, + (void *)fabric_state->recv_data, + fabric_state->recv_data_len, + (unsigned long long)(fabric_state->remote_address[src] + offset), + (unsigned long long)fabric_state->remote_key[src], + (unsigned long long)offset); break; + } if (rc == -FI_EAVAIL) { struct fi_cq_err_entry ee = {0}; diff --git a/src/pyddstore.pyx b/src/pyddstore.pyx index f85b976..58ee538 100644 --- a/src/pyddstore.pyx +++ b/src/pyddstore.pyx @@ -2,6 +2,8 @@ # cython: language_level=3 # cython: language=c++ +import os + import mpi4py.MPI as MPI cimport mpi4py.MPI as MPI cimport mpi4py.libmpi as libmpi @@ -28,6 +30,34 @@ cpdef bytes s2b(str x): else: return x.encode() +def _is_cuda_tensor(obj): + """True if obj is a torch.Tensor on a CUDA/HIP device. + + Torch is optional — imported lazily so DDStore2 has no hard dependency + on it. If unimportable, no object is ever considered a CUDA tensor. + """ + try: + import torch + except ImportError: + return False + return isinstance(obj, torch.Tensor) and obj.is_cuda + +# Mirrors libfabric's enum fi_hmem_iface (rdma/fi_domain.h): FI_HMEM_SYSTEM=0, +# FI_HMEM_CUDA=1, FI_HMEM_ROCR=2. Kept as plain ints here (rather than +# cimporting the C enum) since only these two values are ever produced by +# _hmem_iface_for() below -- torch itself is either a CUDA or a ROCm build, +# never both. +_FI_HMEM_CUDA = 1 +_FI_HMEM_ROCR = 2 + +def _hmem_iface_for(tensor): + """fi_hmem_iface value for a CUDA tensor: ROCr on a ROCm/HIP build of + torch (AMD), CUDA otherwise (NVIDIA). Only call when _is_cuda_tensor() + is already True. + """ + import torch + return _FI_HMEM_ROCR if torch.version.hip is not None else _FI_HMEM_CUDA + cdef extern from "ddstore.hpp": ctypedef struct VarInfo: string name @@ -45,7 +75,7 @@ cdef extern from "ddstore.hpp": # Method 2: extra member (no MPI communicator) DDStore(int method, string handshake_dir, int n_core) void add[T](string name, T* buffer, long nrows, int disp) except + - void get[T](string name, long start, long count, T* buffer) except + + void get[T](string name, long start, long count, T* buffer, int hmem_iface) except + void epoch_begin() void epoch_end() void free() @@ -63,6 +93,7 @@ cdef class PyDDstoreVarinfo: cdef class PyDDStore: cdef DDStore *c_ddstore + cdef int method def __cinit__(self, comm_or_none=None, int method=0, str handshake_dir="", int n_core=0, nic_map=None): @@ -82,6 +113,7 @@ cdef class PyDDStore: set in the environment. """ cdef MPI.Comm mpi_comm + self.method = method if method != 0: cpu_nic_map.select_fabric_iface(nic_map=nic_map) if method == 2: @@ -117,41 +149,81 @@ cdef class PyDDStore: del self.c_ddstore self.c_ddstore = NULL - def add(self, str name, np.ndarray arr): - assert arr.flags.c_contiguous - cdef long nrows = arr.shape[0] - cdef int disp = arr.size // arr.shape[0] - if arr.dtype == np.int32: - self.c_ddstore.add(s2b(name), arr.data, nrows, disp) - elif arr.dtype == np.int64: - self.c_ddstore.add(s2b(name), arr.data, nrows, disp) - elif arr.dtype == np.uint8: - self.c_ddstore.add(s2b(name), arr.data, nrows, disp) - elif arr.dtype == np.float32: - self.c_ddstore.add(s2b(name), arr.data, nrows, disp) - elif arr.dtype == np.float64: - self.c_ddstore.add(s2b(name), arr.data, nrows, disp) - elif arr.dtype == np.bool_: - self.c_ddstore.add(s2b(name), arr.data, nrows, disp) + def add(self, str name, arr): + if _is_cuda_tensor(arr): + raise NotImplementedError( + "GPU source buffers are not yet supported by add() " + "(GPU-to-GPU is a future phase); pass arr.cpu().numpy() instead") + cdef np.ndarray np_arr = arr + assert np_arr.flags.c_contiguous + cdef long nrows = np_arr.shape[0] + cdef int disp = np_arr.size // np_arr.shape[0] + if np_arr.dtype == np.int32: + self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp) + elif np_arr.dtype == np.int64: + self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp) + elif np_arr.dtype == np.uint8: + self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp) + elif np_arr.dtype == np.float32: + self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp) + elif np_arr.dtype == np.float64: + self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp) + elif np_arr.dtype == np.bool_: + self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp) else: raise NotImplementedError - def get(self, str name, np.ndarray arr, long start=0): - assert arr.flags.c_contiguous + def get(self, str name, arr, long start=0): cdef long count = arr.shape[0] - assert arr.shape[0] >= count - if arr.dtype == np.int32: - self.c_ddstore.get(s2b(name), start, count, arr.data) - elif arr.dtype == np.int64: - self.c_ddstore.get(s2b(name), start, count, arr.data) - elif arr.dtype == np.uint8: - self.c_ddstore.get(s2b(name), start, count, arr.data) - elif arr.dtype == np.float32: - self.c_ddstore.get(s2b(name), start, count, arr.data) - elif arr.dtype == np.float64: - self.c_ddstore.get(s2b(name), start, count, arr.data) - elif arr.dtype == np.bool_: - self.c_ddstore.get(s2b(name), start, count, arr.data) + cdef size_t ptr + cdef int iface + if _is_cuda_tensor(arr): + if self.method not in (1, 2): + raise RuntimeError( + "GPU destination buffer requires method=1 or 2 (libfabric), " + "got method=%d" % self.method) + provider = os.environ.get("DDSTORE_FABRIC", "hsn") + if provider != "cxi": + raise RuntimeError( + "GPU destination buffer requires DDSTORE_FABRIC=cxi " + "(current DDSTORE_FABRIC=%r); the hsn (tcp;ofi_rxm) path " + "does not support FI_HMEM. Set DDSTORE_FABRIC=cxi or pass " + "a host (CPU) numpy array instead." % provider) + assert arr.is_contiguous() + import torch + iface = _hmem_iface_for(arr) + ptr = arr.data_ptr() + if arr.dtype == torch.int32: + self.c_ddstore.get(s2b(name), start, count, ptr, iface) + elif arr.dtype == torch.int64: + self.c_ddstore.get(s2b(name), start, count, ptr, iface) + elif arr.dtype == torch.uint8: + self.c_ddstore.get(s2b(name), start, count, ptr, iface) + elif arr.dtype == torch.float32: + self.c_ddstore.get(s2b(name), start, count, ptr, iface) + elif arr.dtype == torch.float64: + self.c_ddstore.get(s2b(name), start, count, ptr, iface) + elif arr.dtype == torch.bool: + self.c_ddstore.get(s2b(name), start, count, ptr, iface) + else: + raise NotImplementedError + return + + cdef np.ndarray np_arr = arr + assert np_arr.flags.c_contiguous + assert np_arr.shape[0] >= count + if np_arr.dtype == np.int32: + self.c_ddstore.get(s2b(name), start, count, np_arr.data, 0) + elif np_arr.dtype == np.int64: + self.c_ddstore.get(s2b(name), start, count, np_arr.data, 0) + elif np_arr.dtype == np.uint8: + self.c_ddstore.get(s2b(name), start, count, np_arr.data, 0) + elif np_arr.dtype == np.float32: + self.c_ddstore.get(s2b(name), start, count, np_arr.data, 0) + elif np_arr.dtype == np.float64: + self.c_ddstore.get(s2b(name), start, count, np_arr.data, 0) + elif np_arr.dtype == np.bool_: + self.c_ddstore.get(s2b(name), start, count, np_arr.data, 0) else: raise NotImplementedError @@ -167,21 +239,26 @@ cdef class PyDDStore: def init(self, str name, long nrows, int disp, int itemsize=1): self.c_ddstore.init(s2b(name), nrows, disp, itemsize) - def update(self, str name, np.ndarray arr, long offset): - assert arr.flags.c_contiguous - cdef long nrows = arr.shape[0] - if arr.dtype == np.int32: - self.c_ddstore.update(s2b(name), arr.data, nrows, offset) - elif arr.dtype == np.int64: - self.c_ddstore.update(s2b(name), arr.data, nrows, offset) - elif arr.dtype == np.uint8: - self.c_ddstore.update(s2b(name), arr.data, nrows, offset) - elif arr.dtype == np.float32: - self.c_ddstore.update(s2b(name), arr.data, nrows, offset) - elif arr.dtype == np.float64: - self.c_ddstore.update(s2b(name), arr.data, nrows, offset) - elif arr.dtype == np.bool_: - self.c_ddstore.update(s2b(name), arr.data, nrows, offset) + def update(self, str name, arr, long offset): + if _is_cuda_tensor(arr): + raise NotImplementedError( + "GPU source buffers are not yet supported by update() " + "(GPU-to-GPU is a future phase); pass arr.cpu().numpy() instead") + cdef np.ndarray np_arr = arr + assert np_arr.flags.c_contiguous + cdef long nrows = np_arr.shape[0] + if np_arr.dtype == np.int32: + self.c_ddstore.update(s2b(name), np_arr.data, nrows, offset) + elif np_arr.dtype == np.int64: + self.c_ddstore.update(s2b(name), np_arr.data, nrows, offset) + elif np_arr.dtype == np.uint8: + self.c_ddstore.update(s2b(name), np_arr.data, nrows, offset) + elif np_arr.dtype == np.float32: + self.c_ddstore.update(s2b(name), np_arr.data, nrows, offset) + elif np_arr.dtype == np.float64: + self.c_ddstore.update(s2b(name), np_arr.data, nrows, offset) + elif np_arr.dtype == np.bool_: + self.c_ddstore.update(s2b(name), np_arr.data, nrows, offset) else: raise NotImplementedError From e588da76d8f742f32d725293f5ad019242cc9dba Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Thu, 3 Sep 2026 12:35:39 -0400 Subject: [PATCH 02/56] add --- test/test_gpu_rdma.py | 169 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 test/test_gpu_rdma.py diff --git a/test/test_gpu_rdma.py b/test/test_gpu_rdma.py new file mode 100644 index 0000000..b7bafe7 --- /dev/null +++ b/test/test_gpu_rdma.py @@ -0,0 +1,169 @@ +""" +GPUDirect RDMA tests (Phase 1: host source -> GPU destination). + +Positive path — run with: DDSTORE_FABRIC=cxi mpirun -n 2 pytest test/test_gpu_rdma.py -v +requires a live cxi/Slingshot fabric and at least one visible GPU per rank +(see run-test-gpu.sh). Negative-path tests need neither and always run. +""" + +import numpy as np +import pytest +from mpi4py import MPI + +import pyddstore as dds + +torch = pytest.importorskip("torch") + +gpu_required = pytest.mark.skipif( + not torch.cuda.is_available(), reason="requires a ROCm/HIP GPU" +) + + +def all_passed(comm, local_ok): + return comm.allreduce(int(local_ok), op=MPI.LAND) + + +# --------------------------------------------------------------------------- +# positive path: host source -> poisoned GPU destination, over cxi +# --------------------------------------------------------------------------- + + +@gpu_required +def test_get_into_gpu_tensor_cxi(comm, monkeypatch): + monkeypatch.setenv("DDSTORE_FABRIC", "cxi") + rank = comm.Get_rank() + size = comm.Get_size() + if size < 2: + pytest.skip("requires at least 2 ranks for a genuine remote read") + nrows, ncols = 8, 4 + + store = dds.PyDDStore(comm, method=1) + data = np.full((nrows, ncols), float(rank + 1), dtype=np.float32) + store.add("x", data) # host source (Phase 1 scope — unchanged) + + store.epoch_begin() + local_ok = True + for target_rank in range(size): + # poison, not zeros: a silent no-op/host-staged-fallback bug would + # leave this value in place instead of the real remote data. + out = torch.full((1, ncols), -999.0, dtype=torch.float32, device="cuda") + store.get("x", out, start=target_rank * nrows) + expected = float(target_rank + 1) + ok = bool(torch.all(out.cpu() == expected)) + print(f"[rank {rank}] target_rank={target_rank} expected={expected} " + f"got={out.cpu().tolist()} ok={ok}", flush=True) + if not ok: + local_ok = False + store.epoch_end() + + assert all_passed(comm, local_ok) + store.free() + + +@gpu_required +def test_get_into_gpu_tensor_cxi_large(comm, monkeypatch): + """Diagnostic: same as test_get_into_gpu_tensor_cxi but with a transfer + well over FI_CXI_SAFE_DEVMEM_COPY_THRESHOLD (default 4096 bytes), to + check whether CXI's small-transfer 'safe load/store' HMEM path is what's + silently no-op'ing, vs. the registration approach itself being broken. + """ + monkeypatch.setenv("DDSTORE_FABRIC", "cxi") + rank = comm.Get_rank() + size = comm.Get_size() + if size < 2: + pytest.skip("requires at least 2 ranks for a genuine remote read") + nrows, ncols = 8, 4096 # 4096 floats/row = 16384 bytes >> 4096-byte threshold + + store = dds.PyDDStore(comm, method=1) + data = np.full((nrows, ncols), float(rank + 1), dtype=np.float32) + store.add("x", data) + + store.epoch_begin() + local_ok = True + for target_rank in range(size): + out = torch.full((1, ncols), -999.0, dtype=torch.float32, device="cuda") + store.get("x", out, start=target_rank * nrows) + expected = float(target_rank + 1) + ok = bool(torch.all(out.cpu() == expected)) + nonpoison = int((out.cpu() != -999.0).sum()) + print(f"[rank {rank}] target_rank={target_rank} expected={expected} " + f"ok={ok} nonpoison_count={nonpoison}/{ncols} " + f"sample={out.cpu().flatten()[:8].tolist()}", flush=True) + if not ok: + local_ok = False + store.epoch_end() + + assert all_passed(comm, local_ok) + store.free() + + +@gpu_required +def test_get_host_to_host_cxi(comm, monkeypatch): + """Diagnostic: same transfer as above, but into a host numpy buffer + (bypasses HMEM entirely) -- isolates whether plain host-to-host RDMA + over cxi works correctly in this environment, independent of GPU support. + """ + monkeypatch.setenv("DDSTORE_FABRIC", "cxi") + rank = comm.Get_rank() + size = comm.Get_size() + if size < 2: + pytest.skip("requires at least 2 ranks for a genuine remote read") + nrows, ncols = 8, 4 + + store = dds.PyDDStore(comm, method=1) + data = np.full((nrows, ncols), float(rank + 1), dtype=np.float32) + store.add("x", data) + + store.epoch_begin() + local_ok = True + for target_rank in range(size): + out = np.full((1, ncols), -999.0, dtype=np.float32) + store.get("x", out, start=target_rank * nrows) + expected = float(target_rank + 1) + ok = bool(np.all(out == expected)) + print(f"[rank {rank}] target_rank={target_rank} expected={expected} " + f"got={out.tolist()} ok={ok}", flush=True) + if not ok: + local_ok = False + store.epoch_end() + + assert all_passed(comm, local_ok) + store.free() + + +# --------------------------------------------------------------------------- +# negative paths: clear, early errors -- no live cxi fabric required +# --------------------------------------------------------------------------- + + +@gpu_required +def test_gpu_buffer_rejected_on_hsn(comm, monkeypatch): + monkeypatch.setenv("DDSTORE_FABRIC", "hsn") + store = dds.PyDDStore(comm, method=1) + data = np.ones((4, 4), dtype=np.float32) + store.add("x", data) + + out = torch.zeros((1, 4), dtype=torch.float32, device="cuda") + with pytest.raises(RuntimeError, match="cxi"): + store.get("x", out, start=0) + store.free() + + +@gpu_required +def test_gpu_buffer_rejected_on_method0(comm): + store = dds.PyDDStore(comm, method=0) + data = np.ones((4, 4), dtype=np.float32) + store.add("x", data) + + out = torch.zeros((1, 4), dtype=torch.float32, device="cuda") + with pytest.raises(RuntimeError, match="method"): + store.get("x", out, start=0) + store.free() + + +@gpu_required +def test_gpu_source_rejected_by_add(comm): + store = dds.PyDDStore(comm, method=1) + data = torch.ones((4, 4), dtype=torch.float32, device="cuda") + with pytest.raises(NotImplementedError): + store.add("x", data) From 31f5269e6f7eb30b62879b71562a771ff04de5ad Mon Sep 17 00:00:00 2001 From: Jong Youl Choi Date: Thu, 3 Sep 2026 10:11:05 -0700 Subject: [PATCH 03/56] fix for unittest --- test/conftest.py | 11 ++++++++++- test/test_gpu_rdma.py | 8 +++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 09ff14a..ac93c81 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -9,4 +9,13 @@ @pytest.fixture(scope="function") def comm(): - return MPI.COMM_WORLD + """Provide MPI.COMM_WORLD and barrier after each test. + + The barrier ensures all ranks finish the current test (including + store.free() and any fabric endpoint teardown) before any rank + begins the next test's handshake/MPI_Allgather. Without this, + CXI endpoint cleanup on a fast rank can desynchronize the ranks + enough to deadlock the next test's collective in add(). + """ + yield MPI.COMM_WORLD + MPI.COMM_WORLD.Barrier() diff --git a/test/test_gpu_rdma.py b/test/test_gpu_rdma.py index b7bafe7..8a71d5f 100644 --- a/test/test_gpu_rdma.py +++ b/test/test_gpu_rdma.py @@ -138,11 +138,17 @@ def test_get_host_to_host_cxi(comm, monkeypatch): @gpu_required def test_gpu_buffer_rejected_on_hsn(comm, monkeypatch): - monkeypatch.setenv("DDSTORE_FABRIC", "hsn") + # Set up with cxi so that add() and init_fabric succeed (hsn is not + # available on all machines, e.g. Perlmutter which is CXI-only). + # Then switch DDSTORE_FABRIC to hsn before get() — the Python-level check + # in pyddstore.pyx reads the env var at get() time and rejects GPU buffers + # with a clear error before touching the fabric. + monkeypatch.setenv("DDSTORE_FABRIC", "cxi") store = dds.PyDDStore(comm, method=1) data = np.ones((4, 4), dtype=np.float32) store.add("x", data) + monkeypatch.setenv("DDSTORE_FABRIC", "hsn") out = torch.zeros((1, 4), dtype=torch.float32, device="cuda") with pytest.raises(RuntimeError, match="cxi"): store.get("x", out, start=0) From ee945735d235c46f6ce78ed6592bde4375ff487f Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Thu, 3 Sep 2026 14:26:53 -0400 Subject: [PATCH 04/56] gpudirect for vae --- examples/vae/distdataset.py | 53 ++++++++++++++++++++++++--------- examples/vae/vae-ddp.py | 18 ++++++++++- examples/vae/vae_extra_train.py | 24 +++++++++++++-- src/common.cxx | 18 +++++------ 4 files changed, 84 insertions(+), 29 deletions(-) diff --git a/examples/vae/distdataset.py b/examples/vae/distdataset.py index 6f07f58..ba7f4a0 100644 --- a/examples/vae/distdataset.py +++ b/examples/vae/distdataset.py @@ -16,12 +16,18 @@ def nsplit(a, n): class DistDataset(Dataset): """Distributed dataset class""" - def __init__(self, data, label, comm=MPI.COMM_WORLD, ddstore_width=None): + def __init__(self, data, label, comm=MPI.COMM_WORLD, ddstore_width=None, device=None): super().__init__() self.dataset = list() self.label = label self.comm = comm + # None -> get() allocates host buffers (default, unchanged). + # torch.device/"cuda"/"cuda:N" -> get() allocates its destination + # buffer directly on that device (GPUDirect RDMA, Phase 1); requires + # DDSTORE_METHOD in (1, 2) and DDSTORE_FABRIC=cxi (pyddstore raises + # a clear error otherwise). + self.device = device self.rank = self.comm.Get_rank() self.comm_size = self.comm.Get_size() print("init", self.rank, self.comm_size) @@ -92,22 +98,33 @@ def len(self): def __len__(self): return self.len() - def get(self, idx): + def get(self, idx, device=None): ## first dim must be the row count (1), not the flattened feature ## width, since ddstore.get() infers count from arr.shape[0] - val = np.zeros((1, 28 * 28), dtype=np.float32) + # Label stays host-only regardless of `device` -- both training + # loops discard it, so a GPU-resident label buffer would add + # complexity for no benefit. label = np.zeros(1, dtype=np.int32) - val = np.ascontiguousarray(val) - assert val.data.contiguous + if device is not None: + # RDMA fully overwrites this buffer, so torch.empty (not + # zeros): for MNIST's mostly-zero background pixels, an + # all-zeros buffer would look deceptively plausible even if the + # read silently no-op'd. + val = torch.empty((1, 28 * 28), dtype=torch.float32, device=device) + else: + val = np.zeros((1, 28 * 28), dtype=np.float32) + val = np.ascontiguousarray(val) + assert val.data.contiguous self.ddstore.get(f"{self.label}data", val, idx) self.ddstore.get(f"{self.label}labels", label, idx) # print("rank", self.rank, "fetching idx", idx) - val = torch.tensor(val) + if device is None: + val = torch.tensor(val) val = torch.reshape(val, (1, 28, 28)) return (val, label[0]) def __getitem__(self, idx): - return self.get(idx) + return self.get(idx, device=self.device) class DistDatasetReader(Dataset): @@ -119,9 +136,11 @@ class DistDatasetReader(Dataset): core rank's memory. """ - def __init__(self, label, handshake_dir, n_core): + def __init__(self, label, handshake_dir, n_core, device=None): super().__init__() self.label = label + # See DistDataset.__init__ for what `device` does. + self.device = device self.ddstore = dds.PyDDStore( None, method=2, handshake_dir=handshake_dir, n_core=n_core @@ -146,18 +165,24 @@ def len(self): def __len__(self): return self.len() - def get(self, idx): + def get(self, idx, device=None): ## first dim must be the row count (1), not the flattened feature ## width, since ddstore.get() infers count from arr.shape[0] - val = np.zeros((1, self.data_disp), dtype=np.float32) + # Label stays host-only regardless of `device` -- see DistDataset.get(). label = np.zeros(1, dtype=np.int32) - val = np.ascontiguousarray(val) - assert val.data.contiguous + if device is not None: + # torch.empty, not zeros -- see DistDataset.get() for why. + val = torch.empty((1, self.data_disp), dtype=torch.float32, device=device) + else: + val = np.zeros((1, self.data_disp), dtype=np.float32) + val = np.ascontiguousarray(val) + assert val.data.contiguous self.ddstore.get(f"{self.label}data", val, idx) self.ddstore.get(f"{self.label}labels", label, idx) - val = torch.tensor(val) + if device is None: + val = torch.tensor(val) val = torch.reshape(val, (1, self.side, self.side)) return (val, label[0]) def __getitem__(self, idx): - return self.get(idx) + return self.get(idx, device=self.device) diff --git a/examples/vae/vae-ddp.py b/examples/vae/vae-ddp.py index f7bed6e..a30eb5e 100644 --- a/examples/vae/vae-ddp.py +++ b/examples/vae/vae-ddp.py @@ -54,6 +54,15 @@ metavar="N", help="how many batches to wait before logging training status", ) +parser.add_argument( + "--gpu-dest", + action="store_true", + default=False, + help="Allocate the DDStore get() destination buffer directly on the " + "training device (GPUDirect RDMA, Phase 1), skipping the " + "host->device copy. Requires DDSTORE_METHOD in (1, 2) and " + "DDSTORE_FABRIC=cxi (e.g. DDSTORE_METHOD=2 as in run-vae.sh).", +) args = parser.parse_args() args.cuda = not args.no_cuda and torch.cuda.is_available() use_mps = not args.no_mps and torch.backends.mps.is_available() @@ -82,7 +91,7 @@ else: device = torch.device("cpu") -print("DDP setup:", comm_size, rank, device) +print("DDP setup:", comm_size, rank, device, "gpu_dest:", args.gpu_dest) if rank == 0: os.makedirs("results", exist_ok=True) @@ -95,11 +104,18 @@ # kwargs = {'num_workers': 1, 'pin_memory': True} if args.cuda else {} # kwargs = {'pin_memory': True} if args.cuda else {} kwargs = {} +# --gpu-dest returns CUDA/HIP tensors from __getitem__; DataLoader worker +# processes can't safely own GPU state across a fork, so this only works +# with num_workers=0 (today's default). Don't add num_workers>0 here +# without redesigning the buffer/collate strategy. +if args.gpu_dest: + assert kwargs.get("num_workers", 0) == 0 trainset = DistDataset( datasets.MNIST("data", train=True, download=True, transform=transforms.ToTensor()), "train", comm, + device=device if args.gpu_dest else None, ) # trainset = datasets.MNIST('data', train=True, download=True,transform=transforms.ToTensor()) comm.Barrier() diff --git a/examples/vae/vae_extra_train.py b/examples/vae/vae_extra_train.py index 9cda3a2..0948b8b 100644 --- a/examples/vae/vae_extra_train.py +++ b/examples/vae/vae_extra_train.py @@ -89,6 +89,15 @@ default=int(os.environ.get("DDSTORE_N_CORE", "4")), help="number of core ranks that published the data", ) +parser.add_argument( + "--gpu-dest", + action="store_true", + default=False, + help="Allocate the DDStore get() destination buffer directly on the " + "training device (GPUDirect RDMA, Phase 1), skipping the " + "host->device copy. Requires DDSTORE_FABRIC=cxi and a " + "libfabric-backed method (already the case for this script).", +) args = parser.parse_args() args.cuda = not args.no_cuda and torch.cuda.is_available() use_mps = not args.no_mps and torch.backends.mps.is_available() @@ -115,15 +124,24 @@ else: device = torch.device("cpu") -print("DDP setup:", comm_size, rank, device) +print("DDP setup:", comm_size, rank, device, "gpu_dest:", args.gpu_dest) model = VAE().to(device) model = torch.nn.parallel.DistributedDataParallel(model) optimizer = optim.Adam(model.parameters(), lr=1e-3) kwargs = {} - -trainset = DistDatasetReader("train", args.handshake_dir, args.n_core) +# --gpu-dest returns CUDA/HIP tensors from __getitem__; DataLoader worker +# processes can't safely own GPU state across a fork, so this only works +# with num_workers=0 (today's default). Don't add num_workers>0 here +# without redesigning the buffer/collate strategy. +if args.gpu_dest: + assert kwargs.get("num_workers", 0) == 0 + +trainset = DistDatasetReader( + "train", args.handshake_dir, args.n_core, + device=device if args.gpu_dest else None, +) sampler = torch.utils.data.distributed.DistributedSampler(trainset) train_loader = torch.utils.data.DataLoader( diff --git a/src/common.cxx b/src/common.cxx index d4eeb7a..df1cadc 100644 --- a/src/common.cxx +++ b/src/common.cxx @@ -752,17 +752,13 @@ int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset rc = fi_cq_read(fabric_state->cq_signal, &CQEntry, 1); if (rc == 1) { - if (recv_is_hmem) - fprintf(stderr, - "[hmem debug] cq entry: len=%zu flags=0x%llx " - "recv_data=%p recv_data_len=%zu remote_addr=%llu " - "remote_key=%llu src_offset=%llu\n", - CQEntry.len, (unsigned long long)CQEntry.flags, - (void *)fabric_state->recv_data, - fabric_state->recv_data_len, - (unsigned long long)(fabric_state->remote_address[src] + offset), - (unsigned long long)fabric_state->remote_key[src], - (unsigned long long)offset); + /* NOTE: CQEntry.len is NOT a reliable success signal on this + * provider/CQ format — it reads 0 even for host-to-host + * transfers independently verified to deliver correct data, so + * it can't be used to distinguish a real silent-no-op (observed + * once, for an HMEM/ROCr destination) from a normal completion. + * A hard check on it was tried and reverted: it false-positived + * on the working host path. Left unchecked deliberately. */ break; } if (rc == -FI_EAVAIL) From 8c568722becb4da7e1f490b4ece23d062399cc0d Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Thu, 3 Sep 2026 15:00:24 -0400 Subject: [PATCH 05/56] wip: gpudirect vae --- include/common.h | 10 ++++ include/ddstore.hpp | 106 ++++++++++++++++++++++++++-------- src/common.cxx | 51 +++++++++++++---- src/pyddstore.pyx | 98 +++++++++++++++++++++++-------- test/test_gpu_rdma.py | 130 +++++++++++++++++++++++++++++++++++++++++- 5 files changed, 334 insertions(+), 61 deletions(-) diff --git a/include/common.h b/include/common.h index 745fbef..44f2185 100644 --- a/include/common.h +++ b/include/common.h @@ -46,6 +46,16 @@ extern "C" fi_addr_t *comm_partner; char *send_data; size_t send_data_len; + /* FI_HMEM_SYSTEM (0) if send_data is host memory; otherwise the + * fi_hmem_iface value identifying what kind of GPU memory it is. + * Set by the caller (see ddstore.hpp's add()) and forwarded into + * fi_mr_regattr()'s attr.iface in handshake() (method=1) / add()'s + * inline registration (method=2). A separate field from + * recv_hmem_iface: one fabric_state can be simultaneously the send + * side (registered once at add() time) and the recv side + * (re-registered per get() call, including self-reads) -- these + * are independent MRs with independent lifetimes. */ + int send_hmem_iface; char *recv_data; size_t recv_data_len; /* FI_HMEM_SYSTEM (0) if recv_data is host memory; otherwise the diff --git a/include/ddstore.hpp b/include/ddstore.hpp index 94f30c6..0d4b58d 100644 --- a/include/ddstore.hpp +++ b/include/ddstore.hpp @@ -63,17 +63,43 @@ class DDStore /* Method 2 extra member: discover variable published by core members. */ void join(std::string name); + /* hmem_iface: 0 (FI_HMEM_SYSTEM) for a host buffer, or an fi_hmem_iface + * value (FI_HMEM_CUDA, FI_HMEM_ROCR, ...) identifying what kind of GPU + * memory `buffer` is. Mirrors get()'s hmem_iface parameter. + * + * LIFETIME CONTRACT: for hmem_iface == 0 (host), DDStore makes its own + * private copy of `buffer` (as it always has) -- the caller's buffer + * may be freed/reused immediately after add() returns. For + * hmem_iface != 0 (GPU), DDStore does NOT copy -- it registers the + * caller's own device pointer directly. The caller must keep that GPU + * allocation alive (not garbage-collected, not reused) for as long as + * this variable stays registered, i.e. until free() or this DDStore's + * destruction. pyddstore.pyx enforces this for Python callers via a + * keepalive dict; direct C++ callers must manage it themselves. */ template - void add(std::string name, T *buffer, long nrows, int disp) + void add(std::string name, T *buffer, long nrows, int disp, int hmem_iface = 0) { + if (this->method == 0 && hmem_iface != 0) + throw std::runtime_error("GPU source buffer is not supported with method=0 (MPI_Win)"); + void *base = NULL; - // (2025/03) jyc: necessary to avoid memory error - int err = MPI_Alloc_mem((MPI_Aint)(nrows * disp * sizeof(T)), MPI_INFO_NULL, &base); - if (err) + if (hmem_iface == 0) { - exit(1); + // (2025/03) jyc: necessary to avoid memory error + int err = MPI_Alloc_mem((MPI_Aint)(nrows * disp * sizeof(T)), MPI_INFO_NULL, &base); + if (err) + { + exit(1); + } + memcpy(base, buffer, nrows * disp * sizeof(T)); + } + else + { + /* GPU source buffer: register the caller's own pointer + * directly. No MPI_Alloc_mem, no copy -- see lifetime + * contract above. */ + base = (void *)buffer; } - memcpy(base, buffer, nrows * disp * sizeof(T)); MPI_Win win = MPI_WIN_NULL; struct fabric_state *fabric_state = NULL; @@ -90,40 +116,72 @@ class DDStore else if (this->method == 1) { fabric_state = (struct fabric_state *)calloc(1, sizeof(struct fabric_state)); - fabric_state->send_data = (char *)base; - fabric_state->send_data_len = nrows * disp * sizeof(T); - fabric_state->world_size = this->comm_size; - fabric_state->rank = this->rank; + fabric_state->send_data = (char *)base; + fabric_state->send_data_len = nrows * disp * sizeof(T); + fabric_state->send_hmem_iface = hmem_iface; + fabric_state->world_size = this->comm_size; + fabric_state->rank = this->rank; init_fabric(fabric_state); if (!fabric_state->info) throw std::runtime_error("init_fabric failed: no suitable fabric found"); + if (hmem_iface != 0 && !is_hmem_capable(fabric_state)) + throw std::runtime_error( + "GPU source buffer requires DDSTORE_FABRIC=cxi " + "(current fabric does not support FI_HMEM)"); if (handshake(fabric_state, this->comm) != 0) throw std::runtime_error("handshake failed (method=1)"); } else if (this->method == 2) { fabric_state = (struct fabric_state *)calloc(1, sizeof(struct fabric_state)); - fabric_state->send_data = (char *)base; - fabric_state->send_data_len = nrows * disp * sizeof(T); - fabric_state->world_size = this->n_core; - fabric_state->rank = this->rank; + fabric_state->send_data = (char *)base; + fabric_state->send_data_len = nrows * disp * sizeof(T); + fabric_state->send_hmem_iface = hmem_iface; + fabric_state->world_size = this->n_core; + fabric_state->rank = this->rank; init_fabric(fabric_state); if (!fabric_state->info) throw std::runtime_error("init_fabric failed: no suitable fabric found"); + if (hmem_iface != 0 && !is_hmem_capable(fabric_state)) + throw std::runtime_error( + "GPU source buffer requires DDSTORE_FABRIC=cxi " + "(current fabric does not support FI_HMEM)"); - /* Register the send buffer as an MR before writing the record. */ - int mr_rc = fi_mr_reg( - fabric_state->domain, - fabric_state->send_data, - fabric_state->send_data_len, - FI_WRITE | FI_REMOTE_READ, - 0, 0, 0, - &fabric_state->mr, - NULL); + /* Register the send buffer as an MR before writing the record -- + * same fi_mr_reg-vs-fi_mr_regattr branch as handshake(), + * duplicated here the same way the host-only version already + * is (method=2 registers inline instead of via handshake()). */ + bool send_is_hmem = hmem_iface != 0; + int mr_rc; + if (send_is_hmem) + { + struct iovec iov = {fabric_state->send_data, fabric_state->send_data_len}; + struct fi_mr_attr attr; + memset(&attr, 0, sizeof(attr)); + attr.mr_iov = &iov; + attr.iov_count = 1; + attr.access = FI_WRITE | FI_REMOTE_READ; + attr.iface = (enum fi_hmem_iface)hmem_iface; + attr.device.reserved = 0; + mr_rc = fi_mr_regattr(fabric_state->domain, &attr, 0, &fabric_state->mr); + } + else + { + mr_rc = fi_mr_reg( + fabric_state->domain, + fabric_state->send_data, + fabric_state->send_data_len, + FI_WRITE | FI_REMOTE_READ, + 0, 0, 0, + &fabric_state->mr, + NULL); + } if (mr_rc != FI_SUCCESS) - throw std::runtime_error(std::string("fi_mr_reg failed: ") + fi_strerror(mr_rc)); + throw std::runtime_error( + std::string(send_is_hmem ? "fi_mr_regattr failed: " : "fi_mr_reg failed: ") + + fi_strerror(mr_rc)); /* CXI (FI_MR_ENDPOINT): bind MR to endpoint and enable it before * use. The provider-assigned key is only valid after diff --git a/src/common.cxx b/src/common.cxx index df1cadc..941445a 100644 --- a/src/common.cxx +++ b/src/common.cxx @@ -542,19 +542,48 @@ int handshake(struct fabric_state *fabric_state, MPI_Comm comm) int world_size = fabric_state->world_size; int rank = fabric_state->rank; - int mr_rc = fi_mr_reg( - fabric_state->domain, - fabric_state->send_data, - fabric_state->send_data_len, - FI_WRITE | FI_REMOTE_READ, - 0, - 0, - 0, - &fabric_state->mr, - NULL); + bool send_is_hmem = fabric_state->send_hmem_iface != FI_HMEM_SYSTEM; + if (send_is_hmem && !is_hmem_capable(fabric_state)) + { + fprintf(stderr, "GPU (HMEM) send buffer requested but fabric is not cxi\n"); + return 1; + } + + int mr_rc; + if (send_is_hmem) + { + /* GPU source buffer. This MR is only ever the passive TARGET of + * other ranks' fi_read() (via remote_key/remote_address, exchanged + * below) -- never the local operand of a local fi_read/fi_write on + * this rank -- so unlike recv_mr in read_from_remote(), no + * fi_mr_desc()/local descriptor is needed here at all. */ + struct iovec iov = {fabric_state->send_data, fabric_state->send_data_len}; + struct fi_mr_attr attr; + memset(&attr, 0, sizeof(attr)); + attr.mr_iov = &iov; + attr.iov_count = 1; + attr.access = FI_WRITE | FI_REMOTE_READ; + attr.iface = (enum fi_hmem_iface)fabric_state->send_hmem_iface; + attr.device.reserved = 0; + mr_rc = fi_mr_regattr(fabric_state->domain, &attr, 0, &fabric_state->mr); + } + else + { + mr_rc = fi_mr_reg( + fabric_state->domain, + fabric_state->send_data, + fabric_state->send_data_len, + FI_WRITE | FI_REMOTE_READ, + 0, + 0, + 0, + &fabric_state->mr, + NULL); + } if (mr_rc != FI_SUCCESS) { - fprintf(stderr, "fi_mr_reg failed: %s\n", fi_strerror(mr_rc)); + fprintf(stderr, "%s (send) failed: %s\n", + send_is_hmem ? "fi_mr_regattr" : "fi_mr_reg", fi_strerror(mr_rc)); return 1; } diff --git a/src/pyddstore.pyx b/src/pyddstore.pyx index 58ee538..b5fca99 100644 --- a/src/pyddstore.pyx +++ b/src/pyddstore.pyx @@ -58,6 +58,22 @@ def _hmem_iface_for(tensor): import torch return _FI_HMEM_ROCR if torch.version.hip is not None else _FI_HMEM_CUDA +def _check_gpu_fabric_preconditions(int method, str what): + """Shared method=1/2 + DDSTORE_FABRIC=cxi precondition check for a GPU + (CUDA/HIP) buffer passed to add() or get(). `what` customizes the error + wording ("GPU source buffer" / "GPU destination buffer"). + """ + if method not in (1, 2): + raise RuntimeError( + "%s requires method=1 or 2 (libfabric), got method=%d" % (what, method)) + provider = os.environ.get("DDSTORE_FABRIC", "hsn") + if provider != "cxi": + raise RuntimeError( + "%s requires DDSTORE_FABRIC=cxi (current DDSTORE_FABRIC=%r); " + "the hsn (tcp;ofi_rxm) path does not support FI_HMEM. Set " + "DDSTORE_FABRIC=cxi or pass a host (CPU) numpy array instead." + % (what, provider)) + cdef extern from "ddstore.hpp": ctypedef struct VarInfo: string name @@ -74,7 +90,7 @@ cdef extern from "ddstore.hpp": string handshake_dir) # Method 2: extra member (no MPI communicator) DDStore(int method, string handshake_dir, int n_core) - void add[T](string name, T* buffer, long nrows, int disp) except + + void add[T](string name, T* buffer, long nrows, int disp, int hmem_iface) except + void get[T](string name, long start, long count, T* buffer, int hmem_iface) except + void epoch_begin() void epoch_end() @@ -94,6 +110,11 @@ cdef class PyDDstoreVarinfo: cdef class PyDDStore: cdef DDStore *c_ddstore cdef int method + # Keepalive for GPU tensors passed to add(): C++ holds a raw pointer + # into them with no copy and no refcounting (see ddstore.hpp add()'s + # lifetime-contract comment) -- this dict keeps the Python reference + # alive for as long as the variable stays registered. + cdef dict _gpu_owned_buffers def __cinit__(self, comm_or_none=None, int method=0, str handshake_dir="", int n_core=0, nic_map=None): @@ -114,6 +135,7 @@ cdef class PyDDStore: """ cdef MPI.Comm mpi_comm self.method = method + self._gpu_owned_buffers = {} if method != 0: cpu_nic_map.select_fabric_iface(nic_map=nic_map) if method == 2: @@ -148,28 +170,65 @@ cdef class PyDDStore: if self.c_ddstore != NULL: del self.c_ddstore self.c_ddstore = NULL + self._gpu_owned_buffers.clear() def add(self, str name, arr): + cdef size_t ptr + cdef int iface + cdef long nrows + cdef int disp if _is_cuda_tensor(arr): - raise NotImplementedError( - "GPU source buffers are not yet supported by add() " - "(GPU-to-GPU is a future phase); pass arr.cpu().numpy() instead") + _check_gpu_fabric_preconditions(self.method, "GPU source buffer") + assert arr.is_contiguous() + if name in self._gpu_owned_buffers: + raise RuntimeError( + "add() called again for variable '%s' with a GPU source " + "buffer; re-adding an existing variable name is not " + "supported (the original registration would remain " + "active in C++ while its Python keepalive reference is " + "replaced here, risking a dangling pointer)" % name) + import torch + iface = _hmem_iface_for(arr) + ptr = arr.data_ptr() + nrows = arr.shape[0] + disp = arr.numel() // arr.shape[0] + if arr.dtype == torch.int32: + self.c_ddstore.add(s2b(name), ptr, nrows, disp, iface) + elif arr.dtype == torch.int64: + self.c_ddstore.add(s2b(name), ptr, nrows, disp, iface) + elif arr.dtype == torch.uint8: + self.c_ddstore.add(s2b(name), ptr, nrows, disp, iface) + elif arr.dtype == torch.float32: + self.c_ddstore.add(s2b(name), ptr, nrows, disp, iface) + elif arr.dtype == torch.float64: + self.c_ddstore.add(s2b(name), ptr, nrows, disp, iface) + elif arr.dtype == torch.bool: + self.c_ddstore.add(s2b(name), ptr, nrows, disp, iface) + else: + raise NotImplementedError + # Keepalive: DDStore now holds a raw pointer into arr's storage + # with no copy and no C++-level refcounting -- see ddstore.hpp + # add()'s lifetime-contract doc comment. Must outlive this + # variable's registration; cleared in free()/__dealloc__. + self._gpu_owned_buffers[name] = arr + return + cdef np.ndarray np_arr = arr assert np_arr.flags.c_contiguous - cdef long nrows = np_arr.shape[0] - cdef int disp = np_arr.size // np_arr.shape[0] + nrows = np_arr.shape[0] + disp = np_arr.size // np_arr.shape[0] if np_arr.dtype == np.int32: - self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp) + self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp, 0) elif np_arr.dtype == np.int64: - self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp) + self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp, 0) elif np_arr.dtype == np.uint8: - self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp) + self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp, 0) elif np_arr.dtype == np.float32: - self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp) + self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp, 0) elif np_arr.dtype == np.float64: - self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp) + self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp, 0) elif np_arr.dtype == np.bool_: - self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp) + self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp, 0) else: raise NotImplementedError @@ -178,17 +237,7 @@ cdef class PyDDStore: cdef size_t ptr cdef int iface if _is_cuda_tensor(arr): - if self.method not in (1, 2): - raise RuntimeError( - "GPU destination buffer requires method=1 or 2 (libfabric), " - "got method=%d" % self.method) - provider = os.environ.get("DDSTORE_FABRIC", "hsn") - if provider != "cxi": - raise RuntimeError( - "GPU destination buffer requires DDSTORE_FABRIC=cxi " - "(current DDSTORE_FABRIC=%r); the hsn (tcp;ofi_rxm) path " - "does not support FI_HMEM. Set DDSTORE_FABRIC=cxi or pass " - "a host (CPU) numpy array instead." % provider) + _check_gpu_fabric_preconditions(self.method, "GPU destination buffer") assert arr.is_contiguous() import torch iface = _hmem_iface_for(arr) @@ -235,7 +284,8 @@ cdef class PyDDStore: def free(self): self.c_ddstore.free() - + self._gpu_owned_buffers.clear() + def init(self, str name, long nrows, int disp, int itemsize=1): self.c_ddstore.init(s2b(name), nrows, disp, itemsize) diff --git a/test/test_gpu_rdma.py b/test/test_gpu_rdma.py index 8a71d5f..2059e28 100644 --- a/test/test_gpu_rdma.py +++ b/test/test_gpu_rdma.py @@ -168,8 +168,134 @@ def test_gpu_buffer_rejected_on_method0(comm): @gpu_required -def test_gpu_source_rejected_by_add(comm): +def test_gpu_source_rejected_on_method0(comm): + store = dds.PyDDStore(comm, method=0) + data = torch.ones((4, 4), dtype=torch.float32, device="cuda") + with pytest.raises(RuntimeError, match="method"): + store.add("x", data) + + +@gpu_required +def test_gpu_source_rejected_on_hsn(comm, monkeypatch): + monkeypatch.setenv("DDSTORE_FABRIC", "hsn") store = dds.PyDDStore(comm, method=1) data = torch.ones((4, 4), dtype=torch.float32, device="cuda") - with pytest.raises(NotImplementedError): + with pytest.raises(RuntimeError, match="cxi"): store.add("x", data) + + +# --------------------------------------------------------------------------- +# Phase 2: GPU-resident producer (add()) -- host/GPU destination, over cxi +# --------------------------------------------------------------------------- +# +# Frontier will still fail these (the open, unrelated OLCF ROCm+CXI driver +# issue affects get(), which every one of these tests also exercises to +# check correctness) -- validation target is Perlmutter, same as Phase 1. + + +@gpu_required +def test_add_from_gpu_tensor_host_dest_cxi(comm, monkeypatch): + """GPU source -> host (poisoned) destination. Isolates that the SEND + side specifically works, independent of Phase 1's already-proven + receive side. + """ + monkeypatch.setenv("DDSTORE_FABRIC", "cxi") + rank = comm.Get_rank() + size = comm.Get_size() + if size < 2: + pytest.skip("requires at least 2 ranks for a genuine remote read") + nrows, ncols = 8, 4 + + store = dds.PyDDStore(comm, method=1) + data = torch.full((nrows, ncols), float(rank + 1), dtype=torch.float32, device="cuda") + store.add("x", data) # GPU source -- Phase 2 + + store.epoch_begin() + local_ok = True + for target_rank in range(size): + out = np.full((1, ncols), -999.0, dtype=np.float32) + store.get("x", out, start=target_rank * nrows) + ok = bool(np.all(out == float(target_rank + 1))) + if not ok: + local_ok = False + store.epoch_end() + + assert all_passed(comm, local_ok) + store.free() + + +@gpu_required +def test_add_from_gpu_tensor_gpu_dest_cxi(comm, monkeypatch): + """Full Phase 2 scenario: both ends device memory, method=1.""" + monkeypatch.setenv("DDSTORE_FABRIC", "cxi") + rank = comm.Get_rank() + size = comm.Get_size() + if size < 2: + pytest.skip("requires at least 2 ranks for a genuine remote read") + nrows, ncols = 8, 4 + + store = dds.PyDDStore(comm, method=1) + data = torch.full((nrows, ncols), float(rank + 1), dtype=torch.float32, device="cuda") + store.add("x", data) + + store.epoch_begin() + local_ok = True + for target_rank in range(size): + out = torch.full((1, ncols), -999.0, dtype=torch.float32, device="cuda") + store.get("x", out, start=target_rank * nrows) + expected = float(target_rank + 1) + ok = bool(torch.all(out.cpu() == expected)) + if not ok: + local_ok = False + store.epoch_end() + + assert all_passed(comm, local_ok) + store.free() + + +@gpu_required +def test_add_from_gpu_tensor_gpu_dest_cxi_method2(comm, monkeypatch, tmp_path): + """Same as test_add_from_gpu_tensor_gpu_dest_cxi but method=2 + (file-based handshake, core+extra split) -- the transport + examples/vae/distdataset.py's DistDatasetReader actually uses. Includes + a self-read check (core rank both add()s and get()s its own data, + mirroring test_method2_core.py's self-check pattern) to verify the + independent send_hmem_iface/mr vs recv_hmem_iface/recv_mr fields don't + interfere with each other. + """ + monkeypatch.setenv("DDSTORE_FABRIC", "cxi") + rank = comm.Get_rank() + size = comm.Get_size() + if size < 2: + pytest.skip("requires at least 2 ranks for a genuine remote read") + nrows, ncols = 8, 4 + + hs_dir = comm.bcast(str(tmp_path / "ddstore_hs_add_method2") if rank == 0 else None, root=0) + + core_store = dds.PyDDStore(comm, method=2, handshake_dir=hs_dir) + data = torch.full((nrows, ncols), float(rank + 1), dtype=torch.float32, device="cuda") + core_store.add("x", data) # GPU source -- Phase 2 + comm.Barrier() + + # Self-read: every core rank reads its own just-added shard back. + local_ok = True + out_self = torch.full((1, ncols), -999.0, dtype=torch.float32, device="cuda") + core_store.get("x", out_self, start=rank * nrows) + if not bool(torch.all(out_self.cpu() == float(rank + 1))): + local_ok = False + + # Extra member: a separate instance joins and reads every rank's shard. + if rank == 0: + extra_store = dds.PyDDStore(None, method=2, handshake_dir=hs_dir, n_core=size) + extra_store.join("x") + for target_rank in range(size): + out = torch.full((1, ncols), -999.0, dtype=torch.float32, device="cuda") + extra_store.get("x", out, start=target_rank * nrows) + expected = float(target_rank + 1) + if not bool(torch.all(out.cpu() == expected)): + local_ok = False + extra_store.free() + + comm.Barrier() + assert all_passed(comm, local_ok) + core_store.free() From 0816225ccecfec35a5d8f81bc3c93d2b021b05ca Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Thu, 3 Sep 2026 15:08:32 -0400 Subject: [PATCH 06/56] wip: gpudirect vae --- examples/vae/distdataset.py | 52 +++++++++++++++++++++++---------- examples/vae/vae-ddp.py | 14 ++++++++- examples/vae/vae_core_server.py | 25 ++++++++++++++-- 3 files changed, 71 insertions(+), 20 deletions(-) diff --git a/examples/vae/distdataset.py b/examples/vae/distdataset.py index ba7f4a0..203fc0c 100644 --- a/examples/vae/distdataset.py +++ b/examples/vae/distdataset.py @@ -16,7 +16,8 @@ def nsplit(a, n): class DistDataset(Dataset): """Distributed dataset class""" - def __init__(self, data, label, comm=MPI.COMM_WORLD, ddstore_width=None, device=None): + def __init__(self, data, label, comm=MPI.COMM_WORLD, ddstore_width=None, + device=None, add_device=None): super().__init__() self.dataset = list() @@ -28,6 +29,15 @@ def __init__(self, data, label, comm=MPI.COMM_WORLD, ddstore_width=None, device= # DDSTORE_METHOD in (1, 2) and DDSTORE_FABRIC=cxi (pyddstore raises # a clear error otherwise). self.device = device + # None -> add() makes a private host copy of this rank's shard + # (default, unchanged). torch.device/"cuda"/"cuda:N" -> the shard is + # stacked directly on that device and add() registers it in place, + # no host copy (GPUDirect RDMA, Phase 2) -- same DDSTORE_METHOD/ + # DDSTORE_FABRIC requirements as `device` above. Independent of + # `device`: this controls the SOURCE side, `device` controls the + # DESTINATION side, so source and destination can be tested + # separately or together. + self.add_device = add_device self.rank = self.comm.Get_rank() self.comm_size = self.comm.Get_size() print("init", self.rank, self.comm_size) @@ -70,25 +80,35 @@ def __init__(self, data, label, comm=MPI.COMM_WORLD, ddstore_width=None, device= self.dataset.append(data[i]) print(self.rank, len(self.dataset)) - self.data = list() - self.labels = list() - - nbytes = 0 - for data, label in self.dataset: - val = data.cpu().numpy() - val = val.flatten() - self.data.append(val) - self.labels.append(label) - - # np.stack (not concatenate) keeps one row per image (nrows, 784) so - # ddstore.add() infers disp=784 instead of flattening into a single - # (nrows*784,) vector, which it would read back as disp=1. - self.data = np.stack(self.data) - self.data = np.ascontiguousarray(self.data) + # Label stays host-only regardless of add_device -- see get()'s + # matching comment; a GPU-resident int32 label buffer buys nothing. + self.labels = [label for _, label in self.dataset] self.labels = np.array(self.labels, dtype=np.int32) self.labels = np.ascontiguousarray(self.labels) + if self.add_device is not None: + # GPUDirect RDMA source (Phase 2): stack directly on device, no + # host round-trip. torch.stack (not cat) keeps one row per image + # (nrows, 784) -- see the np.stack comment below for why that + # shape matters to ddstore.add()'s disp inference. + self.data = torch.stack( + [d.reshape(-1) for d, _ in self.dataset] + ).to(self.add_device) + self.data = self.data.contiguous() + else: + data_list = list() + for data, _ in self.dataset: + val = data.cpu().numpy() + val = val.flatten() + data_list.append(val) + + # np.stack (not concatenate) keeps one row per image (nrows, 784) + # so ddstore.add() infers disp=784 instead of flattening into a + # single (nrows*784,) vector, which it would read back as disp=1. + self.data = np.stack(data_list) + self.data = np.ascontiguousarray(self.data) + self.ddstore.add(f"{self.label}data", self.data) self.ddstore.add(f"{self.label}labels", self.labels) diff --git a/examples/vae/vae-ddp.py b/examples/vae/vae-ddp.py index a30eb5e..8f182e8 100644 --- a/examples/vae/vae-ddp.py +++ b/examples/vae/vae-ddp.py @@ -63,6 +63,16 @@ "host->device copy. Requires DDSTORE_METHOD in (1, 2) and " "DDSTORE_FABRIC=cxi (e.g. DDSTORE_METHOD=2 as in run-vae.sh).", ) +parser.add_argument( + "--gpu-source", + action="store_true", + default=False, + help="Stack this rank's local shard directly on the training device " + "and add() it in place (GPUDirect RDMA source, Phase 2), skipping " + "the host round-trip. Same DDSTORE_METHOD/DDSTORE_FABRIC " + "requirements as --gpu-dest; independent of it -- use either or " + "both.", +) args = parser.parse_args() args.cuda = not args.no_cuda and torch.cuda.is_available() use_mps = not args.no_mps and torch.backends.mps.is_available() @@ -91,7 +101,8 @@ else: device = torch.device("cpu") -print("DDP setup:", comm_size, rank, device, "gpu_dest:", args.gpu_dest) +print("DDP setup:", comm_size, rank, device, + "gpu_dest:", args.gpu_dest, "gpu_source:", args.gpu_source) if rank == 0: os.makedirs("results", exist_ok=True) @@ -116,6 +127,7 @@ "train", comm, device=device if args.gpu_dest else None, + add_device=device if args.gpu_source else None, ) # trainset = datasets.MNIST('data', train=True, download=True,transform=transforms.ToTensor()) comm.Barrier() diff --git a/examples/vae/vae_core_server.py b/examples/vae/vae_core_server.py index 5956f8e..e285e6b 100644 --- a/examples/vae/vae_core_server.py +++ b/examples/vae/vae_core_server.py @@ -8,7 +8,14 @@ is done. Usage: - srun -n python examples/vae/vae_core_server.py [handshake_dir] + srun -n python examples/vae/vae_core_server.py [handshake_dir] [--gpu-source] + + --gpu-source Stack this rank's shard directly on the GPU and add() it in + place (GPUDirect RDMA source, Phase 2), skipping the host + round-trip. Requires DDSTORE_METHOD=2 (already set below) + and DDSTORE_FABRIC=cxi, and one visible GPU per rank + (--gpus-per-task=1, unlike the --gpus-per-task=0 this + script normally runs with). Environment: DDSTORE_HANDSHAKE_DIR overrides handshake_dir positional arg @@ -27,6 +34,7 @@ ## before mpi4py triggers MPI_Init, or - if GPU/NCCL use is ever added here - ## their static destructors run in the wrong order at interpreter exit and ## corrupt the heap. Do not reorder these imports. +import torch from torchvision import datasets, transforms import mpi4py @@ -44,13 +52,21 @@ def _resolve_dir(arg): return os.environ.get("DDSTORE_HANDSHAKE_DIR", "./ddstore_hs") -hs_dir = _resolve_dir(sys.argv[1] if len(sys.argv) > 1 else "") +gpu_source = "--gpu-source" in sys.argv +positional_args = [a for a in sys.argv[1:] if not a.startswith("--")] +hs_dir = _resolve_dir(positional_args[0] if positional_args else "") os.environ["DDSTORE_METHOD"] = "2" os.environ["DDSTORE_HANDSHAKE_DIR"] = hs_dir comm = MPI.COMM_WORLD rank = comm.Get_rank() +add_device = None +if gpu_source: + if not torch.cuda.is_available(): + raise RuntimeError("--gpu-source requires a visible CUDA/HIP GPU") + add_device = torch.device("cuda") + if rank == 0: os.makedirs(hs_dir, exist_ok=True) for fname in os.listdir(hs_dir): @@ -62,9 +78,12 @@ def _resolve_dir(arg): trainset = datasets.MNIST( "data", train=True, download=True, transform=transforms.ToTensor() ) -dds_trainset = DistDataset(trainset, "train", comm) +dds_trainset = DistDataset(trainset, "train", comm, add_device=add_device) comm.Barrier() +if rank == 0: + print("gpu_source:", gpu_source, flush=True) + if rank == 0: print( f"[core] published {len(dds_trainset)} training rows, waiting for extras...", From f03ab785279b90504d860694ab978f3543b24862 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Thu, 3 Sep 2026 16:16:09 -0400 Subject: [PATCH 07/56] add DDSTORE_GPU_SYNC --- README.md | 53 ++++++++++++++- src/pyddstore.pyx | 43 ++++++++++++ test/test_gpu_rdma.py | 152 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 246 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 64fe9d4..d034d4f 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ Register a NumPy array as a named variable. Each rank contributes its local shar | Parameter | Type | Description | |---|---|---| | `name` | `str` | Variable identifier | -| `arr` | `np.ndarray` | C-contiguous 2-D (or 1-D) array. Supported dtypes: `int32`, `int64`, `uint8`, `float32`, `float64`, `bool_` | +| `arr` | `np.ndarray` or `torch.Tensor` | C-contiguous 2-D (or 1-D) array/tensor. Supported dtypes: `int32`, `int64`, `uint8`, `float32`, `float64`, `bool_`/`bool`. A CUDA/HIP tensor registers GPU memory directly — see [GPUDirect RDMA](#gpudirect-rdma-gpu-resident-buffers) below | --- @@ -137,7 +137,7 @@ Read `arr.shape[0]` consecutive rows starting at global index `start` into `arr` | Parameter | Type | Description | |---|---|---| | `name` | `str` | Variable identifier | -| `arr` | `np.ndarray` | Pre-allocated, C-contiguous output buffer | +| `arr` | `np.ndarray` or `torch.Tensor` | Pre-allocated, C-contiguous output buffer. A CUDA/HIP tensor writes the RDMA transfer directly into GPU memory — see [GPUDirect RDMA](#gpudirect-rdma-gpu-resident-buffers) below | | `start` | `int` | Global row index | --- @@ -239,6 +239,42 @@ See [test/test_method2_core.py](test/test_method2_core.py) / [test/test_method2_ `ddstore_width` grouping (below) is not currently supported with `method=2` — every core rank in `comm` is treated as one group. +## GPUDirect RDMA (GPU-resident buffers) + +`add()` and `get()` accept a CUDA/HIP `torch.Tensor` in place of a NumPy array, letting RDMA read from or write directly into GPU memory — no host staging buffer, no `.cpu()`/`.to(device)` copy. Requires `method=1` or `2`, `DDSTORE_FABRIC=cxi` (the `hsn` provider does not support this), and a CUDA- or ROCm/HIP-enabled PyTorch build. + +```python +import torch +data = torch.rand(1024, 64, dtype=torch.float32, device="cuda") +store.add("features", data) # GPU source -- no host copy + +out = torch.empty((1, 64), dtype=torch.float32, device="cuda") +store.get("features", out, start=2048) # GPU destination -- no host copy +``` + +Passing a GPU tensor to `add()` registers a **raw pointer into your tensor's own storage — no copy is made.** You must keep that tensor alive (not garbage-collected, not reused) for as long as the variable stays registered, i.e. until `free()`. `PyDDStore` holds its own reference internally as a safety net, but calling `add()` again for the same variable name with a GPU tensor is rejected outright rather than silently dropping the earlier reference. This differs from the NumPy path, where `add()` always makes a private copy and the caller's array can be freed or reused immediately after the call returns. `get()`'s destination buffer has no such caveat — it's yours as usual. + +Not supported: `init()`/`update()` (the incremental-fill path) remain host-only; `method=0` (MPI RMA) does not support GPU buffers on either `add()` or `get()`. Both raise a clear error naming the actual requirement if you try. + +See [test/test_gpu_rdma.py](test/test_gpu_rdma.py) for runnable examples covering both directions, both libfabric methods, and the negative/error cases, and the `--gpu-dest`/`--gpu-source` flags on [examples/vae/vae-ddp.py](examples/vae/vae-ddp.py) / [examples/vae/vae_extra_train.py](examples/vae/vae_extra_train.py) / [examples/vae/vae_core_server.py](examples/vae/vae_core_server.py) for a full DDP training example using it. + +### `DDSTORE_GPU_SYNC` + +GPU kernels execute asynchronously: a compute kernel that just wrote to (or is about to read) a buffer may not have fully retired by the time that buffer is handed to RDMA. On at least one ROCm+CXI build, this produced a real, confirmed bug: the RDMA transfer reported success, but the destination buffer could still show stale, pre-transfer content, because the GPU's cache hadn't been reconciled with the external NIC write. Under sustained, real-workload conditions (not just short unit tests) this showed up as hard GPU faults, not just wrong data. To guard against this, `PyDDStore` synchronizes the GPU device before registering a buffer for RDMA whenever needed: + +| Value | Behavior | +|---|---| +| `auto` (default) | Synchronize on ROCm/HIP builds of PyTorch, skip on CUDA builds | +| `always` | Always synchronize, on any platform | +| `never` | Never synchronize — only set this once you've independently verified your workload is safe without it | + +The `auto` default reflects what's actually been observed, not a platform guarantee: NVIDIA's long-hardened GPUDirect RDMA stack has shown no evidence of this race so far, but that evidence comes from lighter testing than what exposed it on ROCm (a real, sustained training loop, not just short unit tests) — so treat it as "no evidence of a problem on CUDA," not "proven safe on CUDA." Synchronizing has a real performance cost: it's a blocking, whole-device sync before every GPU-buffer `add()`/`get()` call, which can serialize GPU compute against RDMA transfers when called at high frequency (e.g. once per sample in a data loader). Set `DDSTORE_GPU_SYNC=always` for extra safety on CUDA too; set `never` only after confirming it's unnecessary for your specific workload and platform. + +```bash +export DDSTORE_GPU_SYNC=always # force the safety margin everywhere +export DDSTORE_GPU_SYNC=never # disable it (only if you've verified it's safe) +``` + ## Partitioned / Sub-communicator Usage `PyDDStore` itself always spans the full communicator you pass it — there is no built-in "ranks per group" option. To run several independent stores side by side (e.g. one per node), split `comm` yourself before constructing `PyDDStore`, giving each group its own sub-communicator. Each group then holds a full replica of the dataset, partitioned across its own members. @@ -269,6 +305,12 @@ See [examples/vae/distdataset.py](examples/vae/distdataset.py) for a `torch.util mpirun -n 4 python examples/vae/vae-ddp.py ``` +`vae-ddp.py` and `vae_extra_train.py`/`vae_core_server.py` (the [method=2 split](#file-based-handshake-method2) variant) also accept `--gpu-dest`/`--gpu-source` to exercise [GPUDirect RDMA](#gpudirect-rdma-gpu-resident-buffers) end-to-end in a real training loop — `--gpu-dest` allocates the fetched batch directly on the training device, `--gpu-source` stores the local shard GPU-resident too: + +```bash +DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --gpu-dest --gpu-source +``` + ## Testing ### Unit tests (pytest) @@ -291,10 +333,17 @@ mpirun -n 1 python -m pytest test/test_single.py -v mpirun -n 4 python -m pytest test/test_multirank.py -v ``` +**GPUDirect RDMA** — requires a live `cxi` fabric and a CUDA/HIP GPU per rank (skipped automatically otherwise); see [GPUDirect RDMA](#gpudirect-rdma-gpu-resident-buffers): + +```bash +DDSTORE_FABRIC=cxi mpirun -n 2 python -m pytest test/test_gpu_rdma.py -v +``` + | Test file | Min ranks | What is tested | |---|---|---| | `test/test_single.py` | 1 | All dtypes, `add`/`get`, `init`/`update`/`get`, error handling, double `free()` | | `test/test_multirank.py` | 2 (4 recommended) | Remote reads, shard boundaries, multiple variables, `ddstore_width` grouping | +| `test/test_gpu_rdma.py` | 2 | GPU-resident `add()`/`get()` in both directions, both libfabric methods, negative/error cases | ### Integration scripts diff --git a/src/pyddstore.pyx b/src/pyddstore.pyx index b5fca99..352c493 100644 --- a/src/pyddstore.pyx +++ b/src/pyddstore.pyx @@ -58,6 +58,40 @@ def _hmem_iface_for(tensor): import torch return _FI_HMEM_ROCR if torch.version.hip is not None else _FI_HMEM_CUDA +def _should_sync_before_rdma(tensor): + """Whether to torch.cuda.synchronize() a GPU buffer before handing it + to RDMA (see the sync call sites in add()/get() for what this guards + against). Controlled by DDSTORE_GPU_SYNC (case-insensitive): + "always" -- always sync. + "never" -- never sync (only override this if you've independently + confirmed your workload is safe without it -- see below). + "auto" (default, or any other/unset value) -- sync on ROCm/HIP + builds of torch, skip on CUDA builds. + + Rationale for the auto default: confirmed by direct testing that + ROCm's HMEM-over-CXI path needs this sync -- without it, a GPU + buffer's RDMA transfer can be silently masked by stale GPU cache + content from a preceding, not-yet-retired compute-kernel write to the + same memory (observed as both silent data corruption in small tests + and HSA_STATUS_ERROR_EXCEPTION hardware faults in a real, sustained + training loop). CUDA has not shown this failure in either an + equivalent poison-value test or one real training run, plausibly + because NVIDIA's decade-hardened GPUDirect RDMA stack already + enforces this coherence transparently -- but that evidence is narrower + than what exposed the ROCm bug (a real training loop, not just + isolated tests), so this is treated as "no evidence of the problem on + CUDA yet" rather than "proven unnecessary on CUDA". DDSTORE_GPU_SYNC + exists specifically so this default can be overridden the moment + either direction needs it, without a code change. + """ + import torch + override = os.environ.get("DDSTORE_GPU_SYNC", "auto").strip().lower() + if override == "always": + return True + if override == "never": + return False + return torch.version.hip is not None + def _check_gpu_fabric_preconditions(int method, str what): """Shared method=1/2 + DDSTORE_FABRIC=cxi precondition check for a GPU (CUDA/HIP) buffer passed to add() or get(). `what` customizes the error @@ -188,6 +222,11 @@ cdef class PyDDStore: "active in C++ while its Python keepalive reference is " "replaced here, risking a dangling pointer)" % name) import torch + # Flush any pending/async GPU compute-kernel writes to `arr` + # before handing it to RDMA -- see _should_sync_before_rdma() + # for what this guards against and why it's conditional. + if _should_sync_before_rdma(arr): + torch.cuda.synchronize(device=arr.device) iface = _hmem_iface_for(arr) ptr = arr.data_ptr() nrows = arr.shape[0] @@ -240,6 +279,10 @@ cdef class PyDDStore: _check_gpu_fabric_preconditions(self.method, "GPU destination buffer") assert arr.is_contiguous() import torch + # See _should_sync_before_rdma() / the matching comment in + # add() for what this guards against and why it's conditional. + if _should_sync_before_rdma(arr): + torch.cuda.synchronize(device=arr.device) iface = _hmem_iface_for(arr) ptr = arr.data_ptr() if arr.dtype == torch.int32: diff --git a/test/test_gpu_rdma.py b/test/test_gpu_rdma.py index 2059e28..02ed2b0 100644 --- a/test/test_gpu_rdma.py +++ b/test/test_gpu_rdma.py @@ -60,6 +60,158 @@ def test_get_into_gpu_tensor_cxi(comm, monkeypatch): store.free() +@gpu_required +def test_get_into_gpu_tensor_cxi_compute_kernel_read(comm, monkeypatch): + """Diagnostic: does reading the RDMA destination via a GPU COMPUTE + KERNEL (not a .cpu() DMA-engine copy) crash/fault, unlike every other + test in this file which always reads back via .cpu()? A real training + loop (examples/vae/vae-ddp.py --gpu-dest) feeds the destination tensor + directly into model(data) -- a compute-kernel read -- and hit + HSA_STATUS_ERROR_EXCEPTION hardware faults at real batch-loop scale, + something none of the .cpu()-based tests in this file have ever + reproduced. Hypothesis: the NIC's P2P write into GPU memory isn't + visible/coherent to compute cores (cache/TLB gap) the way it is to the + DMA engine .cpu() uses -- untested by every other test here. Also loops + many iterations back-to-back (no pauses) to mirror DataLoader's rapid + per-sample get() calls, in case repeated register/deregister at a + reused address (PyTorch's allocator likely returns the same block each + time for same-shape torch.empty() in a tight loop) is a contributing + factor rather than the compute-kernel read alone. + """ + monkeypatch.setenv("DDSTORE_FABRIC", "cxi") + rank = comm.Get_rank() + size = comm.Get_size() + if size < 2: + pytest.skip("requires at least 2 ranks for a genuine remote read") + nrows, ncols = 8, 4 + n_iters = 200 + + store = dds.PyDDStore(comm, method=1) + data = np.full((nrows, ncols), float(rank + 1), dtype=np.float32) + store.add("x", data) + + store.epoch_begin() + target_rank = (rank + 1) % size + expected = float(target_rank + 1) + for i in range(n_iters): + # torch.empty (not full/poisoned): mirrors DistDataset.get()'s real + # allocation exactly, and a fresh, uninitialized block is what a + # compute kernel would actually read if the transfer no-op'd -- + # closer to the real crash scenario than a poisoned buffer. + out = torch.empty((1, ncols), dtype=torch.float32, device="cuda") + store.get("x", out, start=target_rank * nrows) + # GPU compute-kernel read (not .cpu()): elementwise op launches a + # real kernel touching `out`'s memory from the compute cores. + diff = (out - expected).abs().sum() + # Forces the host to wait for the kernel and surfaces any async + # HIP error at this point (torch raises a RuntimeError mentioning + # the HIP error, or the process aborts, same as the real crash). + torch.cuda.synchronize() + if i % 50 == 0: + print(f"[rank {rank}] iter={i} diff={diff.item()}", flush=True) + store.epoch_end() + print(f"[rank {rank}] completed {n_iters} iterations without a HIP error", flush=True) + store.free() + + +@gpu_required +def test_get_into_gpu_tensor_cxi_matrix(comm, monkeypatch): + """Isolates which factor actually determines pass/fail: buffer + allocation method (torch.empty, uninitialized vs torch.full, poisoned + via a GPU compute-kernel write) crossed with readback method (.cpu() + DMA copy vs GPU compute-kernel read + torch.cuda.synchronize()). + test_get_into_gpu_tensor_cxi (poison + .cpu()) reliably fails on + Frontier; test_get_into_gpu_tensor_cxi_compute_kernel_read (empty + + compute-kernel read) just passed cleanly, 200/200 iterations, diff=0.0. + This runs all 4 combinations back-to-back in one job to find out which + axis (allocation vs readback) actually matters, rather than guessing. + """ + monkeypatch.setenv("DDSTORE_FABRIC", "cxi") + rank = comm.Get_rank() + size = comm.Get_size() + if size < 2: + pytest.skip("requires at least 2 ranks for a genuine remote read") + nrows, ncols = 8, 4 + POISON = -999.0 + + store = dds.PyDDStore(comm, method=1) + data = np.full((nrows, ncols), float(rank + 1), dtype=np.float32) + store.add("x", data) + store.epoch_begin() + + target_rank = (rank + 1) % size + expected = float(target_rank + 1) + results = {} + for alloc in ("empty", "poison"): + for readback in ("cpu", "kernel"): + if alloc == "empty": + out = torch.empty((1, ncols), dtype=torch.float32, device="cuda") + else: + out = torch.full((1, ncols), POISON, dtype=torch.float32, device="cuda") + store.get("x", out, start=target_rank * nrows) + if readback == "cpu": + snapshot = out.cpu() + ok = bool(torch.all(snapshot == expected)) + detail = snapshot.tolist() + else: + diff = (out - expected).abs().sum() + torch.cuda.synchronize() + ok = bool(diff.item() == 0.0) + detail = f"diff={diff.item()}" + key = f"alloc={alloc},readback={readback}" + results[key] = ok + print(f"[rank {rank}] {key} ok={ok} detail={detail}", flush=True) + + store.epoch_end() + gathered = comm.gather(results, root=0) + if rank == 0: + print(f"[rank 0] ALL RESULTS: {gathered}", flush=True) + store.free() + # Fail loudly with the full matrix visible in the log even if only one + # combination is wrong -- this test is diagnostic, not a pass/fail gate. + assert all(results.values()), f"[rank {rank}] matrix results: {results}" + + +@gpu_required +def test_get_into_gpu_tensor_cxi_sync_before_get(comm, monkeypatch): + """Follow-up to test_get_into_gpu_tensor_cxi_matrix's finding: RDMA into + a torch.full()-poisoned (GPU-kernel-written) buffer fails, but into a + torch.empty() (untouched) buffer works -- readback method is + irrelevant. Hypothesis: a cache-coherency gap where the NIC's RDMA + write doesn't invalidate whatever the GPU cache still holds from the + prior compute-kernel write. Tests whether an explicit + torch.cuda.synchronize() between the poisoning kernel and the RDMA + get() call (forcing the kernel write to fully retire/flush first) is + enough to fix it. + """ + monkeypatch.setenv("DDSTORE_FABRIC", "cxi") + rank = comm.Get_rank() + size = comm.Get_size() + if size < 2: + pytest.skip("requires at least 2 ranks for a genuine remote read") + nrows, ncols = 8, 4 + POISON = -999.0 + + store = dds.PyDDStore(comm, method=1) + data = np.full((nrows, ncols), float(rank + 1), dtype=np.float32) + store.add("x", data) + store.epoch_begin() + + target_rank = (rank + 1) % size + expected = float(target_rank + 1) + + out = torch.full((1, ncols), POISON, dtype=torch.float32, device="cuda") + torch.cuda.synchronize() # <-- the fix under test: flush the poison write first + store.get("x", out, start=target_rank * nrows) + snapshot = out.cpu() + ok = bool(torch.all(snapshot == expected)) + print(f"[rank {rank}] sync-before-get: ok={ok} got={snapshot.tolist()}", flush=True) + + store.epoch_end() + store.free() + assert all_passed(comm, ok) + + @gpu_required def test_get_into_gpu_tensor_cxi_large(comm, monkeypatch): """Diagnostic: same as test_get_into_gpu_tensor_cxi but with a transfer From 15152de76f29c8c3ea94e1e6ce75345ddaf9e8b1 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Thu, 3 Sep 2026 17:15:44 -0400 Subject: [PATCH 08/56] profile --- examples/vae/vae-ddp.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/examples/vae/vae-ddp.py b/examples/vae/vae-ddp.py index 8f182e8..8470683 100644 --- a/examples/vae/vae-ddp.py +++ b/examples/vae/vae-ddp.py @@ -4,6 +4,7 @@ ## Do not reorder these imports. import argparse import os +import time import torch import torch.utils.data from torch import optim @@ -145,11 +146,31 @@ ) +# VAE_PROFILE=1 splits each epoch's wall time into "fetch" (time spent +# inside the DataLoader producing a batch -- __getitem__/get()/collate) vs +# "compute" (forward/backward/optimizer.step()), to see whether a data- +# loading change (e.g. --gpu-dest/--gpu-source) is actually moving the +# needle relative to the rest of the step, rather than guessing from total +# wall time alone. Off by default -- adds one time.perf_counter() pair per +# batch, negligible but not zero. +PROFILE = os.environ.get("VAE_PROFILE") == "1" + + def train(epoch): model.train() train_loss = 0 + fetch_time = 0.0 + compute_time = 0.0 train_loader.dataset.ddstore.epoch_begin() - for batch_idx, (data, _) in enumerate(train_loader): + data_iter = iter(train_loader) + batch_idx = 0 + while True: + t0 = time.perf_counter() + try: + data, _ = next(data_iter) + except StopIteration: + break + t1 = time.perf_counter() train_loader.dataset.ddstore.epoch_end() # print(rank, device) data = data.to(device) @@ -165,6 +186,14 @@ def train(epoch): # print(rank, "backward") optimizer.step() # print(rank, "step") + # Skip epoch 1: CUDA/HIP kernel compilation, MIOpen/cuDNN algo + # selection, and allocator warmup make it dominated by one-time + # costs unrelated to steady-state fetch/compute timing. + if PROFILE and epoch > 1: + torch.cuda.synchronize(device=device) + t2 = time.perf_counter() + fetch_time += t1 - t0 + compute_time += t2 - t1 if batch_idx % args.log_interval == 0: print( "Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}".format( @@ -177,6 +206,7 @@ def train(epoch): ) train_loader.dataset.ddstore.epoch_begin() + batch_idx += 1 train_loader.dataset.ddstore.epoch_end() if rank == 0: @@ -185,6 +215,13 @@ def train(epoch): epoch, train_loss / len(train_loader.dataset) ) ) + if PROFILE and epoch > 1: + print( + "[profile] epoch {}: fetch={:.3f}s compute={:.3f}s".format( + epoch, fetch_time, compute_time + ), + flush=True, + ) def test(epoch): From 6212df1a252fb0511ef865a3ee819582ed327476 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Thu, 3 Sep 2026 18:21:06 -0400 Subject: [PATCH 09/56] update README --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index d034d4f..a1293c0 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,16 @@ export DDSTORE_GPU_SYNC=always # force the safety margin everywhere export DDSTORE_GPU_SYNC=never # disable it (only if you've verified it's safe) ``` +## Known Limitations + +### Multiple `srun` steps in one job (`method=2`, `cxi`) + +On Frontier, `cxi` requires `#SBATCH --network=job_vni` (or `single_node_vni`) for `method=2`'s separate core/extra `srun` steps to reach each other. Even with that set, a later step in a job with several sequential steps can occasionally fail to start; the cause isn't fully understood. If you hit this, use fewer sequential steps per job, or use `method=1` (single job step). + +### GPU-to-GPU RDMA performance on AMD/ROCm + +On Frontier, the synchronization `DDSTORE_GPU_SYNC` performs before each RDMA call (needed for correctness) can outweigh the benefit of skipping the host copy for small, per-sample transfers — GPU-to-GPU has not shown a speed advantage there yet, though results are correct either way. Larger, batched transfers should benefit more; that usage pattern isn't built yet. Perlmutter skips this sync by default and hasn't shown the same slowdown, but has been tested less. + ## Partitioned / Sub-communicator Usage `PyDDStore` itself always spans the full communicator you pass it — there is no built-in "ranks per group" option. To run several independent stores side by side (e.g. one per node), split `comm` yourself before constructing `PyDDStore`, giving each group its own sub-communicator. Each group then holds a full replica of the dataset, partitioned across its own members. From bdc58f3a2fdb11303248bc5edd8f4ee57541f378 Mon Sep 17 00:00:00 2001 From: Jong Youl Choi Date: Thu, 3 Sep 2026 16:04:29 -0700 Subject: [PATCH 10/56] fix performance issue --- examples/vae/distdataset.py | 57 ++++++++++++-- include/common.h | 14 ++++ include/ddstore.hpp | 63 +++++++++++++++ src/common.cxx | 151 +++++++++++++++++++++++------------- src/pyddstore.pyx | 30 +++++++ 5 files changed, 253 insertions(+), 62 deletions(-) diff --git a/examples/vae/distdataset.py b/examples/vae/distdataset.py index 203fc0c..ae489d1 100644 --- a/examples/vae/distdataset.py +++ b/examples/vae/distdataset.py @@ -112,6 +112,35 @@ def __init__(self, data, label, comm=MPI.COMM_WORLD, ddstore_width=None, self.ddstore.add(f"{self.label}data", self.data) self.ddstore.add(f"{self.label}labels", self.labels) + # Pre-allocate a pool of GPU destination buffers for get() calls. + # + # Problem: DataLoader (num_workers=0) calls __getitem__ batch_size times + # sequentially and keeps all returned tensors alive until collate runs. + # If get() allocates a fresh torch.empty() each call, PyTorch's caching + # allocator gives batch_size distinct pointers. Each distinct pointer + # triggers fi_mr_regattr+fi_mr_bind+fi_mr_enable (expensive on GPU/HSA), + # defeating the recv_mr cache in common.cxx. + # + # Solution: pre-allocate a contiguous (POOL, 784) tensor so all slices + # share one MR registration. Slices are handed out round-robin so + # all batch_size concurrent live tensors have distinct pointers (no + # aliasing) while staying inside the single registered region. + # POOL must be >= the DataLoader's batch_size; 256 covers the default + # 128 with headroom. Only used when device is not None (GPU path). + # Only safe with num_workers=0 (single-threaded DataLoader). + _POOL = 256 + if device is not None: + self._val_pool = torch.empty( + (_POOL, 28 * 28), dtype=torch.float32, device=device + ) + self._val_pool_idx = 0 + # Pre-register the full pool as a single MR so all row-slices + # share one fi_mr_regattr instead of one per __getitem__ call. + self.ddstore.prefetch_recv_mr(f"{self.label}data", self._val_pool) + else: + self._val_pool = None + self._val_pool_idx = 0 + def len(self): return self.total_ns @@ -126,18 +155,19 @@ def get(self, idx, device=None): # complexity for no benefit. label = np.zeros(1, dtype=np.int32) if device is not None: - # RDMA fully overwrites this buffer, so torch.empty (not - # zeros): for MNIST's mostly-zero background pixels, an - # all-zeros buffer would look deceptively plausible even if the - # read silently no-op'd. - val = torch.empty((1, 28 * 28), dtype=torch.float32, device=device) + # Take the next slice from the pool (round-robin). All slices + # share one MR registration → recv_mr cache always hits after the + # first call. No clone() needed: each slice is a distinct pointer + # so the DataLoader can hold all batch_size results simultaneously + # without aliasing. + val = self._val_pool[self._val_pool_idx : self._val_pool_idx + 1] + self._val_pool_idx = (self._val_pool_idx + 1) % self._val_pool.shape[0] else: val = np.zeros((1, 28 * 28), dtype=np.float32) val = np.ascontiguousarray(val) assert val.data.contiguous self.ddstore.get(f"{self.label}data", val, idx) self.ddstore.get(f"{self.label}labels", label, idx) - # print("rank", self.rank, "fetching idx", idx) if device is None: val = torch.tensor(val) val = torch.reshape(val, (1, 28, 28)) @@ -179,6 +209,17 @@ def __init__(self, label, handshake_dir, n_core, device=None): "which is not a perfect square (expected a flattened square image)" ) + _POOL = 256 + if device is not None: + self._val_pool = torch.empty( + (_POOL, self.data_disp), dtype=torch.float32, device=device + ) + self._val_pool_idx = 0 + self.ddstore.prefetch_recv_mr(f"{self.label}data", self._val_pool) + else: + self._val_pool = None + self._val_pool_idx = 0 + def len(self): return self.total_ns @@ -191,8 +232,8 @@ def get(self, idx, device=None): # Label stays host-only regardless of `device` -- see DistDataset.get(). label = np.zeros(1, dtype=np.int32) if device is not None: - # torch.empty, not zeros -- see DistDataset.get() for why. - val = torch.empty((1, self.data_disp), dtype=torch.float32, device=device) + val = self._val_pool[self._val_pool_idx : self._val_pool_idx + 1] + self._val_pool_idx = (self._val_pool_idx + 1) % self._val_pool.shape[0] else: val = np.zeros((1, self.data_disp), dtype=np.float32) val = np.ascontiguousarray(val) diff --git a/include/common.h b/include/common.h index 44f2185..9abc28e 100644 --- a/include/common.h +++ b/include/common.h @@ -66,6 +66,20 @@ extern "C" int recv_hmem_iface; struct fid_mr *mr; struct fid_mr *recv_mr; + /* Cached recv-side MR region: the registered range is + * [recv_mr_base, recv_mr_base + recv_mr_reg_len). Any recv_data + * pointer that falls within this range with recv_data_len bytes + * fitting inside it can reuse recv_mr without re-registration. + * + * This covers both the single-buffer case (recv_data == recv_mr_base, + * recv_data_len == recv_mr_reg_len) and the pool-slice case, where + * Python pre-allocates a (POOL, disp) tensor and hands get() a + * different row-slice each call. All slices share one MR because + * they all lie within the same allocation. + * + * Initialised to NULL/0 so the first call always registers. */ + char *recv_mr_base; + size_t recv_mr_reg_len; uint64_t key; uint64_t *remote_key; uint64_t *remote_address; diff --git a/include/ddstore.hpp b/include/ddstore.hpp index 0d4b58d..f2789a0 100644 --- a/include/ddstore.hpp +++ b/include/ddstore.hpp @@ -479,6 +479,69 @@ class DDStore } } + /* Pre-register a GPU buffer as the recv MR for variable `name`. + * + * Call once with the full pool tensor before the first get() call. + * read_from_remote() reuses this registration for any recv_data pointer + * that falls within [buffer, buffer + nrows*disp*sizeof(T)), so all + * pool slices share one fi_mr_regattr call instead of one per slice. + * No-op for hmem_iface==0 (host path — MR registration is cheap there). */ + template + void prefetch_recv_mr(std::string name, T *buffer, long nrows, int disp, + int hmem_iface) + { + if (hmem_iface == 0) + return; /* host path: no pre-registration needed */ + + if (this->method != 1 && this->method != 2) + return; /* MPI_Win path has no fabric MR */ + + const VarInfo_t& varinfo = this->varlist.at(name); + struct fabric_state *fs = varinfo.fabric_state; + if (!fs) + return; + + /* Close any existing recv MR before registering the new region. */ + if (fs->recv_mr) + { + fi_close(&fs->recv_mr->fid); + fs->recv_mr = NULL; + } + + size_t reg_len = (size_t)nrows * disp * sizeof(T); + struct iovec iov = {(void *)buffer, reg_len}; + struct fi_mr_attr attr; + memset(&attr, 0, sizeof(attr)); + attr.mr_iov = &iov; + attr.iov_count = 1; + attr.access = FI_READ; + attr.iface = (enum fi_hmem_iface)hmem_iface; + attr.device.reserved = 0; + int mr_rc = fi_mr_regattr(fs->domain, &attr, 0, &fs->recv_mr); + if (mr_rc != FI_SUCCESS) + throw std::runtime_error( + std::string("prefetch_recv_mr fi_mr_regattr failed: ") + + fi_strerror(mr_rc)); + + if (is_mr_endpoint(fs)) + { + int rc = fi_mr_bind(fs->recv_mr, &fs->signal->fid, 0); + if (rc != FI_SUCCESS) + throw std::runtime_error( + std::string("prefetch_recv_mr fi_mr_bind failed: ") + + fi_strerror(rc)); + rc = fi_mr_enable(fs->recv_mr); + if (rc != FI_SUCCESS) + throw std::runtime_error( + std::string("prefetch_recv_mr fi_mr_enable failed: ") + + fi_strerror(rc)); + } + + /* Record the registered region so read_from_remote()'s range check hits. */ + fs->recv_mr_base = (char *)buffer; + fs->recv_mr_reg_len = reg_len; + } + private: int method; // 0: MPI, 1: libfabric, 2: file-based handshake (libfabric transport) diff --git a/src/common.cxx b/src/common.cxx index 941445a..6d210ba 100644 --- a/src/common.cxx +++ b/src/common.cxx @@ -660,10 +660,6 @@ int handshake(struct fabric_state *fabric_state, MPI_Comm comm) int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset) { - // register dest buffer; close previous recv MR first to avoid leaking it - if (fabric_state->recv_mr) - fi_close(&fabric_state->recv_mr->fid); - bool recv_is_hmem = fabric_state->recv_hmem_iface != FI_HMEM_SYSTEM; if (recv_is_hmem && !is_hmem_capable(fabric_state)) { @@ -671,61 +667,108 @@ int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset return 1; } - int mr_rc; - if (recv_is_hmem) - { - /* GPU destination buffer (ROCr on AMD, CUDA on NVIDIA -- whichever - * iface the caller set). No host-staged fallback exists: either - * fi_mr_regattr succeeds and fi_read() below DMAs straight into - * device memory, or it fails loudly here (checked below). */ - struct iovec iov = {fabric_state->recv_data, fabric_state->recv_data_len}; - struct fi_mr_attr attr; - memset(&attr, 0, sizeof(attr)); - attr.mr_iov = &iov; - attr.iov_count = 1; - attr.access = FI_READ; - attr.iface = (enum fi_hmem_iface)fabric_state->recv_hmem_iface; - attr.device.reserved = 0; /* ROCr/CUDA both resolve the device from the pointer */ - mr_rc = fi_mr_regattr(fabric_state->domain, &attr, 0, &fabric_state->recv_mr); - } - else - { - mr_rc = fi_mr_reg( - fabric_state->domain, - fabric_state->recv_data, - fabric_state->recv_data_len, - FI_READ, - 0, - 0, - 0, - &fabric_state->recv_mr, - NULL); - } - if (mr_rc != FI_SUCCESS) - { - fprintf(stderr, "%s failed: %s\n", - recv_is_hmem ? "fi_mr_regattr" : "fi_mr_reg", - fi_strerror(mr_rc)); - return 1; - } - - /* CXI (FI_MR_ENDPOINT): bind and enable recv MR before use. No-op for - * hsn/verbs/gni/psm2 (is_mr_endpoint() is false for those). */ - if (is_mr_endpoint(fabric_state)) - { - int rc_mr = fi_mr_bind(fabric_state->recv_mr, &fabric_state->signal->fid, 0); - if (rc_mr != FI_SUCCESS) + /* Cache the recv MR by registered region rather than exact pointer. + * + * The cache hits when recv_data falls within the previously registered + * region [recv_mr_base, recv_mr_base + recv_mr_reg_len) AND the transfer + * length fits within it. This covers two cases: + * + * 1. Single buffer reused across batches (recv_data == recv_mr_base): + * exact match, always hits after first call. + * + * 2. Pool of slices from one contiguous allocation: Python pre-allocates + * a (POOL, disp) tensor; each __getitem__ call takes a different row + * slice. All slices share the same base allocation, so their pointers + * lie within [recv_mr_base, recv_mr_base + recv_mr_reg_len). On the + * first call we register the full allocation (recv_data_len covers one + * row; we extend the registration to the full pool via the stored + * recv_mr_reg_len). Actually for pool slices the caller sets + * recv_data_len to the row size, and recv_data to a row pointer -- + * we register only that row on the first call, then on subsequent + * calls we check whether the new pointer falls in the same region. + * Since pool rows are contiguous and fixed-size, consecutive pointers + * differ by exactly recv_data_len, so they are NOT in the same region + * unless we register the whole pool. + * + * To handle the pool case efficiently, the Python side now passes the + * full pool allocation as recv_data/recv_data_len on the first call + * (via the pool base pointer and total size), and the slices are handled + * by the range check below. + * + * Simpler model: register recv_data..recv_data+recv_data_len on miss, + * and on subsequent calls re-use the MR if the new buffer is a subset of + * the cached region. */ + char *cur_base = fabric_state->recv_data; + size_t cur_len = fabric_state->recv_data_len; + bool in_cached_region = + (fabric_state->recv_mr != NULL) && + (cur_base >= fabric_state->recv_mr_base) && + (cur_base + cur_len <= fabric_state->recv_mr_base + fabric_state->recv_mr_reg_len); + + if (!in_cached_region) + { + /* Close the stale registration before creating a new one. */ + if (fabric_state->recv_mr) + fi_close(&fabric_state->recv_mr->fid); + + int mr_rc; + if (recv_is_hmem) { - fprintf(stderr, "fi_mr_bind (recv) failed: %s\n", fi_strerror(rc_mr)); - return 1; + /* GPU destination buffer (ROCr on AMD, CUDA on NVIDIA -- whichever + * iface the caller set). No host-staged fallback exists: either + * fi_mr_regattr succeeds and fi_read() below DMAs straight into + * device memory, or it fails loudly here (checked below). */ + struct iovec iov = {cur_base, cur_len}; + struct fi_mr_attr attr; + memset(&attr, 0, sizeof(attr)); + attr.mr_iov = &iov; + attr.iov_count = 1; + attr.access = FI_READ; + attr.iface = (enum fi_hmem_iface)fabric_state->recv_hmem_iface; + attr.device.reserved = 0; /* ROCr/CUDA both resolve the device from the pointer */ + mr_rc = fi_mr_regattr(fabric_state->domain, &attr, 0, &fabric_state->recv_mr); } - rc_mr = fi_mr_enable(fabric_state->recv_mr); - if (rc_mr != FI_SUCCESS) + else + { + mr_rc = fi_mr_reg( + fabric_state->domain, + cur_base, + cur_len, + FI_READ, + 0, 0, 0, + &fabric_state->recv_mr, + NULL); + } + if (mr_rc != FI_SUCCESS) { - fprintf(stderr, "fi_mr_enable (recv) failed: %s\n", fi_strerror(rc_mr)); + fprintf(stderr, "%s failed: %s\n", + recv_is_hmem ? "fi_mr_regattr" : "fi_mr_reg", + fi_strerror(mr_rc)); return 1; } - } + + /* CXI (FI_MR_ENDPOINT): bind and enable recv MR before use. No-op for + * hsn/verbs/gni/psm2 (is_mr_endpoint() is false for those). */ + if (is_mr_endpoint(fabric_state)) + { + int rc_mr = fi_mr_bind(fabric_state->recv_mr, &fabric_state->signal->fid, 0); + if (rc_mr != FI_SUCCESS) + { + fprintf(stderr, "fi_mr_bind (recv) failed: %s\n", fi_strerror(rc_mr)); + return 1; + } + rc_mr = fi_mr_enable(fabric_state->recv_mr); + if (rc_mr != FI_SUCCESS) + { + fprintf(stderr, "fi_mr_enable (recv) failed: %s\n", fi_strerror(rc_mr)); + return 1; + } + } + + /* Record the registered region for future range checks. */ + fabric_state->recv_mr_base = cur_base; + fabric_state->recv_mr_reg_len = cur_len; + } /* end !in_cached_region */ void *memory_descriptor = NULL; /* HMEM (device) buffers need their local descriptor passed to fi_read() diff --git a/src/pyddstore.pyx b/src/pyddstore.pyx index 352c493..fd61463 100644 --- a/src/pyddstore.pyx +++ b/src/pyddstore.pyx @@ -126,6 +126,7 @@ cdef extern from "ddstore.hpp": DDStore(int method, string handshake_dir, int n_core) void add[T](string name, T* buffer, long nrows, int disp, int hmem_iface) except + void get[T](string name, long start, long count, T* buffer, int hmem_iface) except + + void prefetch_recv_mr[T](string name, T* buffer, long nrows, int disp, int hmem_iface) except + void epoch_begin() void epoch_end() void free() @@ -329,6 +330,35 @@ cdef class PyDDStore: self.c_ddstore.free() self._gpu_owned_buffers.clear() + def prefetch_recv_mr(self, str name, arr): + """Pre-register a GPU tensor as the recv MR for variable `name`. + + Call once with the full pool tensor (e.g. shape [POOL, disp]) before + the first get() call. read_from_remote() will reuse this registration + for any buffer pointer that falls within the registered region, so all + pool slices share one fi_mr_regattr call instead of one per slice. + No-op for host (non-CUDA) tensors. + """ + if not _is_cuda_tensor(arr): + return # host path: no pre-registration needed + _check_gpu_fabric_preconditions(self.method, "GPU recv pool") + assert arr.is_contiguous() + import torch + cdef size_t ptr = arr.data_ptr() + cdef long nrows = arr.shape[0] + cdef int disp = arr.numel() // arr.shape[0] + cdef int iface = _hmem_iface_for(arr) + if arr.dtype == torch.float32: + self.c_ddstore.prefetch_recv_mr(s2b(name), ptr, nrows, disp, iface) + elif arr.dtype == torch.float64: + self.c_ddstore.prefetch_recv_mr(s2b(name), ptr, nrows, disp, iface) + elif arr.dtype == torch.int32: + self.c_ddstore.prefetch_recv_mr(s2b(name), ptr, nrows, disp, iface) + elif arr.dtype == torch.int64: + self.c_ddstore.prefetch_recv_mr(s2b(name), ptr, nrows, disp, iface) + else: + raise NotImplementedError("prefetch_recv_mr: unsupported dtype %s" % arr.dtype) + def init(self, str name, long nrows, int disp, int itemsize=1): self.c_ddstore.init(s2b(name), nrows, disp, itemsize) From dc2abd2c2956b402e30476309136848c19f0c6a6 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Thu, 3 Sep 2026 22:09:25 -0400 Subject: [PATCH 11/56] logo --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a1293c0..b6c2b4d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # DDStore +DDStore logo + Efficient distributed data loading for distributed data-parallel (DDP) training. Each MPI rank holds a shard of the full dataset in memory. DDStore exposes a global index space so any rank can read any sample via one-sided remote memory access — either MPI RMA (default) or libfabric RDMA — without coordinator synchronization. From 5d60ae7dd522ff43385e380b8d73f8b314ec45fb Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Thu, 3 Sep 2026 22:10:55 -0400 Subject: [PATCH 12/56] logo --- images/DDStore-logo.png | Bin 0 -> 1201093 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 images/DDStore-logo.png diff --git a/images/DDStore-logo.png b/images/DDStore-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e2d4780dc3ca992719386a46c4cdee54e4626689 GIT binary patch literal 1201093 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 JN`meiY5VFwi^*GG{6ok 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≀u4|l2k?z(TQb3I24HfhKG$PWeO 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°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 z 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%0A>ARZwT?nLug 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 z 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

Uhotr;G91eaB2MNSP~# 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�)|nOQqn+y>4c-UJi4w2#D3VFH~lC#|7` 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


4&`Z{#G~0E+9st0?o=B^ zJ&GoK9ogO0#2!VaZnpK#7YBjN(-BjwtYN~btQ3>);GW40?v5+L9t+Py3dLNndO*5V zNcoFAI{44HwD&MM#*F43pHh)31QLl*TM}xOSiSwfTM|yK+R<(gpSTE3Z7gAf4x4h> zv)*As6bc}RnD(B{gQLHPqs)i9@>RtaF$2Wcj(^Oe2MfzF1 z-SLkq?SP|Nn~$Z_(@O3e$4a}@*SlHAAZ#sQ?e_=-mK3(Kk?7P8w6>f1DTF$QGO}wO z;=T{%$xwDQ0ZBa)Llv$AEs8Gz*YdglnHpt7Y)ifaAlbCu(ae*}n8-$P{eMwztTsB+ zTtbG%4=5ZN=CKiZF)KxSS#AmSEt{|YY;D&_w=+nTVrRZOyTf8byd+VD3qWw*GbvL9 zHobs1$VEuw#4r;b${dI$kQzcZqfUVLG~LB_Jjf6(?Zv{lRPj(iWE?jseu3vo`AP)dAw;`ZRk+(kL7%WLrvVpKP(HYQ4ZDyGHN78fJpo^wE8i5xz zN;^+0ut_VIyq7K#1hEgEzXfvrJ&+&&H^5sz4fxELfseW`a8gi@ySXnw>8nG{rO=Pe zqP8cq#*B`@kKx%z8?sy?@Zv{<&v+#8s$YZr^eZ8E{VB@f9$45;zyqPDI2aGpq$~0n z`MFt?M2&s7V{S9GE%_kA8aZ!{d`)KzjU_2}qYwf>CGGaF9snRS>h=cYw)>;}rLO?K z_){SF+dysza3vbO&=$Ak&f7RTF@Ci4CbtuSqtap)U-=u5|Md#+58s8UY=CLM_vKKa zF=Ld<;0)0=8wNzcNIXh~(CBo;6UB*9j$NSj#zpZtcq?UD1^SR8G=1bk z(ukl7v3%R1CS#r~E=s?{U)bAbIAW6v1sw@*9e-C@+&Vd`GP~VLiPsb+5xM-NZcsbu zI-&-y54&UrIDz~bsr8?-@IR0^Cn~X_L}GxesMw(`v|j-yluavr7msO5Y)UY*#C`T2 z0J#tF*?_z%^C@(4v;htR?GWoAC>g0oeUZ?xG5yjO7}0@ER8r`Zy<{%)K+Xx^_(jss z%$nR*r*B|>Y~G4IEu+}S1Ty?$Uk7QIO85EAH$&uB7XDw-uR8|}wBazH$~R);95cR_ zvElIDMLsN2wG8Q^NK!3bZe@Qovz|NxyXy~-y~B4Us@C8a0Hkh(eFw796Z*^0p*LWV zuhf7w92}J*b-EH3I@jK}YX>p$aoEY%fb!3DfkN!~h@nM}9RjoPpoA;Vtx+am(NnF3 zxbS;74!UWN!}G+0!S1HNL)GIeuYYJ`;7?MiAS@h`S{sIbBUT=cmgNkm^pTk(Pw0e;0%qp62y53{OI=-|`>Fv~8b|#fyjUq}~b8?`drZ zv&d8*g3E_r!&?qsCg*s*UdK*Og&+$wST@+E#neagJaL72*5t7ei<;K48TF4Z1T6Y- zQHLhf-w}&4O(FnFA3yFyP&rE}8(uniemUIx4BR;Rb~!ozPo#VhA{%MP`UzD%BmCwnLTl93ZXC&D64;GRaaenW?{U-Ak@$aTb`f$78&XYgENxfTYDS+%ui2F*M z^;5*l_8Mkbt4H=*h<7j)CPUrvVh#{py%x={yBWkX#{QJ>&2Uko1Z&?qv12fl*$`P# zyMPPOIhGp(5zJ+}?Y=+F3r8=V@3{TzVY{1!6soSX^G3E; zP!A5l=RFVn!skN%!X;o9;3IRun3Z;Pni5dQdL#6TG#ZZx*%0;ZcLM+IM}gP>Jf)BW!APvydecZ$BJ(`5^^I;gOy_<*6^{;4_`{tdRaL4FLx?Rj-x?s%*2Ol=2$5;woa_DT0-%W0|B1@Sp_DJf z+@rP4UU&Q4d8WE-HWJRu=+KQlF;eS(>elfxHYK1F$mPbaR_G+P3W!gE-~xba0J|ro z^E2=xh$f)(!3Z2431dvkV?$MC7*C>{FC2uyEW4VBSL@Yy0YTC-FN}zRml}M_6u7#a zVT&2|kxs8NZTlFrSz^DFRq`*nMD{?{z!Q7ruIzPltuL`jGqjC{+|zxYJQ z$=aOK8vz6R!A@t*^#yFyN|dR}?)3gVx%x!x-||j~OzkK)8IN)9>p23{dZicVhmS;D zIjb>4)1Bc+MFh(jB_{OPfQWIvz3Y>?!oLTRcLVrNX&2iP?RcgEBTS+*gh#+!uBI;= z=H)D*Eb4cx#6(Dj#IelCR|TQ)+$PTnG(1$ELF*F!2U$++7&74jN1(1L@|qY7 zh&F(ff7PZQ!v0X!sDJ$(A!t`%rbI^!Jo@yawxvAD%@zg$cJhoz#&!HW|#*UF! zgJ(KWA|~b$aXd(uiT-Gc8vjVJA0oL$mHl#&2SP%J9MbFk9SPOw#$IB0dP z!F7-mgVZF)TB4YGdj|k%7!}4a5=V8beH(uU`2l=rB)%?1Nrq@5_aAqMCA9jPSWmt3&#P)9X^oS<~{wM4OT$n*>(u$oFI@jBLfE`YPYeIM3MjVKs z-^U0GnV=m3q#YQQGjw~9V=YfHElyH=+z==FS$X1eVHAnT06*S(Z(&E9y&dyN{NgP9NIU5U)08` zQdnATRxCRjBYM8!pN;^mKMHa_eHZ`H%OrG1KzC|(hhQr&)EgEA)UhGbj-!D0oLN82MtVv1?&We6%+Cn0aX_?B|{!e6O(-1!3e@Q1$wSFe3a z>j2qx8$6kkhAIJA!5xzTYB)(x*oE30KaAO#N>NQle5?NxZ$ch-42EX>WTm(7HfM;1 z0>HsJpl%^=`5xf+UI#w;rQp+^&U(Kha%X8*CYL5|?f5vN%GhU$DZK;s0bC$(6PqgN z4Hh#XcLR9PCGhXO6nNq%1ONM{A#eLNpq_&J{XQQxL{Q@ej9dW5hOF@bSxA61Qjqw& z+XD~t2;0wcHm75Cg>3?aC?I5eqn(EGgNKFzdvNJ0N1M-wh=-X%^Rv*3Pk7m z0U>(?|Lg|vJ+A|P{HK9C@50{Z0JT)q9;$f85qwrSbs(`eo@w$P>|bbhv_Y=y8eO;0 z@!UMcs@%|(b%B+F`fSMryZ|V*4{Ih&6*zzXcX^+C|9f7%{7Tu|9E(Vic49uP&rSI{ zhLg*xyFCd*2FE9^+pa_`$0uPS!FU#hg0Q2GEX^nw*^A|n)UNXIvwtb~Q-_B=97 z(*t(?|C&9D=@GwoF%h@^S%SLkr4Ax`xC(T5V$UU)n>rvYPK<{1Y}D2^M%o7BH3*N4 zbqnD4amIAo`%9V0wMeqjE(U=^5#s>EdY_2L0$uhNI70aNVU*)#vog-16S6kmO;r#e zDnurhQc>k!LgXm`9t}tZnV0q!w7YfIN4CIX`gIQ!01>--fo@ju+<-$IjvkFLK zzTsOmH0ez!((b}Z5*->4`obSCu+vLxmr748<+_M`9f6Mn`4F_DfC?ZsZCZ%P+TulM zSL3DYQxg3ID{E-7f=BdMB!OCm#0J}IPXu;9fU+M4JS&BXKz6q~NQ@s2Ob^MTP7U(m z$z!i}o3PR{PsIGa3PGtLHz5BbW*ma@UprkJ2xg=Ki*mEa-Z+Y8ElHWGGOsNDcLae$b~OME*5zg zDThKsW~z(w6oP`87dD^B{c}Gk*G~Q|=I#I6Vs~Uhk61Do=_60|&-8xlt^}s~^qq@9 zx0C&uE4IpapG&Y`;u;I1GCsF|WjK=iWnq{GE(Q}5Bv_vk;68{6!VQTXSiX#h`~OPz z%K?tn?lkgbQP)6WQjjy!Sjr&7BvEusTs5jOG$-h^=ChJ%E%G2I^yBAvD>i$7j~5QU zQVNgiF?OgZBqkdZk3)y>r}ggwi5Bvy!wQ{3P?jj3bfdS)sEw1fv@_-`TdeXFen&`k z!U$l(j4CrLFK(X9y?gzD+;Qa};Kr371a=J^i+c2xArsoU{|NWH)iCzz{UOrQ^=`8u zOLg{1zc|i@jTK;A1YlUi>F;WTNT{Q~zwaw;h(Sornc3uqvH#Uc-@jWt+0c0|oq_-H zZHy>q59U$*PM_B1jd{SR3U?NmG4!DuLYt^ko$+DfrIADu7v<^ZCayLn0=+ylY6ff$ zbSsD)*B~+g2Oh)$x&{%MgWE_7;9+^QvKfF5O0OpaXat%sY!v4sVmrLtc)X9qCYZ8B z>}`rDs*pxpaM<@YX8v;dk7bB#0LB$5!%7M49e8BpbzxteajD(%a?v; z{_yQDmD_LsA{<};BoG2y2;ekQ+uNjr#oQ<`>0Mqvi{41rJ23y$PeT6ScTf&Cz~-QxWoIinwXXns ze``|}%S9p?Vk~Bqk07CreOg9;-}6dH>x*`a-fJ|ms&11~Q0gh>ldHgk9)j}Mz6kQ{ z$FzGEjs@6?V_wHXT`Y=SHFn)NmH?r#*uViG$2;Ihe;x9DuLAz??U*))K-plfyY=V@ zquV(6? zKPmUQ^%cDJ*8eF72k)r00D=iDa~ll0-HGq-WFH8=DM1lUx?5CAA_KEX3YKi;V+>YpOgE&Do9TcU+h^Xz@Nc^Ex`34mJx3=p| z+94l(49-}Em(%I%8iN1N}IBilSjyHNr&rYu^WS)}kUmhHUwxa;Q4i^OXMoSt<$-u))8h;4es( ze+lw^-RMjZI})f@WA#e!cY3sRr!P1b?70@Oa718WkEB2T-Zdm8NH~zC!EIb``>4G7 z-ul}lLKP&=NhHdDd+qkXf6EH(S}((Uw^FH$Gb~YZ;4FNS`Xl7gvIe&1VrBW&yhvll zp*(-2yT;V~ws70EZGYzEN00gk(02W{G~vvDwi6GU>tdo{BO6@a|ChM${#RF`oo!A` zP2?O0r5C4br{-_leMe#}Y)$zEE)s|dSzUj^;#&YcYNDXqImR7n zTyn2vc<2JIEJ`X9RVtRJ?il87FrpB!T?a#2j&iQ;#+^va5PmlI_x~vl%Ao}3KRPU0 zF>bfRqH+Y9-c$poh27DC8M~3CO;%W<*6XOma6;nvpx5KFtAj_*03Xf6!+(dP&C^9d zIqkRJq7MT!#3=al+^`~#)uK5{8ks2&C9*{%NBFNPP4X{zJ8cB59L<7|{tSIV z3W^9sPEfgD?#KIH`Y-aK>7jD{u73x}NprmP#mTDJRQhq(eXF&SQfrG}sy-I?e(gLU zTg(Cp>Bsc22R$|YDxv8;R!%SRwI-OCzukz1uvE?UE`gs=8dGb z#{zZa(5hu>e7m1lF{;yqcFupCY-$9r-8^#Am^VTkP=`bobaG>begxCfwZHVWQ*C|*k_BV>|3c$M2RNFKLh;iFNZw*66zH;$9=Ofxqg+K0^lOS%V^gv z671JlxoY&wf?3dZ7hK$fJnu2sd-^9}_akpY{iz=VKKQ37hv(ak_qB})RJ{F6nQ=91 zg$dBME9ZvqX+)OsHVlON3cE{`FX%SDvd#NJRMeAem@ixaU;I~qFMS5;eGh?a0$i>A z@?Gz=JklEm2>YGz-zuuO7o`yt+0--%nNNB0Xmn(+9_RmC2F3x~Hsu2yK!xjFe^|HPit z-=ohpf(j^>XVlXF$A2!nN&Qzk#Koy&kn%DbLM_O6p}_?%>_Ib-pgvZd3{*6{2p*VB zWs;ZPVdlgffni+LGdxoMMV!e(YH&m4Lp<<7!5#2QLKq7f@{e>gz0iqxKu^O~9}0-G zdx75!ya>E8nq00kR9ZQLe%KX&9|iD}EagQ~YxN?$@AL3wEwa*E;&Z{!9?|C+Q<}e| zyAk92D6}ps`mLfxAwJGYA=-7U>aNQCUPj%yB&^0(kB@8>Lwa5I-0Lv%J+t(O$yHB< zy(0)dtkw)lu{==ZzD5g)+Cl*^NcB|)GaPgEf&0)9CPo(jQ# zg5Z?^?nH}Fn_x45T<_@65B?p0H}+>}&zYYAD7`6b#%a|gtXH$(`B}^!uu31RO^!o~ zW$litGA4)}to-TFk??OM=PX`Dofu?EhS&dpKBE~~YiN5PJWRp35ly0qe56nOo4%Le z_;2l`uAs9J6Dl@1$9o_A2i|w{rSlDGR*9HwJyZ2Ruv4F#tZNfEpm}oeHkq|%Mn8*z zF$W>{q_P-mWWJ8}n`44FI2{FZfeRh4t<5skQ&di1xw!c}@aR!;a`I*1?l%b82xMpD zwIsiiV^)-|-WN?Y8L(V?N3DFo6k0U08*v(Qa%+R*ce&!8xe{ejQUb||V%4_J(hU&# zvE1AHr*c>>$*J^vS-iOt`ZUTyzT%1CsPz zQ}^bGpggUdJNy=$pB_<S8o4$03U$}QA(?Y(xUGL5Yh8>ipelg zd(gWy_fF|!hYIMGczTqN^q^nqa~`u7WRLPMJ-zgKq7I*1y?Ug2yAxHq&!+2=Hcn6F zDzsx)lPlhh)3Prl%+-LdLEX>4M+!?BMUT=rZqjbQ2~KQ zhi8kwRDe4;D}aqH*WzOP72N{unOPYG+&P{3(izyEb;o>j7un%{y*~Qy{@rfe7UuV$ zD@rfFohZt-Qdhb|Xf6e{R$Mqb-=`8g0DEj#eQ$)ALF_Q`0N3Xa;QH=^?UvRSqTS@x zH6pSm%Qr#Q>eWsY5m%B%$>>v1UNS^La)`*mAnVe#BSeIlwk#rFqje!;pLhjojDnu%%UUU% z1pDW}{XNJZz8><;zXg2C7eGGaGa(lz;6#9JzlF(0UH$I?zzYBlJ83qW=p&O5&O!>x zQ2{pv^$M`tC-~)`g3U9Yg8BP?0`(2A0k2*`IXs72rAGz>?S4lU0`H9krlTb`6RUXQ zBmXFS!OfL2H~#C^wtoD1J2hNo4Dy#1hXYR z4N&ZVb&x62mIDs}yz2_^@>c_|`f2dW4U~g(kd5w@Foh1?ouQyYNF^|f%o~7r^TN5G;r;IWoxJJt8!DzMySbq3 z0WBIBEj+WA&#(}d^r8Q=#o)o#sZY40m-~l;9y314{IKVP99S^{Q>`E!z!l|6kNC9n zV!Le#Hn!QLP;lh0?k6i+KD>bz`2zt8<;J|lou}`?O^3CwDNP`D9R#;M_k^Rp%aAgm z7E)!k%_yrn{uv_tn~l`%qsoEyPApB-(riy9{qDUw*ti9zhS2W@1)`7;RK+HN%idct zR2DcJG058=>NF}np{m3vDMXsk#rorXQ`>GFSb>y?BTW>vl~~{%orO`5U~2?6=^bP{ zDfC*h&<6uZhP3GH)e?ZD-k@MrQ9O5hGxo8hL)S< z(jg)y^zS;Z@Epre@};Y|8`&lZf_9c1Ao6%XE)cvPkTc|E@7I;xfJqn{o!0YdQFtWB z2H++zK2mFWHCtK?H&GBLF_Rma8@Uq3@-gMWD1>X;HJ6~Yb36IA*4D0~VF&q+hNwjYBPVLmrCJQUOGEItH)f}K=M046>X#DB#We}}-gO2w;L_@^$!qMh$st1_PjO`nvj znf9?Q@wbE`oKP9QF7qegUsB`_+ZAL>wA-Ol)M>K^p54mHzs<=o+amxJ;kxgk3U z!`%ipFYi6sKtEdeVD%6fmmJ1PXU1R|dOte6Ip3@+tyz*{qautXSYEu1Pe$Xmz$vF# znk|#UTJClDB`CLEz(?-*Du{dpwHC0S>60cN1W=);&sdk&7Hlb1uPJJCS{D_DaW~OU zZJB#l5p7droAfpJM}gR#p%U_U(siswv?GV0eV|QsxpcAY{o4I23F{L}7r>JJ-(gJd zaIQ8?j{q9_XLM+5rz}P!aW7Vxivs6DbJ?#mOCQ13!VR!@-8=2dD985&_F|!X;D}EEXrhG z-zZu7MEfZ@cgEfd<;LG}f+(=dMf@?*y|QintMS){`Hn9t?-YQuKwA99%@==_mmdCQ za>rfI#rxj-w{YX?V^Fw3;oKHP>?Ug;evkvfGk|N<1Azcr^Rx2sJTRX?e)hitZ+#>1)R%xyei-B;foo{#rq1{Yz$HS?p;gj# z)D8=YdJDAldjKliiR~u@J^H=9N2TqM*9rNcvqS07BH#PVlNHkogAi@P}ji;+H_4_3^-l;PtteQ5;Vr zO(XVey}>b)(43uT0B~goyz;HU%YPVn_aC858{lBS-8axS^GEF)c$E9!Na~@fEi68d zm{L|QzBd zf=NmtSZbHYKIh35GFEQ(q`w)S8>}%2ieukc-pVKGeDsd3>T%ByA~TZT&PS+l2ORxx zHB;cvRTh;H+h!Z=5`u{YDrQ!m)OTW!BAZ@V+f6xvBB(N9gNm}5QK#BwptUVor^RLX z@psbd6tyq&>y2@ znN+Owd1~H5_EJ#ef+cGq6K8#vqQW@Zg)WLHN-Oj&2@5#9Qhl0KtIINKRD3UhX zW!-)db^Bp#_8u6I2enF_5Qixvh)Z5aVv*Z1wjVIxT%rv1i~(iiCj!A@I|l&Qubltv;ldg9j1Rfw zp(JwLD(M*xsRIW+HeHCyw1NP6!VeoGK~~gQ&iF~k0+kuVLdoB~6}^{W>a;98v@O|% zfp;kek^uWzr$oQXuMZ0J=pSo$c;R_hEcvo;5(XVRHJ=QH3^%6Jz3Mr@4a2EVaHg%^LV3&JtN+|V?V;_)7oop7E} z%Tf90B1hjXr>8ICy!%y1?c2gN^a|N%!vj$5Xb~OP8v#MCg57l=SeJu5GYp3Eo|qaE zjQuL(SM5lqY$1z4Pyn8?OwTDto6oBi)yM2BB0bJM#`T0mUrZKoT~ji=V_f0ah)*rv z%hLc3ix)B0u&umsdJ@kadWt#!I~8b2^iH|wD3hH6EYYHclb6`*+Hss=B-CB2Jdx>kNnwJV|RK3Es{gp zX5H%j){j|jN2X4FUQ?}jHPd8shO<^k-#pij$3j!BGW9V^HQm+Uti3rPSonKw}x_?y_CD|2k z+z7+AkYK{;LW)qnM~FC&%#00SpGNk4}N!VtN|mO4!=co7>tgUbh{=L(Sv;rFpc` z>}4kBp#HOk3YDRrt}hPg{0?ASCF{EY%mF1J-hcHs>6SEJ?8UAtE+7S2`+6U+$c+nXdA__x(QHa`T(z z{eSi)c=x+s%qkB=0a2!Q!@4d6MEPd;VzQby+bKACgm&b?3J6#`B|K(kjuK$D1Ky|O zC#DY=m7_>H|Ahc2@cbdZUY|$LkM+DA8oz$(#$@x#? zJ2wRO0ogS|Cko20?>f0b$PJ=A^1dkF@U@tJ;+L@f(Vu{P;C(avSei`s(e-ZP2CzLBQc%yF^ zQN35keeE4%j$btfZX*Er_F{xc*TY`_FTEG?JwFb-`B%ZZMcF?>JJF&djJkVEB97Df zAll~$xER1dct5y(t?HpLz7h(`zY((ZmADU=>Y(l+JaHv0U+2F92|WJ z_qzGDy!S2t5!2r9)+z#&J(NMwJ+_mnO;zK#@F4tbNNi#6)8sDv0-7{Ux5? ze>pa|O=jEzsr#5`fy~!%vOUI$ybm8Zej{d92n(bDwGvX=7A(C0BC4u>uR$!-wup9_ z*k3VW7zThQjDxdSyrI!~&!PEd7ZCV;hJmxO$e;m97c^^ZrKARnanvxL0L!)T*vzR}cBPMXw&EFnQZhR8j*@_}S;TQti>MSy! z-i7)2gV;OwV1#Lb^Db1FLL>u0woSy_F;I{3r7M^=-tFY5Se~gXT9E<~kyBI@3O0BE zfWM7BekH2>26p&)6ubt&?}NM(s#6y6r7J$20QV^N->X*C+sU<;flOtw+3nCU!!d2) zk~e35#_G)QMBArN>2#`2qy=P$v(Om{x>K9txM!j7D?j5HOVOm;E3;!-U*fR?$GYL< z^5;UIqa8A5CYABgAV{3B5`Ouwj{n%pYtDZNOAF(hL~wwM{HVkKC>QWq^(syQ9%+b8 zsB?wZp$&rwSeGzkpZkZROvM6n$&0R7WgPP(<)Lz9QD9^^^Pdi8zTin4;wd(oED%IR zsV6d*^W`>f4!&2e;&Z{>Z=#||+kqwSLvXw^&$Ro0*v3L?OLVb3&dLRS`v$r!N?YJv zyZK z(gHGcJQT;2_FZkfV%g|GR?{1JCE8BWU#k0JZI6otHvcKaCii?6fvk#ygV9*VzoD$wH*RwOz^y2{E70Y?#Hi`Z`V1ulb+brA2zhQA8 zy%|jg(?5o~Jz9*3{xet81I@`fqZR7a3WFnm$zxX@Z3_zAIoSV>mk*o0539DJ<&Ax{ z&mxPGRyO$H@o(Wn$L}C;5$vv2)^nU>l0w0>Il;;Nf56G0ULE+UnzIH;eFD;G3lJqr zcQ9OP@cX|kzrpG5Kn5T5M?ShSautlIyr9yGCE&+Yh;PK!gkj)d&y~L9O-tKqRKC#r z4;6(KQYQg6+&lb}@_>)|XXU0_Ub*|zcm8wSb^Eh$atxk350I2u!4~j&`Dj)m(EHQ% zPc(-kXe>Y{C9#OV=om|7^(Vn_r4)ITV4ZGDZhHsdB#?K$8TDPi1wQ`S;Ip3#dDvy( z6u>^fJ!8Cxo{O2(f#5#d`Tw(5CzW=zoNbf83+-~#7kmm1pYlY^Kk@U}zV>Gzcf21w zI_&whfSyMLe!yI0)Drz-do2Z{-W&F?op_?>{1bJ)2H6wfX@3d$isu85y$R(QD0j6B zcSTpz!6c7oNBYF?qO5Aa4hS3)__G_3|Na`_$9@L5dKb#(5GZ>s9E9EZA1$6?w5ZuI z{f*d=juA){MZ*-7KIZ2zFdExr?H=%WEuCVzB3QCNZ83!c6japtL~0>%+pTXZ_rKS_ zm!tE)j+wWk%EV3CNktnRvEgbfqvLJFqptS~1q)k7^=Z)U12u4~zZ>u#!!PXvQJk^1 zW&1J?=VKw3Z;T=yS!u$@+Y|CL;pf37YSSj zQBmtwAU812f>};bv4xZt=_3%tOHHIlE}L!We+{p^QA-SrcE#JHYB5T`bxY`hyC z=3q3#)?E?bU7;I7`FT&Xh)f_~55aGe<;mb&L6p`|&+ox7S$A67GoM;hvW<+Yp{Tn8ZgwDw=r$C;YfgmR5cLLj$A5r=C#+8xWm$=_T7%0v(c1IN zK|?NsQ65%W!Nr%MsE&|DickbwX%P|xvEdW3;S*8umDtHU0Q?-PyaB{-gZMMFkQRg$ zV@32~C_i*aY-h3plH|+R@}q5|<&CWJ41XR{j~x){3a^_y4bm-@8|rNPE3KMXPRd6@9olGwzT^= zIMe4R(y^;6Ru439US{5#=MMf0_W23*Mqk8;1rPdb zW8Z87-2@FVoAn`L*(iIGlMJtug|p&yu~rF#xves*eOK*LkCodIdE-iQdgvNNOj1$h z6g%GQ++PQHMehFaKP7jQ05b&AHpKMUkkXIDsM;8!1+lfGfIzh4fHaNaaL*qXe!@Je zJt{p}bzTOz_2EHx!kgV`?WWQOq`4X5cfbKjBd$$QyM7~*n0{rG8)c<)bkiJ`2-^#s z`B7zTE=VMth*9F>$2&6uv`0Qi#yZfo1{>e|7fqfUn(0MI4}_JTRQEanDp841i{e8B zCY}8rEfc_CAWwjhM#=Q(sy>K@PK=YjEAY@en~@$Bf~Zs0Y^Jt)dUo5S4Uxn%F#%pW z@qI5dfgik<)keF^fD_Ew`YfafRwfip!a?u3r|SwP;om&M1+$-W{S)}J!w%0 z1y+2Vky*Vi1wKHaFSU~}{mZLSDT0&u2qQ)$>+(>m)g77lvwMC=HiPT_p#vmmI8k}7#_|wa3 zeHMk9kTQXX=TJmY-~39*+uj5|^M&B^o(9~!4{W6yl+y42$kvood*#EbR<7O1D9vcF zwuOhXtKjpZu+ zkXpof)p=@9HOj`)dsX3auPK5|SVZjckrIg!IaTkaRDVyd`p4vXrxdI{uGWSCxiFQAY z2&A%|_g}>va+XF`qW)q377Nhg6dJYNcWbglx(`Q~aKTs>%SN+z#@9#-Trwj@E+AZn zzvz9`2$Y1_V_EKM3KMv2agn)y=OQdN`rsT1497 zqht3K2d87Bq}@4jJQ@~0y1@lIc0C*({?Lj@Y+Tr~<-9%qOwEiu7i+0r2p6Erbi->4 z=|g2i6crc|o$3rs&?J_wqi(W;2+l_j0QW1@nw)xzlip4Qjsd(v zYJD<#SY7K{?aN>|NB)~=Wu_%l&X_HVsv7~1Z-o(-Iu3x;+Gbt#=%TGLJ^@s=*?xKO(so;O z60qhmz@o7XeCc@Mj0nsOb0D?sEsOZPZk4T3a^TUy+T=(_|H08%jW8a_H$639t^V!_ zw2Y32DZn|Hw_L#hGGl;*Jzv3_C5`P#fxyr4Z|NrF1%c9VK^^}>YVdf$gerTo&-?8C zYh1!hF9zEplBxADG|UwxIW0snqBXrd;}W-Q5)( z*d~u02x5*S_g)%VCpH!{H4ZYICPGk542_LIohIPDSK9AA6gAEZ#f7t!X`#g8>4cIH6-ktxOfoZ9HrQbO;TD91c zoMD+pxjD>@7_w>qk20ACSwR^z4`WUAw#Vcu51~k_C-!&p<<`f#^9?WMNUvmgztYTJ~N1*kNPU^U%IryunGWsl%= zzgG-?)u2&n&xK&A{jNF%mBx4h`=!9qXA|VW9H;}YTp*Jvw3$TjH~X%{vtI(7#AB2d zgx@A}trM4z!z7=%sL=`H2pURBij|SU#V#7%2t!^ExO|@UCuZ#gsS?}j&O+~LX(M=O z>5GL0Lq|HV%%KkDf1qvUL;n|p1S8v`O|mD933FpCee|jXf|%N=Egk-`rzO!mnEjvR zG*rT)^Pyk*X&}+l>%>&+?h6#n0#gJc?MkaQh@JXGumafjrMx0lD&qj${0e{$+^EkZ z<8@9Vo{!3-2u?P+3c@1+6V-#V08Vra(!S{vs&HZp122GRe6;TrgvL*wa~MfMKw?Dl zAn)iV-PpxmKkc8iA#FQ298;Mw-`Lj6`wy5t^)p|{zx3Fz;vNcK_!Ox>_^H_akM9NE z`<@<+KH0sbuuGT~)UvetLeqj{@CKVl$J(|!2n^^KON_W_e?>7V3^HY4lc>>976rNK zvj-s|oWS!p0jGCEUiod1-})t#7rz+zjE{p%1-RY|F4Am_GzcfG2-CwmmluF30=vbj3S>jUxo)Z6 z@W+tv_z~bYeia1=z`-Fz=8!*jG}It@`&Vq+=oxC?4KOl^f#!}F@Z&~u_;M6a-N~49 z$3($nG1{nr;SE0uL@H!^4RWu0qrBvcfxqz=DVHea6wfM;`uHD^pZk^nBLC;@Kg{Eu za4Hk#I*aWlsU~ZUw80p~_R>s!P?`bz7|HDa4{+|}2M#Rkd#-u`pO!vO;Y0bMpQ6q# z#;PF`S1}~pKHH_XBusr@rC~S)!U?sqO}{98u~;amBJ}7b`xF6cn`TJ2kD?253B9~( zOA{R`+9YL4)-CIZB~yoILv%ORvw8E!k1vCn`bBk@kTZJt)1qLzy@q|Ccr&N!I9t|d z)L8ybM_Q;I)33Ma&-tieuL;e%(Av9^t1)a{9|lI9Sq^fCn5_y&9jJ~4{zfQFecL*x zg*dfn-`4|tCR*525wRuWMO%#QHC~MNoc6yo=8U-D@)CTAffpIQ1pqz<IB3?UD0YK8+9AN(Yj5Gayo`SsygwaP;lTy)9&WJt=IrEW0qM!!Gr?x5!mC$f_MpLxemcwQ1KQJzfQsL z2;}`Bj*WnJcycfKti3P;L&lGs`Qz-g+Q!9TAhy5bkM>YnJd(^udy&7lE>N%Z86=sP zF5Br7X`0O;3o(Pn6mEzm_4CcChrtR8k6%38yR1!VSR9=1sTsq-U7`?qBz!KA+h zVCjp5Y7{kCb=PVIpPx_`~L#R+rKW5oh?F=?B=~Y z<%V4nrI=Hk-O-uMW)v`v#Ah|gSgblWv^E>YY|V`<4rvE|w8^V^M_IZh9k9)_jYi!0 zYU(B-Pw&16Evu^DVx{!ng)Vj7&k@@x`iFg()1Mvge+~Efl6ovNfz3M7tzg00P{2fP zB~NA(Qj{vlIeU{|xjIp3*y2r&lXxo@^pnWfx0U>wgUKhSW zcH2LY<126Oc4%%9{i<6<`wY^Z@NOfi{nKN>iaY2H``I@X*lxf!Wazlw<|riyQ9DXV z?;ng@)1@@_I51mQGZ&Zo(Aj8>tc$ou6}L3NNK8r#FonaIuJK^wJ*?d~nCoZ|d-ZXE zZ?DClM>D9CGm9n{j&sz7ruixq^NWEUTA6Qk*7K>G=Gibrg&=B<8s)C2tBY0Mm|i{u ztx?e3iI{koEU7#GlTakUU_VQ5Xm{jKr!{T?kA#Z}VBlW>*H~{1IA~Pn9Rj1T+w#~# zLF1oe>Ia;be

sMfq(sG^k4i{v~T-b$e(%x^ck0+S24o4VQs4ph|wxQ=Ov|@ACB=zfOlQI znDIIr0dlp zD{cZV3UKJaYOt=xEWE9kJ!04K#o6KU*+XWi(>B_B-{Zi~eE|5)k77AnA-hxPZn1ZE z(7sVQBmGNB6_nBd&GgLFwb=&b=Sd_YJsp51d^Z!hTTT`kCR#2CIsE zfseOTAhcVFJAn*!A<{*bccZmcp?ioNU}EdM3eZBGbD~5j48CQ$*cr!1VjgZ#@?C_k zuSx!+n<~4}aj~BWy|OXAhI3V9W|SQ>Q5+(*n11{gKfW#%e*`^MbS#7kv^Vq>c;b`p z(w9E>|AGTfv8qLDe7g=Bx_MX^WzqM4`rpdKpMAfqXdSC+SGNO&_G3)uFvc-JYV4xv zeA2>z(7OTZE&;vHetDUe-XZLBFUem zRccmLj<=pcPktzkHPJEKgmg6J!J_2v@?t<| zqi%6CZ(F=gmwHN&Qkcqhe5WjgZ>7%Pc4VIg)I~XyU!$3VbY9GhhkgdXM*BYgtU3bt z$(O)5I@FRQs1Mdh>4q6UMDX7gNsG4O=LHA))yg};W$a!Qf3JWb1ND-;=<}e>pMsMg zl1NTZQ%tc)QV?(Gn{>A(J)?aZ|3UN7C)^fzgm6BO-*Ad;^bD*s# zn2EODo$Gxo@2DO}f|N}EXxyKRm?s+HKjKkqDlbk9&^M&PDq(yB*H#kA5A8jA$SG>-w zBM67wU(rBC52Wt$n8A2PvR!`}fH6I*IB5eo5^qHgCc3Qzah?{sqt=!eU^S3> z3>816!I_^l`Gx%J`kc!6kOlDUe5Xgy)Ze8ULSl)s^U*xr%kpovHaXB=k7V`sHTtp) zeV^;i^Du|Q`$3-;xTd5V-=)Dmu@9Z*ljAv^CSoKlWAyPydHZxnB;h+y?)XXp8~6_4 ziqc130t~nl2qX_w-eRp;Zyo=zVLDz=`J&p+FrMNH5rz^%TIxL^@IEh4-YQ%L%SNmD z^eG*)K%Fu1$U#gYt37Tk7V;1XSbI{RyTgWx-uPXVCW^w3c1bC?5vLe#8C$fifrYxd z)D`1}n#4fJ1t8x3GakEi6MTWzS=(S7fe^}2Y{9}H>>xv9N}-%ZD%6L07Hu8ZO@P(7 zL`FJL#t7RAh4znp8C#N9dh2UplgR< z9f3a29ESRqwHuvRmVbm_S1fGQDX0;Dq!d^oWV!k^H5u)O5(^k>KmJ?Y>Ep*By&!Jk zop3~1b81xl6_B7y;&p{t}2y*qxSR@jG zt~wk|b^hql7To%k)Xvm-5#nHWo8TM8jY*5_c>4!H1nu+`di8PizxTtyPyaIH&wVrG zkG%?dt7YD=)Mj)h1IZgBOmeP}@d|41kI^H*wGQ1a!0Vn3dF@{Ue)3D8Kk{#Y54;07 zxgE0Xfu_)Q0>~v`*MRkL;0b`d`pJ+tJ^^|gpyvvl^=v=3_-gHMrq86K%O{y&$m$Nj zLo4tL9|eB#9>{}N&~_Jqwl@+UMm^?9H-4<|{K9wp0)_9>Db`vZ$3cxby8=sBpdZks zL!bFf;APJSI++{=q)VicRX{0U zCsyBQ=e~|DANcR`zJy|5j$5>P?T9Xm5&29Jfqtc$k!b106uwYiGd%LRg~50d!5gcq z0$thwU<8aC7U}Zh7kx)RX(ku#pSJjK=d6G zP4v z)vIH5x)8ilF`gX4g6-(B^6c+<5TuNFz*l7<>FzHE23pP^;(Y)EsRd`&w}cKLoi6sm zVhikx`ip%XeCE-X{hG6+KrX7eZ>OG}Z+S#?KL;-u?;9{$sEp(%rw;_+IjhwjWAVkH zqPgRnhgi`%+3r8^Gt>V_but>E^Sr<@(JH>n49_InZ0{HrJ;G-UFAEjJd34B{c=0mP zRmLT@N1Gkn8|fc&qGL#;z~*s_+j~s%2$VVSWFx{J6coy)>;?yMPit8{R)KUf&D{g0 z_$@JP5s83*bkUK!0+3CHUO&13fJ{JgUKjeddgAq2Z?QO9`Lui1;EdN}LECAOs}zS> zFtS54uSF(sG?Dtqk1`DHrxVVd|2HyZHz?Rxt5wcQzWC3QG$l<~oIc3z=C zH_v}cyFRz^@>&A*5m@Ir(Xn}d#_OQ5c;Gwz=r|z%jT}SC>@RaC>Jtfj!ix$Ql}UzY zoOzQ*y-Dti%pAKWnbEmj_&CP(E(>xWL?rS@HmgN`5v$jpIYS|d(h%~w)QLd(kf4MN zcwK5PT4mXz07S{vnjF#mRK`!WmG^J>QO6CntprXnv|2%_W`KSJY^iM(pP=f#jnIupZY!e;g5X33id8p7DgI~9vPnM$|@8_QA$XK<;sT@ z5tr{-J_R>d8&ja2o?3%b04beRD6Cu%?O*&DkPXb=?#lsjp@)+>Z`=B5AFy!&e5cIJN#LtQwjG1x}Cx{%;L)34b z;MFnk*`Zn<8a+j!3xFnQy9?0Id;`cfYr0 z{V@QQ&PAK}E}Lr#Z36k0*Py-bOQ1jWi_o9_dFZDdat8Djc=>IRuf83) z8^9SLXO?Zn6l(;4cUcrM8!R8dggw^EcwD?wKt9xFvWFnL+c7ToZ-+Ec;O46U-@d_&Rc*-9suMzaC!h=i=)l@?+SRl8{U{=FRIwd!2bRvo3}>3@X9V(h1v$SY z^F-s*gX)C^YecpSW%l#rTs6lgNZ65fD0tR8@#e$#q&}^1o(fPbnVWG60M0VQAGSY; zj-9t1UT&8SV)6HUybfPHksO>4Fi~bQ#jRJ|Rd9W5sT16;X9H3P%r+!+L5{ho1@be5 z^h1SW)_*8)I^AM`fm4mXohs2nB9LfC$|~875%;G-duCc*%F3B|a!%;O_;q zF^(3pEuQD`0#gLRX+G$mB7@@nRR{%waWql9R%-MdBpOGD4iuOpFC7t34^$?827g$Y z7@Zax~x##<12ZkWEXDEtj!~KXv zW|oPUjPm^3`LmQy{+Ngtze}R?wnAPLJUW$*GDqa=Xi@jeoyWJvX!dR<_z2mu4>{N#bu$we1BH+X+YS}JIfn0G@8 z!|44ny>0BuJVEzEi-O{694GLFt)BJy;}cVobCzw_Gf6+hII4}C?G3_*D+bX^hOa3& z%HNv!VcW^z&pR!Ae1+zWTjz;TOA5D}g=9rxIe%9j7%7zEI6}r3R7(~=xz-RZ*BJh@0d?nU%vYwWSw?h^N9gbUXvdP_TUrWoo(gw$nR+`HsLnf z)Z`bQpA_CG_$1x&K!Nlh|8OuVcUP?;|3@TF}|ImsmJ1n38 zLbNVXR&8*kiNVD)mQs}(3qbsy^d8qMXo!BP2`8km@pR>dBV#uMJP9rjr_edB(isaz z;egFC=Q?1syWFV@PGh(Wm>Vb}lRtVXdn+w=Z_c$kNflx*=<@6V1?&hQgIgpOWHaxf z+i4D>ut*Tl+Q_W5I`NlQ4rpQl7g30Ibm{#=ANu?9+~WE?Y3juJl2X4Ly>@EOL ze;V|K&jVg`H}pOC03ZASaOLti>K@4BYustuj9;)}^;r<(0S9eT_+9s8_JOK4NStA} zKY{E{(C>L4^sl`i_}N!LzVn*|uX~|t7s$EF0)^;0oND9$qQDvV)~g0HkRtFRO@?e; zRp{*}kiYzOXy5)d(7*E&=>PV|aQ@kM0Wjsx+mF}-RnUFM2k{q8n8N-z z)`>LQwbC}ql8qsX2GxqAr$=-q=^ah4$UzI zqd8F_mEXJ!^%G({;bZ=kg~emye!S9vF_3etkFAy~_WmdwnyQTfvGL}_uc~R88;uD? z-p=;Z2dE1OC`h}}SU|R^zF-f8w>ml60D}p)#0!;IsQn=Mj69zcX#K^qK=ra&m&-a% zrq$7{Dlk{bkG7GDM-SX5rcy@de7#YgaN}a6KHm3IIHB zlUPD4GFXx5za>k~M{^*rxql`d$Eb%KA;(_kds#2%I4&b22#~4CjxT?Kyw59m$H1JN zqSJj30`$Z!)cP2{#$*nWyQ$3=i2;jYXx!5{lh6C}Omi~sxw;9T+lB|^0x#q>#XY-b z8ij(G2*a{QG-25C`#AsKP@5i;&q+su^jwi|hE3)tTiv$#9k;nHsY7u1gzB;mW0-iM zBm#S=3WH&Bn9bME4pW{SB-cm$G~krb%ysHL86$=#`GLkvay5d_6Wu(p=s(F0ZjQcj z@I2AV>vYNEg1ma~`8ti3c+0?k4dM!5Nq#k1V9fiiqKlW~M`-+R8z$WoI*OTG9pgmz zV7VLcb?AXGu%x&qQK6v!z28h38IRavk>3bMWK`x zQHNMj6deB7n=OO_2mU90giKYP6ZEs|u~xk1q1pF%Y7f{XffBz{y?KN&)+5pEQsU#J8}jfM4}F zWK}|tGM37jYP7}Z7*KBb6n-Xtz?Nr1fmWuhO`i<-iZxPB9fqVm4O|3%R72uc5Drc# zpk{$hTR3RreDMf&ii|U%GmwwS(r68k)vd^YS8esw-aTN!OGBC8_wzQ*C6TtrqnAI4 zcfaR{+g-2xz79FS)1HE9wPA;j16F*&>0Rvw&-rfsy?5W!WcMNMV>Lt6awARAK%wR3 zkan%{GDt-|e|%PMsaSczpbBrL0J#5&1=Fada*0GBy2tI1c_1_&5l~m$GxJZn)O$-V zt9rxXboSb|IImTkx47E_mo5UAZUWX0?03N3&jz0LG~ffz0p9s;;G_2fhikrry9g`i z!6?W%@Ij{vA#Tq)71E^LtWDS%6cNbD31sc)zwz79xBedR^?zLO-Cqa1;C8gf5993n zCeYTfwq>*)^x~#d3nqzD_Tt?aqQKr3->x?3GcQ2?=AVLmQXhg6X4+~|Wp$eaGO?MmQWbw)CC^F`sz4#4da>jD0$%c> zztEq4(;IN6igEOhAMQLFSdBm@IKz8B{A2RSL!Uw0iP}sh9Wp~@WQFtu-m~|6k0X8j zc%L{*r=0a~W$Zp@r^J2NyKW;KXZE}}ii+~PW|mksUYe5}hoe1h6%lywEKe@VV=hOw zDkfiU3XDC~m6pq&!p;JA`$Eh`{@uo3@^N|KQQiL>V+~&=j$`otjeCT%yFBBVhQqp^ zdcN^Tr(&~Bo77?9d(zi7j>H>{)nzIs6_bDkT}ihV=Z?#5^)u^(;0GY^uheE{SEq{M zaVdOVlXORc$MNmTh@TmJ)x40OFn@UPQee-8qaMCy*}X?(6&sHS-JB-`)6Qh(6C}=* zpDPsz|CcMf-SQ-vo=4ev|I^5*MdYpmALCHoO0;Ku@q&G#o#A5};0qH*-w0^9uGi>u z;HXhw=?O(Zr!|w$A&+h=Qto^pX+I*!M~Db>8N6rrh=t0>=NyH<38VH37*)#R4?og) z#$N!ew)5F_RxG0VPC?%p&5JW0b!AFww68Hx!mrsgM1VAR1`JxXXg7BH#K;|Xr$xT| zsQ6&cFWtKhhC%2Fb^L+qI&6X9_`Q8E%cKt-#{upn%cN^Ub^<3}Y1YU5NNYPfS1H0u zwB~v=wz4_~Al_-0ojQN(bRB38*{O2os1)uq>=%LWyb%Grv}sw$+emw$)MimuO1P9H zL&zhccaXu3stg*Z?k)&6yu=;0XWp+cHnT5t(@HyrJh)#R?|d}3H+Gr2os4gzlgBr_ zJo6H>Q;3~}PUB2>vQ#x#OS-pyoDRfu;eT*3d+K8g<)n;6!m)SDu`K5=XX$LS*#2O@ zY(MAw8nj@#r2R-9*0>?btI-GT^6K;q01w`?lC!%y9`+5HkE+UXdDDb1wVc^+=WWgY zL++*EYQUZH;pPf|TlIkwq9;{E=+Kb;}^~77lLH z!SAH&Ym?g#!N9yH)4dH}G8}Q~Jm~NeU6sn0T>(~yjwh|jw7CG_ZTmcjk2Ec1N>)CX z@pl3ELV@ey+yyZ2GKhML=-1(3KAyibYd^=5d|18Tcugex-}bxkFsbRXD}a^1!)H2T z#|>64&@eP9c8X1)V$xe|N-G}_@I^3O!`1$k879~UgD3(T{}rPk$m5*u8dv2KQ-izEfR*@$xuvxfw|Cri$tqL6D6i6DQVrb z&iz^aDE}YvQH#g;(Lr4>AGH$mr8w>Z5p)RyZ^~MIM}Rv~&QI<%#0z01SweLc>=hrn z_g^i~e8$)H7vK3RT*qpCD?|l?j;?|xt#>(ZFMa-N@R5&xlRR?e?*O=>Dyo}ceov+5 z_n=c5|083-0U@XpfXJkTUrl`T{i(m$R!53A-isS(HIrWWGAUgrI*!`hsgj1Rowhgdz*qf^j=>ORp zfhX=Dk9V|Hv0DdAt|I8Im7qnyuSyuJN?8DCYx!$0z%`@gYo0o8Uj^{^8o1Xo0b*R) z*O6@CD1XeE*jvAk9Dtwr0PqXr(Anb?od(R3pBd z>?psOwY>Dtu7v24z0&*z^L_@H9 zD|R@?CoaDYAOG0Ti9)fig7jqBM~$O&Le5-oTc-b5*hSUhd=!w^GsknzW=V&3((y&# zgY0WkUj3LjD<7VZJFqc&%{HQbT`E%mLIS1^0|=fYpV;m_J?EB$+R$JJ-ImtUdr>P~ za=wbfBt|&~Dxq^V?)L_|^Vp#0?u##{`IsgGHM?8`o}xBGb)`M7c9B5Tik$3K3*fn( zo6Iq3k*dZCcDu-cvTlZ)omfjnH-iEMGZAQfA^Q{U$t$2iQ!`4YIdw|0BisqYrexCT zC;@66mB1v2)L%>qoY4w{IsA$Z{H`k&&t*0; z&iZ*gj|BmYF>w5J-WgFb2c-2A)QM2(vBD$dJ*nHu(YI11PIL*pJ5SB>V%IX?gY7Pk zgP^Uo;Nubek7Ee{(A!aS0HJ(6D8O$qE_R-H08u6ap%75S6D9k<1nt-HtcmTmw#Lbi zD?6x4uo`(iCs^=K4U%u=GA#F#{Fl#$caQO=1bw%>-Z=op#JY6T@h^;mSR+HuY_GEn&@JVqbk%WI|&lnhM+vULn^qN9KBO*P^ zxD6nyfO`gSh)ayW;?Y6&jQ?23GG8^;ikiu)fBPKX*K|=woUigp9 zMn&dQUj~JpufPl8lzhly>%IZO!y>@ly2T(X+sb+&H}E7Z zgM;U5nBe8#Ht-_f^qjS|C!v7G@n$_)7-{+zkhGebULL_T1-wGv4~H zUuuxIt~w6gRP8#BeNYX>-~y_MK+VVi<>$Tb5O9Uisjl6NIfY+`asnx|s!zE-Z+T)p z5@p$-5GSP^I}Zj6AkL(8rYvV&2JZ^O1*{zy|FLJtEHbT#`-Lw6bYR&5%QE`8xCag$ zxHtmJxONWQeiQJzR{_s_3h-OM1Kj&z;QE!3RUn@AmPi=#mVz4cN8P8?<>F|W7O=dr z9LCSA*`bjA4zhb9^nr(=fBOfp{>txQ{fpm<_EmRde-7*}pN;ZM8-vfN`K`hG! z=E)QL7d@9cF)UxEg&@dDi>AR>GJ^jjtI=mEwvE4c;I1cQ`6I7D z|DxwZZ}DA6I?DJWz`=l7?0o|30n*o@3!w!vhoEFB&DRlhR}8JGH~TQy#QfAx`>B0$me<6KGNk*-Wh#F-w+9!M>%z%#ontKUfyY*R+5iwWX}MW5uvAwO zDf|%=NNnzK3>+QPpU`SAB6-C|U~;`VRYn<(2@iz7JY(H{C!-%P zhlIglBPEc}xGuDkk=ji5lF&s=t1$b*V`N7pC-JTm>M}&?uAsQL0ius8^-q39ac*d= zK`+C(4F|?)PaQG^}6AS@EZvb907?kq=Q30dy&e+skiN!(Ml&israjzus#Q< zb2=^ftZg_v5^ZIsz0=r|#vFaxM=}EO67L|~NSu#X()2q#!dM{I+_Vxjr9hA`uyG34$*c_&n3wpt9U{@NL65BT>TV^KxD{=II>pw_VgO z2O^xzL4H9aXCj7A^Uc7U%ct|FE0Q3@5jHJUK_bN&;mFNRAI6M?)pHbdE>o@9TnJ_47v&2#)otfiG*&9DV8Bn-j#jx*8= zF*I+}M;X@^XYjcp+xx(5^`#5HMG~U7N%#sMI`J;4AP*{?-ZFLK)^C>$J={2%q!IYh8ELMI%p&^5)D|MtQ&XO#S3*0A*)DX6IW! ze#XDyY3QoC{SHJKdS-nJ6dH51T!cu<*mGwcgVA}l9Pz{UFTU$81x|>KF$Fx6ZZImv zaYIb=sZOy~xxE-WS7?Q%NgcY$M3eGdiUOGHNM*phTNsdqc=MU>YxX%m5GBC_k}>s2 zL$g%M4AqT*ag)pCh5D1DzZui)Pe`;keuV-cFgd-znU^O4#11r^E#idQcLk<~x44Hi zb_{4_9h6BY@k4nJz|_Acx+8NyCB6%Ma03P?`_+x(f@B0|!q5I9~Y zDj4bgHn@g^$`1PJ&-{YC?ftLEYhUyxJJ}z|GX_GR>p50^{33Domtml4ZW{*_EuZ80h*=2!%9{s@|<652w6vL*a9DKszP6N_F ziHM5osj5B!dBMH`sy*S)0`iNrk6?k@~*q|DXUl@Cw9B z0^#~lfagC4@)ci({^BPOI2Q$AyG~Sros6vcU4veyY9cp|sCCnW2Vs;Q`nNl%1yaL_}y{bV^5e&*~kjUq{lMAZD*%2NgvK)L}HHil!u z1Is0Ul3MO1s>=!zblI9(6-yLe(p1#=q5p>gKj| zL~=1LcJQRw9haz@dYPI6<2q4pIxrbwf*|?dkF0Rs=khL-5rp9J1uBUDQQHiZ%>3#o z(+q+X`5;>r^y6TI!(T@f2|uGu`j=j&=!WRa2*CH@V=INz zHwZ+!VFEmDd61uUQQ(?!pm5uj!&gLPQUzlre6%%!X@XQza=_InZpZ*)UD8!zp) zFWDhwXCNif|?YY|X3Rm)*=_Y?EaGPW@@L^*QJ!2@aMFKH<7i((1Gl^LjSucX! z#Nq=UK^)~lJY4+ONLWg+D#R1-)TT;l_8)k>l9zeG{-DeGF`01rf+FWQ9M-MEn(W}T zQo@_V$GfmJP^lBVWel4^_)q#RR5nx#jh}X$gub41jf3f&6Jz!+bfOXCwAdiz_=`*> z4){SCGuu4>Es{7am89Z~VRacJTUK{&Ap+8@N~7t?-y2maQt5bRelQzQ^&tlQG(@3i zQJeX)5`*p$-%j5(V2&C$qcVdg6s!+sQA?uoel+Gsp243xwvFELP6(y;3GA3lDUC1Z zT-873xfr63`dIjRu{G4Tk_?JY{8#lmQH+oO-!SosxYP8RG) zjMg)qjzCshQy;SAvv6|58e#K^tYiT8Hrvt45Jt7t>rfgLm>({o=QcNM1lUI zw1~X^JSGBUoT;6e|E&yhoaIU>6smUtcq$^5SCIj6nSe+p$@poQFNCgu7>?Jm13+b* z=YDA!ng9KKShd|U$}|D)xDEItuY!ERozS+sHfpo9>I1f z;L7kZkEVqB6OqUu5QUsx0@e=wiJ!vZEx(KPd;bjNPkt%dEeqPC2eh?Ao1nX_bvH~X zxiM=8}hj`wEab3zhDi9p8J`! zs|!0BG@caKY>a4#^tn3*+(sw^#QD1Gn0yO>-gc0e+zou?tFS)p34?F;4I&+4XJ?If zjhOIVI2rE}=qB8U1Jc=uDLdRcY!F8{PUc9>rM(uvuT~^x;95Y+uHQRNIq* zh9kI}ZRCW~M6J2$p^vsxk#ZFF+U=bdL+Ou=6xN0aAbl)`*&?!x!{P_7dqO?Vfc0$! zV^z3mU)Gy%dYydH7rhQ>H{rTv`|lxGq+^im9*TQP%d%_a|O(huZ zdFcD%NE_8dk1Mw4TY8%slLRogOzuFaa_5tHf5DKw*q!E8xfyEbXwV7d{HjZkTO5}e z&hm%xQtAzHJ5Oy*G2gT|)PCUE4QPB^Z)Cl3P0HV8Fq+6gq>Rc@nW2VJancYg%-qnh%kIRqq@|8^=$Dt8^n`Nr;_0rJTdq`-T|k*@U)MM&p3~T2C200%yfa6-naJ) z2mpI!i`tt(g?S-BQsnWTOl}*c2x38*g81_UJM>K){WEOTaM)XxiThrkVD%kp7VHG(GFOo%F<(lugOsi45}_jsN)=L9O+R zUr_ETmG`!@XjS?`%@ieB>jX`xe$bfY(g%Peb>n{yDdzK(&KtGQFim5RVKV-?dD0hT z%QJ|N&^b!isgLLJJFX-JDh%svpQdY{7*M}OhnUV+8QJvyUXZ|x0Alk=jrp_5W7nb2 z>fjRud*QF&^(^7w-|qS_@$G9|&aPwFgGb6Dr{h_WdL#aXO+?@$+6~!dQn){g`YDT9 z#LEV3K9!Pro%636ElWG=BzOKJdl9~e^92HX5s2KfG=b)Al2u|}*~knbVwPXFj{#Gy z&ny}rKC-CTyS$ev7;I~jo`DgeHjaJLtmLA>)OCTc6a6*ETU3+dncElg%EB&E>Ps>@ z+`r>7b_`iwa*iz9%7md@azjEV7FK+`moPh`M& z6?ZU;qkimTgBk#MYatpr4|8ElE@yrRq`(!=Ha3wg3p^)NvTrdsSoapFB`$^ zk0(N+Rx_MI!hmoIyVp};M!#t%M+I+->y~A0o%$EoO{F0bc-C}I9{671MH7dl0DQ}x z5X=dsO%j+0jyT&1P=@YKh61rGz=QmX;S-BF__j?~TFc{yq`M=Vt#*tT4ZDPR``Q&M zt9EpG^wGELZ~TWJ#{MBqY;Ihk;>1B&q|N@ z5IFwBd0eY2&H7RrISpMkUmvb%&ntZ4FIKn&cF z@d8h>eX27;k?_yFPvaihb}OWx0XT!~4nSX_H{T3-@pGZCekuA>Zvl1+>~-jySg14w zyjU>hqydKo<4CF%SkHmB3p-g&$Prqk1C|EOABl8mf1v^wA$U9$wi0S6%%mN$&|bCe zgi_ZJM8{CUl4b-hj_DrGBZ$}-^EwB5yk8Sb=@>gDoINS&KlFFA?O)yX{!sdAZI2j> zLKM=p_1a=4MHt(utaNV4))pO=i&xakQZQzC*$9FydIO05ZHev4-*BO6n5 zgCFy5gQMa)5Az0PSq4J{%wUAwvZox+@$$?kWkgz^ZR9a*Xw5K79pQ@;e3N)Y63z|o zm{$p)SnL3>%}JaWuiSpP#!CgEk}|NNCY5EgiO@e&Rx0)Hu&a+BD8wF3GjkapynG;n z<|0TG$majVO+rDmRG8wEBW2RBiT`TJq`3?zZ)hvb5W_brGla>@2%{J|GuSdyS$Vm@$wM23nuha~kS;WLdV%>gN>Kc*~#DFKA!!;+mKsV5^MYFLf=H1)@eH^qRn z2a@f?bAor^V5?D%Dm-hORLG4#oL~}$AKRe$kxnvRITz5{UR6}ouy=Rk=b^J?dhWb= zq=m$-)_dL#1+;6MAtaLyXF7Tt6U+g&OntE~m(PP!+5_rlA}>NmCZ`dtH1Xv3S=UDb zgJhyh=m{o&!Fxz6Lc1w)6Aqybj*jN~k#rB?A{P#%`lYgMZ!}ZsBxHP)aOWfRdQC2&To&@iG+s< zW@Xxu-xrNB%TJBZS}Kd^5oB9{oY;AzTX-xn#0kGepcz;6#fy4{ep>bXBnPm%H4!84 z^O)isg)&rwa8`5mqI{BPe7SGJ!&EI3jsn7?JmT|;k*qN5FipU@yfrQ|`Xw1#B zP7j`rgw-3A&mFP$t$L*W0HoHTK1(J&fE$j9%d7Kyw#~jBw-Ne~pM&8{Tt#w)a}B8w zVVVI*pV8XsI(7iCb_}~?N;D_HDPQzeV!g2{!K>L0l~gx5jSBULsci*uG-c!lr1Qs< z0<)da;mzH{lvd7-35MkrKCnQ7eS$!`7K2^CIhevk;+4Z-m^ki6sl2a}E3^@`l%I&3-}We)_qTgpP$yya#fA9lndFGB648F}PUQO^?f@K5&*Qk}^G1)K{U>jG4 zFyWIxm-@u^Qk|c_SWZr!fu(&wRYgP&!{4#+NZ1k1#02Sj(x?90?HaN9G593#m4P4Y zw7#E(-hs$R2(UKrH-JFKLhZ0O)~P$RV&%F8$GnW&JJme;3o_Oh`zm_JQ{Jdw_~O^% zn(hEJEKOixmUd9Vimp4oibYQKT_66Se&Un=MzBBc+Q&3eAy8eVtCXO9*KtZ6;o+=!M$=Ejt6e9P4nTzK6zpU%bDxhZ@!VI3X?*;@WF)IkIQSK7iVES=70&BcIVn zPWplmxUEp%TmOdP3=09uLN-|Ptn&1CnD`b$)hk|`K}-gTXVW|p`6Y0I-t=Ec^;Zp65I1FxG)I@rhPd zozs$jM;N4d)GoUrGbl0vhmWrgAYx5(J}j4tA8wT?2+MS`bKol@elH-hAvqAL?5e zj(nby?{v25Qpj;Mn5<3ka|?!@a(CDx8Zz-#`AG6!%7OEV zH5Z%{x4bx!eNkD~H!RBq84sJ#1qv7?)h z7|I>cnBW4C$G66FS}5_~W!^=6p~zbhmt?X&R3WP;#lr_H=5vDWlL1Fc3h@*U zp70rh?GpmJ9+zxwIPjT|+#R#uG6qG*)8 zbQjbEM0QYl2%moV2SnRLc**|hddp3>%IWD&mSqQEoDbaqta^^;-|=eP_ku6hx4->Y zMOCERIS&!+2kI*3_{$?32&WqbW`a(Nkd<55YS!(bMTO*sicfL)5Kmdw205HBynrNp zr7gH7BD%*jg|v>Y7ohrb0OuOB6$Gylp+}`7{fr)bvINsLD{%1y?bR>C`jp#%OAB;| z5tKb&Bcng~w|8J&2dm}v_dU6!=m+Re|19upzX|+_Hvxa@Yk+560-ueCs^39)b>+# z^)a2EZQQ6a*0pWukJCNmCC>+LJ4IhkAh+HGz3CJErzYz2}4f6xhFaaI|z1fEM~y`&d}(-G)>3 z@0=Bqy9KN*-sJ^^es|oIj3WhQrWj7id5-=8b2KSP8~mSjtyb6$#&_6*04snFN=HE2 zZGn&3x{?W|fX)7oF+mX9bql$rl;h{@3yh+~i*5&_LKcq6Y|$>JZ*@qCPPp2_Wp(I| z-r)b^bwAI?eQ2(!{EbR)7~Vr}K7Wtq#ME-p#UyVWxOVz`KgI2<3YV${d{w)ksykI$ zRPaVpGtI{pG3}HdZ2GVv6?3%lB~b7hyeWzH;6ds??XpgFx2pT-JyP9UQdJps7&b*G zS?dK0EA5myit33J9%5U87Ru)mvWoi`wnK3 zM2SC>UO}G_KbLPRstl>~WL;49{-u<4T|^oEq0W_9E61DiEv1!8cA}N*4$_}v(a92R z)Y>e&a3%Y}7;}|>(>cTg?uUjwP*}1U=}-4}$XKL#N&br7J^d{f)-#q7m$HRKI zy=kXj^W3JkM{^7%UEy-C*&mOW<8heqlCoUok!9r=UxV=NxY@{cX;%ZxB=)$mgBzb| zwK#o?LCVufWsjBQ9Z~k0j076c-Bjz%*?E|2+E}c>ka|N%Yy^%akjch z8)tSm2%t@xh_p81Hhl#Y`=JLE0QRyTF5`jE{(JfC6W@eqf5B^HS-Pl-$MkSO()St-Ua?EGUBT1d)4Q>y*iz-@cSke0zq4!r{q*Lvf2Ps)GVBk!Xge~PI%J&s| zdK3D~o&(%@bD z7L&1Di4mg@72+HjqCbHP(n0AIldb?e4JZb5EZqA+VB z3C_>IR!;W+tG!fML_{NVcN#TkmbhtWdW)px3c5^3D7Xzs(CxPfwKFXglYJO}cIJOz z-Hz~~2%rX!lD~Uj~H^rgL3ts%j{(>)f1+Mo41Unjrpg~k|kS4$hZo+TA z@3-;rBfo&LtwW`Ypld_#O;t5yyLUy`J{Ao%U&J> zn676>knBm^pUNi)bpHn5TyCC_1?R(UL^zlMK!_o zrxPQC`eoON`6Vf5zAJ#?(sVm(27)o(GzM>T2Fm$eeNsR=P6D$NPBf4lY&{|0^`n;C zHDI0Oui0z~cJfZHExXN4UX*!Yin^Fj{c@)N&<9yH6FP{-r29;%k>T-=_hmHqq!BsN zpS7Rx5HgPP5{eI50$pAp<}U+BnfaXHlS(92uAARpp#;>WW@0gCOI9X;3&=s^H_Dp+G!gVgJRbUA?9`6)5QN;+l@tL#} z(MlD@x4$z$3^sc*%8#7`yx3I9!aENDf%o5{Tpr8he+?=fG8ra+u4dN~LV;_;EMw2j z4~zU48FQHdjDj=$ht87B^bbbNaO0SR5&qTscfD4!{23qQ4|X6HLaeQrf3kjb`x3O~ z#Umei$z^7e*P8@#*fK9%e3y`_A(z~E@qIL4&B`vY-f#X+l~NRW0%+u@CGwakEt!>5 zVaJ)nKO>|tcgnugQH*Z%s3w}10r+^1!vLEDr=265Y$;{c0`jz5A|=z`0S*&k*~RQT z`l3}*5vBcc)G9rQ_9gKnV#!gG&sP~frQ3S(@q1H*&=PzocziBI3+q~N zH?HG^1I9nR`TVgd=Pl4K0x5*oD98z&_1yqTlVaP{wUZQ>NCZ~+1?GN8Uqkhn$%*~K> z!R&RaM9 zOo4~`$erq1rohRSLcutJs!?=7Jl5Y2g#ai#r_uRHc-`&-zJsF#E|> zFF6XlD^}92RfmzbR%$1e9CREcw2S(u31N4#xHnY>LA8}ZKqKH_d z6Z4h|oJ>PbL?{QxpIF&8ltHZA^KqV}Oni&uXu69X9;nN*JG4Vz+V%6Bv@Nn;+FxD| z=ZfC<0xSwW2V{@)^Lz2(dw*nk+S6XTzVMm1LbU66GbPk4yLMgi#FIaY7v24b^uY(; z($3B-c5gyYI|1a#z52T<3MtAa6;`}208_o9z+4g5@9L3Htl`W>=nn%bE+q?Uh&PJ3x;3$B!@%-ch^p=ajz5#8oIIKfq-E=zI zzP2JLf z`u}(XJ6Tg*E*!@S zFo0UBG4;5r?p8%RSKM?FI6W2I)D-(wfO9~*j!beuPZr?94mdHM+u61vpSuIl3;U7r zzuyD99kA>MaH7ezsb(YavfZV@gBFFL3FM%C{lidw8ARR@@v&h`)UC@g>vO~p*p%Mj zpC(U!oL(&0+4I16=pup|v?IZT{p83B5W)U1hRp&c{{EO|2536#axB0Ku&MwD)hFG4 z8@}WZeY5UlkFE#nU_`n^yY?nsMCDWBE+Xemn&7+( zun2aoqo1j+eZh#74JGO?bD#8T9<_sX5)IjbGsi6xBG{Fr-(EDO{7kuSW|`zJ0CF6o znDHL{X8RH_pk8k#0?<;9wcAjDLgULi;it?2L+1gWcv1Vz!_J{o_#Vu=KAx0*NqTS% zX7spL8h+N{mbz@-7zV#)n^`n{>C_V;@|nyuNstSi8f_II3of%SEe!;&ysDzOE62s7 zW>GqS2P(W0>p{mAZ~Agw=k>&w)(G%`f3%m!UJ$ORe9(AeMW&xmECXj5Zu5i_&EomiYH6XHBPA1y;s}zclW_T>g&cV{e&z4M50(Cv@yB}r#tz15 z$AA!?!>9>{MIK2<>TSTY^(8CPf(?`oJ&#FxGZ~P?skQG6D%02iDKt@K{xKqiZuYWT z>i|eVx4#ls?UTfUV9RUv zbMghQEDG$SZb1lmXZv?Uxz9pAZz=i%DE=n>K4+d4s#o1tce>#`S1o_SJ~?Oz6!ufe`+;CJn7G-gP{l1#_7AD&i%% zAp;uMob|8Vx%aE1e%#dj%V4|KXH&US*ZGs4G0X{%;nK#SKqStlAFBbUh*o|HeCM4c zm5tOBumm*sL+NAVP5E5W$L;)VaO~pgu^9<~8#`*$2|w~h19KKOHQ5PPUh%4#J!OTM zXgqYOgacFL)0F+ZSm1%fAcrEIuW7u=?#IW==t$>LPzFk43TMj(R^P~tEy}5#$haiD zL_$U>uZTA(((>%=pGawhyYfJqIi-GPSkWWoObY!9MG&eJV-nuDE2eJJtT;iQZ0#1? zi0G-%T@dF-+X9XSi$=>GQ1OTpY%mHmZD`VEk>0NB&G^)(zU*+{{okUGUHM|{_K(Rk zp8W5Y=RECK`mVjF_d|ooqJTh_6M6X2UzYdZ^Xu~bXM9sXUCwL<jONWGlK|xDS-epoHssrc~5z4K>4r?M2$JTs*F@ ztJ)DgK3v6cbo`dfd>?$5uzH3i#>;>k?HG`ON;RD?0_m+OUjD@|(K{~PjWb;VSzJB< z6|AQI_KUQxr~TL8`ZN03hkr@h{$#8I;yOeQP2~yl@a3=SAN#~t;E~IBVb>m!Cq3ak za@Ui87N`67>3Xix*QU7$pj97F*&{_)B6MyjJS5uc_EtcmEu%%$&xQN4os_PFeu_d9 zrFKd{6-G~`GT=6O7|KmfDyzbg%tnPKJ5se|`Vp*Z+zzF)GiecWmJ6#BOBU@wNe3Rt z9PR~ABqsgKpX_+!89JB(4~)}*mHU_Qc|&5|m(mQt>O{X$-FI2!_<^jO;_8HB*}PGE zlkQcg4h?t?EISURA}V(p6-|yj28sf=)FA7kF^nva`B;a-ctlUkpB!1LD6+?{b0Qm_ z+ta$;-s_VOKaD(KpdxUB`1s5FS&+di*U1}?s_53F1tTNR!cTYuzcEL0pPky5DO1tA z4}FhfPc%wW0#V$ubN>fZ1iv^32xaEiae4_26*5FbMz){$bKiyF9rj`|C2-tAo`MDf zOc_Rq7XU{CBkSL7OCmx5G=n8y7Cf?B8V$jB7SLbJO*4z%-$OQM?q^0G&EJ%gv_NC` ziEU&plQt4x_?-Eb_=oti9FL-OFL*_2dFLNHKRvU&3n4!LZ(kti4`pX_Kwa}<{1+3+ zEKf=ce%y)|51&OeXi?;cbYn@C3z-ocIxl8N!TvyTCJHFjJadj1AweA49`AB|8^O5UNPP6=jV&_Oo)gO zj@(}L+;L)f8>eI0?it$&;_nb7qB%-~MnM-z946}}ybfDY35Fxrf1wtXXj-~wZ#izVe({7{1y*BJG~lMcV(T;krf2g@d<#!g)5%eWtB&Lo ztDo<|P%arj6j+I9UhP`r(UCa+g)>0(1D#gwNvBM9l(0=&Q8P z-XO`X-)4Ot?OP@G5qx5a8LsHwAXW0W$I?dMMn~-^a9m2N624M!=U zF2>Pr#nvTL1oqA_4R6O@2qw!aXL_sN_nEIgeDst5J3MyzmD2Sh6f3S>L%-+ISM^VQ z=09uC{emA^p8SNL>pS^~>iME=JrP+RT|au?KbQC4_Zqz7Sx>^5_VLo>-u{1-{db^s zTU8$n|HfQUkl(txkINcy~~OI8Tz#U z+Lr5Pa|O^Da?M%jH3z`4^mU&$@voD#iE6X+b!mXRD!}PIU|S*e0J!!lVEaJm;W6;A zn}Fk$>L2|vU%-@Vcq5V8` zIe8MICsF_6HPFBMOUN@n8|5pW4SmeDO*Y&1=!)nJ^he7pit9TT26P~z+r~ ze>@0-#4LNO)$piSo)H_cSNgeyQ`-7QT*VoSA*qCZ07_z`jUr<%$fe${dA92%@vC%VV zpsXEh`)aBv=DL-WSn2EE^N0GjxBdvWn*){YML|(WIU)C5{&aos`@g*2a_gtzc)Qp0 z`dV+l9eD5izfo?y|I6jUH-4Y)uil0246Qn$S+s#|cSR+vMaJBY^K1%s+kWt4ppEAo zFCsp``%vnG6H*>nd35d9%tqL~!}_x8`UR%-LuW=_#%wArhN$0(Hik*g@xFh$?Fk8R zv_0#Buq7qOe!NNRx2xl- zMnbS`(`X%JkQX+X;>=PW*rY}~P1G^r70Wzcj-5Xa?>lTDwqLHBxpRAPgV9BP7#MHPVZst> zMIsbZJc>zxC2z?-0P=mFn9bCtiGE|r$QtCxqD>;`-{X`O>pza3^OpqEB7lp)<-v8w zw%I7`sHOs2=xBN^-PikKR6nm@dOdZ0_2_ItHSt7A#%N1T87%RvH|@t4noikfFxsA9 z%-Y;F@vyAqSd(_@UZH)L=au|HLj1X{m^M$-hJZv}CjDi8)%6%W4lzdoA$wCQ!`>>XPRtR+Vw1VJJ+(qK3?I)eh~{7+I$}uX zUE93z#qrf_-sk|jTQ@&Q6Y=)D(86l+L9&ODetVj!w-=_LH=zbzWE_n<^v!(FVxM8< z7*8b1fZI@zY;>Vhvuu+Q^SW#gJ9MVQZFRC;bT#@D8S3lq`b2c3Ivy+g#Y$M;3J_MP z7i$FV!p+69A$DDf;=vnZf!*ipLiPC@KWP~+Zx4I&(iYov$l?|%Nq60s$J=cvvj}iI z@N~3A!-Uah2w%ps{cg@RtdQZH3xsa%zy-(3&YP;!7mjS#qb`>Zy4rUeKco#-^qbr9 z{@g5MUToa??6Hqr~nAYNNd{s0s=o z(^N|>2e|FrC(iHvz&GeU_kA+v`HYAhtJW=`J*UtUde1$NsBbv`t+@8;XO)LN;AiF9 zt6yDL<^5W5fbI76`i3|Enmp=3UxI@&%i+9kFQODEWDV3~MLk~*)>{=7%yU<{wk|Bc zGN)68Kk8=-1HSWLZD(};=^1;JHiD1(p{L@){D>I2oQ4+K(xtpH_e3mwwSu8IXfU{!4N(K=J86u~q% zo~!i5l#{1gy(+LH$2iz>oN44~mk0Ciak%|bZ3sATAE=iv|0_9t>Wfk5Lv4qk)imsO zwMoZkvJq9JV+=Z=h8Ss?I0b{$Fcf{KVKLk&&dE?Cy6SlCJHVjbPZg;4#1`m1Hx!5s zqdF_@Viyi)ef>M%gh$-;=XmI;C*n|6*l0yjK~=?eu5w~Ji90U5NB{g!zZaM8{Q$~Z z3RZjN;^vw410Vc?`o3E~14l>uQlvtrV;h%9DMc?`zMsDH-Cv_0yzO)30r&g)@}L`E zs;lx|-OkWj+m_eIQBGfd=(a%pE8E#qo3yUA_%|2q~Lzw$aFA#O34s zh!V{m4?Wk2fjs+9ccI9=yx)+x6X*ofz|;&p;eA|dzGhELsAHpo^RI#mU4F&^&+!yw--#JCyPc71l)6JeHQis=)Iif*E!#|`QB ztIUri`!pV%(!>+VEV&9rsD=J0uNnt$9Q@qSdgq>tY3hTYJg_RJ6ziw;Z%?9Ve=3W3 z(;?Sm`}a0YRx!nzCno$+wunb&Q>-)M(I%g)zMsu6{oFWmc%|>=C9O{nDu~4avTf!2 zO26%C1(Sp2NbNs+23R^TzWd)7{Uzk=36Yh_-&L)==DOXte4<}LG{yaHN#T~{kMVo( zq2$-R1*O|lDF9~jZOIecShl$nZ_|fRpLA~S*os)}P}k*Re&8Sa5FV$%YhM!n4tNM4 z<(~jyZ*pCv_r}<@KBmpG(8M}##A};{)O{iP8l;(cVR|I-J8s;7w=dqaV$^wvCJ)1S zdY%jCn&Y(U2wm162{g$hT#o#`3(kT{jj@q0?S4f#koX(w>cGn&A2GsB-)4hA#4C0o?@y@SFy)>|8<(?t8S<^{ zJDL7~DSHwKN%y*_#5j|D|i^12&x9z`q4!-xMRKZf(QFks9niu$R^YwFB#i^GUPA8uhrmy%adn=;N#?uiQRpAp%V>!T(U+#gUyxh+iAm zIU0Bl&SMY29)OH8gFG?nKg#hFr+MBX{#k_}to9JxWqHk68;mAB;>zHS_!^HaJ8Gwv z^BBekTby+8UkT86FUr?A04+h!I+T1}sWit_AoxN!w<3eDwUg%W0ACQWry=5=(4Wq$ zCz9fvjG*B33++O(GF;H;B*aqKV1r07$6?g>02}TpR2HUB?#n@k0w1GvQBPJ~Zdu52 zqpyZ4Rd2hWNz!ebOgl58h?cTi%Y~y)nBRN*H{i~@o`mh@Yyq|k-9qcCHCo+Iq!fkf zX}#m#$Jcu=d<4#(c}scNgMMDmUh~_!U*50pef!Vg56^xip7Qv|<7BxE#agOXT-cn$ z;pI2W`)+@^zV@&G4CgNbDt5M)bWpSLkpV?4-;MtbXH2*6i2HycG6JZwE!a^)gO&Rv zQ4$+WuVM0nMOBP1a`R+tFko#P%`4{mNIl;ETPV)3!?;?(rkw%CT~7qCUFv;sYrM?jRhYWpKu&0q|!MDI07yPsV0`p{%DL);f!}*8z8s zrO6o`Z3Ur&u<4NT7~hrX?D~u+%+=*-cI}VXGkhl&x_&JFQ2jJ5%kP}=*|AQe?F6$& zZtlM0P+s%Luf#jwcq<H-DO*mIvZ;op3yhoGvpq$G7Y6{Lz2GZ6AEINI5Kr z+fT$TxBZj(@8AD493GyOb_UVhKw#c;TdPokD2mK_c=`VN&i8(c-gVFC$pdeEX}RIr zpHk7gQRl{#0lVXMHy4IrGeaGcJHD@=q&g?eVmk5T-r((}_USto-%ZaNIAuvbvPs55 zLjPy@y?kO}=#Gkju=#y1S;0HCyo)Xh#s??^rVFo zZJ5D3>0#4F-w7QFN}KV1=-$^&eK9MBZa+JV_!!Om8(W&07n?LYA;EN{4~~ zrhDi=@Q>RAVIzDD<+51t;Do~ocUvNBOtjb{Kzs46(aTSyh3|(w_*7xPD7ip%&9=t2 zWc_=XwQDPes_&YnMNRiM#;aq&r zrq5gTBpo>hV91SWC%Xx{THW4wjSV|8cI@`dy6O7ZF@9;fPyjWLbF6LGr~36!lG%BM z>?jlOY||aIMfvf`0QOb46PHQQ?z(XMGR)IdW-K2$Jz)=kwCj=W)AMfTKcpT{iV@CN zD?Xo`($>;VVzc1M)QmJcz_D_na8< zu+Y0vczLDo7D!bj@Nvna%W)J_`gFz|iWULD330i#D}s27ysHI51Q_3KrOY!7*g)AQ z_=!$mq!Eel0GR=U3I{|_ADq5+sirZ3FER*x!%mVp6gszd{hE~*T4gkTN;s2q?h3ar zb3qX;tLa49=*QOge(;>us;bJ$JuK?>zUHa^dKl z%;-BqEBQA$Ycn(4bNs!bzAZl=DOE!G19lMKvh zhXCg!fbS-=kEU-7_F`W%9yy<_H$?@e2?ZyhZ+|E1xBdszSA7QX70-q|_I@avis@*J zzLlN8p#m#yW8OOsf!}xs@O$qB?!F9J9RSk`wP;hT?^+j61bBy2FlRVH6w^Q^>1PLq zHaRlLMojhDDJ8|B0`9xDZSf}MX`d{-8nC!>H5>XjzQy)MP=lvvvJ@Eu_rbJ_;uvpx~D zrQI|;94?&?zO<0sz8fAq7iqRkym5YM_D9wE(Olv7v%8^(E$7|iS)$YglSY{777)5p z^Iz_EfHIFA0rcTh-lj5P)FtDbK^^b>$uz~hrqaViR|GAeoR7RS7mu<_+zMP!W-hlQ z5!VK;0+V!!CV=`JYuI8)bhIbWJKgqlFgRwvg@GWsPaMn5Edm6U`Hz<}R z|AOZ7+NB`WgWEp!mp&2O1%&s|@Hx|C@~;pUFUHtd2jImd!-rU`wI|8O@SJ=r*|&vT zUXZW;{;tK&epP$dZHM*l{|Sr3R~*GUzcFDa(7u!3CI#CANoi(87bg0|3K4P31KD>g8kHYIWiEDxFq^$u88G(JlH+ z<170ZP<7i&yUTkjw3xX}i-NWb2O0mUfaKJTEN-h>m@r8`7KGP8s57xh>J92Y;q8&AA8mEL6W&+WuM>R!B*EJoyt1M z9N-O!DK1fhkhf;~a6V#28zTOwMO%4JuGADoYRE4oCTG~vp9by@Q!l7u2D zt{Gt#7Yqwxfp$!q@R%EsZ9;-*^ZX!Vri4%v zFZ`4rK$|~$dGiV8+Yv~6|5g`l{$?9NSDFvjXd`mN>UNtpL=ms3HPL2m@H;l6P4l7t zUma-}&j0T=8?vI`1J6!Rk`?n3nCEiB5L8_O2l+a4HG;DDWkH1y)ST0sazbcA zZ{#tx&p#Fk+bl4bVbv&68brx4m|&vs1H5a zo{5>e@C3kGflFK9mG1_A_pQLY??su`(Dh!25MJ^Z#5U*1KGNih(X5=xq}ZvhlY}*r z{u(Aq(xn93gyRg`-nLEti)fpDUMZ$YA#^5=Wx>5oiw7>_!KUn$fIWwRZUH>-df>Vn zfb;K%KNgXx8L*p3Uq&;Iwg9w1F+=D3>4l5mFK13aN2c<2Rc+JhJ+lb-?>I?hL{e%( z8gPD*DE8Yt!32dp?jEF(X07NBp7Rc?{;`*YNvBWxL`kt1zW1}1`>|{NCGd)=Ih0l$ zi^{F~-uJ&k-}~Ob!CzhbKs|f)d4je#ScQ*MoHH?L zB-B3mPYGt1v5{l3M9r6w?S%{~c6BS=)?7Yt%;Sfvl4A@AZ;Y-HSG~X7UZ(5HA73-# z0b=jjc^X{Lt3)!By5sqk23onr+)Dl$Mv!fk0n3Z;;%~Sn(A_t5@g)szj!<$1IT$7*Aa{)8BhdBl(PTme=UEqZOqH5{FEEO_W*qXa z#u#HfguUk5alxO$1ce;(Ar5Xze6>H>fR*}-`W$qKX1C#JE@Giw8wR5!gjn{?_{QN< z2i>&pcKJr2f`9W|%C;qq!IwXp>UykU7ZJAFP@wov-O*gcrDPex1pytN9`JBKkQEcI zn(qn8ZQzjoM5#QRLb4Gd`$;*>V1pEo<4GXKC)(D=n;}?7;00h%S$1;n%Lp9g$}!%) zm@E=QiGj?E74P(ge=dMCP0Eb+osI>u`wKkc@_>z}gJz;nn;cR*nH=Ji>vJLXcA`x# zPS`s-7FPiAohzv3#j8OKc@YgUEnkQlZNG#>}WBcu_4fg55 zI(vh$@L~4XTp>G{cxl36&M{r)gFGd8PjV$fc-CWEFNZ=23iKSR4|Lk5Mf1b`G%%I~mrl0~bM-{xJcPH??7R(Mb{Fmm<^54EFLG`U(eurALD9O$e7COa8b3r1Hc2g5o;?C z3>s>zcg7uu#f{rvu0VF}0m*)wQwzW^*RVEtQo;tz98}ZeF}4WMuiGP&De#VwqvS^ zARY`*%b{Cqn*oi>Ibj4QA(&Y8EMXlys5NskakZw}%Y0Mf)MizRl%mr#oziv)0@$K<_3dUeeRs78%H9ER@iOX9{}Spe z{|M9bJ`?o|KMnPMd%&i)-PNzV3;2b<2Ht!tN}V9XiG-w?|U^yAYWYQqm>$ zKw>uU_UMA|H%N9uaO!C7$y$&W_S@eO-RAqHw7C&K!jI6{p+0*-?9dEbRJ4ah+|)Jz zu3Z5SzZrPvJK779J8s#etRVZSPt*n5E?i-l4RD`SrQ-PbBlP_FpO&jmKUYNFqivyW zM&3T&pTNqKGc13%OGWE!K}btH+LLgE|GbOV#7{b8-Gw_xtR~OvhS6*c1?`CxK8V`? z+F8zM^cV&yjlLPToP$*>AF+-Bd7*0iuaNp#_mXSHS&UpeS``j-BD1B_l zvWc7FSv!7=3!O{a#OZ1NL%YAbpEAE@X8zqaOL%WQ*}wwpbqHQ!C$%it9pjD3E9kdo zX%OML_CgTgBTq)1a(war`%R_T!cXD`ZidX9?yMe(mMG zT{*t>xqVMZ0ZG0e=l)Y&i6;jFO{uQeGk%C{j@dLGhPP)t(l%xvqlTRFlO&qUuyWm} z$NNUJ^*?~26A3TRL%O6C)+@rkfxurL{2Hcrl*@~ONmx;VhuILs1Q^LT@Dh>4GW?A| zG{}?10-$ep2m=n8_e%03pJF*Uo2&vq0MWLw1If^1&9<9)aBwfL+y;? zv85k&k4XG<+p>5~bOc-|*saE#cA^bALrmT#cKjk9xg7!k z?}!3g)9q63!ih@0L-jSnwkTGUXcXU^eJ!6$3pbRQE173jy;l=XVX;jz{~ z|GQ5q@44+`*Ec`lhwG^mKTzj+3oQk;PbXIG{_bAs`2)>Q!bgVombHFgRhN_hUaf;k z7mD!{yVFLn3v5jqWrN+NsNqt2%(C!yVu%PPDP@*fudauO-={}M&z1Iel&RHfAuu9P zrp6`)?O%Ab&u6f9f?PO#1f_s@z(ZcQ4mNg_jRF2%W{Xpdn)`s8($yOTn~%3VP?=*#6fa zh5XhZ17Gqvz=uB!c=h{%|ML!%!wQj;(3PT={zM9t)M!As=q5zcziZrp4L}UF`QC0} zFbFbU=%QBMRl^<*&Tx9vEGnoK8yz;v4uKLq5on8~0s|Bth@!h&t-wYg2L<}LM+2{V z1&W?PRhww3Ajx@O#)&RK+>#IRK1|ahS`?VE*?fZDci~scRj0mECV8Eel}r%b)I-3%#VY<@Hvq|?kp9*0~Zep*9|_+Yvr8ijcGNg7bOp+10o zd`+|~w6Yxwx>=zq6(N2W<11!N^Wux&?}AGHNkSNN|1>T#9%u|>O-jGsM3S@;efT6C z;-1sm-H-H}9GT=l`cK#r`EsW|c#g7JU6xm9R-bVB0nE95P$j#)LoXJx#wP~GIbfTN zY=tb`h(`@y7_j?h<3y2}WoF&v#~5$&P6VVnZBty9UuuiSZ{tR*MguW8OiH#t=07>+ zq%t4s(xM3KqU9=#nHayb3Ta_CH+f|`!c{F(S^BL*TaDkXfTZ6-V{}tqf;6jR{-f0O zsH4F&ul!Nx-RyJu&Njs6%o5jQQXix{_2YS(PANtY%rvsq?PoEpr+XnL{yuWj@lL`+ zq6g(tseGGik)-={0?jVG74l`l4inLE!jVe1ivc&NuXerrS0`*SaxrG?$dmX(Vp_dvA=A_J|Pti0NcUSLkz5#%xf7{sk#S zQPQ8n?W1+m8p*m2OjRCB5M9CyASUH`vupd)j|ZjP$L{kY08ilCZ|~a}_3wd=)=t=` zI)C9mS?=n1&}pfe7Z9ykSJ|HuKgp0;M(Q!x=(YdG{K0Hi=oFUdx8JXdRqKp{IDf8? z=@>DC^()XP3@t&G-1(sUNEEFOVr7E?$aY9D4U#wGQZvz)SU?PJkgHg#<05t_Q@;sI z>Ra+#a$=S;Hl)oW>}<9s9(+je#?Vi~`#kWH9m%L0>%OgE`a>KcGTPM`s%n=c1^=Km zM@o096kLUL0@(2(DaoeY53Y|AJ*nT2kedOo2;vY}Cb*T6GO1kNURCeB;|u58@BKEJ zw+|6Wg-BJ^mBdt?3D$u@`+jC0G&m0?0EMbpKA~+cTSVIdrjRmi^@Dex+TL~U8>R(naSe@%U2BO(qYsst?B9F!c{1ead4Xt!$cW2)7moEw1oy>fXn z5dIjzQt-)XiS$W0k7}ivv23xakFJM@-=oKyPX}~{YEjZIX_neED~sPIH1oH(MOn8h z^ym=sv5!K%@dU7)z16m3Voo>}i-$cR8#tpSnbokNDzFum3h43i2HVR8I0c|it)iV& ztKAKK&xhK0&mS_8#Etdb&C4cO!X!ulQQi}x?4NKPigt4&86Q2g+Kx4#`M_$zBxj(1 z_h#t7eGl-6PXdmvf?y9)CRAz06v!b+wvd4>-C8g;sNS&DtJ@s(faU3 zZ@YeDQ;gE$EeBT@GVJI!8$`A_`J{@}XIrofqhX#ocKSKXv(Z=jB425YR7js~*@~1I z=Py56KXB_m(cA9)64}lxlywEFsLm59E)oyp(M8ZAP;+wi>*kAMF}peYB+x3NGY&6( zxW4z&&*9GfKPuOs{eR@@Gk=CMsj5z$2{j*5W)eLfc!Fy-5qQzAbAm?`B57dKb!9ji zDqr}k#RX2x7wuEu)~-1zKTf=uc#CjE>qlhYo}>hAVLf*Ke^^8wgGVf3uW0whX%kSx zY+WwjG5*1Y5@sL$B#0yIw!vNyYxvHCUWx(Ke!9>7a=ah|R`SquG8fzjpbHClL)MS)WU&Ug~-LDG+}l zto2E<@sV#-PJ`8Gb=xz|J&zK1?wA4%cqa;)x&UG6b$nFHh+2#OdId zeb)Bq`aL21==P*UYfK7v8|v>wkdEt%^W&kNTw9%bV;KSSHv6M=;O~{d8J8CQJWr5E9?5 zd~9T4-KBJ|>%EDYC?D_>KrDb%L~iTmK-g(xBw^_V4w-yIy%rxrr%ttmtdf*0J|W-l z6AS=habUVE_)BNPlWL8pXj@$fF@L1iqqiAwE8f1aRao5WXRLEPF@JP|-ET>DMiJ@9 zucjh-(Q(|MyKCIyPoinWtI&b+11)ZEp^gw-%z2R9Y1+B%Ka^qSgBsZj$;TdHF&iXP88KXIE!Vzl7gH6aW z6F><|e%fH@l6a0JjN3BE6YRV_{yXLYI+mqtJKI`*T+{=$JL!b`3}Bx0+b>^sH=`8? z5q?xR4cffAdeBdfKtatP(aXw8O?ZS4PFm(%ii(KJrXJ`W=bk>_arf8a^6@7LbPw$$ z^FC1vXxg%pcaI3!<$$BTukLhAnpIM$icV56V{Q~eW zv(~zTN)_}u@CEYP0z`E{YBUB&wg1A=9ndt?<$=wlv;%;x%dq7gUN>AzkE28ShDy@| z08wdY|CcI?eZ742<$84VEtt1A1G0hE$+39ZTkz!WITmD|n4iu?oq?%9KK1c!l6tFc zr?K-M$&mTKc$b-H^FT1_}_LI5)kSPWstE6Y#yK%hk+`==ms037W>r&G|nMU@J*m;6F9 zF;{BeXj|}T`WVO{yOV$t-+v+LJjjXhkh z9}ARyOz@{i#yMhYSO9HflHPZ-pXx6x(<4Foumj$7q;Jz&E^?$V*fj3y?%qo_O{|#$FVz9(=lxAvMqLmy)FmF zh(PTaAD7(n?H&bh+xf_kIkzu?(H!c%Yl~UhbkttkP^E+R3eTWopyup03wxB z(gC@bdJ99?VZK;)mUWrwup3)YSR@F6(Xat;!j9An=R48AQbSfyykU+VRwp3}eNvkv zq?=81e&iT6G5$;Bd$$-&%_@imd1>!&tS?m}b0`Wptx9S-6Tag&$g;yy@C`vg9m{zQqfB7~M26aKb*)m}Tr@+1Cs=~Oha=+`|Z)BN~!SdGsO zv?+-Q@VwTMcygVyZHUtV#$;w9`Q{R%LuA>O)a{F6&|=b$n;Q&EOvZL4?sGoPdbGtI z+GXL)n51oyq!o)c2hv(U-mQ&AHB4FojGHsO+t?W6bv0-j=Pm%Wh2n(2QCOhI z+FZg*@Zx#1ZB-#(WL~*FZ%AUXJIRZTOhUq7x)I}=R@a+srHLP&6LTVsj$!_+l{Y#i z+SDD`G0yLdWsG-kT2omW${|>8Z7cC#)$zR9f8>(VeKBE9asp+j%=*zjpc z99@RgKkUDuJy5;Yx4K;_*9Va1<5GQtutu2(6t4i(i(yTK%Pqvm3Vn60u}AX=8}%vm5xiD@=Nz)dzB!vkLX;q6=F zc_xk;Y7%l&_t+fmEPJO$NJRdWgGC{PWBXa5Mk`lNZL){Ob)Qg7f)nSBb6e$vtCtIM zrSFPneGP*u>&Irb!rs$rd(_pCZyr^~H996Hf^rG4TL`(rcf`1?6k^R$zjVxJIx?urGr>dT@;GIAc{$qtUggss&ILMa9MQA2q+_ z!b@=Gs;8BQ-uS)csxxm@XhE$Lq|}It0HE#MX{~L}ukZXkW$RgCOe=#vF zEY+Pp2Y8TfI~V#(ZjLVlH$NPB{G)(ln|P(RrT4~aWO%W!ntljsRaAj)x0}_+-TaD! zYtF1Mz3p9}THo=mCt=>4l`^S9w@_I-4~<1}q~G=IXIG-%PCh+2;tpYi*UI2((OBMB z!`SBl*VWFtP(Ws2QlQQu;}U26dmqK*xw{`{ZeRz14Wc|Z0BE-rV;vFdc1>@Y9x%(9 z_sNyS8@g;uE1@zAFe#1;_RG8F!B_u^JmADH>y4+Lk1ZYz=pIUG!w`QVNcXV32%sJ( zco1j4ENC)3S%6RYc+}Uv7ArYvVbJDQ3{_oA4z?V~+#kp2XtKk<-V0otz;)*(7s3eFvZHF>`l3NvjEjj1}Ehy3I@J@0r2k zLwi$_50eYHjI^rw&i{V$4kjsrGRs^l?mGAQ`TZaKmpFg;Bmh^5lo@q9X?qevc(>)Q zSR51S{-x_Zb~MH#+>X4EcC3>&Yp4@ME2grMT334SeJ{|9htHI&uKL+>zZWn|!h5a_fV9#pnR{$25!Mg0d~!HHh=!IRx2ka%@*s*rB4(qJ#{#C5fjb z!=e3{*kt}g{>k0Zp7yYJ%*BM3c)V)62?enz++&14y2de}e|LJNuWjroM`ZxxH@?g8 zJ>FT@-a;9-QvwnwVp3o8My*y^!GGxtzNJ^-Dtbn8-jxx@%R) z){=ADWmOs|IknONUKR+isuzl1dWwK={xJyl(eE*Sp%R zw)dBJGCO@PUmB$nj{8*xWYa(KoN3LA(Tq=y&5pm~6YxF}Z_@5Gxbt`~p|q4ANW%~% z(_cWU-Rm#X@^CENBFhNjdqSi*4~aDYZn`#KaDD)KK!v|6^FP#Oev%DPd5VQ0OvOSM z#vAx!vgGoHP1&|QbdYAncp2&QRm_fwN{zUil7*;xGg;PyZ5!*F=LCZ_ucBrqRo(v{~>uLSta|XeyRMzPDg*B6BJtg4k|nzB>SO(e;y}{U1%)IG@P-Q z8f%C^wZ-OSGlokvo>3BzIgkJC*K3@q?|ErBTH=YvNIJ9=;`|Ww7=MN+$-YCL(T^Q?UX+X-LmT2`RWNh63vz?7r!H?+R#LsyT-Y|;6JC=cVaal)_)cYRw%K(%a z@V33W*`IkQ6}7<}<5uU6XcoE`*LOd%rSQ{}!IPQIA)<*Mss#lFbJ0m~;pj2-u6w^p z&!2x5>h@Zo^of6MyE_eJ+_%U{;QT(kJc)xD1h_>z*R+6+8@>Q;*t~ui4@i7pC>D{1G2_UikgnGYy;6hzA>mnS?-Ws&Yht$4_sH%$UsutXF>CDMLmydnSpH2_F@&C+kdfT(~ zt~;Ly=qZ#k_nNCPp1u#m{aO>2q{%1|;y987Cv}&o#_PMG)qQhZ%U>R+#qt2!+iN#~ zZ2Ls^kP$c&(bJN^-GnJ6+$MNshAfcoz(lw2Xe_hKVK^Bz02FOc;S`9}g1vg1+<4+; zc;Km@#F_Ow>*{DvR`Ui$wwOglMA>zAFo%@5T`7b~i%I3vpM?6qe-V1$F{JFpBv=sM z9kkoyV3Wk0UtBCvi-TTYU6rjsug2l!Z`RAl&%udue`htu|*w3U0)?o70=s&4+s2iV*g&hCvuUQ1w_G?r8qFV2vcr$OLy^9G|m@3M+y$98c_rjuym_U$_lhD5k6V_)-vwgjz8 zShm3MxRLqnzw&}aZ+~c@`OdCbL;meT$y+{*%!H)L2eNggx%owpRG+e)ML1%AWSP?6 zpiMnYvwtZQPv4`uEgsSCUI2VlP%Oao(7`ly9MJ%9ux&Lr%J!2mrW*#M;?kMv=fHRP zYMSbdzT6g!*VX`UhOs?H!F$hsT~@;LH(`TXV-kod9-vpon>#{=!Hr|4Px$vE>(o#s z#*O|mVX;U0ZDSdbX0wbs_c`m|;xNg?LzwT^?rLExW}FO}kyhwCVZXO;{ZUS9^jKdy zDBB5a<-~;?nAOzG?Qs)3Y{Tw%yh%zMoIRykNx3Ang?zK0rVn?=nE=TrCL7u4>ASm$ z4mu+I=)%c368c||mvjmU8$93>L}LDC?JH=*9|FTM&_#YUZbDd)VO^7tSTicT06I1x z+4uc#zV0syHiF!DqM3L)NIe&K`o&~1+9}!7AkXxPS+{)O?4i4TZr!xFt9{1(EcskN zUyV4;hRD8@H$$?0F;hF^HTA}R%KkS#_`xyEzj~K3DV)kJcqh0R$mZWE4zdd`c#Xf> zea$BR6YWW~Ht|5-r)2dhu7bo*{zmkTkf?@RzR(?6C+mwQ7;@|#J7H!9oeW&0wI=K9 zN>IlQ3dzCSNgcAhC6^iVIlmWweGJ4lt2TzAw%uMjU8=o3+RQ|`tk>ynsg8Yu`lSAM z@TNoGe#hwq*q&3qHqw;*gr2No1v+L7`3GqNAP+v0q>wtzPx0pZ>Qc@=*P0Ul}ggWh?bl#_H&djFwEQm8W{qbDd4TO-F zIT9xkW~iq3g&3T~mMM!!6`;!H&Hd`#cYlqZyZC%;j~^}~TUFUWv`;uk@5xCW_;upvR?)QV`#v6VEMb2y0 z2~`!qj0QM%3|W61i4Y%>&-piiA_~mWnI^3PE-~p(vg!Zvaphw5RLmvkD+)r(D z)++rN6Hw&WIR|2{|9NXwzW;v(RdQ+v|Di65vmsZ!F{nPsQw>+c1<6WO4=kEJB znbx9$Eg+NjaNli2yj{aRr%7C=$ydz@Jc-Pdh7}sJ29GXQ;j%L5lZDOvMvc6xuH={Q zxl0gY5FCFK&BF!id}vUIBBK=8PGh!Dkakg9hTkXu1qHIXAlFR4fd`%WX}#g#4|FYC z$b5i;%Ro6ND9~Elxu`4YulxY8Ydg;sQX@Z4wc9a{fa%d!Lq7Aln1AvIu{!Y}%x(L+ zfo7YkHoVBVt6H|xdi|Io|RAIYhc|4H`Ne+a=wfeDZ~ zcccjmMTE}pm%h^+1_mzaa5W{^9TpvJ!OM7*j&FtR%Eax)t+7keZ)yQVrA$?hw`c2Z zw>@9)yyr#O%#V{YZK1M-%xF{TG}*uS&oTs#tz*DMI8~BAkdvvNYzA{iZIPZCim-N$K`?C-1fvD?z8piXkLC*`s;K7+0=BRmnB&|K z{u@50>uMMUmk-~wNu4n{-Kt8TYItsyzPZ;D=(1gk%1QQk9M#qgnkw+#WBm>CD8T#e;*C*xz zzeK8^S08Z&9-`75kC0A7%3U{&Kp$LQaWawpC5W^M#8$L|XOLXpK51>XC-8UuB)Jpz zg8a`GY0U*awbd*PN6V>qLSOTQt!2-Ouu8 zUc9aT-D!fyDBdjRV`;<=*(M+Kr^G=rM6}nbZFogb0ebtaJ3?wJFY!IfGTvt&N;@ps zudG?~SEG^rm;IU8aML7T3t(eIXN(h{B)9g{Nd-Ie1HRT968i3JxkD0QB4lN^5{_mX z1Lt8;BV^U(Xy@efc-t_x?eGtM`B20#A<4v0E%3(baBRr;dws_;b$R3s&_QPtN2}dL zui+LFUXyUX*D(50Z+6rR2lzcda(EbiI&=yutD(BS^W802x) zF4XvuR^2t_5Ig76s6F;NvqRVFj{MUJ90RC?zXcBoexs_}%FwdfX1vY^%fjk8CXv4} zeo!)?oqt3B;W59~HW6yEKOMj7HqH4!my5E$ISA8Yl5}%dJ7pKH zDEq$lvV=cmGR}cLNv3pN)lJj2CGZ&T93{LZ{#}Gh=C5Hv+IaRSSyf?AM=%YJA7k!_ zz&r z1cP_RB&*Mk)!(^fvvy4!WRQ+n+Fr6N(_A+7RK4%~ zm)CpGe-$>{M?o+{%2okE(G?IwqroC6$0Oqyf3rk2WER0Nm$wO`823=$(*M`0wu94; zk3L@C_kkaq?>ql$x#9XBkh5ogA4NAh*EQOX`6>h_PRR(G1U3Pf#H@zM)7}sV!3>C_ z40b*KuJO<&1gNMufz9UA^yuhoG0#te)=3rHR>mj)Bb6qXBH-xVFOA>xG~2B;64Zl< z%nFqXdERG2AAAb9q(1rY1FzA%sk=1UxC`joj9tZyt*lm;b()Vcm95TI=Dof5lt(}O zM^_Ji;H&Ff-tl?*-uFEnM@NssYPIPTeUqY;q)tZJ-6U}#jFbBdIgR$d7<6>Z;h(z_ zPaX7=b_Owi$VhqaF36{Q{{X0miNYD()yJavs5}2c$SZ)_ky|1vbHxGvN*-|Xd-1@N zzpMvk)`AtJtT9cBY2IL3T|$u=(%%sP5jw6WBqd9_63-?_i)}@)-2l^zJ`?!!KfwI1 zTSV4-s?|E6HyZZ3X@}SX!-8G*4;r76|LY2E1hf%o)$R7vaN*+rrR(*t;N-z~U^Tr3 zkkTJ1tUZBMix!a(Yw$&nwnyirOrc043q`?(8RA4sgakJ~?x#d3g%*fbDKbmNzTS54 z=hfTp{CZrz{J5^S4YU^3e$GE^++IO14b2WfzMpNuQ$n1D=dPgLCOr-kF;pn|L~oN7G7fSV{0YLI z6Gml`j68Z6*)h0{TO)Ny{Um7d+e5kjJz{;dKk79Pl)cozgq0VEAzn0yW(UQPt{|Ws zZx{?^Xh{5}@YHQ%q|~}pVkhi22vQpGj$y?M53x@uF-?mWBYy_XT}j>)+lTj?TZu`> zV3-%=99q)^q|q0oXFIV;f_9y04t|!`qP%2T+advnbs{laO$-{PH|n|PP%&Xn^hT2d z-QHZeWKc|Z7AZOsP&sl!y1Yn1G#ZQUxv<%Ik3D0aJ*Ftk1{{W+faOsOK}Is7KAA4s zPVe&xF$#*NvlxGN+I-?syzh8DQV_1hJ3pltxrT;`8NRm;sC}Ygl+D8FH2wC)FJQU5 zni_C2S-^?ED84J+<*+c)NWQYuIIM8SRF3h56!*Iz)zF{YfRReZ-$4tX0qj92<81I^WMSbCGgvTVQuy;15hNpBXTa+Fv-0 z!s+AqoBVI|8|Hfln7PfR_68R0BecmWwHJAZ`xDEVWxDg70CB%ThC2I;Csmafj_v*u z51MYezxcvWOdf}dclm$t>qIfX$vn)?$vIJsvTwY@fSuNI70Mx>lpZGG>Oc zutQPF4w^hT$;qZH0il<)q;SW)V3g{7#CgOL2C!qYf!)#*n@(T%I2X0cZV`~ggNu?7 zeepu0vi3IZzeRb*86VBG#v3c*1zOR*d&K)tFW-V73CM6p;(IXCA8ksjq&>u1Qs*nC zpwBJ`+$w6{%3DYd9fdf&yT)qBtVOTB#b$pWd` zCd@^FsgY6o<|vg6s1ZxD)7owa1DQZMtv1^y6qp0pVpYzH_i%ld{e98Zwl=|* zsv?+Yg`7U|=JN56{rmE;hy0)U@815Edh4y9j=8R7DjQL(xo0w?znT>a$U-*0(~NOufi2vAv?z%l;?oDM!BfNyqRjC$u-Q2CiCmY> zz)7RIZM>ElI_djx!`?5;gHHb!oLb#hS6HLSgfb~gsVD^~S|Cz^0+d>@foOqXVur(V zlLVbsyG_@Py{ee+zlMCn*FgWp{|!MEDL|beF`HvD5_%Duu@pacMaRG@Vcl4eEKn6~ zTPBY&SJC!8F~bs-@={(OHVE>PO84hC5!cq5~hZ!<$tnjy#r| zQS;LVG0JVxqM(YD6)s%<)Ozb3-;8q?p43Xn+<4TrV}a)i+KfaS(y+}t+IC~>$k*@r zX>wVghQ%)TxSu>(McU4yaes)ECeJcMis*&IPuEMgezIJ1)sM;9YraQL?!8CndD6BV zQ^ka3VZcezxPi`FgtN|@CYswVTY}-ZNU>XoYc$XeS&7GUZ$m=5i-&z&>mfr+4ftBl zt7yj`$FzpdB#_>|(?jtQAh0ne%%-DQ-X%Ta7`X%4xJ|Oe?quFsL@CW6f2DucN)%Phw`lbc-| zpF;J26;M`Mj$Us6i)}_G9}J zam@1vwteGGqOa7M`LB3e;$=bYN4#|(Va0?&4Pc74^~i!CGLw0ie)v}w1cNx^fRZf2 z?|6!Eha@7Nm^2vgJns{SHGk65F^e1WM8aE6Ub%}G?!1qe6Hc1=Ky>+i4ITOAu-(wV z#DsI_Lo#KpE6SG_f&!9^tzX7>vlHdr=piMHT+se+^0$y5Em3rpa$OS*$=l@HnN0dZ z;F8mCHqKY9j`_Nvk46I@lFr2inm#tk@8bK{qpN z(Xh$IQ=O`?UGMSk2wP+rXz67WZC1YLVBRM6CH=eD?)r3J2AOBxW3pX+j9@&<~I?l(m{6*3= zx)H7;W)yiM+Fq~=NV*@wY4>9QWuV=|l2AJc8hO8*zxa>JNGD4BU*Fg*G1=JKjfrF5 zwIRjj!E}S#48GuWMkwt#Ir0LoL-awSzg?N?km!V%7HAiL(g4J6NmNNRA?supJ-hyz z%|P4n+z^y$0xlgtvfgv<+i~gW^F(zG#kS=G?WmvFjle{k)2MjtBL^27xOESVa}aIn zx;Z(D8~>(Ets`r><1T55WpLZ-J{$Lw?{tKtKKgz@h1@>rxC3 zN9*W6JSq)-Vy3JLt@9RBnNj4z>ev3{nf2ED{#q3))Hg#{ha#q->yd4XBv>`;jCbSX1t>_~Z!`CLO?zNj6DRcWmvIa)=?a>AKS_yMM&h-na}hXtN&*6op7Z(Q~+JdWAgT z#P`-~)^EbPY#?Qg^`w{tm*2Y(a#k{?1ja~*V^TMQirQ&~>@W00BT1gS42jKa5YW791aeGiBN zE7@EBCHD8f3;TP&0Y8wZKfN^cWv1fp!!e9wmH&ry-|ar#>t##CPZfcb(gJM}fnY`{ zg5&Mu^v-*}6?fh9Ow99vD7Fw0g|3>p=V#;yb0jBgl@djDJZiK|fm zy&$Wz%S+c>X1Nn3P+F+|u4F&;!2&}kitv1~6Ta)gcZMzozGirqFw-$*ob0E5mwlx7 z=eRv^A0&s=M(osKd53(K*SVja$1aiY#$pWeZv@Tuuj@gmePVNne$nR+gJe9>p}f*} z4=XCpuf?0iZX{&=rU`xaP^V`Z0Miau;zqlHN8HqWKmDSMy_ppHb{1M*<~FYo@`mcN ztAi*O>R*%(`gX-VE3*vbqyBOl5REIfJx*$+FFouT&eL?|k{|ES#tZReLQCDsKzoe0 zf`1vlkN5jugKB5c+N0l6ygeQIQtB(la8`!Cr?J>_97@9VHhq#`tX-qo=q)sM<(NtJ zc}zTvuR_wf|MvfoN%nr~XEBD)-$RCUONA!Ha-S|!n#gT3&TTExi0ZR1un!CYCD@l_ zhB1k>I|@gSSGu$02LmtMb*5@pkAhK*$~pjG4>WS^#J}W;@tvS1=?`KoP4`(;Opi!r z*%ydO*E?LW)s4P*N+-GOSXV)7R=T5IS(vn4@^paig?z+$OpvgPhM})iXOK^7V^M$i zJ+XQ9N~L27FytP2+mZ$w4_SHbt$1C|Z&_%A?L~i@1xpj};=eWQ#`EiC8#cMmNBk&a zu{g#dJNl+F0CEn_5H^ev(%c3uCM03942U-7gLM!arHF5kb&|ZsCl=Yq#J`e7r}0^v zyjumW4Kaj~-U=gMGLnoi16ZFMv>>3i;gJMGTh=-0Z;sfIJSD2VNo(H7ORIfMR=@6k z-yJRJ=>|3=p<(#LPR1SSM5jKH$lV7BuJtnSJ*y5cGC$u*9F8oD;uY;Nb=@kTw@mRJPcwcir>VdhX(jW!8rP z*s91DqBJW5*qo}%+vci7Gz~}-!$zFKc7%y`=hhAC;So>^OJtTNgwi72C~?di&kqruSZWcDdo~50$f5{fbWIZk_Aomfe=8%@Z6j#}!e# zmce*e+AX9jk`Jq`?>!c16~#4rbo>=KI{GTi^TP$$sOm;VCjb-N%$h)KSBqU}KAEMU zraYF6#%{y>A>vH!a|PDG^fh0G`Qsl5TvkA)T*fG1%m9BUDoGY%*XF7sBD$IYL8;Sf z#x(WlC{^ghCY>h5JWH+HJIVvj{^WU>|?w0L@ z@W=6xf{F_5>d2qGxJe9CC$~eej1`M0M^9t^PKv@UOgszx$KjR@=E8c%ODvnx4|KNtT(_bNt{W zliH0EQN|xjwPS0ChHWt=WH-ho^1E@O-(7-tJP|n@?{l;9-RyxUoYUe!^u;-=Wj7I% zyFDoF?4rJcCc7V$Fr>Pt8$E+qc81BsSm-<0;mPzKSM0ml2UEbhq``8#9-ASI&g9D$ z%LxmZ?3k>h-ANcWnz-$l&D4Qy%bVTE{W|wJ=DVCX*|67khB|TDZO$jNW4CG)8FoVR z*g--1#Sr&6#u09FjCXnTBP)n!FqxLK_qG~2;pgs&!u}rg2F^yumOr*L_U*_FYtv*x z@koMq_fL`VY3jq}W&S~%v)Lcg#E%%3Ds17&<>B>>E*d-9i+dy9Y-^?T)r!QB_p97#UkLbsP7i8NI5!VhdrGhGN-^rZ1o2khaKWP_d$ycZ1CnY z+&O4UoROKTGO9?8bD!n6WoGp?%Y^z(?_W^!mRRr*$c}z2&J2-{`V%tYsLr^-zq{N-=_SC* zU)a-H%OXNSfyy~^EW$AFh4Ul=2{|)*w>~W@sHBzhR@|b7UExK9DC3XNa?tRE%$#6o z6F|IVip$$C3HqqblnAmYJI2URCgNlVb)LwC-2+n0l4!7z8v%jbDCb!u(*SOyzRVF_ zq+Wg0>yBn4RKOf4{L09_!9T0;l86}aD2my4` zwy#tLI^+20qxF{CzhCdW@I1Nk`tO!gCtr1ZaU^5A7AHFe7vL2@***7(XJeC1XgU zuK&XoOZ!hNuu{lB`BLcfp9nds{SjsbBp=`8?e10uGH@Vt+aIA)0WkH0Sk}{wRjHUe zWU46qMQPe{@gk*eXQ|NlmWSNOfx-}lewcfI?IbaQk@1alhz>dKvzqjsWm z2-2Q}@Cq|Y$tOA(dj^I_^8D(n1oB*s!Z4>`c6sg_B+R(%%{1u9-(82+P z`QYD-5t>BcyABIbMNm&oz>B{Mxa%I&zj+hN-pTfaPs(ONgARk0(Vi`;dW94mEUpMQ z>BCEuwz*FgkSfp>j*nlUo6XbY;NaiO-ri4RnhyI=q%8`{c#14MU!vv7#r<|+h>r_V zb{UC?3fij?N~tpIHG2O13-r!={y8omJsha*X#mw#V_ugXcV8xGL>Lnjm6h1wxj<@# zZnwaLACC2xJsW!Q61G3}GU(m6Lk{-)5zQ5YlEZHaTNW`$(~bVGjkae&dp|(I=J-kV z_IsXy`&O?gXU}|>92~s4J+)i=6IY%MyZ)VZ%{USH7%}gpHgq2!=T9GYSI%FC!9@GY ze&A|fluJ-d|3+xAc+#iJvP&urc_0JMQZyWSfSY9FDGK=#gs73D-qknDhri(&-HcE>`p4_s0x8Wtn?{unQ~ z`vpCgsth)^tDR^((Ge18_tVZv8g-64sd%G{kWEC@<>FVuLY{2Q4X117%W8dQisQRG zdn=zOtBeMZ3{H>h99g!pV8rpp%5-W*J0AU7Mghf>09JlH{%J?{lge9~nI=uf?KQoy zvYZs~mw!^TOWW+Ngt%b-m1S(a1&pRDLf1TQrv_4+ECxxm%c9z{4JCS7KQ`&(3+}#g z2q0w=AWQVc?dm8m^{UO^13y^$MvviDOwhAF63~!*OrX2)c1{YQ{TTA=O4)%lM})Nvs< zb2s9ediyaK)64U?E#XV{f5V^fsgVy{;dhj``59YKFjIA0J;@d+e&)Wd@?bzbQRwoDyhM3Mdn~6M7lKEdebUN( zasH?zo2CDFoldX&U#0l9BVICyE*J7!S=ych>xNJ1+Xe?*ETiG;L$##}D zH&GelX0S_5abv6-Vv0o#C~c5Q^l1EH`&M2kurf3rLKNLaU-rkV9tmHqgatSccZm=- zvj|RkIv^c=!$R{!w3Xy@{XHF}m8>`)D`@cnl1=RQ>+1J&Tk3>%fmi|s`tFmXRK6!y zyc2@&3!`Mc3;$Yn_3)764T$09B1ghB`=8Is!tq z2l8HqfWyxoBf(kX&_)YY1qImFoAl`Dn{a&mLYeE;P~D&aZQ?rPRN6fkNpc#~DchcA z%Q6#5SgeXT0Np^j?})<|_>vc*e8cCqy4$vlU#0ZNS}7w-6B}JO4oZY!p|+;ex$kLQ z$&8f%1^1TUeC<)mhBs)FsdV!KtX3T$U!o*~<9+c7uKitToi?X0q`D$o_~yyuDY zyWjJ5xa-bm0(AxWZ1RM*?K~0ONUVbo2WIA=6aL;`fkFHZ{_ehF$vm}bvN1f=J>X%V z7DoUTZ;e4cf(Cz&MzfMBC>b$HEeR_Cu)T_LA_`RLyD=5El}I46U+yeduYXL=?!Qz| zmG{;Of+CYnrM8LxB3O%J5|MRjJO3wvPEt_H3VT{{^9^5*N8kTnpyCXwPAHfF1=a#g zB!EJ6+28~;MPZxDft*YV+A%kqh97v_ZNN8wJLImrQOXLn3V>B#3?TkFKm!4Mt`_t0 zK2@UD@rXa22!eg_Q7EcFW&o3_%$Uk+aN^*5u($qQh#aDwU(h%k;<)7sVrJ~WBVK8b z&k$(~m{wKt5IuEq3ePepm*Gq8EsX<$~!TkeJa{BJ{F{Yv2S1<2k$fC`

y_T(QHIq35G!& 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∨)B+< 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 Q{mnS7sH`u$OenZ%)A_(0 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 iZRNSy8c9Z+TKj*FHr3oWC%-)a0000 Date: Thu, 3 Sep 2026 22:13:14 -0400 Subject: [PATCH 13/56] logo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b6c2b4d..6543293 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # DDStore -DDStore logo +DDStore logo Efficient distributed data loading for distributed data-parallel (DDP) training. From a8e6a2429e7b6112e636c2d9d951087df9aa36e4 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Fri, 11 Sep 2026 13:15:52 -0400 Subject: [PATCH 14/56] add --- README.md | 42 +++++---- examples/vae/script/job-vae-core-extra.sh | 100 ++++++++++++++++++++++ examples/vae/script/job-vae-single.sh | 64 ++++++++++++++ src/pyddstore.pyx | 47 ++-------- 4 files changed, 193 insertions(+), 60 deletions(-) create mode 100755 examples/vae/script/job-vae-core-extra.sh create mode 100755 examples/vae/script/job-vae-single.sh diff --git a/README.md b/README.md index 6543293..655334c 100644 --- a/README.md +++ b/README.md @@ -243,7 +243,9 @@ See [test/test_method2_core.py](test/test_method2_core.py) / [test/test_method2_ ## GPUDirect RDMA (GPU-resident buffers) -`add()` and `get()` accept a CUDA/HIP `torch.Tensor` in place of a NumPy array, letting RDMA read from or write directly into GPU memory — no host staging buffer, no `.cpu()`/`.to(device)` copy. Requires `method=1` or `2`, `DDSTORE_FABRIC=cxi` (the `hsn` provider does not support this), and a CUDA- or ROCm/HIP-enabled PyTorch build. +`add()` and `get()` accept a CUDA/HIP `torch.Tensor` in place of a NumPy array, letting RDMA read from or write directly into GPU memory — no host staging buffer, no `.cpu()`/`.to(device)` copy. Requires `method=1` or `2` and a CUDA- or ROCm/HIP-enabled PyTorch build. + +**Requires `DDSTORE_FABRIC=cxi`. The `hsn` provider does not support GPUDirect RDMA at all** — passing a GPU tensor to `add()`/`get()` while `DDSTORE_FABRIC=hsn` (the default) raises a clear error rather than silently falling back to a host copy. ```python import torch @@ -260,32 +262,21 @@ Not supported: `init()`/`update()` (the incremental-fill path) remain host-only; See [test/test_gpu_rdma.py](test/test_gpu_rdma.py) for runnable examples covering both directions, both libfabric methods, and the negative/error cases, and the `--gpu-dest`/`--gpu-source` flags on [examples/vae/vae-ddp.py](examples/vae/vae-ddp.py) / [examples/vae/vae_extra_train.py](examples/vae/vae_extra_train.py) / [examples/vae/vae_core_server.py](examples/vae/vae_core_server.py) for a full DDP training example using it. -### `DDSTORE_GPU_SYNC` - -GPU kernels execute asynchronously: a compute kernel that just wrote to (or is about to read) a buffer may not have fully retired by the time that buffer is handed to RDMA. On at least one ROCm+CXI build, this produced a real, confirmed bug: the RDMA transfer reported success, but the destination buffer could still show stale, pre-transfer content, because the GPU's cache hadn't been reconciled with the external NIC write. Under sustained, real-workload conditions (not just short unit tests) this showed up as hard GPU faults, not just wrong data. To guard against this, `PyDDStore` synchronizes the GPU device before registering a buffer for RDMA whenever needed: - -| Value | Behavior | -|---|---| -| `auto` (default) | Synchronize on ROCm/HIP builds of PyTorch, skip on CUDA builds | -| `always` | Always synchronize, on any platform | -| `never` | Never synchronize — only set this once you've independently verified your workload is safe without it | - -The `auto` default reflects what's actually been observed, not a platform guarantee: NVIDIA's long-hardened GPUDirect RDMA stack has shown no evidence of this race so far, but that evidence comes from lighter testing than what exposed it on ROCm (a real, sustained training loop, not just short unit tests) — so treat it as "no evidence of a problem on CUDA," not "proven safe on CUDA." Synchronizing has a real performance cost: it's a blocking, whole-device sync before every GPU-buffer `add()`/`get()` call, which can serialize GPU compute against RDMA transfers when called at high frequency (e.g. once per sample in a data loader). Set `DDSTORE_GPU_SYNC=always` for extra safety on CUDA too; set `never` only after confirming it's unnecessary for your specific workload and platform. - -```bash -export DDSTORE_GPU_SYNC=always # force the safety margin everywhere -export DDSTORE_GPU_SYNC=never # disable it (only if you've verified it's safe) -``` +GPU kernels execute asynchronously: a compute kernel that just wrote to (or is about to read) a buffer may not have fully retired by the time that buffer is handed to RDMA. On at least one ROCm+CXI build, this produced a real, confirmed bug: the RDMA transfer reported success, but the destination buffer could still show stale, pre-transfer content, because the GPU's cache hadn't been reconciled with the external NIC write. Under sustained, real-workload conditions (not just short unit tests) this showed up as hard GPU faults, not just wrong data. To guard against this, `PyDDStore` always synchronizes the GPU device (`torch.cuda.synchronize()`) before registering a buffer for RDMA in `add()`/`get()`. This is a blocking, whole-device sync, which can serialize GPU compute against RDMA transfers when called at high frequency (e.g. once per sample in a data loader) — see the performance note below. ## Known Limitations ### Multiple `srun` steps in one job (`method=2`, `cxi`) -On Frontier, `cxi` requires `#SBATCH --network=job_vni` (or `single_node_vni`) for `method=2`'s separate core/extra `srun` steps to reach each other. Even with that set, a later step in a job with several sequential steps can occasionally fail to start; the cause isn't fully understood. If you hit this, use fewer sequential steps per job, or use `method=1` (single job step). +On Frontier, `method=2`'s separate core/extra `srun` steps within one job have shown intermittent RDMA connectivity issues between steps, and a later step in a job with several sequential steps can occasionally fail to start. The `--network=job_vni`/`single_node_vni` `sbatch` options have not reliably fixed this. The cause isn't fully understood. If you hit this, use fewer sequential steps per job, or use `method=1` (single job step), which doesn't have this issue. ### GPU-to-GPU RDMA performance on AMD/ROCm -On Frontier, the synchronization `DDSTORE_GPU_SYNC` performs before each RDMA call (needed for correctness) can outweigh the benefit of skipping the host copy for small, per-sample transfers — GPU-to-GPU has not shown a speed advantage there yet, though results are correct either way. Larger, batched transfers should benefit more; that usage pattern isn't built yet. Perlmutter skips this sync by default and hasn't shown the same slowdown, but has been tested less. +On Frontier, the GPU synchronization performed before each RDMA call (needed for correctness) can outweigh the benefit of skipping the host copy for small, per-sample transfers — GPU-to-GPU has not shown a speed advantage there yet, though results are correct either way. Larger, batched transfers should benefit more; that usage pattern isn't built yet. + +### Troubleshooting: RDMA fails to connect (`cxi`, Frontier) + +If a `cxi` job fails to connect over RDMA, try adding `#SBATCH --network=single_node_vni` — this has been needed specifically for **single-node** (`-N 1`) jobs on Frontier. For jobs spanning multiple nodes, or multiple `srun` steps in one job, this hasn't reliably helped (see above); if you hit connection issues there, reducing the number of sequential steps or using `method=1` is more likely to help. ## Partitioned / Sub-communicator Usage @@ -323,6 +314,19 @@ mpirun -n 4 python examples/vae/vae-ddp.py DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --gpu-dest --gpu-source ``` +### Slurm job scripts (Frontier) + +[examples/vae/script/job-vae-single.sh](examples/vae/script/job-vae-single.sh) and [examples/vae/script/job-vae-core-extra.sh](examples/vae/script/job-vae-core-extra.sh) wrap the same VAE example for `sbatch` on Frontier — `job-vae-single.sh` runs plain DDP (one `srun` step), `job-vae-core-extra.sh` runs the [method=2 core/extra split](#file-based-handshake-method2) (two independent `srun` steps). Both take the same independent options — each has its own fixed default and none implicitly changes another: + +```bash +sbatch examples/vae/script/job-vae-single.sh # method=0, cxi +sbatch examples/vae/script/job-vae-single.sh --method=1 --gpudirect +sbatch examples/vae/script/job-vae-core-extra.sh # method=2, cxi, colocate layout +sbatch examples/vae/script/job-vae-core-extra.sh --gpudirect --layout=split-node --core-nnodes=2 +``` + +Run `--help` on either script for the full option list (`--method`, `--fabric`, `--gpudirect`; `job-vae-core-extra.sh` additionally has `--layout=colocate|split-node` and `--core-nnodes`). Note `--layout=colocate` together with `--gpudirect` will over-request GPUs per node (core and extra each ask for a full node's worth of GPUs on the same nodes) — use `--layout=split-node` when testing GPUDirect on `job-vae-core-extra.sh`. + ## Testing ### Unit tests (pytest) diff --git a/examples/vae/script/job-vae-core-extra.sh b/examples/vae/script/job-vae-core-extra.sh new file mode 100755 index 0000000..5b85e08 --- /dev/null +++ b/examples/vae/script/job-vae-core-extra.sh @@ -0,0 +1,100 @@ +#!/bin/bash +#SBATCH -A FUS184 +#SBATCH -J GX-core-extra +#SBATCH -o job-%j.out +#SBATCH -e job-%j.out +#SBATCH -N 4 +#SBATCH -t 30:00 +#SBATCH -q debug +# +# Core/extra split VAE DDP run (two independent srun steps). + +usage() { + cat < >(sed 's/^/[core] /') 2> >(sed 's/^/[core] /') & +sleep 5 + +MASTER_PORT=8891 DDSTORE_HANDSHAKE_TIMEOUT_S=60 srun -N$EXTRA_NNODES -n$EXTRA_NTASKS -c6 --gpus-per-task=1 --cpu-bind=verbose,core -l \ + python -u examples/vae/vae_extra_train.py --handshake-dir ddstore_hs_vae --n-core $CORE_NTASKS --epochs 3 $EXTRA_EXTRA_ARGS \ + > >(sed 's/^/[extr] /') 2> >(sed 's/^/[extr] /') +sleep 5 + +wait diff --git a/examples/vae/script/job-vae-single.sh b/examples/vae/script/job-vae-single.sh new file mode 100755 index 0000000..ee1acc8 --- /dev/null +++ b/examples/vae/script/job-vae-single.sh @@ -0,0 +1,64 @@ +#!/bin/bash +#SBATCH -A FUS184 +#SBATCH -J GX-single +#SBATCH -o job-%j.out +#SBATCH -e job-%j.out +#SBATCH -N 4 +#SBATCH -t 30:00 +#SBATCH -q debug +# +# Baseline VAE DDP run. + +usage() { + cat < >(sed 's/^/[core] /') 2> >(sed 's/^/[core] /') diff --git a/src/pyddstore.pyx b/src/pyddstore.pyx index fd61463..bbb17cc 100644 --- a/src/pyddstore.pyx +++ b/src/pyddstore.pyx @@ -58,39 +58,6 @@ def _hmem_iface_for(tensor): import torch return _FI_HMEM_ROCR if torch.version.hip is not None else _FI_HMEM_CUDA -def _should_sync_before_rdma(tensor): - """Whether to torch.cuda.synchronize() a GPU buffer before handing it - to RDMA (see the sync call sites in add()/get() for what this guards - against). Controlled by DDSTORE_GPU_SYNC (case-insensitive): - "always" -- always sync. - "never" -- never sync (only override this if you've independently - confirmed your workload is safe without it -- see below). - "auto" (default, or any other/unset value) -- sync on ROCm/HIP - builds of torch, skip on CUDA builds. - - Rationale for the auto default: confirmed by direct testing that - ROCm's HMEM-over-CXI path needs this sync -- without it, a GPU - buffer's RDMA transfer can be silently masked by stale GPU cache - content from a preceding, not-yet-retired compute-kernel write to the - same memory (observed as both silent data corruption in small tests - and HSA_STATUS_ERROR_EXCEPTION hardware faults in a real, sustained - training loop). CUDA has not shown this failure in either an - equivalent poison-value test or one real training run, plausibly - because NVIDIA's decade-hardened GPUDirect RDMA stack already - enforces this coherence transparently -- but that evidence is narrower - than what exposed the ROCm bug (a real training loop, not just - isolated tests), so this is treated as "no evidence of the problem on - CUDA yet" rather than "proven unnecessary on CUDA". DDSTORE_GPU_SYNC - exists specifically so this default can be overridden the moment - either direction needs it, without a code change. - """ - import torch - override = os.environ.get("DDSTORE_GPU_SYNC", "auto").strip().lower() - if override == "always": - return True - if override == "never": - return False - return torch.version.hip is not None def _check_gpu_fabric_preconditions(int method, str what): """Shared method=1/2 + DDSTORE_FABRIC=cxi precondition check for a GPU @@ -224,10 +191,10 @@ cdef class PyDDStore: "replaced here, risking a dangling pointer)" % name) import torch # Flush any pending/async GPU compute-kernel writes to `arr` - # before handing it to RDMA -- see _should_sync_before_rdma() - # for what this guards against and why it's conditional. - if _should_sync_before_rdma(arr): - torch.cuda.synchronize(device=arr.device) + # before handing it to RDMA -- otherwise the transfer can be + # silently masked by stale GPU cache content from a preceding, + # not-yet-retired write to the same memory. + torch.cuda.synchronize(device=arr.device) iface = _hmem_iface_for(arr) ptr = arr.data_ptr() nrows = arr.shape[0] @@ -280,10 +247,8 @@ cdef class PyDDStore: _check_gpu_fabric_preconditions(self.method, "GPU destination buffer") assert arr.is_contiguous() import torch - # See _should_sync_before_rdma() / the matching comment in - # add() for what this guards against and why it's conditional. - if _should_sync_before_rdma(arr): - torch.cuda.synchronize(device=arr.device) + # See the matching comment in add() for what this guards against. + torch.cuda.synchronize(device=arr.device) iface = _hmem_iface_for(arr) ptr = arr.data_ptr() if arr.dtype == torch.int32: From 5060d86497d608cadc331759ca7c3b9b59043352 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Fri, 2 Oct 2026 18:52:50 -0700 Subject: [PATCH 15/56] release GIL --- src/pyddstore.pyx | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/pyddstore.pyx b/src/pyddstore.pyx index bbb17cc..876073b 100644 --- a/src/pyddstore.pyx +++ b/src/pyddstore.pyx @@ -92,7 +92,7 @@ cdef extern from "ddstore.hpp": # Method 2: extra member (no MPI communicator) DDStore(int method, string handshake_dir, int n_core) void add[T](string name, T* buffer, long nrows, int disp, int hmem_iface) except + - void get[T](string name, long start, long count, T* buffer, int hmem_iface) except + + void get[T](string name, long start, long count, T* buffer, int hmem_iface) except + nogil void prefetch_recv_mr[T](string name, T* buffer, long nrows, int disp, int hmem_iface) except + void epoch_begin() void epoch_end() @@ -270,18 +270,29 @@ cdef class PyDDStore: cdef np.ndarray np_arr = arr assert np_arr.flags.c_contiguous assert np_arr.shape[0] >= count + # The host read runs without the GIL, so other Python threads (e.g. a + # training loop while a background thread prefetches) keep running. + # Method 1/2 reads make no MPI calls. + cdef string cname = s2b(name) + cdef char* data = np_arr.data if np_arr.dtype == np.int32: - self.c_ddstore.get(s2b(name), start, count, np_arr.data, 0) + with nogil: + self.c_ddstore.get(cname, start, count, data, 0) elif np_arr.dtype == np.int64: - self.c_ddstore.get(s2b(name), start, count, np_arr.data, 0) + with nogil: + self.c_ddstore.get(cname, start, count, data, 0) elif np_arr.dtype == np.uint8: - self.c_ddstore.get(s2b(name), start, count, np_arr.data, 0) + with nogil: + self.c_ddstore.get(cname, start, count, data, 0) elif np_arr.dtype == np.float32: - self.c_ddstore.get(s2b(name), start, count, np_arr.data, 0) + with nogil: + self.c_ddstore.get(cname, start, count, data, 0) elif np_arr.dtype == np.float64: - self.c_ddstore.get(s2b(name), start, count, np_arr.data, 0) + with nogil: + self.c_ddstore.get(cname, start, count, data, 0) elif np_arr.dtype == np.bool_: - self.c_ddstore.get(s2b(name), start, count, np_arr.data, 0) + with nogil: + self.c_ddstore.get(cname, start, count, data, 0) else: raise NotImplementedError From 7200558ec04fc78d7a5cb14d15c062e442a9914d Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sat, 3 Oct 2026 06:58:43 -0400 Subject: [PATCH 16/56] add ThreadDataLoader --- README.md | 17 +++- examples/vae/distdataset.py | 78 ++++++++++------ examples/vae/script/job-vae-single.sh | 24 ++++- examples/vae/vae-ddp.py | 96 ++++++++++++++++---- examples/vae/vae_extra_train.py | 50 +++++++++-- test/test_gpu_rdma.py | 123 +++++++++++++++++++++++--- 6 files changed, 319 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 655334c..876ba7d 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,10 @@ GPU kernels execute asynchronously: a compute kernel that just wrote to (or is a On Frontier, `method=2`'s separate core/extra `srun` steps within one job have shown intermittent RDMA connectivity issues between steps, and a later step in a job with several sequential steps can occasionally fail to start. The `--network=job_vni`/`single_node_vni` `sbatch` options have not reliably fixed this. The cause isn't fully understood. If you hit this, use fewer sequential steps per job, or use `method=1` (single job step), which doesn't have this issue. +### Default (forked-process) `DataLoader` with `--num-workers > 1` and DDStore + +With `--loader=default`, `--num-workers > 1` forks worker processes that each inherit the parent's live MPI state (mpi4py/`MPI_Init` has already run before the `DataLoader` is constructed). Forking after `MPI_Init` is a known MPI hazard — the child processes don't get a clean, independent MPI runtime — and in practice this hangs rather than erroring out cleanly once DDStore is in the picture. Use `--num-workers=1` (or 0) with `--loader=default`, or switch to `--loader=threaded` (real threads, no fork, no MPI conflict) for `--num-workers > 1`. + ### GPU-to-GPU RDMA performance on AMD/ROCm On Frontier, the GPU synchronization performed before each RDMA call (needed for correctness) can outweigh the benefit of skipping the host copy for small, per-sample transfers — GPU-to-GPU has not shown a speed advantage there yet, though results are correct either way. Larger, batched transfers should benefit more; that usage pattern isn't built yet. @@ -314,18 +318,27 @@ mpirun -n 4 python examples/vae/vae-ddp.py DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --gpu-dest --gpu-source ``` +In `vae-ddp.py`, `--num-workers` (default 1) controls the training `DataLoader`'s parallelism. By default (`--loader=default`), it's passed straight through to PyTorch's normal `DataLoader`, which forks worker processes — forced back to 0 whenever `--gpu-dest`/`--gpu-source` is set (forked processes can't safely own GPU state), and capped at 1 otherwise: `--num-workers > 1` with `--loader=default` raises a clear error rather than hanging, since forking after MPI has already initialized is a known hazard with DDStore (see [Known Limitations](#default-forked-process-dataloader-with---num-workers--1-and-ddstore)). The applied value is printed at startup either way: `train_loader: DataLoader, num_workers=N`. `--loader=threaded` switches to [examples/vae/ddstore_dataloader.py](examples/vae/ddstore_dataloader.py)'s `ThreadDataLoader` instead, which parallelizes fetches across a thread pool — real threads, no fork, so both the GPU-state and MPI hazards above don't apply, and `--num-workers > 1` is safe together with `--gpu-dest`/`--gpu-source`. Concurrent `get()` calls from multiple threads are serialized internally by a lock in `DistDataset`/`DistDatasetReader` (the underlying RDMA transfer has no locking of its own), so threads gain overlap on everything except the RDMA call itself. `--loader=threaded` requires `DDSTORE_METHOD` 1 or 2 (libfabric). `vae_extra_train.py` has the same `--loader`/`--num-workers` flags, but its default loader always uses `num_workers=0` unconditionally (no `--num-workers` override) — only its `--loader=threaded` path is parallel: + +```bash +# vae-ddp.py, threaded loader, 4 worker threads, together with GPUDirect +DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --loader=threaded --num-workers=4 --gpu-dest --gpu-source +``` + ### Slurm job scripts (Frontier) -[examples/vae/script/job-vae-single.sh](examples/vae/script/job-vae-single.sh) and [examples/vae/script/job-vae-core-extra.sh](examples/vae/script/job-vae-core-extra.sh) wrap the same VAE example for `sbatch` on Frontier — `job-vae-single.sh` runs plain DDP (one `srun` step), `job-vae-core-extra.sh` runs the [method=2 core/extra split](#file-based-handshake-method2) (two independent `srun` steps). Both take the same independent options — each has its own fixed default and none implicitly changes another: +[examples/vae/script/job-vae-single.sh](examples/vae/script/job-vae-single.sh) and [examples/vae/script/job-vae-core-extra.sh](examples/vae/script/job-vae-core-extra.sh) wrap the same VAE example for `sbatch` on Frontier — `job-vae-single.sh` runs plain DDP (one `srun` step), `job-vae-core-extra.sh` runs the [method=2 core/extra split](#file-based-handshake-method2) (two independent `srun` steps). Both share `--method`/`--fabric`/`--gpudirect`, each with its own fixed default and none implicitly changing another: ```bash sbatch examples/vae/script/job-vae-single.sh # method=0, cxi sbatch examples/vae/script/job-vae-single.sh --method=1 --gpudirect +sbatch examples/vae/script/job-vae-single.sh --method=1 --thread --num-workers=4 # threaded loader, 4 worker threads +sbatch examples/vae/script/job-vae-single.sh --method=1 --gpudirect --thread --num-workers=4 # threaded loader + GPUDirect sbatch examples/vae/script/job-vae-core-extra.sh # method=2, cxi, colocate layout sbatch examples/vae/script/job-vae-core-extra.sh --gpudirect --layout=split-node --core-nnodes=2 ``` -Run `--help` on either script for the full option list (`--method`, `--fabric`, `--gpudirect`; `job-vae-core-extra.sh` additionally has `--layout=colocate|split-node` and `--core-nnodes`). Note `--layout=colocate` together with `--gpudirect` will over-request GPUs per node (core and extra each ask for a full node's worth of GPUs on the same nodes) — use `--layout=split-node` when testing GPUDirect on `job-vae-core-extra.sh`. +Run `--help` on either script for the full option list. `job-vae-single.sh` additionally has `--thread` (switch to `ThreadDataLoader`, see above) and `--num-workers` (default 1; must stay <= 1 for the default loader, forced to 0 there when `--gpudirect` is set — use `--thread` for real parallelism). `job-vae-core-extra.sh` additionally has `--layout=colocate|split-node` and `--core-nnodes`. Note `--layout=colocate` together with `--gpudirect` will over-request GPUs per node (core and extra each ask for a full node's worth of GPUs on the same nodes) — use `--layout=split-node` when testing GPUDirect on `job-vae-core-extra.sh`. ## Testing diff --git a/examples/vae/distdataset.py b/examples/vae/distdataset.py index ae489d1..6b678b7 100644 --- a/examples/vae/distdataset.py +++ b/examples/vae/distdataset.py @@ -1,6 +1,7 @@ from mpi4py import MPI import numpy as np import os +import threading import torch from torch.utils.data import Dataset @@ -16,8 +17,15 @@ def nsplit(a, n): class DistDataset(Dataset): """Distributed dataset class""" - def __init__(self, data, label, comm=MPI.COMM_WORLD, ddstore_width=None, - device=None, add_device=None): + def __init__( + self, + data, + label, + comm=MPI.COMM_WORLD, + ddstore_width=None, + device=None, + add_device=None, + ): super().__init__() self.dataset = list() @@ -92,9 +100,9 @@ def __init__(self, data, label, comm=MPI.COMM_WORLD, ddstore_width=None, # host round-trip. torch.stack (not cat) keeps one row per image # (nrows, 784) -- see the np.stack comment below for why that # shape matters to ddstore.add()'s disp inference. - self.data = torch.stack( - [d.reshape(-1) for d, _ in self.dataset] - ).to(self.add_device) + self.data = torch.stack([d.reshape(-1) for d, _ in self.dataset]).to( + self.add_device + ) self.data = self.data.contiguous() else: data_list = list() @@ -128,6 +136,15 @@ def __init__(self, data, label, comm=MPI.COMM_WORLD, ddstore_width=None, # POOL must be >= the DataLoader's batch_size; 256 covers the default # 128 with headroom. Only used when device is not None (GPU path). # Only safe with num_workers=0 (single-threaded DataLoader). + # Guards pool-slice selection/increment + the ddstore.get() calls in + # get() below -- needed once a caller (e.g. ThreadDataLoader) can + # invoke get() from multiple threads concurrently. The underlying + # DDStore::get() releases the GIL for its blocking transfer but has + # no internal locking of its own, so concurrent calls here would + # race on both the pool round-robin index and DDStore's CQ/MR-cache + # state. + self._lock = threading.Lock() + _POOL = 256 if device is not None: self._val_pool = torch.empty( @@ -154,20 +171,21 @@ def get(self, idx, device=None): # loops discard it, so a GPU-resident label buffer would add # complexity for no benefit. label = np.zeros(1, dtype=np.int32) - if device is not None: - # Take the next slice from the pool (round-robin). All slices - # share one MR registration → recv_mr cache always hits after the - # first call. No clone() needed: each slice is a distinct pointer - # so the DataLoader can hold all batch_size results simultaneously - # without aliasing. - val = self._val_pool[self._val_pool_idx : self._val_pool_idx + 1] - self._val_pool_idx = (self._val_pool_idx + 1) % self._val_pool.shape[0] - else: - val = np.zeros((1, 28 * 28), dtype=np.float32) - val = np.ascontiguousarray(val) - assert val.data.contiguous - self.ddstore.get(f"{self.label}data", val, idx) - self.ddstore.get(f"{self.label}labels", label, idx) + with self._lock: + if device is not None: + # Take the next slice from the pool (round-robin). All slices + # share one MR registration → recv_mr cache always hits after the + # first call. No clone() needed: each slice is a distinct pointer + # so the DataLoader can hold all batch_size results simultaneously + # without aliasing. + val = self._val_pool[self._val_pool_idx : self._val_pool_idx + 1] + self._val_pool_idx = (self._val_pool_idx + 1) % self._val_pool.shape[0] + else: + val = np.zeros((1, 28 * 28), dtype=np.float32) + val = np.ascontiguousarray(val) + assert val.data.contiguous + self.ddstore.get(f"{self.label}data", val, idx) + self.ddstore.get(f"{self.label}labels", label, idx) if device is None: val = torch.tensor(val) val = torch.reshape(val, (1, 28, 28)) @@ -209,6 +227,9 @@ def __init__(self, label, handshake_dir, n_core, device=None): "which is not a perfect square (expected a flattened square image)" ) + # See DistDataset.__init__ for what this guards. + self._lock = threading.Lock() + _POOL = 256 if device is not None: self._val_pool = torch.empty( @@ -231,15 +252,16 @@ def get(self, idx, device=None): ## width, since ddstore.get() infers count from arr.shape[0] # Label stays host-only regardless of `device` -- see DistDataset.get(). label = np.zeros(1, dtype=np.int32) - if device is not None: - val = self._val_pool[self._val_pool_idx : self._val_pool_idx + 1] - self._val_pool_idx = (self._val_pool_idx + 1) % self._val_pool.shape[0] - else: - val = np.zeros((1, self.data_disp), dtype=np.float32) - val = np.ascontiguousarray(val) - assert val.data.contiguous - self.ddstore.get(f"{self.label}data", val, idx) - self.ddstore.get(f"{self.label}labels", label, idx) + with self._lock: + if device is not None: + val = self._val_pool[self._val_pool_idx : self._val_pool_idx + 1] + self._val_pool_idx = (self._val_pool_idx + 1) % self._val_pool.shape[0] + else: + val = np.zeros((1, self.data_disp), dtype=np.float32) + val = np.ascontiguousarray(val) + assert val.data.contiguous + self.ddstore.get(f"{self.label}data", val, idx) + self.ddstore.get(f"{self.label}labels", label, idx) if device is None: val = torch.tensor(val) val = torch.reshape(val, (1, self.side, self.side)) diff --git a/examples/vae/script/job-vae-single.sh b/examples/vae/script/job-vae-single.sh index ee1acc8..9caf4d0 100755 --- a/examples/vae/script/job-vae-single.sh +++ b/examples/vae/script/job-vae-single.sh @@ -21,6 +21,15 @@ Options: --fabric=X DDSTORE_FABRIC: hsn or cxi. Default: cxi. --gpudirect Test GPUDirect RDMA. Requires --method=1 or 2 and --fabric=cxi. + --thread Use ThreadDataLoader (thread-pool DataLoader) instead of the + default forked-process DataLoader. Needed for + --num-workers > 0 together with --gpudirect. Requires + --method=1 or 2. + --num-workers=N Worker processes for the default loader (must stay <= 1 + -- forking after MPI_Init hangs with DDStore for N > 1; + use --thread instead), or worker threads with --thread. + Forced to 0 for the default loader when --gpudirect is + set (fork-safety guard). Default: 1. -h, --help Show this help message and exit. Examples: @@ -28,6 +37,7 @@ Examples: $(basename "$0") --method=1 --gpudirect # GPUDirect over libfabric $(basename "$0") --method=2 --gpudirect # GPUDirect over file-based handshake $(basename "$0") --fabric=hsn # baseline over hsn instead + $(basename "$0") --method=1 --gpudirect --thread --num-workers=4 EOF } @@ -45,17 +55,27 @@ export VAE_PROFILE=1 METHOD= FABRIC= -EXTRA_ARGS="" +GPUDIRECT_ARGS="" +THREAD=0 +NUM_WORKERS= for arg in "$@"; do case "$arg" in --method=*) METHOD="${arg#--method=}" ;; --fabric=*) FABRIC="${arg#--fabric=}" ;; - --gpudirect) EXTRA_ARGS="--gpu-dest --gpu-source" ;; + --gpudirect) GPUDIRECT_ARGS="--gpu-dest --gpu-source" ;; + --thread) THREAD=1 ;; + --num-workers=*) NUM_WORKERS="${arg#--num-workers=}" ;; esac done export DDSTORE_FABRIC="${FABRIC:-cxi}" METHOD="${METHOD:-0}" +NUM_WORKERS="${NUM_WORKERS:-1}" + +EXTRA_ARGS="$GPUDIRECT_ARGS --num-workers=$NUM_WORKERS" +if [ "$THREAD" == "1" ]; then + EXTRA_ARGS="$EXTRA_ARGS --loader=threaded" +fi echo "DDSTORE_METHOD=$METHOD DDSTORE_FABRIC=$DDSTORE_FABRIC EXTRA_ARGS=\"$EXTRA_ARGS\"" diff --git a/examples/vae/vae-ddp.py b/examples/vae/vae-ddp.py index 8470683..6efd7c4 100644 --- a/examples/vae/vae-ddp.py +++ b/examples/vae/vae-ddp.py @@ -20,6 +20,7 @@ import distdataset from distdataset import DistDataset +from ddstore_dataloader import ThreadDataLoader from ddp_utils import setup_ddp, get_local_rank from vae_model import VAE, loss_function @@ -60,19 +61,43 @@ action="store_true", default=False, help="Allocate the DDStore get() destination buffer directly on the " - "training device (GPUDirect RDMA, Phase 1), skipping the " - "host->device copy. Requires DDSTORE_METHOD in (1, 2) and " - "DDSTORE_FABRIC=cxi (e.g. DDSTORE_METHOD=2 as in run-vae.sh).", + "training device (GPUDirect RDMA, Phase 1), skipping the " + "host->device copy. Requires DDSTORE_METHOD in (1, 2) and " + "DDSTORE_FABRIC=cxi (e.g. DDSTORE_METHOD=2 as in run-vae.sh).", ) parser.add_argument( "--gpu-source", action="store_true", default=False, help="Stack this rank's local shard directly on the training device " - "and add() it in place (GPUDirect RDMA source, Phase 2), skipping " - "the host round-trip. Same DDSTORE_METHOD/DDSTORE_FABRIC " - "requirements as --gpu-dest; independent of it -- use either or " - "both.", + "and add() it in place (GPUDirect RDMA source, Phase 2), skipping " + "the host round-trip. Same DDSTORE_METHOD/DDSTORE_FABRIC " + "requirements as --gpu-dest; independent of it -- use either or " + "both.", +) +parser.add_argument( + "--loader", + choices=["default", "threaded"], + default="default", + help="DataLoader implementation for the training set. 'threaded' uses " + "ThreadDataLoader (examples/vae/ddstore_dataloader.py), a " + "thread-pool-based loader that allows --num-workers > 0 together " + "with --gpu-dest/--gpu-source (the default loader forks worker " + "processes, which cannot safely own GPU state, so it stays " + "single-threaded for those flags). Requires DDSTORE_METHOD 1 or 2. " + "Default: default.", +) +parser.add_argument( + "--num-workers", + type=int, + default=1, + metavar="N", + help="Number of worker threads (--loader=threaded) or worker processes " + "(--loader=default). With --loader=default, must stay <= 1 -- " + "forking worker processes after MPI_Init hangs with DDStore; use " + "--loader=threaded for real parallelism instead. Also forced to 0 " + "for --loader=default when --gpu-dest/--gpu-source is set " + "(fork-safety guard). Default: 1.", ) args = parser.parse_args() args.cuda = not args.no_cuda and torch.cuda.is_available() @@ -102,8 +127,16 @@ else: device = torch.device("cpu") -print("DDP setup:", comm_size, rank, device, - "gpu_dest:", args.gpu_dest, "gpu_source:", args.gpu_source) +print( + "DDP setup:", + comm_size, + rank, + device, + "gpu_dest:", + args.gpu_dest, + "gpu_source:", + args.gpu_source, +) if rank == 0: os.makedirs("results", exist_ok=True) @@ -115,13 +148,23 @@ # kwargs = {'num_workers': 1, 'pin_memory': True} if args.cuda else {} # kwargs = {'pin_memory': True} if args.cuda else {} -kwargs = {} -# --gpu-dest returns CUDA/HIP tensors from __getitem__; DataLoader worker -# processes can't safely own GPU state across a fork, so this only works -# with num_workers=0 (today's default). Don't add num_workers>0 here -# without redesigning the buffer/collate strategy. -if args.gpu_dest: - assert kwargs.get("num_workers", 0) == 0 +# --gpu-dest/--gpu-source return CUDA/HIP tensors from __getitem__/add(); +# DataLoader worker processes can't safely own GPU state across a fork, so +# the default (forked-process) loader is forced to num_workers=0 whenever +# either is set -- use --loader=threaded for num_workers > 0 with GPU +# buffers instead. Separately, forking *at all* after MPI_Init is a known +# MPI hazard that hangs with DDStore even without GPU buffers -- fail fast +# instead of hanging silently. +if args.loader == "default" and args.num_workers > 1: + raise RuntimeError( + "--num-workers > 1 with --loader=default forks worker processes " + "after MPI_Init, which hangs with DDStore. Use --num-workers=1, or " + "--loader=threaded for real parallelism." + ) +if args.gpu_dest or args.gpu_source: + kwargs = {} +else: + kwargs = {"num_workers": args.num_workers} if args.num_workers > 0 else {} trainset = DistDataset( datasets.MNIST("data", train=True, download=True, transform=transforms.ToTensor()), @@ -134,8 +177,25 @@ comm.Barrier() sampler = torch.utils.data.distributed.DistributedSampler(trainset) -train_loader = torch.utils.data.DataLoader( - trainset, batch_size=args.batch_size, shuffle=False, **kwargs, sampler=sampler +if args.loader == "threaded": + if int(os.environ.get("DDSTORE_METHOD", "0")) == 0: + raise RuntimeError("--loader=threaded requires DDSTORE_METHOD=1 or 2") + train_loader = ThreadDataLoader( + trainset, + batch_size=args.batch_size, + shuffle=False, + sampler=sampler, + num_workers=args.num_workers, + ) +else: + # --num-workers applies here too (forked processes), unless --gpu-dest/ + # --gpu-source forced kwargs back to {} above (fork-safety guard). + train_loader = torch.utils.data.DataLoader( + trainset, batch_size=args.batch_size, shuffle=False, **kwargs, sampler=sampler + ) + +print( + f"train_loader: {type(train_loader).__name__}, num_workers={train_loader.num_workers}" ) testset = datasets.MNIST( diff --git a/examples/vae/vae_extra_train.py b/examples/vae/vae_extra_train.py index 0948b8b..b00e349 100644 --- a/examples/vae/vae_extra_train.py +++ b/examples/vae/vae_extra_train.py @@ -44,6 +44,7 @@ from ddp_utils import setup_ddp, get_local_rank from distdataset import DistDatasetReader +from ddstore_dataloader import ThreadDataLoader from vae_model import VAE, loss_function parser = argparse.ArgumentParser(description="VAE MNIST Example - extra (reader) group") @@ -94,9 +95,28 @@ action="store_true", default=False, help="Allocate the DDStore get() destination buffer directly on the " - "training device (GPUDirect RDMA, Phase 1), skipping the " - "host->device copy. Requires DDSTORE_FABRIC=cxi and a " - "libfabric-backed method (already the case for this script).", + "training device (GPUDirect RDMA, Phase 1), skipping the " + "host->device copy. Requires DDSTORE_FABRIC=cxi and a " + "libfabric-backed method (already the case for this script).", +) +parser.add_argument( + "--loader", + choices=["default", "threaded"], + default="default", + help="DataLoader implementation for the training set. 'threaded' uses " + "ThreadDataLoader (examples/vae/ddstore_dataloader.py), a " + "thread-pool-based loader that allows --num-workers > 0 together " + "with --gpu-dest (the default loader forks worker processes, " + "which cannot safely own GPU state, so it stays single-threaded " + "for that flag). Default: default.", +) +parser.add_argument( + "--num-workers", + type=int, + default=4, + metavar="N", + help="Number of worker threads for --loader=threaded. Ignored with " + "--loader=default (always 0 there). Default: 4.", ) args = parser.parse_args() args.cuda = not args.no_cuda and torch.cuda.is_available() @@ -139,14 +159,30 @@ assert kwargs.get("num_workers", 0) == 0 trainset = DistDatasetReader( - "train", args.handshake_dir, args.n_core, + "train", + args.handshake_dir, + args.n_core, device=device if args.gpu_dest else None, ) sampler = torch.utils.data.distributed.DistributedSampler(trainset) -train_loader = torch.utils.data.DataLoader( - trainset, batch_size=args.batch_size, shuffle=False, **kwargs, sampler=sampler -) +if args.loader == "threaded": + # DistDatasetReader always joins via method=2 (file-based handshake), + # so no DDSTORE_METHOD=0 guard is needed here (unlike vae-ddp.py). + train_loader = ThreadDataLoader( + trainset, + batch_size=args.batch_size, + shuffle=False, + sampler=sampler, + num_workers=args.num_workers, + ) +else: + # --num-workers is ignored here: kwargs never carries num_workers for + # the default loader (see the fork-safety guard above), so this is + # always the existing num_workers=0 behavior regardless of its value. + train_loader = torch.utils.data.DataLoader( + trainset, batch_size=args.batch_size, shuffle=False, **kwargs, sampler=sampler + ) testset = datasets.MNIST( "data", train=False, download=True, transform=transforms.ToTensor() diff --git a/test/test_gpu_rdma.py b/test/test_gpu_rdma.py index 02ed2b0..cf6cf9c 100644 --- a/test/test_gpu_rdma.py +++ b/test/test_gpu_rdma.py @@ -6,6 +6,8 @@ (see run-test-gpu.sh). Negative-path tests need neither and always run. """ +import threading + import numpy as np import pytest from mpi4py import MPI @@ -50,8 +52,11 @@ def test_get_into_gpu_tensor_cxi(comm, monkeypatch): store.get("x", out, start=target_rank * nrows) expected = float(target_rank + 1) ok = bool(torch.all(out.cpu() == expected)) - print(f"[rank {rank}] target_rank={target_rank} expected={expected} " - f"got={out.cpu().tolist()} ok={ok}", flush=True) + print( + f"[rank {rank}] target_rank={target_rank} expected={expected} " + f"got={out.cpu().tolist()} ok={ok}", + flush=True, + ) if not ok: local_ok = False store.epoch_end() @@ -110,7 +115,9 @@ def test_get_into_gpu_tensor_cxi_compute_kernel_read(comm, monkeypatch): if i % 50 == 0: print(f"[rank {rank}] iter={i} diff={diff.item()}", flush=True) store.epoch_end() - print(f"[rank {rank}] completed {n_iters} iterations without a HIP error", flush=True) + print( + f"[rank {rank}] completed {n_iters} iterations without a HIP error", flush=True + ) store.free() @@ -238,9 +245,12 @@ def test_get_into_gpu_tensor_cxi_large(comm, monkeypatch): expected = float(target_rank + 1) ok = bool(torch.all(out.cpu() == expected)) nonpoison = int((out.cpu() != -999.0).sum()) - print(f"[rank {rank}] target_rank={target_rank} expected={expected} " - f"ok={ok} nonpoison_count={nonpoison}/{ncols} " - f"sample={out.cpu().flatten()[:8].tolist()}", flush=True) + print( + f"[rank {rank}] target_rank={target_rank} expected={expected} " + f"ok={ok} nonpoison_count={nonpoison}/{ncols} " + f"sample={out.cpu().flatten()[:8].tolist()}", + flush=True, + ) if not ok: local_ok = False store.epoch_end() @@ -273,8 +283,11 @@ def test_get_host_to_host_cxi(comm, monkeypatch): store.get("x", out, start=target_rank * nrows) expected = float(target_rank + 1) ok = bool(np.all(out == expected)) - print(f"[rank {rank}] target_rank={target_rank} expected={expected} " - f"got={out.tolist()} ok={ok}", flush=True) + print( + f"[rank {rank}] target_rank={target_rank} expected={expected} " + f"got={out.tolist()} ok={ok}", + flush=True, + ) if not ok: local_ok = False store.epoch_end() @@ -359,7 +372,9 @@ def test_add_from_gpu_tensor_host_dest_cxi(comm, monkeypatch): nrows, ncols = 8, 4 store = dds.PyDDStore(comm, method=1) - data = torch.full((nrows, ncols), float(rank + 1), dtype=torch.float32, device="cuda") + data = torch.full( + (nrows, ncols), float(rank + 1), dtype=torch.float32, device="cuda" + ) store.add("x", data) # GPU source -- Phase 2 store.epoch_begin() @@ -387,7 +402,9 @@ def test_add_from_gpu_tensor_gpu_dest_cxi(comm, monkeypatch): nrows, ncols = 8, 4 store = dds.PyDDStore(comm, method=1) - data = torch.full((nrows, ncols), float(rank + 1), dtype=torch.float32, device="cuda") + data = torch.full( + (nrows, ncols), float(rank + 1), dtype=torch.float32, device="cuda" + ) store.add("x", data) store.epoch_begin() @@ -422,10 +439,14 @@ def test_add_from_gpu_tensor_gpu_dest_cxi_method2(comm, monkeypatch, tmp_path): pytest.skip("requires at least 2 ranks for a genuine remote read") nrows, ncols = 8, 4 - hs_dir = comm.bcast(str(tmp_path / "ddstore_hs_add_method2") if rank == 0 else None, root=0) + hs_dir = comm.bcast( + str(tmp_path / "ddstore_hs_add_method2") if rank == 0 else None, root=0 + ) core_store = dds.PyDDStore(comm, method=2, handshake_dir=hs_dir) - data = torch.full((nrows, ncols), float(rank + 1), dtype=torch.float32, device="cuda") + data = torch.full( + (nrows, ncols), float(rank + 1), dtype=torch.float32, device="cuda" + ) core_store.add("x", data) # GPU source -- Phase 2 comm.Barrier() @@ -451,3 +472,81 @@ def test_add_from_gpu_tensor_gpu_dest_cxi_method2(comm, monkeypatch, tmp_path): comm.Barrier() assert all_passed(comm, local_ok) core_store.free() + + +# --------------------------------------------------------------------------- +# thread-safety: concurrent get() calls from multiple Python threads +# --------------------------------------------------------------------------- + + +def test_concurrent_get_thread_safety(comm, monkeypatch): + """DDStore::get() releases the GIL for its blocking transfer (see the + `with nogil:` block in pyddstore.pyx) but has no internal locking of its + own -- concurrent calls from multiple threads on the same store race on + the CQ poll loop and the recv-MR region cache in common.cxx. + + examples/vae/distdataset.py's DistDataset/DistDatasetReader (used by + ThreadDataLoader, examples/vae/ddstore_dataloader.py, to parallelize + __getitem__ across threads) close this with a `threading.Lock` around + each get() call. This test reproduces that exact pattern directly + against PyDDStore: N threads issue concurrent get() calls serialized by + a lock, each checked against a trusted sequential reference -- it would + have caught the race if the lock were missing or misplaced. + """ + monkeypatch.setenv("DDSTORE_FABRIC", "cxi") + rank = comm.Get_rank() + size = comm.Get_size() + if size < 2: + pytest.skip("requires at least 2 ranks for a genuine remote read") + nrows, ncols = 8, 4 + + store = dds.PyDDStore(comm, method=1) + data = np.full((nrows, ncols), float(rank + 1), dtype=np.float32) + store.add("x", data) + comm.Barrier() + + store.epoch_begin() + lock = threading.Lock() + results = {} + errors = [] + + def locked_get(target_rank): + out = np.full((1, ncols), -999.0, dtype=np.float32) + with lock: + store.get("x", out, start=target_rank * nrows) + results[target_rank] = out.copy() + + def worker(target_ranks): + try: + for target_rank in target_ranks: + locked_get(target_rank) + except Exception as exc: # noqa: BLE001 - surface any thread exception + errors.append(exc) + + n_threads = 4 + threads = [ + threading.Thread(target=worker, args=(list(range(t, size, n_threads)),)) + for t in range(min(n_threads, size)) + ] + for t in threads: + t.start() + for t in threads: + t.join() + store.epoch_end() + + assert not errors, f"worker thread(s) raised: {errors}" + local_ok = True + for target_rank in range(size): + expected = float(target_rank + 1) + got = results[target_rank] + ok = bool(np.all(got == expected)) + if not ok: + local_ok = False + print( + f"[rank {rank}] target_rank={target_rank} expected={expected} " + f"got={got.tolist()} ok={ok}", + flush=True, + ) + + assert all_passed(comm, local_ok) + store.free() From 9baa4b5253b85dc8a6f43c0b4f69901dbdedcfa8 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sat, 3 Oct 2026 07:03:44 -0400 Subject: [PATCH 17/56] add --- examples/vae/ddstore_dataloader.py | 119 +++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 examples/vae/ddstore_dataloader.py diff --git a/examples/vae/ddstore_dataloader.py b/examples/vae/ddstore_dataloader.py new file mode 100644 index 0000000..388b0ea --- /dev/null +++ b/examples/vae/ddstore_dataloader.py @@ -0,0 +1,119 @@ +import logging +import os +import socket +import queue +import multiprocessing as mp +from concurrent.futures import ThreadPoolExecutor + +import torch +from torch.utils.data import DataLoader +from torch.utils.data.dataloader import _DatasetKind + +logger = logging.getLogger(__name__) + + +class ThreadDataLoader(DataLoader): + """DataLoader that parallelizes __getitem__ across a thread pool instead + of forked worker processes. Threads share the parent's CUDA context and + Python objects directly, so GPU-resident buffers (DDStore's --gpu-dest/ + --gpu-source path) stay safe across workers -- the default DataLoader's + forked processes cannot own GPU state, which is why it's capped at + num_workers=0 for that path. + """ + + def __init__(self, dataset, **kwargs): + super().__init__(dataset, **kwargs) + self._dataset_fetcher = _DatasetKind.create_fetcher( + self._dataset_kind, + self.dataset, + self._auto_collation, + self.collate_fn, + self.drop_last, + ) + + self.fs = queue.Queue() + # Persistent across epochs -- recreating the pool in every __iter__() + # would leak OS threads since the old pool is never shut down. + self._counter = mp.Value("i", 0) + self.executor = ThreadPoolExecutor( + max_workers=self.num_workers or 1, + initializer=self.worker_init, + initargs=(self._counter,), + ) + + logger.debug("num_workers: %s", self.num_workers) + logger.debug("len: %s", len(self._index_sampler)) + + @staticmethod + def worker_init(counter): + core_width = int(os.environ.get("DDSTORE_AFFINITY_WIDTH", "0")) + core_offset = int(os.environ.get("DDSTORE_AFFINITY_OFFSET", "0")) + if core_width <= 0 or not hasattr(os, "sched_getaffinity"): + return 0 + + with counter.get_lock(): + wid = counter.value + counter.value += 1 + + affinity = list(os.sched_getaffinity(0)) + affinity_mask = set( + affinity[ + core_width * wid + core_offset : core_width * (wid + 1) + core_offset + ] + ) + if affinity_mask: + os.sched_setaffinity(0, affinity_mask) + hostname = socket.gethostname() + logger.debug( + "Worker: pid=%s hostname=%s ID=%s affinity=%s", + os.getpid(), + hostname, + wid, + os.sched_getaffinity(0), + ) + return 0 + + @staticmethod + def fetch(dataset, ibatch, index, pin_memory=False): + batch = [dataset[i] for i in index] + if pin_memory: + batch = torch.utils.data._utils.pin_memory.pin_memory(batch) + return (ibatch, batch) + + def __iter__(self): + if self.fs.qsize() > 0: + for future in iter(self.fs.get, None): + future.cancel() + + self._num_yielded = 0 + self._sampler_iter = iter(self._index_sampler) + self.fs_iter = iter(self.fs.get, None) + for i in range(len(self._index_sampler)): + index = next(self._sampler_iter) + future = self.executor.submit( + self.fetch, + self.dataset, + i, + index, + pin_memory=self.pin_memory, + ) + self.fs.put(future) + self.fs.put(None) + return self + + def __next__(self): + future = next(self.fs_iter) + ibatch, data = future.result() + self._num_yielded += 1 + if self.collate_fn is not None: + data = self.collate_fn(data) + return data + + def clean(self): + if self.fs.qsize() > 0: + for future in iter(self.fs.get, None): + future.cancel() + + def __del__(self): + self.clean() + self.executor.shutdown(wait=False) From 63b62423a58783b1052eaa98cd891fc5117b877a Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sat, 3 Oct 2026 21:03:44 -0400 Subject: [PATCH 18/56] Confirm ThreadDataLoader lock necessity, find and guard GPU pool race Direct Frontier experiments confirmed the threading.Lock in DistDataset/DistDatasetReader.get() is necessary: disabling it crashed with "double free or corruption" under concurrent thread access (the libfabric CQ/MR-cache state in src/common.cxx has no locking of its own). Found a separate, deeper bug while verifying: ThreadDataLoader's GPU buffer pool hands out slots per-sample via lock-acquisition order, not batch order, so bounding in-flight batch count (implemented here, with a refill-ordering fix and pool_size sizing) reduces but cannot fully eliminate silent data corruption with --num-workers > 1 together with --gpu-dest/--gpu-source. vae-ddp.py/vae_extra_train.py now raise a clear RuntimeError for that combination instead of running silently wrong. Kept examples/vae/stress_threaded_loader.py (the diagnostic harness used to find both issues) and documented the root cause and what a complete fix needs in the README for a later revisit. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01443BqKw3NHnbGg8HSeoLXJ --- README.md | 23 ++++- examples/vae/ddstore_dataloader.py | 45 +++++++- examples/vae/distdataset.py | 35 +++++-- examples/vae/script/job-vae-single.sh | 11 +- examples/vae/stress_threaded_loader.py | 136 +++++++++++++++++++++++++ examples/vae/vae-ddp.py | 40 +++++++- examples/vae/vae_extra_train.py | 31 +++++- 7 files changed, 287 insertions(+), 34 deletions(-) create mode 100644 examples/vae/stress_threaded_loader.py diff --git a/README.md b/README.md index 876ba7d..c7193b3 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,16 @@ On Frontier, `method=2`'s separate core/extra `srun` steps within one job have s With `--loader=default`, `--num-workers > 1` forks worker processes that each inherit the parent's live MPI state (mpi4py/`MPI_Init` has already run before the `DataLoader` is constructed). Forking after `MPI_Init` is a known MPI hazard — the child processes don't get a clean, independent MPI runtime — and in practice this hangs rather than erroring out cleanly once DDStore is in the picture. Use `--num-workers=1` (or 0) with `--loader=default`, or switch to `--loader=threaded` (real threads, no fork, no MPI conflict) for `--num-workers > 1`. +### `ThreadDataLoader` with GPU buffers and `--num-workers > 1` + +Confirmed by direct experiment on Frontier: `--loader=threaded` together with `--gpu-dest`/`--gpu-source` and `--num-workers > 1` **silently corrupts data** — not a crash, wrong pixel values at a low but nonzero rate (observed ~0.2-1% of samples per epoch across several configurations). `vae-ddp.py`/`vae_extra_train.py` raise a clear `RuntimeError` for this combination rather than letting it run quietly wrong; **use `--num-workers=1` with GPU buffers**. + +Root cause: `DistDataset`/`DistDatasetReader`'s GPU destination-buffer pool (`examples/vae/distdataset.py`) hands out slots via a round-robin index shared across all `get()` calls, at **per-sample** granularity. Multiple `ThreadDataLoader` worker threads race for the lock protecting that index, so the order samples are physically written into pool slots is determined by lock-acquisition order, not by which batch submitted them or which batch gets consumed first. Bounding how many *batches* can be in flight at once (and sizing the pool accordingly) was tried and measurably helped (cut the corruption rate by roughly 50-99% across configurations) but didn't eliminate it, because the bound is on a batch *count*, while the actual hazard is at the individual-sample level — a batch count staying within budget doesn't guarantee a specific physical slot isn't reused by another batch's thread before the batch that currently owns it has been read. A complete fix needs `get()` to accept a caller-supplied destination buffer, so the loader can allocate one dedicated, exclusively-owned region per in-flight batch instead of sharing a flat per-sample pool — not implemented. + +This only affects the GPU-buffer path (`_val_pool` is unused on the host path, which was separately confirmed safe with `--num-workers > 1` under the lock — see the `threading.Lock` in `DistDataset`/`DistDatasetReader`'s `get()`, also confirmed necessary by direct experiment: disabling it crashed with `double free or corruption` under concurrent access). + +[examples/vae/stress_threaded_loader.py](examples/vae/stress_threaded_loader.py) is the diagnostic script used to find and confirm both issues above (checks every batch against ground-truth MNIST values, not just whether training runs) — kept in the repo for revisiting the per-batch-region fix later rather than re-deriving a repro from scratch. + ### GPU-to-GPU RDMA performance on AMD/ROCm On Frontier, the GPU synchronization performed before each RDMA call (needed for correctness) can outweigh the benefit of skipping the host copy for small, per-sample transfers — GPU-to-GPU has not shown a speed advantage there yet, though results are correct either way. Larger, batched transfers should benefit more; that usage pattern isn't built yet. @@ -318,11 +328,14 @@ mpirun -n 4 python examples/vae/vae-ddp.py DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --gpu-dest --gpu-source ``` -In `vae-ddp.py`, `--num-workers` (default 1) controls the training `DataLoader`'s parallelism. By default (`--loader=default`), it's passed straight through to PyTorch's normal `DataLoader`, which forks worker processes — forced back to 0 whenever `--gpu-dest`/`--gpu-source` is set (forked processes can't safely own GPU state), and capped at 1 otherwise: `--num-workers > 1` with `--loader=default` raises a clear error rather than hanging, since forking after MPI has already initialized is a known hazard with DDStore (see [Known Limitations](#default-forked-process-dataloader-with---num-workers--1-and-ddstore)). The applied value is printed at startup either way: `train_loader: DataLoader, num_workers=N`. `--loader=threaded` switches to [examples/vae/ddstore_dataloader.py](examples/vae/ddstore_dataloader.py)'s `ThreadDataLoader` instead, which parallelizes fetches across a thread pool — real threads, no fork, so both the GPU-state and MPI hazards above don't apply, and `--num-workers > 1` is safe together with `--gpu-dest`/`--gpu-source`. Concurrent `get()` calls from multiple threads are serialized internally by a lock in `DistDataset`/`DistDatasetReader` (the underlying RDMA transfer has no locking of its own), so threads gain overlap on everything except the RDMA call itself. `--loader=threaded` requires `DDSTORE_METHOD` 1 or 2 (libfabric). `vae_extra_train.py` has the same `--loader`/`--num-workers` flags, but its default loader always uses `num_workers=0` unconditionally (no `--num-workers` override) — only its `--loader=threaded` path is parallel: +In `vae-ddp.py`, `--num-workers` (default 1) controls the training `DataLoader`'s parallelism. By default (`--loader=default`), it's passed straight through to PyTorch's normal `DataLoader`, which forks worker processes — forced back to 0 whenever `--gpu-dest`/`--gpu-source` is set (forked processes can't safely own GPU state), and capped at 1 otherwise: `--num-workers > 1` with `--loader=default` raises a clear error rather than hanging, since forking after MPI has already initialized is a known hazard with DDStore (see [Known Limitations](#default-forked-process-dataloader-with---num-workers--1-and-ddstore)). The applied value is printed at startup either way: `train_loader: DataLoader, num_workers=N`. `--loader=threaded` switches to [examples/vae/ddstore_dataloader.py](examples/vae/ddstore_dataloader.py)'s `ThreadDataLoader` instead, which parallelizes fetches across a thread pool — real threads, no fork, so the MPI hazard above doesn't apply, and `--num-workers > 1` is safe on the **host** path. It is **not** currently safe together with `--gpu-dest`/`--gpu-source` — `--num-workers > 1` with GPU buffers raises a clear error rather than running silently wrong (see [Known Limitations](#threaddataloader-with-gpu-buffers-and---num-workers--1)). Concurrent `get()` calls from multiple threads are serialized internally by a lock in `DistDataset`/`DistDatasetReader` (the underlying RDMA transfer has no locking of its own; confirmed necessary by direct experiment), so threads gain overlap on everything except the RDMA call itself. `--loader=threaded` requires `DDSTORE_METHOD` 1 or 2 (libfabric). `vae_extra_train.py` has the same `--loader`/`--num-workers` flags, but its default loader always uses `num_workers=0` unconditionally (no `--num-workers` override) — only its `--loader=threaded` path is parallel: ```bash -# vae-ddp.py, threaded loader, 4 worker threads, together with GPUDirect -DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --loader=threaded --num-workers=4 --gpu-dest --gpu-source +# vae-ddp.py, threaded loader, 4 worker threads, host path (no GPU buffers) +DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --loader=threaded --num-workers=4 + +# threaded loader + GPUDirect together -- num-workers must stay at 1 +DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --loader=threaded --num-workers=1 --gpu-dest --gpu-source ``` ### Slurm job scripts (Frontier) @@ -333,12 +346,12 @@ DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py - sbatch examples/vae/script/job-vae-single.sh # method=0, cxi sbatch examples/vae/script/job-vae-single.sh --method=1 --gpudirect sbatch examples/vae/script/job-vae-single.sh --method=1 --thread --num-workers=4 # threaded loader, 4 worker threads -sbatch examples/vae/script/job-vae-single.sh --method=1 --gpudirect --thread --num-workers=4 # threaded loader + GPUDirect +sbatch examples/vae/script/job-vae-single.sh --method=1 --gpudirect --thread --num-workers=1 # threaded loader + GPUDirect (num-workers must stay 1) sbatch examples/vae/script/job-vae-core-extra.sh # method=2, cxi, colocate layout sbatch examples/vae/script/job-vae-core-extra.sh --gpudirect --layout=split-node --core-nnodes=2 ``` -Run `--help` on either script for the full option list. `job-vae-single.sh` additionally has `--thread` (switch to `ThreadDataLoader`, see above) and `--num-workers` (default 1; must stay <= 1 for the default loader, forced to 0 there when `--gpudirect` is set — use `--thread` for real parallelism). `job-vae-core-extra.sh` additionally has `--layout=colocate|split-node` and `--core-nnodes`. Note `--layout=colocate` together with `--gpudirect` will over-request GPUs per node (core and extra each ask for a full node's worth of GPUs on the same nodes) — use `--layout=split-node` when testing GPUDirect on `job-vae-core-extra.sh`. +Run `--help` on either script for the full option list. `job-vae-single.sh` additionally has `--thread` (switch to `ThreadDataLoader`, see above) and `--num-workers` (default 1; must stay <= 1 for the default loader and, with `--thread`, must also stay <= 1 whenever `--gpudirect` is set — real multi-worker parallelism with GPU buffers is not yet safe, see Known Limitations). `job-vae-core-extra.sh` additionally has `--layout=colocate|split-node` and `--core-nnodes`. Note `--layout=colocate` together with `--gpudirect` will over-request GPUs per node (core and extra each ask for a full node's worth of GPUs on the same nodes) — use `--layout=split-node` when testing GPUDirect on `job-vae-core-extra.sh`. ## Testing diff --git a/examples/vae/ddstore_dataloader.py b/examples/vae/ddstore_dataloader.py index 388b0ea..a0637dc 100644 --- a/examples/vae/ddstore_dataloader.py +++ b/examples/vae/ddstore_dataloader.py @@ -88,22 +88,57 @@ def __iter__(self): self._num_yielded = 0 self._sampler_iter = iter(self._index_sampler) self.fs_iter = iter(self.fs.get, None) - for i in range(len(self._index_sampler)): - index = next(self._sampler_iter) + self._next_batch_i = 0 + self._inflight = 0 + self._sampler_exhausted = False + # Bound how many batches can be in flight (submitted but not yet + # consumed via __next__) at once, instead of submitting the whole + # epoch up front. GPU-resident consumers (DistDataset/ + # DistDatasetReader's buffer pool) only have a bounded number of + # slots; submitting far ahead of consumption lets a slower + # consumer's batch get overwritten by the pool's round-robin reuse + # before it's actually read. Mirrors torch's own prefetch_factor + # (default 2 per worker). + self._max_inflight = max(1, (self.num_workers or 1) * (self.prefetch_factor or 2)) + self._refill() + return self + + def _refill(self): + while self._inflight < self._max_inflight: + try: + index = next(self._sampler_iter) + except StopIteration: + if not self._sampler_exhausted: + self._sampler_exhausted = True + self.fs.put(None) + return future = self.executor.submit( self.fetch, self.dataset, - i, + self._next_batch_i, index, pin_memory=self.pin_memory, ) self.fs.put(future) - self.fs.put(None) - return self + self._next_batch_i += 1 + self._inflight += 1 def __next__(self): + # Refill *before* popping this call's batch, not after: the caller + # (outside this class) hasn't read the batch we're about to return + # yet, and won't until this call returns and its loop body runs. + # Refilling here uses the slot freed by the *previous* call's + # batch, which -- by ordinary for-loop semantics -- the caller's + # loop body has already fully consumed by the time it asks for the + # next item (i.e. calls __next__ again). Refilling after popping + # (the previous version of this code) let a new fetch reuse that + # slot before the caller had read it, corrupting ~0.3-0.6% of + # samples per epoch even with max_inflight/pool_size otherwise + # correctly bounded -- confirmed empirically before this fix. + self._refill() future = next(self.fs_iter) ibatch, data = future.result() + self._inflight -= 1 self._num_yielded += 1 if self.collate_fn is not None: data = self.collate_fn(data) diff --git a/examples/vae/distdataset.py b/examples/vae/distdataset.py index 6b678b7..3d3cf1e 100644 --- a/examples/vae/distdataset.py +++ b/examples/vae/distdataset.py @@ -25,6 +25,7 @@ def __init__( ddstore_width=None, device=None, add_device=None, + pool_size=None, ): super().__init__() @@ -131,21 +132,34 @@ def __init__( # # Solution: pre-allocate a contiguous (POOL, 784) tensor so all slices # share one MR registration. Slices are handed out round-robin so - # all batch_size concurrent live tensors have distinct pointers (no - # aliasing) while staying inside the single registered region. - # POOL must be >= the DataLoader's batch_size; 256 covers the default - # 128 with headroom. Only used when device is not None (GPU path). - # Only safe with num_workers=0 (single-threaded DataLoader). + # all concurrently-live tensors have distinct pointers (no aliasing) + # while staying inside the single registered region. + # + # Sizing: POOL must be >= the number of samples that can be "in + # flight" (returned by get() but not yet consumed by collate) at + # once. With the default num_workers=0 DataLoader, that's at most + # one batch, so 256 safely covers the default batch_size=128. With + # ThreadDataLoader (examples/vae/ddstore_dataloader.py), multiple + # batches can be in flight simultaneously -- it bounds this to + # num_workers * prefetch_factor batches -- so POOL must be sized + # accordingly via `pool_size`; pass it in (e.g. + # `pool_size = num_workers * prefetch_factor * batch_size`) when + # using ThreadDataLoader, or get() silently hands out a slice that's + # still live in another batch (confirmed by direct experiment: this + # produced ~99.5% wrong-data rate with 8 workers / 256-slot pool / + # batch_size=64, well before exhausting the pool's raw capacity). + # # Guards pool-slice selection/increment + the ddstore.get() calls in # get() below -- needed once a caller (e.g. ThreadDataLoader) can # invoke get() from multiple threads concurrently. The underlying # DDStore::get() releases the GIL for its blocking transfer but has # no internal locking of its own, so concurrent calls here would # race on both the pool round-robin index and DDStore's CQ/MR-cache - # state. + # state (confirmed by direct experiment: disabling this lock crashed + # with "double free or corruption" under concurrent access). self._lock = threading.Lock() - _POOL = 256 + _POOL = pool_size if pool_size is not None else 256 if device is not None: self._val_pool = torch.empty( (_POOL, 28 * 28), dtype=torch.float32, device=device @@ -204,7 +218,7 @@ class DistDatasetReader(Dataset): core rank's memory. """ - def __init__(self, label, handshake_dir, n_core, device=None): + def __init__(self, label, handshake_dir, n_core, device=None, pool_size=None): super().__init__() self.label = label # See DistDataset.__init__ for what `device` does. @@ -227,10 +241,11 @@ def __init__(self, label, handshake_dir, n_core, device=None): "which is not a perfect square (expected a flattened square image)" ) - # See DistDataset.__init__ for what this guards. + # See DistDataset.__init__ for what this guards, and for the + # `pool_size` sizing requirement when using ThreadDataLoader. self._lock = threading.Lock() - _POOL = 256 + _POOL = pool_size if pool_size is not None else 256 if device is not None: self._val_pool = torch.empty( (_POOL, self.data_disp), dtype=torch.float32, device=device diff --git a/examples/vae/script/job-vae-single.sh b/examples/vae/script/job-vae-single.sh index 9caf4d0..f7fb6eb 100755 --- a/examples/vae/script/job-vae-single.sh +++ b/examples/vae/script/job-vae-single.sh @@ -23,13 +23,13 @@ Options: --fabric=cxi. --thread Use ThreadDataLoader (thread-pool DataLoader) instead of the default forked-process DataLoader. Needed for - --num-workers > 0 together with --gpudirect. Requires - --method=1 or 2. + --num-workers > 1. Requires --method=1 or 2. --num-workers=N Worker processes for the default loader (must stay <= 1 -- forking after MPI_Init hangs with DDStore for N > 1; use --thread instead), or worker threads with --thread. - Forced to 0 for the default loader when --gpudirect is - set (fork-safety guard). Default: 1. + With --gpudirect, must stay <= 1 even with --thread -- + confirmed unsafe above that (silent data corruption, not + a crash; see README Known Limitations). Default: 1. -h, --help Show this help message and exit. Examples: @@ -37,7 +37,8 @@ Examples: $(basename "$0") --method=1 --gpudirect # GPUDirect over libfabric $(basename "$0") --method=2 --gpudirect # GPUDirect over file-based handshake $(basename "$0") --fabric=hsn # baseline over hsn instead - $(basename "$0") --method=1 --gpudirect --thread --num-workers=4 + $(basename "$0") --method=1 --thread --num-workers=4 # threaded loader, no GPU buffers + $(basename "$0") --method=1 --gpudirect --thread --num-workers=1 # GPUDirect, threaded loader EOF } diff --git a/examples/vae/stress_threaded_loader.py b/examples/vae/stress_threaded_loader.py new file mode 100644 index 0000000..6e06672 --- /dev/null +++ b/examples/vae/stress_threaded_loader.py @@ -0,0 +1,136 @@ +""" +Diagnostic harness for ThreadDataLoader + DistDataset concurrency +correctness. Kept in the repo intentionally (not deleted) for revisiting +later -- see "ThreadDataLoader with GPU buffers and --num-workers > 1" in +the README's Known Limitations section for the full writeup. + +What it does: builds a DistDataset, wraps it in ThreadDataLoader +(--num-workers, --batch-size, --epochs configurable), and checks every +(data, label) batch the loader returns against the known-correct MNIST +value at that global index (the raw torchvision dataset, read directly -- +no RDMA involved in computing the expected value). Any mismatch means a +race actually corrupted data; the script prints per-sample MISMATCH lines +plus a final GLOBAL mismatches=N summary and exits nonzero if N > 0. + +Use --device=cpu for the host-only path (isolates the libfabric/CQ race +in src/common.cxx from the GPU pool entirely -- confirmed necessary via +the threading.Lock in distdataset.py's get(); disabling it crashed with +"double free or corruption" under concurrent access) and --device=cuda +for the GPU-buffer path (--gpu-dest/--gpu-source equivalent; confirmed to +still corrupt data with --num-workers > 1 even with that lock held and a +correctly-sized pool -- see the README section above for the root cause +and what a complete fix needs). + +Example (run inside an active salloc, DDSTORE_FABRIC=cxi, DDSTORE_METHOD=1): + srun -N1 -n2 -c7 python -u stress_threaded_loader.py --device=cpu --num-workers=8 --epochs=5 + srun -N1 -n2 -c7 --gpus-per-task=1 python -u stress_threaded_loader.py --device=cuda --num-workers=8 --epochs=5 +""" + +## torch (and the RCCL/HIP shared libraries it pulls in) must finish loading +## before mpi4py triggers MPI_Init, or their static destructors run in the +## wrong order at interpreter exit and corrupt the heap. +## Do not reorder these imports. +import argparse +import sys +import time +import torch +import torch.utils.data +from torchvision import datasets, transforms + +import mpi4py + +mpi4py.rc.thread_level = "serialized" +mpi4py.rc.threads = False +from mpi4py import MPI + +from distdataset import DistDataset +from ddstore_dataloader import ThreadDataLoader + +from ddp_utils import setup_ddp, get_local_rank + +parser = argparse.ArgumentParser() +parser.add_argument("--device", choices=["cpu", "cuda"], default="cpu") +parser.add_argument("--num-workers", type=int, default=8) +parser.add_argument("--epochs", type=int, default=5) +parser.add_argument("--batch-size", type=int, default=64) +args = parser.parse_args() + +comm_size, rank = setup_ddp() +comm = MPI.COMM_WORLD +local_rank = get_local_rank(rank) + +if args.device == "cuda": + if torch.cuda.device_count() > 1: + torch.cuda.set_device(local_rank) + device = torch.device(f"cuda:{local_rank}") + else: + device = torch.device("cuda") + gpu_device = device +else: + gpu_device = None + +raw = datasets.MNIST("data", train=True, download=True, transform=transforms.ToTensor()) + +# See the matching comment in vae-ddp.py: ThreadDataLoader bounds in-flight +# batches to num_workers * prefetch_factor (default 2); the GPU pool must +# be sized to match or get() hands out a slice that's still live elsewhere. +pool_size = (args.num_workers * 2 + 1) * args.batch_size + +trainset = DistDataset( + raw, + "train", + comm, + device=gpu_device, + add_device=gpu_device, + pool_size=pool_size, +) +comm.Barrier() + +loader = ThreadDataLoader( + trainset, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers +) + +mismatches = 0 +total = 0 +t0 = time.time() +for epoch in range(args.epochs): + idx_cursor = 0 + for data, label in loader: + bs = data.shape[0] + for b in range(bs): + idx = idx_cursor + b + expected_img, expected_label = raw[idx] + got_label = int(label[b].item()) + total += 1 + if got_label != int(expected_label): + mismatches += 1 + print( + f"[rank {rank}] epoch={epoch} idx={idx} LABEL MISMATCH " + f"expected={expected_label} got={got_label}", + flush=True, + ) + continue + got_img = data[b].detach().cpu() + if not torch.allclose(got_img, expected_img, atol=1e-5): + maxdiff = (got_img - expected_img).abs().max().item() + mismatches += 1 + print( + f"[rank {rank}] epoch={epoch} idx={idx} DATA MISMATCH maxdiff={maxdiff}", + flush=True, + ) + idx_cursor += bs +elapsed = time.time() - t0 + +print( + f"[rank {rank}] DONE device={args.device} num_workers={args.num_workers} " + f"batch_size={args.batch_size} epochs={args.epochs} total={total} " + f"mismatches={mismatches} elapsed={elapsed:.1f}s", + flush=True, +) + +comm.Barrier() +all_mismatches = comm.allreduce(mismatches, op=MPI.SUM) +if rank == 0: + print(f"GLOBAL mismatches={all_mismatches}", flush=True) + if all_mismatches > 0: + sys.exit(1) diff --git a/examples/vae/vae-ddp.py b/examples/vae/vae-ddp.py index 6efd7c4..6963f8f 100644 --- a/examples/vae/vae-ddp.py +++ b/examples/vae/vae-ddp.py @@ -81,11 +81,12 @@ default="default", help="DataLoader implementation for the training set. 'threaded' uses " "ThreadDataLoader (examples/vae/ddstore_dataloader.py), a " - "thread-pool-based loader that allows --num-workers > 0 together " - "with --gpu-dest/--gpu-source (the default loader forks worker " - "processes, which cannot safely own GPU state, so it stays " - "single-threaded for those flags). Requires DDSTORE_METHOD 1 or 2. " - "Default: default.", + "thread-pool-based loader that allows --num-workers > 1 (the default " + "loader forks worker processes, which hangs with DDStore above 1). " + "With --gpu-dest/--gpu-source, --num-workers must stay <= 1 even with " + "--loader=threaded -- confirmed unsafe above that (silent data " + "corruption, not a crash; see README Known Limitations). Requires " + "DDSTORE_METHOD 1 or 2. Default: default.", ) parser.add_argument( "--num-workers", @@ -161,17 +162,46 @@ "after MPI_Init, which hangs with DDStore. Use --num-workers=1, or " "--loader=threaded for real parallelism." ) +# --loader=threaded + (--gpu-dest or --gpu-source) + --num-workers > 1 is +# confirmed unsafe by direct experiment: it silently corrupts data (not a +# crash -- wrong pixel values at a low but nonzero rate). Root cause: the +# GPU buffer pool in distdataset.py hands out slots via a per-SAMPLE +# round-robin index shared across threads; slot write order is determined +# by lock-acquisition order, not batch submission/consumption order, so a +# bounded in-flight *batch count* doesn't actually bound which physical +# slots can get overwritten while unread. A real fix needs get() to accept +# a caller-supplied destination buffer so ThreadDataLoader can allocate one +# dedicated region per in-flight batch (not per sample) -- not done yet. +# num_workers<=1 has no concurrent writers, so it's unaffected. +if args.loader == "threaded" and (args.gpu_dest or args.gpu_source) and args.num_workers > 1: + raise RuntimeError( + "--loader=threaded with --gpu-dest/--gpu-source and --num-workers > 1 " + "is known to silently corrupt data (confirmed by direct experiment -- " + "see README Known Limitations). Use --num-workers=1 with GPU buffers, " + "or drop --gpu-dest/--gpu-source for real multi-worker parallelism." + ) if args.gpu_dest or args.gpu_source: kwargs = {} else: kwargs = {"num_workers": args.num_workers} if args.num_workers > 0 else {} +# ThreadDataLoader bounds in-flight batches to num_workers * prefetch_factor +# (default prefetch_factor=2, matching torch's own default); the GPU pool +# must hold at least that many batches' worth of samples or get() hands out +# a slice that's still "live" in an unconsumed batch -- see the pool-sizing +# comment in distdataset.py. +1 batch of headroom. +if args.loader == "threaded": + pool_size = (args.num_workers * 2 + 1) * args.batch_size +else: + pool_size = None + trainset = DistDataset( datasets.MNIST("data", train=True, download=True, transform=transforms.ToTensor()), "train", comm, device=device if args.gpu_dest else None, add_device=device if args.gpu_source else None, + pool_size=pool_size, ) # trainset = datasets.MNIST('data', train=True, download=True,transform=transforms.ToTensor()) comm.Barrier() diff --git a/examples/vae/vae_extra_train.py b/examples/vae/vae_extra_train.py index b00e349..d8bc0e0 100644 --- a/examples/vae/vae_extra_train.py +++ b/examples/vae/vae_extra_train.py @@ -105,10 +105,12 @@ default="default", help="DataLoader implementation for the training set. 'threaded' uses " "ThreadDataLoader (examples/vae/ddstore_dataloader.py), a " - "thread-pool-based loader that allows --num-workers > 0 together " - "with --gpu-dest (the default loader forks worker processes, " - "which cannot safely own GPU state, so it stays single-threaded " - "for that flag). Default: default.", + "thread-pool-based loader that allows --num-workers > 1 (the default " + "loader forks worker processes, which hangs with DDStore above 1). " + "With --gpu-dest, --num-workers must stay <= 1 even with " + "--loader=threaded -- confirmed unsafe above that (silent data " + "corruption, not a crash; see README Known Limitations). " + "Default: default.", ) parser.add_argument( "--num-workers", @@ -158,11 +160,32 @@ if args.gpu_dest: assert kwargs.get("num_workers", 0) == 0 +# --loader=threaded + --gpu-dest + --num-workers > 1 is confirmed unsafe by +# direct experiment (silent data corruption, not a crash) -- see the +# matching guard and comment in vae-ddp.py and the README Known Limitations +# entry for the root cause. num_workers<=1 has no concurrent writers. +if args.loader == "threaded" and args.gpu_dest and args.num_workers > 1: + raise RuntimeError( + "--loader=threaded with --gpu-dest and --num-workers > 1 is known " + "to silently corrupt data (see README Known Limitations). Use " + "--num-workers=1 with --gpu-dest, or drop --gpu-dest for real " + "multi-worker parallelism." + ) + +# See the matching comment in vae-ddp.py: ThreadDataLoader bounds in-flight +# batches to num_workers * prefetch_factor (default 2); the GPU pool must +# be sized to match or get() hands out a slice that's still live elsewhere. +if args.loader == "threaded": + pool_size = (args.num_workers * 2 + 1) * args.batch_size +else: + pool_size = None + trainset = DistDatasetReader( "train", args.handshake_dir, args.n_core, device=device if args.gpu_dest else None, + pool_size=pool_size, ) sampler = torch.utils.data.distributed.DistributedSampler(trainset) From 20f02f3780dbddf0557fcae94f3d5ada7d5a38ca Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 08:07:58 -0400 Subject: [PATCH 19/56] base --- README.md | 30 +++--- examples/vae/ddstore_dataloader.py | 26 ++--- examples/vae/distdataset.py | 118 +++++---------------- examples/vae/script/job-vae-single.sh | 24 ++--- examples/vae/stress_threaded_loader.py | 136 ------------------------- examples/vae/vae-ddp.py | 88 ++-------------- examples/vae/vae_extra_train.py | 66 +++--------- include/common.h | 38 +++++++ include/ddstore.hpp | 74 ++------------ src/pyddstore.pyx | 30 ------ test/test_gpu_rdma.py | 33 +++--- 11 files changed, 146 insertions(+), 517 deletions(-) delete mode 100644 examples/vae/stress_threaded_loader.py diff --git a/README.md b/README.md index c7193b3..da5c247 100644 --- a/README.md +++ b/README.md @@ -270,19 +270,13 @@ GPU kernels execute asynchronously: a compute kernel that just wrote to (or is a On Frontier, `method=2`'s separate core/extra `srun` steps within one job have shown intermittent RDMA connectivity issues between steps, and a later step in a job with several sequential steps can occasionally fail to start. The `--network=job_vni`/`single_node_vni` `sbatch` options have not reliably fixed this. The cause isn't fully understood. If you hit this, use fewer sequential steps per job, or use `method=1` (single job step), which doesn't have this issue. -### Default (forked-process) `DataLoader` with `--num-workers > 1` and DDStore +### `get()` has no GPU destination-buffer pool -With `--loader=default`, `--num-workers > 1` forks worker processes that each inherit the parent's live MPI state (mpi4py/`MPI_Init` has already run before the `DataLoader` is constructed). Forking after `MPI_Init` is a known MPI hazard — the child processes don't get a clean, independent MPI runtime — and in practice this hangs rather than erroring out cleanly once DDStore is in the picture. Use `--num-workers=1` (or 0) with `--loader=default`, or switch to `--loader=threaded` (real threads, no fork, no MPI conflict) for `--num-workers > 1`. +`DistDataset`/`DistDatasetReader`'s `get()` allocates a fresh GPU tensor per call on the GPU path (`--gpu-dest`), rather than reusing a pre-allocated pool. An earlier pooled design (round-robin slices of one pre-registered buffer, to amortize `fi_mr_regattr` cost) was removed after it was confirmed by direct experiment to corrupt data under `ThreadDataLoader` with `--num-workers > 1`: multiple worker threads raced for pool slots at per-sample granularity, and bounding how many batches could be in flight at once didn't bound which physical slots got overwritten, since slot-write order was determined by lock-acquisition order, not batch order. Removing the pool removes that race entirely — each call's destination is privately owned, nothing to reuse. The tradeoff: a fresh `fi_mr_regattr` per call instead of one registration shared across many. Revisit with a pool later if that registration cost matters (`--num-workers > 0` is otherwise known to work per the next section). -### `ThreadDataLoader` with GPU buffers and `--num-workers > 1` +### Thread-safety of concurrent `get()` calls -Confirmed by direct experiment on Frontier: `--loader=threaded` together with `--gpu-dest`/`--gpu-source` and `--num-workers > 1` **silently corrupts data** — not a crash, wrong pixel values at a low but nonzero rate (observed ~0.2-1% of samples per epoch across several configurations). `vae-ddp.py`/`vae_extra_train.py` raise a clear `RuntimeError` for this combination rather than letting it run quietly wrong; **use `--num-workers=1` with GPU buffers**. - -Root cause: `DistDataset`/`DistDatasetReader`'s GPU destination-buffer pool (`examples/vae/distdataset.py`) hands out slots via a round-robin index shared across all `get()` calls, at **per-sample** granularity. Multiple `ThreadDataLoader` worker threads race for the lock protecting that index, so the order samples are physically written into pool slots is determined by lock-acquisition order, not by which batch submitted them or which batch gets consumed first. Bounding how many *batches* can be in flight at once (and sizing the pool accordingly) was tried and measurably helped (cut the corruption rate by roughly 50-99% across configurations) but didn't eliminate it, because the bound is on a batch *count*, while the actual hazard is at the individual-sample level — a batch count staying within budget doesn't guarantee a specific physical slot isn't reused by another batch's thread before the batch that currently owns it has been read. A complete fix needs `get()` to accept a caller-supplied destination buffer, so the loader can allocate one dedicated, exclusively-owned region per in-flight batch instead of sharing a flat per-sample pool — not implemented. - -This only affects the GPU-buffer path (`_val_pool` is unused on the host path, which was separately confirmed safe with `--num-workers > 1` under the lock — see the `threading.Lock` in `DistDataset`/`DistDatasetReader`'s `get()`, also confirmed necessary by direct experiment: disabling it crashed with `double free or corruption` under concurrent access). - -[examples/vae/stress_threaded_loader.py](examples/vae/stress_threaded_loader.py) is the diagnostic script used to find and confirm both issues above (checks every batch against ground-truth MNIST values, not just whether training runs) — kept in the repo for revisiting the per-batch-region fix later rather than re-deriving a repro from scratch. +`DDStore::get()` releases the GIL for its blocking RDMA transfer (`nogil` in `src/pyddstore.pyx`) but has no internal locking of its own (confirmed by direct experiment: disabling the lock in `DistDataset`/`DistDatasetReader`'s `get()` crashed with `double free or corruption` under concurrent thread access). `ThreadDataLoader` (`examples/vae/ddstore_dataloader.py`) relies on that lock to serialize concurrent `get()` calls from its worker threads — don't remove it. ### GPU-to-GPU RDMA performance on AMD/ROCm @@ -328,14 +322,14 @@ mpirun -n 4 python examples/vae/vae-ddp.py DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --gpu-dest --gpu-source ``` -In `vae-ddp.py`, `--num-workers` (default 1) controls the training `DataLoader`'s parallelism. By default (`--loader=default`), it's passed straight through to PyTorch's normal `DataLoader`, which forks worker processes — forced back to 0 whenever `--gpu-dest`/`--gpu-source` is set (forked processes can't safely own GPU state), and capped at 1 otherwise: `--num-workers > 1` with `--loader=default` raises a clear error rather than hanging, since forking after MPI has already initialized is a known hazard with DDStore (see [Known Limitations](#default-forked-process-dataloader-with---num-workers--1-and-ddstore)). The applied value is printed at startup either way: `train_loader: DataLoader, num_workers=N`. `--loader=threaded` switches to [examples/vae/ddstore_dataloader.py](examples/vae/ddstore_dataloader.py)'s `ThreadDataLoader` instead, which parallelizes fetches across a thread pool — real threads, no fork, so the MPI hazard above doesn't apply, and `--num-workers > 1` is safe on the **host** path. It is **not** currently safe together with `--gpu-dest`/`--gpu-source` — `--num-workers > 1` with GPU buffers raises a clear error rather than running silently wrong (see [Known Limitations](#threaddataloader-with-gpu-buffers-and---num-workers--1)). Concurrent `get()` calls from multiple threads are serialized internally by a lock in `DistDataset`/`DistDatasetReader` (the underlying RDMA transfer has no locking of its own; confirmed necessary by direct experiment), so threads gain overlap on everything except the RDMA call itself. `--loader=threaded` requires `DDSTORE_METHOD` 1 or 2 (libfabric). `vae_extra_train.py` has the same `--loader`/`--num-workers` flags, but its default loader always uses `num_workers=0` unconditionally (no `--num-workers` override) — only its `--loader=threaded` path is parallel: +`--num-workers` (default 0) controls the training `DataLoader`'s parallelism. `0` uses PyTorch's normal `DataLoader`, single-threaded (no forked worker processes at all, so no MPI-after-`MPI_Init`-fork hazard). Any `--num-workers > 0` switches to [examples/vae/ddstore_dataloader.py](examples/vae/ddstore_dataloader.py)'s `ThreadDataLoader` instead — real threads, no fork, so it's safe together with `--gpu-dest`/`--gpu-source` too. The applied loader and worker count are printed at startup: `train_loader: DataLoader, num_workers=N` or `train_loader: ThreadDataLoader, num_workers=N`. `ThreadDataLoader` requires `DDSTORE_METHOD` 1 or 2 (libfabric) in `vae-ddp.py`. Concurrent `get()` calls from multiple threads are serialized internally by a lock in `DistDataset`/`DistDatasetReader` (the underlying RDMA transfer has no locking of its own; confirmed necessary by direct experiment — see Known Limitations), so threads gain overlap on everything except the RDMA call itself. `vae_extra_train.py` has the same `--num-workers` flag and behavior: ```bash -# vae-ddp.py, threaded loader, 4 worker threads, host path (no GPU buffers) -DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --loader=threaded --num-workers=4 +# ThreadDataLoader, 4 worker threads, host path (no GPU buffers) +DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --num-workers=4 -# threaded loader + GPUDirect together -- num-workers must stay at 1 -DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --loader=threaded --num-workers=1 --gpu-dest --gpu-source +# ThreadDataLoader + GPUDirect together +DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --num-workers=4 --gpu-dest --gpu-source ``` ### Slurm job scripts (Frontier) @@ -345,13 +339,13 @@ DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py - ```bash sbatch examples/vae/script/job-vae-single.sh # method=0, cxi sbatch examples/vae/script/job-vae-single.sh --method=1 --gpudirect -sbatch examples/vae/script/job-vae-single.sh --method=1 --thread --num-workers=4 # threaded loader, 4 worker threads -sbatch examples/vae/script/job-vae-single.sh --method=1 --gpudirect --thread --num-workers=1 # threaded loader + GPUDirect (num-workers must stay 1) +sbatch examples/vae/script/job-vae-single.sh --method=1 --num-workers=4 # ThreadDataLoader, 4 worker threads +sbatch examples/vae/script/job-vae-single.sh --method=1 --gpudirect --num-workers=4 # ThreadDataLoader + GPUDirect sbatch examples/vae/script/job-vae-core-extra.sh # method=2, cxi, colocate layout sbatch examples/vae/script/job-vae-core-extra.sh --gpudirect --layout=split-node --core-nnodes=2 ``` -Run `--help` on either script for the full option list. `job-vae-single.sh` additionally has `--thread` (switch to `ThreadDataLoader`, see above) and `--num-workers` (default 1; must stay <= 1 for the default loader and, with `--thread`, must also stay <= 1 whenever `--gpudirect` is set — real multi-worker parallelism with GPU buffers is not yet safe, see Known Limitations). `job-vae-core-extra.sh` additionally has `--layout=colocate|split-node` and `--core-nnodes`. Note `--layout=colocate` together with `--gpudirect` will over-request GPUs per node (core and extra each ask for a full node's worth of GPUs on the same nodes) — use `--layout=split-node` when testing GPUDirect on `job-vae-core-extra.sh`. +Run `--help` on either script for the full option list. `job-vae-single.sh` additionally has `--num-workers` (default 0; `> 0` switches to `ThreadDataLoader`, see above). `job-vae-core-extra.sh` additionally has `--layout=colocate|split-node` and `--core-nnodes`. Note `--layout=colocate` together with `--gpudirect` will over-request GPUs per node (core and extra each ask for a full node's worth of GPUs on the same nodes) — use `--layout=split-node` when testing GPUDirect on `job-vae-core-extra.sh`. ## Testing diff --git a/examples/vae/ddstore_dataloader.py b/examples/vae/ddstore_dataloader.py index a0637dc..0c7b751 100644 --- a/examples/vae/ddstore_dataloader.py +++ b/examples/vae/ddstore_dataloader.py @@ -93,11 +93,8 @@ def __iter__(self): self._sampler_exhausted = False # Bound how many batches can be in flight (submitted but not yet # consumed via __next__) at once, instead of submitting the whole - # epoch up front. GPU-resident consumers (DistDataset/ - # DistDatasetReader's buffer pool) only have a bounded number of - # slots; submitting far ahead of consumption lets a slower - # consumer's batch get overwritten by the pool's round-robin reuse - # before it's actually read. Mirrors torch's own prefetch_factor + # epoch up front -- keeps memory use (GPU tensors included) bounded + # regardless of dataset size. Mirrors torch's own prefetch_factor # (default 2 per worker). self._max_inflight = max(1, (self.num_workers or 1) * (self.prefetch_factor or 2)) self._refill() @@ -124,17 +121,14 @@ def _refill(self): self._inflight += 1 def __next__(self): - # Refill *before* popping this call's batch, not after: the caller - # (outside this class) hasn't read the batch we're about to return - # yet, and won't until this call returns and its loop body runs. - # Refilling here uses the slot freed by the *previous* call's - # batch, which -- by ordinary for-loop semantics -- the caller's - # loop body has already fully consumed by the time it asks for the - # next item (i.e. calls __next__ again). Refilling after popping - # (the previous version of this code) let a new fetch reuse that - # slot before the caller had read it, corrupting ~0.3-0.6% of - # samples per epoch even with max_inflight/pool_size otherwise - # correctly bounded -- confirmed empirically before this fix. + # Refill *before* popping this call's batch, not after: refilling + # here only uses capacity freed by the *previous* call's batch, + # which -- by ordinary for-loop semantics -- the caller's loop body + # has already fully consumed by the time it asks for the next item + # (i.e. calls __next__ again). Bounds how far the executor can race + # ahead of consumption (memory, not correctness -- distdataset.py's + # get() allocates a fresh destination per call, nothing shared to + # race on). self._refill() future = next(self.fs_iter) ibatch, data = future.result() diff --git a/examples/vae/distdataset.py b/examples/vae/distdataset.py index 3d3cf1e..fae0cf8 100644 --- a/examples/vae/distdataset.py +++ b/examples/vae/distdataset.py @@ -1,7 +1,6 @@ from mpi4py import MPI import numpy as np import os -import threading import torch from torch.utils.data import Dataset @@ -25,7 +24,6 @@ def __init__( ddstore_width=None, device=None, add_device=None, - pool_size=None, ): super().__init__() @@ -121,56 +119,15 @@ def __init__( self.ddstore.add(f"{self.label}data", self.data) self.ddstore.add(f"{self.label}labels", self.labels) - # Pre-allocate a pool of GPU destination buffers for get() calls. + # get() allocates a fresh GPU tensor per call on the GPU path (no + # buffer pool) -- simpler, at the cost of a fresh fi_mr_regattr per + # distinct pointer on every call instead of one pre-registered + # region reused across calls. Revisit with a pool later if that + # registration cost matters. # - # Problem: DataLoader (num_workers=0) calls __getitem__ batch_size times - # sequentially and keeps all returned tensors alive until collate runs. - # If get() allocates a fresh torch.empty() each call, PyTorch's caching - # allocator gives batch_size distinct pointers. Each distinct pointer - # triggers fi_mr_regattr+fi_mr_bind+fi_mr_enable (expensive on GPU/HSA), - # defeating the recv_mr cache in common.cxx. - # - # Solution: pre-allocate a contiguous (POOL, 784) tensor so all slices - # share one MR registration. Slices are handed out round-robin so - # all concurrently-live tensors have distinct pointers (no aliasing) - # while staying inside the single registered region. - # - # Sizing: POOL must be >= the number of samples that can be "in - # flight" (returned by get() but not yet consumed by collate) at - # once. With the default num_workers=0 DataLoader, that's at most - # one batch, so 256 safely covers the default batch_size=128. With - # ThreadDataLoader (examples/vae/ddstore_dataloader.py), multiple - # batches can be in flight simultaneously -- it bounds this to - # num_workers * prefetch_factor batches -- so POOL must be sized - # accordingly via `pool_size`; pass it in (e.g. - # `pool_size = num_workers * prefetch_factor * batch_size`) when - # using ThreadDataLoader, or get() silently hands out a slice that's - # still live in another batch (confirmed by direct experiment: this - # produced ~99.5% wrong-data rate with 8 workers / 256-slot pool / - # batch_size=64, well before exhausting the pool's raw capacity). - # - # Guards pool-slice selection/increment + the ddstore.get() calls in - # get() below -- needed once a caller (e.g. ThreadDataLoader) can - # invoke get() from multiple threads concurrently. The underlying - # DDStore::get() releases the GIL for its blocking transfer but has - # no internal locking of its own, so concurrent calls here would - # race on both the pool round-robin index and DDStore's CQ/MR-cache - # state (confirmed by direct experiment: disabling this lock crashed - # with "double free or corruption" under concurrent access). - self._lock = threading.Lock() - - _POOL = pool_size if pool_size is not None else 256 - if device is not None: - self._val_pool = torch.empty( - (_POOL, 28 * 28), dtype=torch.float32, device=device - ) - self._val_pool_idx = 0 - # Pre-register the full pool as a single MR so all row-slices - # share one fi_mr_regattr instead of one per __getitem__ call. - self.ddstore.prefetch_recv_mr(f"{self.label}data", self._val_pool) - else: - self._val_pool = None - self._val_pool_idx = 0 + # Thread-safety for concurrent get() calls (e.g. from ThreadDataLoader + # worker threads) is handled inside DDStore itself (a per-variable + # lock in include/common.h's fabric_state) -- no lock needed here. def len(self): return self.total_ns @@ -185,21 +142,14 @@ def get(self, idx, device=None): # loops discard it, so a GPU-resident label buffer would add # complexity for no benefit. label = np.zeros(1, dtype=np.int32) - with self._lock: - if device is not None: - # Take the next slice from the pool (round-robin). All slices - # share one MR registration → recv_mr cache always hits after the - # first call. No clone() needed: each slice is a distinct pointer - # so the DataLoader can hold all batch_size results simultaneously - # without aliasing. - val = self._val_pool[self._val_pool_idx : self._val_pool_idx + 1] - self._val_pool_idx = (self._val_pool_idx + 1) % self._val_pool.shape[0] - else: - val = np.zeros((1, 28 * 28), dtype=np.float32) - val = np.ascontiguousarray(val) - assert val.data.contiguous - self.ddstore.get(f"{self.label}data", val, idx) - self.ddstore.get(f"{self.label}labels", label, idx) + if device is not None: + val = torch.empty((1, 28 * 28), dtype=torch.float32, device=device) + else: + val = np.zeros((1, 28 * 28), dtype=np.float32) + val = np.ascontiguousarray(val) + assert val.data.contiguous + self.ddstore.get(f"{self.label}data", val, idx) + self.ddstore.get(f"{self.label}labels", label, idx) if device is None: val = torch.tensor(val) val = torch.reshape(val, (1, 28, 28)) @@ -218,7 +168,7 @@ class DistDatasetReader(Dataset): core rank's memory. """ - def __init__(self, label, handshake_dir, n_core, device=None, pool_size=None): + def __init__(self, label, handshake_dir, n_core, device=None): super().__init__() self.label = label # See DistDataset.__init__ for what `device` does. @@ -241,20 +191,8 @@ def __init__(self, label, handshake_dir, n_core, device=None, pool_size=None): "which is not a perfect square (expected a flattened square image)" ) - # See DistDataset.__init__ for what this guards, and for the - # `pool_size` sizing requirement when using ThreadDataLoader. - self._lock = threading.Lock() - - _POOL = pool_size if pool_size is not None else 256 - if device is not None: - self._val_pool = torch.empty( - (_POOL, self.data_disp), dtype=torch.float32, device=device - ) - self._val_pool_idx = 0 - self.ddstore.prefetch_recv_mr(f"{self.label}data", self._val_pool) - else: - self._val_pool = None - self._val_pool_idx = 0 + # See DistDataset.__init__ -- thread-safety lives inside DDStore + # itself, no lock needed here; and no buffer pool on the GPU path. def len(self): return self.total_ns @@ -267,16 +205,14 @@ def get(self, idx, device=None): ## width, since ddstore.get() infers count from arr.shape[0] # Label stays host-only regardless of `device` -- see DistDataset.get(). label = np.zeros(1, dtype=np.int32) - with self._lock: - if device is not None: - val = self._val_pool[self._val_pool_idx : self._val_pool_idx + 1] - self._val_pool_idx = (self._val_pool_idx + 1) % self._val_pool.shape[0] - else: - val = np.zeros((1, self.data_disp), dtype=np.float32) - val = np.ascontiguousarray(val) - assert val.data.contiguous - self.ddstore.get(f"{self.label}data", val, idx) - self.ddstore.get(f"{self.label}labels", label, idx) + if device is not None: + val = torch.empty((1, self.data_disp), dtype=torch.float32, device=device) + else: + val = np.zeros((1, self.data_disp), dtype=np.float32) + val = np.ascontiguousarray(val) + assert val.data.contiguous + self.ddstore.get(f"{self.label}data", val, idx) + self.ddstore.get(f"{self.label}labels", label, idx) if device is None: val = torch.tensor(val) val = torch.reshape(val, (1, self.side, self.side)) diff --git a/examples/vae/script/job-vae-single.sh b/examples/vae/script/job-vae-single.sh index f7fb6eb..74fdee5 100755 --- a/examples/vae/script/job-vae-single.sh +++ b/examples/vae/script/job-vae-single.sh @@ -21,15 +21,10 @@ Options: --fabric=X DDSTORE_FABRIC: hsn or cxi. Default: cxi. --gpudirect Test GPUDirect RDMA. Requires --method=1 or 2 and --fabric=cxi. - --thread Use ThreadDataLoader (thread-pool DataLoader) instead of the - default forked-process DataLoader. Needed for - --num-workers > 1. Requires --method=1 or 2. - --num-workers=N Worker processes for the default loader (must stay <= 1 - -- forking after MPI_Init hangs with DDStore for N > 1; - use --thread instead), or worker threads with --thread. - With --gpudirect, must stay <= 1 even with --thread -- - confirmed unsafe above that (silent data corruption, not - a crash; see README Known Limitations). Default: 1. + --num-workers=N DataLoader workers. 0 uses the default (forked-process) + DataLoader, single-threaded. > 0 switches to + ThreadDataLoader with that many worker threads (requires + --method=1 or 2). Default: 0. -h, --help Show this help message and exit. Examples: @@ -37,8 +32,8 @@ Examples: $(basename "$0") --method=1 --gpudirect # GPUDirect over libfabric $(basename "$0") --method=2 --gpudirect # GPUDirect over file-based handshake $(basename "$0") --fabric=hsn # baseline over hsn instead - $(basename "$0") --method=1 --thread --num-workers=4 # threaded loader, no GPU buffers - $(basename "$0") --method=1 --gpudirect --thread --num-workers=1 # GPUDirect, threaded loader + $(basename "$0") --method=1 --num-workers=4 # ThreadDataLoader, no GPU buffers + $(basename "$0") --method=1 --gpudirect --num-workers=4 # GPUDirect + ThreadDataLoader EOF } @@ -57,26 +52,21 @@ export VAE_PROFILE=1 METHOD= FABRIC= GPUDIRECT_ARGS="" -THREAD=0 NUM_WORKERS= for arg in "$@"; do case "$arg" in --method=*) METHOD="${arg#--method=}" ;; --fabric=*) FABRIC="${arg#--fabric=}" ;; --gpudirect) GPUDIRECT_ARGS="--gpu-dest --gpu-source" ;; - --thread) THREAD=1 ;; --num-workers=*) NUM_WORKERS="${arg#--num-workers=}" ;; esac done export DDSTORE_FABRIC="${FABRIC:-cxi}" METHOD="${METHOD:-0}" -NUM_WORKERS="${NUM_WORKERS:-1}" +NUM_WORKERS="${NUM_WORKERS:-0}" EXTRA_ARGS="$GPUDIRECT_ARGS --num-workers=$NUM_WORKERS" -if [ "$THREAD" == "1" ]; then - EXTRA_ARGS="$EXTRA_ARGS --loader=threaded" -fi echo "DDSTORE_METHOD=$METHOD DDSTORE_FABRIC=$DDSTORE_FABRIC EXTRA_ARGS=\"$EXTRA_ARGS\"" diff --git a/examples/vae/stress_threaded_loader.py b/examples/vae/stress_threaded_loader.py deleted file mode 100644 index 6e06672..0000000 --- a/examples/vae/stress_threaded_loader.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Diagnostic harness for ThreadDataLoader + DistDataset concurrency -correctness. Kept in the repo intentionally (not deleted) for revisiting -later -- see "ThreadDataLoader with GPU buffers and --num-workers > 1" in -the README's Known Limitations section for the full writeup. - -What it does: builds a DistDataset, wraps it in ThreadDataLoader -(--num-workers, --batch-size, --epochs configurable), and checks every -(data, label) batch the loader returns against the known-correct MNIST -value at that global index (the raw torchvision dataset, read directly -- -no RDMA involved in computing the expected value). Any mismatch means a -race actually corrupted data; the script prints per-sample MISMATCH lines -plus a final GLOBAL mismatches=N summary and exits nonzero if N > 0. - -Use --device=cpu for the host-only path (isolates the libfabric/CQ race -in src/common.cxx from the GPU pool entirely -- confirmed necessary via -the threading.Lock in distdataset.py's get(); disabling it crashed with -"double free or corruption" under concurrent access) and --device=cuda -for the GPU-buffer path (--gpu-dest/--gpu-source equivalent; confirmed to -still corrupt data with --num-workers > 1 even with that lock held and a -correctly-sized pool -- see the README section above for the root cause -and what a complete fix needs). - -Example (run inside an active salloc, DDSTORE_FABRIC=cxi, DDSTORE_METHOD=1): - srun -N1 -n2 -c7 python -u stress_threaded_loader.py --device=cpu --num-workers=8 --epochs=5 - srun -N1 -n2 -c7 --gpus-per-task=1 python -u stress_threaded_loader.py --device=cuda --num-workers=8 --epochs=5 -""" - -## torch (and the RCCL/HIP shared libraries it pulls in) must finish loading -## before mpi4py triggers MPI_Init, or their static destructors run in the -## wrong order at interpreter exit and corrupt the heap. -## Do not reorder these imports. -import argparse -import sys -import time -import torch -import torch.utils.data -from torchvision import datasets, transforms - -import mpi4py - -mpi4py.rc.thread_level = "serialized" -mpi4py.rc.threads = False -from mpi4py import MPI - -from distdataset import DistDataset -from ddstore_dataloader import ThreadDataLoader - -from ddp_utils import setup_ddp, get_local_rank - -parser = argparse.ArgumentParser() -parser.add_argument("--device", choices=["cpu", "cuda"], default="cpu") -parser.add_argument("--num-workers", type=int, default=8) -parser.add_argument("--epochs", type=int, default=5) -parser.add_argument("--batch-size", type=int, default=64) -args = parser.parse_args() - -comm_size, rank = setup_ddp() -comm = MPI.COMM_WORLD -local_rank = get_local_rank(rank) - -if args.device == "cuda": - if torch.cuda.device_count() > 1: - torch.cuda.set_device(local_rank) - device = torch.device(f"cuda:{local_rank}") - else: - device = torch.device("cuda") - gpu_device = device -else: - gpu_device = None - -raw = datasets.MNIST("data", train=True, download=True, transform=transforms.ToTensor()) - -# See the matching comment in vae-ddp.py: ThreadDataLoader bounds in-flight -# batches to num_workers * prefetch_factor (default 2); the GPU pool must -# be sized to match or get() hands out a slice that's still live elsewhere. -pool_size = (args.num_workers * 2 + 1) * args.batch_size - -trainset = DistDataset( - raw, - "train", - comm, - device=gpu_device, - add_device=gpu_device, - pool_size=pool_size, -) -comm.Barrier() - -loader = ThreadDataLoader( - trainset, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers -) - -mismatches = 0 -total = 0 -t0 = time.time() -for epoch in range(args.epochs): - idx_cursor = 0 - for data, label in loader: - bs = data.shape[0] - for b in range(bs): - idx = idx_cursor + b - expected_img, expected_label = raw[idx] - got_label = int(label[b].item()) - total += 1 - if got_label != int(expected_label): - mismatches += 1 - print( - f"[rank {rank}] epoch={epoch} idx={idx} LABEL MISMATCH " - f"expected={expected_label} got={got_label}", - flush=True, - ) - continue - got_img = data[b].detach().cpu() - if not torch.allclose(got_img, expected_img, atol=1e-5): - maxdiff = (got_img - expected_img).abs().max().item() - mismatches += 1 - print( - f"[rank {rank}] epoch={epoch} idx={idx} DATA MISMATCH maxdiff={maxdiff}", - flush=True, - ) - idx_cursor += bs -elapsed = time.time() - t0 - -print( - f"[rank {rank}] DONE device={args.device} num_workers={args.num_workers} " - f"batch_size={args.batch_size} epochs={args.epochs} total={total} " - f"mismatches={mismatches} elapsed={elapsed:.1f}s", - flush=True, -) - -comm.Barrier() -all_mismatches = comm.allreduce(mismatches, op=MPI.SUM) -if rank == 0: - print(f"GLOBAL mismatches={all_mismatches}", flush=True) - if all_mismatches > 0: - sys.exit(1) diff --git a/examples/vae/vae-ddp.py b/examples/vae/vae-ddp.py index 6963f8f..7d22632 100644 --- a/examples/vae/vae-ddp.py +++ b/examples/vae/vae-ddp.py @@ -75,30 +75,17 @@ "requirements as --gpu-dest; independent of it -- use either or " "both.", ) -parser.add_argument( - "--loader", - choices=["default", "threaded"], - default="default", - help="DataLoader implementation for the training set. 'threaded' uses " - "ThreadDataLoader (examples/vae/ddstore_dataloader.py), a " - "thread-pool-based loader that allows --num-workers > 1 (the default " - "loader forks worker processes, which hangs with DDStore above 1). " - "With --gpu-dest/--gpu-source, --num-workers must stay <= 1 even with " - "--loader=threaded -- confirmed unsafe above that (silent data " - "corruption, not a crash; see README Known Limitations). Requires " - "DDSTORE_METHOD 1 or 2. Default: default.", -) parser.add_argument( "--num-workers", type=int, - default=1, + default=0, metavar="N", - help="Number of worker threads (--loader=threaded) or worker processes " - "(--loader=default). With --loader=default, must stay <= 1 -- " - "forking worker processes after MPI_Init hangs with DDStore; use " - "--loader=threaded for real parallelism instead. Also forced to 0 " - "for --loader=default when --gpu-dest/--gpu-source is set " - "(fork-safety guard). Default: 1.", + help="Number of DataLoader workers. 0 uses the default (forked-process) " + "DataLoader, single-threaded. > 0 switches to ThreadDataLoader " + "(examples/vae/ddstore_dataloader.py), with that many worker threads " + "-- forked processes can't safely own GPU state or MPI's live state, " + "so any --num-workers > 0 goes through threads, never a fork. " + "Requires DDSTORE_METHOD 1 or 2 when > 0. Default: 0.", ) args = parser.parse_args() args.cuda = not args.no_cuda and torch.cuda.is_available() @@ -147,69 +134,20 @@ model = torch.nn.parallel.DistributedDataParallel(model) optimizer = optim.Adam(model.parameters(), lr=1e-3) -# kwargs = {'num_workers': 1, 'pin_memory': True} if args.cuda else {} -# kwargs = {'pin_memory': True} if args.cuda else {} -# --gpu-dest/--gpu-source return CUDA/HIP tensors from __getitem__/add(); -# DataLoader worker processes can't safely own GPU state across a fork, so -# the default (forked-process) loader is forced to num_workers=0 whenever -# either is set -- use --loader=threaded for num_workers > 0 with GPU -# buffers instead. Separately, forking *at all* after MPI_Init is a known -# MPI hazard that hangs with DDStore even without GPU buffers -- fail fast -# instead of hanging silently. -if args.loader == "default" and args.num_workers > 1: - raise RuntimeError( - "--num-workers > 1 with --loader=default forks worker processes " - "after MPI_Init, which hangs with DDStore. Use --num-workers=1, or " - "--loader=threaded for real parallelism." - ) -# --loader=threaded + (--gpu-dest or --gpu-source) + --num-workers > 1 is -# confirmed unsafe by direct experiment: it silently corrupts data (not a -# crash -- wrong pixel values at a low but nonzero rate). Root cause: the -# GPU buffer pool in distdataset.py hands out slots via a per-SAMPLE -# round-robin index shared across threads; slot write order is determined -# by lock-acquisition order, not batch submission/consumption order, so a -# bounded in-flight *batch count* doesn't actually bound which physical -# slots can get overwritten while unread. A real fix needs get() to accept -# a caller-supplied destination buffer so ThreadDataLoader can allocate one -# dedicated region per in-flight batch (not per sample) -- not done yet. -# num_workers<=1 has no concurrent writers, so it's unaffected. -if args.loader == "threaded" and (args.gpu_dest or args.gpu_source) and args.num_workers > 1: - raise RuntimeError( - "--loader=threaded with --gpu-dest/--gpu-source and --num-workers > 1 " - "is known to silently corrupt data (confirmed by direct experiment -- " - "see README Known Limitations). Use --num-workers=1 with GPU buffers, " - "or drop --gpu-dest/--gpu-source for real multi-worker parallelism." - ) -if args.gpu_dest or args.gpu_source: - kwargs = {} -else: - kwargs = {"num_workers": args.num_workers} if args.num_workers > 0 else {} - -# ThreadDataLoader bounds in-flight batches to num_workers * prefetch_factor -# (default prefetch_factor=2, matching torch's own default); the GPU pool -# must hold at least that many batches' worth of samples or get() hands out -# a slice that's still "live" in an unconsumed batch -- see the pool-sizing -# comment in distdataset.py. +1 batch of headroom. -if args.loader == "threaded": - pool_size = (args.num_workers * 2 + 1) * args.batch_size -else: - pool_size = None - trainset = DistDataset( datasets.MNIST("data", train=True, download=True, transform=transforms.ToTensor()), "train", comm, device=device if args.gpu_dest else None, add_device=device if args.gpu_source else None, - pool_size=pool_size, ) # trainset = datasets.MNIST('data', train=True, download=True,transform=transforms.ToTensor()) comm.Barrier() sampler = torch.utils.data.distributed.DistributedSampler(trainset) -if args.loader == "threaded": +if args.num_workers > 0: if int(os.environ.get("DDSTORE_METHOD", "0")) == 0: - raise RuntimeError("--loader=threaded requires DDSTORE_METHOD=1 or 2") + raise RuntimeError("--num-workers > 0 requires DDSTORE_METHOD=1 or 2") train_loader = ThreadDataLoader( trainset, batch_size=args.batch_size, @@ -218,10 +156,8 @@ num_workers=args.num_workers, ) else: - # --num-workers applies here too (forked processes), unless --gpu-dest/ - # --gpu-source forced kwargs back to {} above (fork-safety guard). train_loader = torch.utils.data.DataLoader( - trainset, batch_size=args.batch_size, shuffle=False, **kwargs, sampler=sampler + trainset, batch_size=args.batch_size, shuffle=False, sampler=sampler ) print( @@ -231,9 +167,7 @@ testset = datasets.MNIST( "data", train=False, download=True, transform=transforms.ToTensor() ) -test_loader = torch.utils.data.DataLoader( - testset, batch_size=args.batch_size, shuffle=False, **kwargs -) +test_loader = torch.utils.data.DataLoader(testset, batch_size=args.batch_size, shuffle=False) # VAE_PROFILE=1 splits each epoch's wall time into "fetch" (time spent diff --git a/examples/vae/vae_extra_train.py b/examples/vae/vae_extra_train.py index d8bc0e0..4f79cce 100644 --- a/examples/vae/vae_extra_train.py +++ b/examples/vae/vae_extra_train.py @@ -99,26 +99,16 @@ "host->device copy. Requires DDSTORE_FABRIC=cxi and a " "libfabric-backed method (already the case for this script).", ) -parser.add_argument( - "--loader", - choices=["default", "threaded"], - default="default", - help="DataLoader implementation for the training set. 'threaded' uses " - "ThreadDataLoader (examples/vae/ddstore_dataloader.py), a " - "thread-pool-based loader that allows --num-workers > 1 (the default " - "loader forks worker processes, which hangs with DDStore above 1). " - "With --gpu-dest, --num-workers must stay <= 1 even with " - "--loader=threaded -- confirmed unsafe above that (silent data " - "corruption, not a crash; see README Known Limitations). " - "Default: default.", -) parser.add_argument( "--num-workers", type=int, - default=4, + default=0, metavar="N", - help="Number of worker threads for --loader=threaded. Ignored with " - "--loader=default (always 0 there). Default: 4.", + help="Number of DataLoader workers. 0 uses the default (forked-process) " + "DataLoader, single-threaded. > 0 switches to ThreadDataLoader " + "(examples/vae/ddstore_dataloader.py), with that many worker threads " + "-- forked processes can't safely own GPU state, so any " + "--num-workers > 0 goes through threads, never a fork. Default: 0.", ) args = parser.parse_args() args.cuda = not args.no_cuda and torch.cuda.is_available() @@ -152,44 +142,15 @@ model = torch.nn.parallel.DistributedDataParallel(model) optimizer = optim.Adam(model.parameters(), lr=1e-3) -kwargs = {} -# --gpu-dest returns CUDA/HIP tensors from __getitem__; DataLoader worker -# processes can't safely own GPU state across a fork, so this only works -# with num_workers=0 (today's default). Don't add num_workers>0 here -# without redesigning the buffer/collate strategy. -if args.gpu_dest: - assert kwargs.get("num_workers", 0) == 0 - -# --loader=threaded + --gpu-dest + --num-workers > 1 is confirmed unsafe by -# direct experiment (silent data corruption, not a crash) -- see the -# matching guard and comment in vae-ddp.py and the README Known Limitations -# entry for the root cause. num_workers<=1 has no concurrent writers. -if args.loader == "threaded" and args.gpu_dest and args.num_workers > 1: - raise RuntimeError( - "--loader=threaded with --gpu-dest and --num-workers > 1 is known " - "to silently corrupt data (see README Known Limitations). Use " - "--num-workers=1 with --gpu-dest, or drop --gpu-dest for real " - "multi-worker parallelism." - ) - -# See the matching comment in vae-ddp.py: ThreadDataLoader bounds in-flight -# batches to num_workers * prefetch_factor (default 2); the GPU pool must -# be sized to match or get() hands out a slice that's still live elsewhere. -if args.loader == "threaded": - pool_size = (args.num_workers * 2 + 1) * args.batch_size -else: - pool_size = None - trainset = DistDatasetReader( "train", args.handshake_dir, args.n_core, device=device if args.gpu_dest else None, - pool_size=pool_size, ) sampler = torch.utils.data.distributed.DistributedSampler(trainset) -if args.loader == "threaded": +if args.num_workers > 0: # DistDatasetReader always joins via method=2 (file-based handshake), # so no DDSTORE_METHOD=0 guard is needed here (unlike vae-ddp.py). train_loader = ThreadDataLoader( @@ -200,19 +161,18 @@ num_workers=args.num_workers, ) else: - # --num-workers is ignored here: kwargs never carries num_workers for - # the default loader (see the fork-safety guard above), so this is - # always the existing num_workers=0 behavior regardless of its value. train_loader = torch.utils.data.DataLoader( - trainset, batch_size=args.batch_size, shuffle=False, **kwargs, sampler=sampler + trainset, batch_size=args.batch_size, shuffle=False, sampler=sampler ) +print( + f"train_loader: {type(train_loader).__name__}, num_workers={train_loader.num_workers}" +) + testset = datasets.MNIST( "data", train=False, download=True, transform=transforms.ToTensor() ) -test_loader = torch.utils.data.DataLoader( - testset, batch_size=args.batch_size, shuffle=False, **kwargs -) +test_loader = torch.utils.data.DataLoader(testset, batch_size=args.batch_size, shuffle=False) def train(epoch): diff --git a/include/common.h b/include/common.h index 9abc28e..7a3d294 100644 --- a/include/common.h +++ b/include/common.h @@ -86,6 +86,24 @@ extern "C" int world_size; int rank; + + /* Serializes concurrent get() calls on THIS variable's + * fabric_state -- see fabric_state_lock_guard below. libfabric + * itself doesn't guarantee thread safety unless the domain is + * opened with FI_THREAD_SAFE (it isn't here; see + * init_fabric_hsn()'s FI_THREAD_DOMAIN hint and init_fabric_cxi()'s + * unconstrained NULL-hints query), and even then that would only + * cover libfabric's own objects, not the plain fields above + * (recv_data/recv_mr/recv_mr_base/recv_mr_reg_len) that read_from_ + * remote() reads and writes as a cache. + * Confirmed necessary by direct experiment: concurrent get() calls + * without this crashed with "double free or corruption". + * Zero-initialized by calloc() at every allocation site below, but + * explicitly pthread_mutex_init()'d right after each one anyway -- + * relying on zero-initialized pthread_mutex_t being equivalent to + * PTHREAD_MUTEX_INITIALIZER is a common but implementation-defined + * assumption; init explicitly instead. */ + pthread_mutex_t recv_lock; }; static bool is_local_mr_req(struct fabric_state *f) @@ -174,4 +192,24 @@ extern "C" #ifdef __cplusplus } + +/* RAII guard for struct fabric_state::recv_lock -- locks on construction, + * unlocks on destruction (including when leaving via an exception), so + * every exit path of the critical section it wraps is covered without + * having to hand-place lock/unlock calls on each one. See the comment on + * recv_lock above for what this protects and why. */ +struct fabric_state_lock_guard +{ + struct fabric_state *fs; + explicit fabric_state_lock_guard(struct fabric_state *fs) : fs(fs) + { + pthread_mutex_lock(&fs->recv_lock); + } + ~fabric_state_lock_guard() + { + pthread_mutex_unlock(&fs->recv_lock); + } + fabric_state_lock_guard(const fabric_state_lock_guard &) = delete; + fabric_state_lock_guard &operator=(const fabric_state_lock_guard &) = delete; +}; #endif diff --git a/include/ddstore.hpp b/include/ddstore.hpp index f2789a0..72451fa 100644 --- a/include/ddstore.hpp +++ b/include/ddstore.hpp @@ -116,6 +116,7 @@ class DDStore else if (this->method == 1) { fabric_state = (struct fabric_state *)calloc(1, sizeof(struct fabric_state)); + pthread_mutex_init(&fabric_state->recv_lock, NULL); fabric_state->send_data = (char *)base; fabric_state->send_data_len = nrows * disp * sizeof(T); fabric_state->send_hmem_iface = hmem_iface; @@ -135,6 +136,7 @@ class DDStore else if (this->method == 2) { fabric_state = (struct fabric_state *)calloc(1, sizeof(struct fabric_state)); + pthread_mutex_init(&fabric_state->recv_lock, NULL); fabric_state->send_data = (char *)base; fabric_state->send_data_len = nrows * disp * sizeof(T); fabric_state->send_hmem_iface = hmem_iface; @@ -288,6 +290,7 @@ class DDStore else if (this->method == 1) { fabric_state = (struct fabric_state *)calloc(1, sizeof(struct fabric_state)); + pthread_mutex_init(&fabric_state->recv_lock, NULL); fabric_state->send_data = (char *)base; fabric_state->send_data_len = nrows * disp * itemsize; fabric_state->world_size = this->comm_size; @@ -302,6 +305,7 @@ class DDStore else if (this->method == 2) { fabric_state = (struct fabric_state *)calloc(1, sizeof(struct fabric_state)); + pthread_mutex_init(&fabric_state->recv_lock, NULL); fabric_state->send_data = (char *)base; fabric_state->send_data_len = nrows * disp * itemsize; fabric_state->world_size = this->n_core; @@ -463,7 +467,13 @@ class DDStore } else if (this->method == 1 || this->method == 2) { - /* Methods 1 and 2 both use libfabric fi_read — same path. */ + /* Methods 1 and 2 both use libfabric fi_read — same path. + * Locked for the whole branch: the recv_data/recv_data_len/ + * recv_hmem_iface writes below are themselves racy across + * concurrent get() calls on this variable, not just the + * read_from_remote() call that follows them -- see recv_lock's + * comment in common.h. */ + fabric_state_lock_guard lock(varinfo.fabric_state); if (hmem_iface != 0 && !is_hmem_capable(varinfo.fabric_state)) throw std::runtime_error( "GPU destination buffer requires DDSTORE_FABRIC=cxi " @@ -479,68 +489,6 @@ class DDStore } } - /* Pre-register a GPU buffer as the recv MR for variable `name`. - * - * Call once with the full pool tensor before the first get() call. - * read_from_remote() reuses this registration for any recv_data pointer - * that falls within [buffer, buffer + nrows*disp*sizeof(T)), so all - * pool slices share one fi_mr_regattr call instead of one per slice. - * No-op for hmem_iface==0 (host path — MR registration is cheap there). */ - template - void prefetch_recv_mr(std::string name, T *buffer, long nrows, int disp, - int hmem_iface) - { - if (hmem_iface == 0) - return; /* host path: no pre-registration needed */ - - if (this->method != 1 && this->method != 2) - return; /* MPI_Win path has no fabric MR */ - - const VarInfo_t& varinfo = this->varlist.at(name); - struct fabric_state *fs = varinfo.fabric_state; - if (!fs) - return; - - /* Close any existing recv MR before registering the new region. */ - if (fs->recv_mr) - { - fi_close(&fs->recv_mr->fid); - fs->recv_mr = NULL; - } - - size_t reg_len = (size_t)nrows * disp * sizeof(T); - struct iovec iov = {(void *)buffer, reg_len}; - struct fi_mr_attr attr; - memset(&attr, 0, sizeof(attr)); - attr.mr_iov = &iov; - attr.iov_count = 1; - attr.access = FI_READ; - attr.iface = (enum fi_hmem_iface)hmem_iface; - attr.device.reserved = 0; - int mr_rc = fi_mr_regattr(fs->domain, &attr, 0, &fs->recv_mr); - if (mr_rc != FI_SUCCESS) - throw std::runtime_error( - std::string("prefetch_recv_mr fi_mr_regattr failed: ") + - fi_strerror(mr_rc)); - - if (is_mr_endpoint(fs)) - { - int rc = fi_mr_bind(fs->recv_mr, &fs->signal->fid, 0); - if (rc != FI_SUCCESS) - throw std::runtime_error( - std::string("prefetch_recv_mr fi_mr_bind failed: ") + - fi_strerror(rc)); - rc = fi_mr_enable(fs->recv_mr); - if (rc != FI_SUCCESS) - throw std::runtime_error( - std::string("prefetch_recv_mr fi_mr_enable failed: ") + - fi_strerror(rc)); - } - - /* Record the registered region so read_from_remote()'s range check hits. */ - fs->recv_mr_base = (char *)buffer; - fs->recv_mr_reg_len = reg_len; - } private: int method; // 0: MPI, 1: libfabric, 2: file-based handshake (libfabric transport) diff --git a/src/pyddstore.pyx b/src/pyddstore.pyx index 876073b..82a97c9 100644 --- a/src/pyddstore.pyx +++ b/src/pyddstore.pyx @@ -93,7 +93,6 @@ cdef extern from "ddstore.hpp": DDStore(int method, string handshake_dir, int n_core) void add[T](string name, T* buffer, long nrows, int disp, int hmem_iface) except + void get[T](string name, long start, long count, T* buffer, int hmem_iface) except + nogil - void prefetch_recv_mr[T](string name, T* buffer, long nrows, int disp, int hmem_iface) except + void epoch_begin() void epoch_end() void free() @@ -306,35 +305,6 @@ cdef class PyDDStore: self.c_ddstore.free() self._gpu_owned_buffers.clear() - def prefetch_recv_mr(self, str name, arr): - """Pre-register a GPU tensor as the recv MR for variable `name`. - - Call once with the full pool tensor (e.g. shape [POOL, disp]) before - the first get() call. read_from_remote() will reuse this registration - for any buffer pointer that falls within the registered region, so all - pool slices share one fi_mr_regattr call instead of one per slice. - No-op for host (non-CUDA) tensors. - """ - if not _is_cuda_tensor(arr): - return # host path: no pre-registration needed - _check_gpu_fabric_preconditions(self.method, "GPU recv pool") - assert arr.is_contiguous() - import torch - cdef size_t ptr = arr.data_ptr() - cdef long nrows = arr.shape[0] - cdef int disp = arr.numel() // arr.shape[0] - cdef int iface = _hmem_iface_for(arr) - if arr.dtype == torch.float32: - self.c_ddstore.prefetch_recv_mr(s2b(name), ptr, nrows, disp, iface) - elif arr.dtype == torch.float64: - self.c_ddstore.prefetch_recv_mr(s2b(name), ptr, nrows, disp, iface) - elif arr.dtype == torch.int32: - self.c_ddstore.prefetch_recv_mr(s2b(name), ptr, nrows, disp, iface) - elif arr.dtype == torch.int64: - self.c_ddstore.prefetch_recv_mr(s2b(name), ptr, nrows, disp, iface) - else: - raise NotImplementedError("prefetch_recv_mr: unsupported dtype %s" % arr.dtype) - def init(self, str name, long nrows, int disp, int itemsize=1): self.c_ddstore.init(s2b(name), nrows, disp, itemsize) diff --git a/test/test_gpu_rdma.py b/test/test_gpu_rdma.py index cf6cf9c..4c7ac5f 100644 --- a/test/test_gpu_rdma.py +++ b/test/test_gpu_rdma.py @@ -481,17 +481,20 @@ def test_add_from_gpu_tensor_gpu_dest_cxi_method2(comm, monkeypatch, tmp_path): def test_concurrent_get_thread_safety(comm, monkeypatch): """DDStore::get() releases the GIL for its blocking transfer (see the - `with nogil:` block in pyddstore.pyx) but has no internal locking of its - own -- concurrent calls from multiple threads on the same store race on - the CQ poll loop and the recv-MR region cache in common.cxx. - - examples/vae/distdataset.py's DistDataset/DistDatasetReader (used by - ThreadDataLoader, examples/vae/ddstore_dataloader.py, to parallelize - __getitem__ across threads) close this with a `threading.Lock` around - each get() call. This test reproduces that exact pattern directly - against PyDDStore: N threads issue concurrent get() calls serialized by - a lock, each checked against a trusted sequential reference -- it would - have caught the race if the lock were missing or misplaced. + `with nogil:` block in pyddstore.pyx), so multiple Python threads can + genuinely be inside DDStore::get() at the same time. Without + synchronization, concurrent calls on the same variable would race on + the CQ poll loop and the recv-MR region cache in common.cxx (confirmed + by direct experiment: disabling the protection crashed with "double + free or corruption"). + + That protection now lives inside DDStore itself -- a per-variable + `pthread_mutex_t` on `struct fabric_state` (include/common.h), taken + via the `fabric_state_lock_guard` RAII helper around get()'s critical + section in include/ddstore.hpp. This test calls PyDDStore.get() + directly from multiple threads with **no lock at the Python level at + all** -- it would catch a regression if + that C++-level protection were ever removed or narrowed. """ monkeypatch.setenv("DDSTORE_FABRIC", "cxi") rank = comm.Get_rank() @@ -506,20 +509,18 @@ def test_concurrent_get_thread_safety(comm, monkeypatch): comm.Barrier() store.epoch_begin() - lock = threading.Lock() results = {} errors = [] - def locked_get(target_rank): + def unlocked_get(target_rank): out = np.full((1, ncols), -999.0, dtype=np.float32) - with lock: - store.get("x", out, start=target_rank * nrows) + store.get("x", out, start=target_rank * nrows) results[target_rank] = out.copy() def worker(target_ranks): try: for target_rank in target_ranks: - locked_get(target_rank) + unlocked_get(target_rank) except Exception as exc: # noqa: BLE001 - surface any thread exception errors.append(exc) From 8b88a53e63b652d22b37a6416ed69bddd527d3b2 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 11:03:16 -0400 Subject: [PATCH 20/56] Simplify add()/get(), drop mpi4py.rc settings, document threading findings - pyddstore.pyx: add() and get() normalize GPU-tensor / numpy input to (ptr, itemsize, iface) and make one C++ call dispatched on item size (DDStore::add/get only use T via sizeof). Shared _check_dtype keeps the supported-dtype list. get() now releases the GIL on the GPU path too. The per-call torch.cuda.synchronize() in get() is kept: removing it made every --gpu-dest VAE run abort with HSA_STATUS_ERROR_EXCEPTION. - Remove mpi4py.rc.thread_level/threads from examples, tests and README: only the main thread calls MPI; SINGLE/FUNNELED/MULTIPLE gave identical results and timing on Frontier. - test_gpu_rdma.py: barrier before free() in the compute-kernel-read test (fast rank freed its endpoint while the peer was still reading -> PTLTE_NOT_FOUND, intermittent before this change too). - README: no-build-isolation install, lock necessity, GIL behaviour, worker-count timings, MPI thread level, HIP hardware-queue limits. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- README.md | 44 ++++++++-- examples/scripts/demo.py | 5 -- examples/scripts/test.py | 6 -- examples/vae/vae-ddp.py | 4 - examples/vae/vae_core_server.py | 4 - examples/vae/vae_extra_train.py | 4 - src/pyddstore.pyx | 149 ++++++++++++++------------------ test/conftest.py | 5 -- test/test_gpu_rdma.py | 3 + 9 files changed, 106 insertions(+), 118 deletions(-) diff --git a/README.md b/README.md index da5c247..68c7ff1 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,21 @@ CC=mpicc CXX=mpicxx pip install -e . CC=mpicc CXX=mpicxx pip install git+https://github.com/ORNL/DDStore.git ``` +To build against the packages already in the current environment (e.g. an `mpi4py` built against Cray MPICH) instead of letting pip fetch fresh build dependencies into an isolated build environment, disable build isolation: + +```bash +CC=cc CXX=CC pip install --no-build-isolation --no-deps -e . +``` + +If that fails with `ModuleNotFoundError: No module named 'distutils.msvccompiler'` (newer setuptools combined with an older system NumPy, e.g. `cray-python/3.11.7` on Frontier), point setuptools at the standard-library `distutils` for the build: + +```bash +SETUPTOOLS_USE_DISTUTILS=stdlib CC=cc CXX=CC pip install --no-build-isolation --no-deps -e . +``` + ## Quick Start ```python -import mpi4py -mpi4py.rc.thread_level = "serialized" -mpi4py.rc.threads = False - import numpy as np from mpi4py import MPI import pyddstore as dds @@ -264,6 +272,8 @@ See [test/test_gpu_rdma.py](test/test_gpu_rdma.py) for runnable examples coverin GPU kernels execute asynchronously: a compute kernel that just wrote to (or is about to read) a buffer may not have fully retired by the time that buffer is handed to RDMA. On at least one ROCm+CXI build, this produced a real, confirmed bug: the RDMA transfer reported success, but the destination buffer could still show stale, pre-transfer content, because the GPU's cache hadn't been reconciled with the external NIC write. Under sustained, real-workload conditions (not just short unit tests) this showed up as hard GPU faults, not just wrong data. To guard against this, `PyDDStore` always synchronizes the GPU device (`torch.cuda.synchronize()`) before registering a buffer for RDMA in `add()`/`get()`. This is a blocking, whole-device sync, which can serialize GPU compute against RDMA transfers when called at high frequency (e.g. once per sample in a data loader) — see the performance note below. +The sync in `get()` was re-checked by removing it: every `--gpu-dest` run of `vae-ddp.py` (`method=1`, `cxi`, 2 Frontier nodes, any `--num-workers` including 0) aborted during the first epoch with `HSA_STATUS_ERROR_EXCEPTION ... code: 0x1016` (GPU memory fault) on every rank, while host-path and `--gpu-source`-only runs were unaffected. With the sync restored the same runs complete normally. Keep it. + ## Known Limitations ### Multiple `srun` steps in one job (`method=2`, `cxi`) @@ -276,7 +286,17 @@ On Frontier, `method=2`'s separate core/extra `srun` steps within one job have s ### Thread-safety of concurrent `get()` calls -`DDStore::get()` releases the GIL for its blocking RDMA transfer (`nogil` in `src/pyddstore.pyx`) but has no internal locking of its own (confirmed by direct experiment: disabling the lock in `DistDataset`/`DistDatasetReader`'s `get()` crashed with `double free or corruption` under concurrent thread access). `ThreadDataLoader` (`examples/vae/ddstore_dataloader.py`) relies on that lock to serialize concurrent `get()` calls from its worker threads — don't remove it. +`get()` is safe to call from multiple threads. For `method=1`/`2`, `DDStore::get()` serializes calls on the same variable with a per-variable mutex (`fabric_state::recv_lock` in `include/common.h`, taken via `fabric_state_lock_guard`), held for the whole RDMA read. It is required: each variable's libfabric domain is opened as `FI_THREAD_DOMAIN` (the application must serialize access), and `get()` writes per-variable fields (`recv_data`, the cached receive MR) that concurrent calls would otherwise race on — without it, concurrent `get()` calls crashed with `double free or corruption`. Different variables have separate domains/endpoints/CQs and don't contend. `method=0` doesn't use the lock. + +`get()` releases the GIL for the transfer on both the host and the GPU-destination path. The GPU-destination path first does a whole-device `torch.cuda.synchronize()` per call (see [GPUDirect RDMA](#gpudirect-rdma-gpu-resident-buffers)), and that sync still dominates: releasing the GIL there made no measurable difference to `--gpu-dest` epoch time. `add()` keeps the GIL (one-time collective setup). + +### MPI thread level + +DDStore and the examples use mpi4py's default initialization (`MPI_Init_thread` requesting `MPI_THREAD_MULTIPLE`); no `mpi4py.rc` settings are needed. Only the main thread calls MPI — `add()`/`init()`/`join()` at setup, plus `epoch_begin()`/`epoch_end()` and `get()` for `method=0` — while `ThreadDataLoader` worker threads only call `get()` with `method=1`/`2`, which makes no MPI calls. So `MPI_THREAD_FUNNELED` is the minimum strictly required; the earlier `mpi4py.rc.threads = False` (which yields `MPI_THREAD_SINGLE`, technically wrong once worker threads exist) was removed. On Frontier (Cray MPICH, 2 nodes × 8 ranks), `vae-ddp.py` and the pytest suites gave identical results and timing with `SINGLE`, `FUNNELED` and `MULTIPLE`. If you call MPI from your own worker threads with `method=0`, keep the default `MULTIPLE`. + +### HIP streams and hardware queues (AMD/ROCm) + +HIP maps streams onto a small pool of hardware queues per GPU per process — `GPU_MAX_HW_QUEUES`, 4 by default — and streams beyond that share a queue round-robin. Work in a shared queue runs in order, so a sync on a "separate" stream can still wait behind another stream's kernels. Measured on Frontier (ROCm 7.2): with the default stream kept busy, 12 of 16 new streams were independent of it by default (every 4th collided, including the first one created), 14 of 16 with `GPU_MAX_HW_QUEUES=8`, 15 of 16 with `16`. If you give data-loading threads their own streams, raise `GPU_MAX_HW_QUEUES` and remember the training stream and RCCL already occupy queues. ### GPU-to-GPU RDMA performance on AMD/ROCm @@ -322,7 +342,19 @@ mpirun -n 4 python examples/vae/vae-ddp.py DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --gpu-dest --gpu-source ``` -`--num-workers` (default 0) controls the training `DataLoader`'s parallelism. `0` uses PyTorch's normal `DataLoader`, single-threaded (no forked worker processes at all, so no MPI-after-`MPI_Init`-fork hazard). Any `--num-workers > 0` switches to [examples/vae/ddstore_dataloader.py](examples/vae/ddstore_dataloader.py)'s `ThreadDataLoader` instead — real threads, no fork, so it's safe together with `--gpu-dest`/`--gpu-source` too. The applied loader and worker count are printed at startup: `train_loader: DataLoader, num_workers=N` or `train_loader: ThreadDataLoader, num_workers=N`. `ThreadDataLoader` requires `DDSTORE_METHOD` 1 or 2 (libfabric) in `vae-ddp.py`. Concurrent `get()` calls from multiple threads are serialized internally by a lock in `DistDataset`/`DistDatasetReader` (the underlying RDMA transfer has no locking of its own; confirmed necessary by direct experiment — see Known Limitations), so threads gain overlap on everything except the RDMA call itself. `vae_extra_train.py` has the same `--num-workers` flag and behavior: +`--num-workers` (default 0) controls the training `DataLoader`'s parallelism. `0` uses PyTorch's normal `DataLoader`, single-threaded (no forked worker processes at all, so no MPI-after-`MPI_Init`-fork hazard). Any `--num-workers > 0` switches to [examples/vae/ddstore_dataloader.py](examples/vae/ddstore_dataloader.py)'s `ThreadDataLoader` instead — real threads, no fork, so it's safe together with `--gpu-dest`/`--gpu-source` too. The applied loader and worker count are printed at startup: `train_loader: DataLoader, num_workers=N` or `train_loader: ThreadDataLoader, num_workers=N`. `ThreadDataLoader` requires `DDSTORE_METHOD` 1 or 2 (libfabric) in `vae-ddp.py`. Concurrent `get()` calls from multiple threads are serialized inside `DDStore::get()` by a per-variable lock (see [Thread-safety](#thread-safety-of-concurrent-get-calls)), so threads gain overlap on everything except the RDMA call itself. `vae_extra_train.py` has the same `--num-workers` flag and behavior. + +Measured with `vae-ddp.py`, `method=1`, `cxi`, 2 Frontier nodes × 8 ranks, `VAE_PROFILE=1`, average per-epoch time over epochs 2–8 (epochs are short, ~0.2 s, so treat as trends): + +| `--num-workers` | host path: fetch / total (s) | `--gpu-dest --gpu-source`: fetch / total (s) | +|---|---|---| +| 0 (`DataLoader`) | 0.11 / 0.22 | 0.15 / 0.25 | +| 1 | 0.02 / 0.14–0.16 | 0.09 / 0.26 | +| 2 | 0.03 / 0.16 | 0.11 / 0.31 | +| 4 | 0.04 / 0.17 | 0.12 / 0.30 | +| 8 | 0.08 / 0.21 | 0.15 / 0.35–0.38 | + +On the host path one worker thread (background prefetch) cuts epoch time ~30%; more workers make it steadily worse as they contend for the per-variable lock and the GIL. On the GPU path threading doesn't help, because of the per-call whole-device sync. All runs finished with the same final loss. Recommended: `--num-workers=1` on the host path, `0` on the GPU path. ```bash # ThreadDataLoader, 4 worker threads, host path (no GPU buffers) diff --git a/examples/scripts/demo.py b/examples/scripts/demo.py index bdc53fc..311d67e 100644 --- a/examples/scripts/demo.py +++ b/examples/scripts/demo.py @@ -1,8 +1,3 @@ -import mpi4py - -mpi4py.rc.thread_level = "serialized" -mpi4py.rc.threads = False - import numpy as np from mpi4py import MPI import argparse diff --git a/examples/scripts/test.py b/examples/scripts/test.py index 1019623..29c6d66 100644 --- a/examples/scripts/test.py +++ b/examples/scripts/test.py @@ -1,9 +1,3 @@ -import mpi4py - -## (2024/12): got an assert error on osx without the following line -mpi4py.rc.thread_level = "serialized" -mpi4py.rc.threads = False - import numpy as np from mpi4py import MPI import argparse diff --git a/examples/vae/vae-ddp.py b/examples/vae/vae-ddp.py index 7d22632..809437e 100644 --- a/examples/vae/vae-ddp.py +++ b/examples/vae/vae-ddp.py @@ -12,10 +12,6 @@ from torchvision.utils import save_image import torch.distributed as dist -import mpi4py - -mpi4py.rc.thread_level = "serialized" -mpi4py.rc.threads = False from mpi4py import MPI import distdataset diff --git a/examples/vae/vae_core_server.py b/examples/vae/vae_core_server.py index e285e6b..0b6b4b2 100644 --- a/examples/vae/vae_core_server.py +++ b/examples/vae/vae_core_server.py @@ -37,10 +37,6 @@ import torch from torchvision import datasets, transforms -import mpi4py - -mpi4py.rc.thread_level = "serialized" -mpi4py.rc.threads = False from mpi4py import MPI from distdataset import DistDataset diff --git a/examples/vae/vae_extra_train.py b/examples/vae/vae_extra_train.py index 4f79cce..8183383 100644 --- a/examples/vae/vae_extra_train.py +++ b/examples/vae/vae_extra_train.py @@ -36,10 +36,6 @@ from torchvision.utils import save_image import torch.distributed as dist -import mpi4py - -mpi4py.rc.thread_level = "serialized" -mpi4py.rc.threads = False from mpi4py import MPI from ddp_utils import setup_ddp, get_local_rank diff --git a/src/pyddstore.pyx b/src/pyddstore.pyx index 82a97c9..a799103 100644 --- a/src/pyddstore.pyx +++ b/src/pyddstore.pyx @@ -75,6 +75,21 @@ def _check_gpu_fabric_preconditions(int method, str what): "DDSTORE_FABRIC=cxi or pass a host (CPU) numpy array instead." % (what, provider)) +def _check_dtype(arr, bint is_gpu): + """Raise NotImplementedError unless arr's dtype is one DDStore supports. + add()/get() dispatch on item size alone (1/4/8 bytes), so this is what + keeps e.g. float16 or complex64 from slipping through on a size match. + """ + if is_gpu: + import torch + ok = arr.dtype in (torch.int32, torch.int64, torch.uint8, + torch.float32, torch.float64, torch.bool) + else: + ok = arr.dtype in (np.int32, np.int64, np.uint8, + np.float32, np.float64, np.bool_) + if not ok: + raise NotImplementedError("unsupported dtype: %s" % arr.dtype) + cdef extern from "ddstore.hpp": ctypedef struct VarInfo: string name @@ -175,10 +190,13 @@ cdef class PyDDStore: def add(self, str name, arr): cdef size_t ptr + cdef int itemsize cdef int iface - cdef long nrows + cdef long nrows = arr.shape[0] cdef int disp - if _is_cuda_tensor(arr): + cdef bint is_gpu = _is_cuda_tensor(arr) + _check_dtype(arr, is_gpu) + if is_gpu: _check_gpu_fabric_preconditions(self.method, "GPU source buffer") assert arr.is_contiguous() if name in self._gpu_owned_buffers: @@ -194,107 +212,70 @@ cdef class PyDDStore: # silently masked by stale GPU cache content from a preceding, # not-yet-retired write to the same memory. torch.cuda.synchronize(device=arr.device) - iface = _hmem_iface_for(arr) ptr = arr.data_ptr() - nrows = arr.shape[0] - disp = arr.numel() // arr.shape[0] - if arr.dtype == torch.int32: - self.c_ddstore.add(s2b(name), ptr, nrows, disp, iface) - elif arr.dtype == torch.int64: - self.c_ddstore.add(s2b(name), ptr, nrows, disp, iface) - elif arr.dtype == torch.uint8: - self.c_ddstore.add(s2b(name), ptr, nrows, disp, iface) - elif arr.dtype == torch.float32: - self.c_ddstore.add(s2b(name), ptr, nrows, disp, iface) - elif arr.dtype == torch.float64: - self.c_ddstore.add(s2b(name), ptr, nrows, disp, iface) - elif arr.dtype == torch.bool: - self.c_ddstore.add(s2b(name), ptr, nrows, disp, iface) - else: - raise NotImplementedError + itemsize = arr.element_size() + disp = arr.numel() // nrows + iface = _hmem_iface_for(arr) + else: + assert arr.flags.c_contiguous + ptr = arr.ctypes.data + itemsize = arr.itemsize + disp = arr.size // nrows + iface = 0 + + # DDStore::add() only uses T through sizeof(T), so dispatching on + # item size is enough. + cdef string cname = s2b(name) + if itemsize == 1: + self.c_ddstore.add(cname, ptr, nrows, disp, iface) + elif itemsize == 4: + self.c_ddstore.add(cname, ptr, nrows, disp, iface) + else: + self.c_ddstore.add(cname, ptr, nrows, disp, iface) + + if is_gpu: # Keepalive: DDStore now holds a raw pointer into arr's storage # with no copy and no C++-level refcounting -- see ddstore.hpp # add()'s lifetime-contract doc comment. Must outlive this # variable's registration; cleared in free()/__dealloc__. self._gpu_owned_buffers[name] = arr - return - - cdef np.ndarray np_arr = arr - assert np_arr.flags.c_contiguous - nrows = np_arr.shape[0] - disp = np_arr.size // np_arr.shape[0] - if np_arr.dtype == np.int32: - self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp, 0) - elif np_arr.dtype == np.int64: - self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp, 0) - elif np_arr.dtype == np.uint8: - self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp, 0) - elif np_arr.dtype == np.float32: - self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp, 0) - elif np_arr.dtype == np.float64: - self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp, 0) - elif np_arr.dtype == np.bool_: - self.c_ddstore.add(s2b(name), np_arr.data, nrows, disp, 0) - else: - raise NotImplementedError def get(self, str name, arr, long start=0): cdef long count = arr.shape[0] cdef size_t ptr + cdef int itemsize cdef int iface - if _is_cuda_tensor(arr): + cdef bint is_gpu = _is_cuda_tensor(arr) + _check_dtype(arr, is_gpu) + if is_gpu: _check_gpu_fabric_preconditions(self.method, "GPU destination buffer") assert arr.is_contiguous() import torch # See the matching comment in add() for what this guards against. torch.cuda.synchronize(device=arr.device) - iface = _hmem_iface_for(arr) ptr = arr.data_ptr() - if arr.dtype == torch.int32: - self.c_ddstore.get(s2b(name), start, count, ptr, iface) - elif arr.dtype == torch.int64: - self.c_ddstore.get(s2b(name), start, count, ptr, iface) - elif arr.dtype == torch.uint8: - self.c_ddstore.get(s2b(name), start, count, ptr, iface) - elif arr.dtype == torch.float32: - self.c_ddstore.get(s2b(name), start, count, ptr, iface) - elif arr.dtype == torch.float64: - self.c_ddstore.get(s2b(name), start, count, ptr, iface) - elif arr.dtype == torch.bool: - self.c_ddstore.get(s2b(name), start, count, ptr, iface) + itemsize = arr.element_size() + iface = _hmem_iface_for(arr) + else: + assert arr.flags.c_contiguous + ptr = arr.ctypes.data + itemsize = arr.itemsize + iface = 0 + + # DDStore::get() only uses T for its sizeof(T) == itemsize check + # and the pointer cast -- the transfer itself is a byte copy -- so + # dispatching on item size is enough. The read runs without the GIL, + # so other Python threads (e.g. a training loop while a background + # thread prefetches) keep running. Method 1/2 reads make no MPI calls. + cdef string cname = s2b(name) + with nogil: + if itemsize == 1: + self.c_ddstore.get(cname, start, count, ptr, iface) + elif itemsize == 4: + self.c_ddstore.get(cname, start, count, ptr, iface) else: - raise NotImplementedError - return + self.c_ddstore.get(cname, start, count, ptr, iface) - cdef np.ndarray np_arr = arr - assert np_arr.flags.c_contiguous - assert np_arr.shape[0] >= count - # The host read runs without the GIL, so other Python threads (e.g. a - # training loop while a background thread prefetches) keep running. - # Method 1/2 reads make no MPI calls. - cdef string cname = s2b(name) - cdef char* data = np_arr.data - if np_arr.dtype == np.int32: - with nogil: - self.c_ddstore.get(cname, start, count, data, 0) - elif np_arr.dtype == np.int64: - with nogil: - self.c_ddstore.get(cname, start, count, data, 0) - elif np_arr.dtype == np.uint8: - with nogil: - self.c_ddstore.get(cname, start, count, data, 0) - elif np_arr.dtype == np.float32: - with nogil: - self.c_ddstore.get(cname, start, count, data, 0) - elif np_arr.dtype == np.float64: - with nogil: - self.c_ddstore.get(cname, start, count, data, 0) - elif np_arr.dtype == np.bool_: - with nogil: - self.c_ddstore.get(cname, start, count, data, 0) - else: - raise NotImplementedError - def epoch_begin(self): self.c_ddstore.epoch_begin() diff --git a/test/conftest.py b/test/conftest.py index ac93c81..0112905 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,8 +1,3 @@ -import mpi4py - -mpi4py.rc.thread_level = "serialized" -mpi4py.rc.threads = False - import pytest from mpi4py import MPI diff --git a/test/test_gpu_rdma.py b/test/test_gpu_rdma.py index 4c7ac5f..7286be1 100644 --- a/test/test_gpu_rdma.py +++ b/test/test_gpu_rdma.py @@ -118,6 +118,9 @@ def test_get_into_gpu_tensor_cxi_compute_kernel_read(comm, monkeypatch): print( f"[rank {rank}] completed {n_iters} iterations without a HIP error", flush=True ) + # Wait for every rank's reads before tearing down: otherwise a fast rank + # can free its endpoint while a peer is still reading (PTLTE_NOT_FOUND). + comm.Barrier() store.free() From 987585a11cd1889eff2b10f621795d52aac41ba5 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 11:03:47 -0400 Subject: [PATCH 21/56] README: drop mpi4py.rc mentions from MPI thread level note Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 68c7ff1..3dc37fd 100644 --- a/README.md +++ b/README.md @@ -292,7 +292,7 @@ On Frontier, `method=2`'s separate core/extra `srun` steps within one job have s ### MPI thread level -DDStore and the examples use mpi4py's default initialization (`MPI_Init_thread` requesting `MPI_THREAD_MULTIPLE`); no `mpi4py.rc` settings are needed. Only the main thread calls MPI — `add()`/`init()`/`join()` at setup, plus `epoch_begin()`/`epoch_end()` and `get()` for `method=0` — while `ThreadDataLoader` worker threads only call `get()` with `method=1`/`2`, which makes no MPI calls. So `MPI_THREAD_FUNNELED` is the minimum strictly required; the earlier `mpi4py.rc.threads = False` (which yields `MPI_THREAD_SINGLE`, technically wrong once worker threads exist) was removed. On Frontier (Cray MPICH, 2 nodes × 8 ranks), `vae-ddp.py` and the pytest suites gave identical results and timing with `SINGLE`, `FUNNELED` and `MULTIPLE`. If you call MPI from your own worker threads with `method=0`, keep the default `MULTIPLE`. +DDStore and the examples use mpi4py's default initialization (`MPI_Init_thread` requesting `MPI_THREAD_MULTIPLE`). Only the main thread calls MPI — `add()`/`init()`/`join()` at setup, plus `epoch_begin()`/`epoch_end()` and `get()` for `method=0` — while `ThreadDataLoader` worker threads only call `get()` with `method=1`/`2`, which makes no MPI calls. So `MPI_THREAD_FUNNELED` is the minimum strictly required. On Frontier (Cray MPICH, 2 nodes × 8 ranks), `vae-ddp.py` and the pytest suites gave identical results and timing with `SINGLE`, `FUNNELED` and `MULTIPLE`. If you call MPI from your own worker threads with `method=0`, keep the default `MULTIPLE`. ### HIP streams and hardware queues (AMD/ROCm) From c82223059d1483af0148987478af4c07e0047045 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 11:17:56 -0400 Subject: [PATCH 22/56] ThreadDataLoader: collate in the worker thread fetch() now runs collate_fn (then pin_memory) in the worker instead of __next__ doing it on the training thread. Moves the per-batch stack off the training loop and makes pin_memory=True effective (previously it pinned per-sample tensors that collate then copied into an unpinned one). Frontier, vae-ddp.py method=1 cxi, 16 ranks, 1 worker, host path: fetch ~0.05 -> ~0.006 s/epoch, epoch 0.20 -> 0.15 s. Loss unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- examples/vae/ddstore_dataloader.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/vae/ddstore_dataloader.py b/examples/vae/ddstore_dataloader.py index 0c7b751..b7e5558 100644 --- a/examples/vae/ddstore_dataloader.py +++ b/examples/vae/ddstore_dataloader.py @@ -74,8 +74,13 @@ def worker_init(counter): return 0 @staticmethod - def fetch(dataset, ibatch, index, pin_memory=False): + def fetch(dataset, ibatch, index, collate_fn=None, pin_memory=False): + # Collate here, in the worker, before pinning: pinning per-sample + # tensors and collating afterwards would just torch.stack them into + # a new, unpinned tensor. batch = [dataset[i] for i in index] + if collate_fn is not None: + batch = collate_fn(batch) if pin_memory: batch = torch.utils.data._utils.pin_memory.pin_memory(batch) return (ibatch, batch) @@ -114,6 +119,7 @@ def _refill(self): self.dataset, self._next_batch_i, index, + collate_fn=self.collate_fn, pin_memory=self.pin_memory, ) self.fs.put(future) @@ -134,8 +140,6 @@ def __next__(self): ibatch, data = future.result() self._inflight -= 1 self._num_yielded += 1 - if self.collate_fn is not None: - data = self.collate_fn(data) return data def clean(self): From 80c3c65ce8c81d12fd938d9d0563203a495a427a Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 11:38:32 -0400 Subject: [PATCH 23/56] Fix stale comments/README; free() releases owned buffers; init join() mutex Docs (comments, docstrings, help text, README): - cxi is the native Slingshot provider on Frontier and Perlmutter (required for GPUDirect), not "Perlmutter only"; drop old branch-name references. - Thread-safety note: only hsn requests FI_THREAD_DOMAIN; the lock is needed for the shared per-variable recv fields. - Remove descriptions of the removed GPU buffer pool (recv-MR cache). - Fix update() signature, epoch no-op scope, demo/test.py backend, script --method/--num-workers help, get_local_rank docstring, stale test notes, missing script references; add post-collate-change timings. Code: - free(): track owns_base per variable and MPI_Free_mem the host buffer DDStore allocated in add()/init(), after closing the window/MRs that reference it; skip MPI calls after MPI_Finalize; destroy recv_lock. - join(): pthread_mutex_init the fabric_state lock like every other allocation site (get() locks it on the extra side too). - ThreadDataLoader: drop unused _dataset_fetcher. Tests (Frontier, cxi): test_single (method 0/1) 14/14, test_multirank 5/5, test_gpu_rdma 14/14. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- README.md | 37 +++++----- examples/vae/README.md | 2 +- examples/vae/ddp_utils.py | 8 +-- examples/vae/ddstore_dataloader.py | 8 --- examples/vae/distdataset.py | 2 +- examples/vae/script/job-vae-core-extra.sh | 6 +- examples/vae/script/job-vae-single.sh | 8 +-- examples/vae/vae-ddp.py | 7 +- examples/vae/vae_extra_train.py | 5 +- include/common.h | 28 ++++---- include/ddstore.hpp | 8 +++ src/common.cxx | 48 ++++--------- src/cpu_nic_map.py | 31 ++++----- src/ddstore.cxx | 83 +++++++++++++---------- src/pyddstore.pyx | 5 +- test/test_gpu_rdma.py | 22 +++--- test/test_multirank.py | 3 +- 17 files changed, 150 insertions(+), 161 deletions(-) diff --git a/README.md b/README.md index 3dc37fd..df34fdc 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ Register a NumPy array as a named variable. Each rank contributes its local shar --- -### `update(name, arr, offset=0)` +### `update(name, arr, offset)` Overwrite a region of the local shard for a variable registered with `init()`. Local operation — does not require epoch or barrier. @@ -170,13 +170,13 @@ Returns `(total_rows, disp, itemsize)` for a variable that has been `add()`-ed o ### `epoch_begin()` / `epoch_end()` -Open and close an MPI RMA access epoch (calls `MPI_Win_fence`). **Collective**. Required around `get()` calls when using `method=0`. No-op for `method=1`. +Open and close an MPI RMA access epoch (calls `MPI_Win_fence`). **Collective**. Required around `get()` calls when using `method=0`. No-op for `method=1`/`2`. --- ### `free()` -Release all MPI windows and allocated memory. Safe to call after `MPI_Finalize`. +Release every variable's MPI window (`method=0`) or libfabric endpoints and memory registrations (`method=1`/`2`), then the host buffer DDStore allocated for it in `add()`/`init()` (a GPU tensor passed to `add()` is the caller's and is not freed). Safe to call more than once. After `MPI_Finalize` the MPI window and buffer can no longer be released and are skipped. ## Backends @@ -190,14 +190,14 @@ Uses `fi_read` for true RDMA transfers over high-speed interconnects (Infiniband **`DDSTORE_FABRIC`** selects which libfabric provider to open, for `method=1`/`2`: -- `hsn` (default, unset) — Frontier: opens the `tcp;ofi_rxm` domain over Cray Slingshot. -- `cxi` — Perlmutter: opens the native `cxi` domain over Cray Slingshot. +- `hsn` (default, unset) — opens the `tcp;ofi_rxm` domain over Cray Slingshot (Frontier). +- `cxi` — opens the native `cxi` domain over Cray Slingshot (Frontier and Perlmutter; Perlmutter is CXI-only). Required for [GPUDirect RDMA](#gpudirect-rdma-gpu-resident-buffers), and the default in the Frontier job scripts. The two are independent code paths (not runtime auto-detection), so set this explicitly per system rather than relying on a guess: ```bash -export DDSTORE_FABRIC=hsn # Frontier (default; usually not needed) -export DDSTORE_FABRIC=cxi # Perlmutter +export DDSTORE_FABRIC=hsn # tcp;ofi_rxm (default) +export DDSTORE_FABRIC=cxi # native CXI: Perlmutter, or Frontier with GPUDirect ``` `PyDDStore` picks the network interface (`FABRIC_IFACE`) automatically for `method=1`/`2`, based on each rank's real CPU affinity (`os.sched_getaffinity`) — no changes needed in your code: @@ -243,7 +243,7 @@ Environment variables: | `DDSTORE_HANDSHAKE_DIR` | `./ddstore_hs` | Shared directory for handshake record files | | `DDSTORE_HANDSHAKE_TIMEOUT_S` | `300` | Seconds to poll for core records / a join before raising a timeout | | `DDSTORE_NIC_MAP` | unset | CPU→NIC map for `FABRIC_IFACE` auto-selection — see [libfabric RDMA](#libfabric-rdma-method1) above | -| `DDSTORE_FABRIC` | `hsn` | `hsn` (Frontier) or `cxi` (Perlmutter) — see [libfabric RDMA](#libfabric-rdma-method1) above | +| `DDSTORE_FABRIC` | `hsn` | `hsn` (`tcp;ofi_rxm`) or `cxi` (native CXI) — see [libfabric RDMA](#libfabric-rdma-method1) above | See [test/test_method2_core.py](test/test_method2_core.py) / [test/test_method2_extra.py](test/test_method2_extra.py) for a minimal runnable pair, and [examples/vae/vae_core_server.py](examples/vae/vae_core_server.py) / [examples/vae/vae_extra_train.py](examples/vae/vae_extra_train.py) for a full DDP training example using this split. @@ -282,11 +282,11 @@ On Frontier, `method=2`'s separate core/extra `srun` steps within one job have s ### `get()` has no GPU destination-buffer pool -`DistDataset`/`DistDatasetReader`'s `get()` allocates a fresh GPU tensor per call on the GPU path (`--gpu-dest`), rather than reusing a pre-allocated pool. An earlier pooled design (round-robin slices of one pre-registered buffer, to amortize `fi_mr_regattr` cost) was removed after it was confirmed by direct experiment to corrupt data under `ThreadDataLoader` with `--num-workers > 1`: multiple worker threads raced for pool slots at per-sample granularity, and bounding how many batches could be in flight at once didn't bound which physical slots got overwritten, since slot-write order was determined by lock-acquisition order, not batch order. Removing the pool removes that race entirely — each call's destination is privately owned, nothing to reuse. The tradeoff: a fresh `fi_mr_regattr` per call instead of one registration shared across many. Revisit with a pool later if that registration cost matters (`--num-workers > 0` is otherwise known to work per the next section). +`DistDataset`/`DistDatasetReader`'s `get()` allocates a fresh GPU tensor per call on the GPU path (`--gpu-dest`), rather than reusing a pre-allocated pool. An earlier pooled design (round-robin slices of one pre-registered buffer, to amortize `fi_mr_regattr` cost) was removed after it was confirmed by direct experiment to corrupt data under `ThreadDataLoader` with `--num-workers > 1`: multiple worker threads raced for pool slots at per-sample granularity, and bounding how many batches could be in flight at once didn't bound which physical slots got overwritten, since slot-write order was determined by lock-acquisition order, not batch order. Removing the pool removes that race entirely — each call's destination is privately owned, nothing to reuse. The tradeoff: a fresh `fi_mr_regattr` whenever the new tensor isn't inside the previously registered range (the receive-MR cache in `read_from_remote()` reuses the registration when PyTorch's allocator hands back the same block, which is common for same-shape `torch.empty()`), instead of one registration shared across many. Revisit with a pool later if that registration cost matters (`--num-workers > 0` is otherwise known to work per the next section). ### Thread-safety of concurrent `get()` calls -`get()` is safe to call from multiple threads. For `method=1`/`2`, `DDStore::get()` serializes calls on the same variable with a per-variable mutex (`fabric_state::recv_lock` in `include/common.h`, taken via `fabric_state_lock_guard`), held for the whole RDMA read. It is required: each variable's libfabric domain is opened as `FI_THREAD_DOMAIN` (the application must serialize access), and `get()` writes per-variable fields (`recv_data`, the cached receive MR) that concurrent calls would otherwise race on — without it, concurrent `get()` calls crashed with `double free or corruption`. Different variables have separate domains/endpoints/CQs and don't contend. `method=0` doesn't use the lock. +`get()` is safe to call from multiple threads. For `method=1`/`2`, `DDStore::get()` serializes calls on the same variable with a per-variable mutex (`fabric_state::recv_lock` in `include/common.h`, taken via `fabric_state_lock_guard`), held for the whole RDMA read. It is required: `get()` writes per-variable fields (`recv_data`, the cached receive MR) that concurrent calls would otherwise race on, and the libfabric objects aren't opened thread-safe either (`hsn` requests `FI_THREAD_DOMAIN`, i.e. the application serializes access; `cxi` takes the provider's default from a NULL-hints `fi_getinfo()`) — without the lock, concurrent `get()` calls crashed with `double free or corruption`. Different variables have separate domains/endpoints/CQs and don't contend. `method=0` doesn't use the lock. `get()` releases the GIL for the transfer on both the host and the GPU-destination path. The GPU-destination path first does a whole-device `torch.cuda.synchronize()` per call (see [GPUDirect RDMA](#gpudirect-rdma-gpu-resident-buffers)), and that sync still dominates: releasing the GIL there made no measurable difference to `--gpu-dest` epoch time. `add()` keeps the GIL (one-time collective setup). @@ -356,17 +356,19 @@ Measured with `vae-ddp.py`, `method=1`, `cxi`, 2 Frontier nodes × 8 ranks, `VAE On the host path one worker thread (background prefetch) cuts epoch time ~30%; more workers make it steadily worse as they contend for the per-variable lock and the GIL. On the GPU path threading doesn't help, because of the per-call whole-device sync. All runs finished with the same final loss. Recommended: `--num-workers=1` on the host path, `0` on the GPU path. +The table predates moving batch collation into the worker thread (`ThreadDataLoader.fetch()`); with that change, the host path measured fetch ≈ 0.006 s / total ≈ 0.15 s per epoch with 1 worker and 0.044 / 0.19 with 2 (GPU path with 1 worker unchanged at 0.09 / 0.25). + ```bash -# ThreadDataLoader, 4 worker threads, host path (no GPU buffers) -DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --num-workers=4 +# ThreadDataLoader, 1 worker thread, host path (no GPU buffers) -- the recommended setting +DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --num-workers=1 # ThreadDataLoader + GPUDirect together -DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --num-workers=4 --gpu-dest --gpu-source +DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --num-workers=1 --gpu-dest --gpu-source ``` ### Slurm job scripts (Frontier) -[examples/vae/script/job-vae-single.sh](examples/vae/script/job-vae-single.sh) and [examples/vae/script/job-vae-core-extra.sh](examples/vae/script/job-vae-core-extra.sh) wrap the same VAE example for `sbatch` on Frontier — `job-vae-single.sh` runs plain DDP (one `srun` step), `job-vae-core-extra.sh` runs the [method=2 core/extra split](#file-based-handshake-method2) (two independent `srun` steps). Both share `--method`/`--fabric`/`--gpudirect`, each with its own fixed default and none implicitly changing another: +[examples/vae/script/job-vae-single.sh](examples/vae/script/job-vae-single.sh) and [examples/vae/script/job-vae-core-extra.sh](examples/vae/script/job-vae-core-extra.sh) wrap the same VAE example for `sbatch` on Frontier — `job-vae-single.sh` runs plain DDP (one `srun` step), `job-vae-core-extra.sh` runs the [method=2 core/extra split](#file-based-handshake-method2) (two independent `srun` steps). Both take `--fabric`/`--gpudirect`; `job-vae-single.sh` also takes `--method` (the core/extra split is always `method=2` — `vae_core_server.py` sets it and the extra side always joins via method 2): ```bash sbatch examples/vae/script/job-vae-single.sh # method=0, cxi @@ -377,7 +379,7 @@ sbatch examples/vae/script/job-vae-core-extra.sh # method=2, c sbatch examples/vae/script/job-vae-core-extra.sh --gpudirect --layout=split-node --core-nnodes=2 ``` -Run `--help` on either script for the full option list. `job-vae-single.sh` additionally has `--num-workers` (default 0; `> 0` switches to `ThreadDataLoader`, see above). `job-vae-core-extra.sh` additionally has `--layout=colocate|split-node` and `--core-nnodes`. Note `--layout=colocate` together with `--gpudirect` will over-request GPUs per node (core and extra each ask for a full node's worth of GPUs on the same nodes) — use `--layout=split-node` when testing GPUDirect on `job-vae-core-extra.sh`. +Run `--help` on either script for the full option list. `job-vae-single.sh` additionally has `--method` and `--num-workers` (default 0; `> 0` switches to `ThreadDataLoader`, see above). `job-vae-core-extra.sh` additionally has `--layout=colocate|split-node` and `--core-nnodes`. Note `--layout=colocate` together with `--gpudirect` will over-request GPUs per node (core and extra each ask for a full node's worth of GPUs on the same nodes) — use `--layout=split-node` when testing GPUDirect on `job-vae-core-extra.sh`. ## Testing @@ -416,10 +418,10 @@ DDSTORE_FABRIC=cxi mpirun -n 2 python -m pytest test/test_gpu_rdma.py -v ### Integration scripts ```bash -# Basic functional test (MPI RMA) +# Basic functional test (libfabric, method=1) mpirun -n 4 python examples/scripts/demo.py -# Integration test with PyTorch DDP +# Integration test with PyTorch DDP (libfabric, method=1) mpirun -n 4 python examples/scripts/test.py ``` @@ -430,6 +432,7 @@ Optional arguments for `examples/scripts/demo.py` and `examples/scripts/test.py` | `--num` | `1048576` | Rows per rank | | `--dim` | `64` | Elements per row | | `--nbatch` | `32` | Number of random reads | +| `--gloo` / `--nccl` | `--gloo` | `test.py` only: `torch.distributed` backend | ### Method 2 (file-based handshake) diff --git a/examples/vae/README.md b/examples/vae/README.md index b903830..2c71631 100644 --- a/examples/vae/README.md +++ b/examples/vae/README.md @@ -2,5 +2,5 @@ Make sure the `pyddstore` module is installed correctly -To run this script and distrubute data across 4 processes: +To run this script and distribute data across 4 processes: `$ mpirun -n 4 python -u vae-ddp.py` diff --git a/examples/vae/ddp_utils.py b/examples/vae/ddp_utils.py index 12b4e69..946a2d0 100644 --- a/examples/vae/ddp_utils.py +++ b/examples/vae/ddp_utils.py @@ -38,9 +38,9 @@ def init_comm_size_and_rank(): def get_local_rank(rank): """ - Determine which GPU on the local node this rank should use. - Falls back to rank % device_count when no launcher-provided local rank - is available (e.g. plain mpirun without per-rank GPU visibility). + Determine which GPU on the local node this rank should use, from the + launcher's local rank (OMPI_COMM_WORLD_LOCAL_RANK or SLURM_LOCALID). + Returns 0 when neither is set; `rank` is currently unused. """ if os.getenv("OMPI_COMM_WORLD_LOCAL_RANK") is not None: return int(os.environ["OMPI_COMM_WORLD_LOCAL_RANK"]) @@ -103,7 +103,7 @@ def parse_slurm_nodelist(nodelist): def setup_ddp(): - """ "Initialize DDP""" + """Initialize DDP""" if os.getenv("DDSTORE_BACKEND") is not None: backend = os.environ["DDSTORE_BACKEND"] diff --git a/examples/vae/ddstore_dataloader.py b/examples/vae/ddstore_dataloader.py index b7e5558..c2ada2a 100644 --- a/examples/vae/ddstore_dataloader.py +++ b/examples/vae/ddstore_dataloader.py @@ -7,7 +7,6 @@ import torch from torch.utils.data import DataLoader -from torch.utils.data.dataloader import _DatasetKind logger = logging.getLogger(__name__) @@ -23,13 +22,6 @@ class ThreadDataLoader(DataLoader): def __init__(self, dataset, **kwargs): super().__init__(dataset, **kwargs) - self._dataset_fetcher = _DatasetKind.create_fetcher( - self._dataset_kind, - self.dataset, - self._auto_collation, - self.collate_fn, - self.drop_last, - ) self.fs = queue.Queue() # Persistent across epochs -- recreating the pool in every __iter__() diff --git a/examples/vae/distdataset.py b/examples/vae/distdataset.py index fae0cf8..bcc29af 100644 --- a/examples/vae/distdataset.py +++ b/examples/vae/distdataset.py @@ -78,7 +78,7 @@ def __init__( self.total_ns = len(data) print("init", self.total_ns) - # WHEN READY FOR WHOLE DATA SET CHANGE THE RANGE TO range(len(data)) + # This rank's contiguous share of the whole dataset. rx = list(nsplit(range(len(data)), self.ddstore_comm_size))[ self.ddstore_comm_rank ] diff --git a/examples/vae/script/job-vae-core-extra.sh b/examples/vae/script/job-vae-core-extra.sh index 5b85e08..479481c 100755 --- a/examples/vae/script/job-vae-core-extra.sh +++ b/examples/vae/script/job-vae-core-extra.sh @@ -16,8 +16,10 @@ Usage: $(basename "$0") [OPTIONS] Runs the core/extra VAE DDP split (default method=2). Options: - --method=N DDSTORE_METHOD: 0=MPI RMA, 1=libfabric, 2=file-based - handshake. Default: 2. + --method=N Exported as DDSTORE_METHOD, but the core/extra split is + always method=2 (file-based handshake): vae_core_server.py + sets it and vae_extra_train.py always joins via method 2. + Default: 2. --fabric=X DDSTORE_FABRIC: hsn or cxi. Default: cxi. --gpudirect Test GPUDirect RDMA. Requires --method=1 or 2 and --fabric=cxi. Also gives the core step a GPU per rank diff --git a/examples/vae/script/job-vae-single.sh b/examples/vae/script/job-vae-single.sh index 74fdee5..3ea6827 100755 --- a/examples/vae/script/job-vae-single.sh +++ b/examples/vae/script/job-vae-single.sh @@ -21,10 +21,10 @@ Options: --fabric=X DDSTORE_FABRIC: hsn or cxi. Default: cxi. --gpudirect Test GPUDirect RDMA. Requires --method=1 or 2 and --fabric=cxi. - --num-workers=N DataLoader workers. 0 uses the default (forked-process) - DataLoader, single-threaded. > 0 switches to - ThreadDataLoader with that many worker threads (requires - --method=1 or 2). Default: 0. + --num-workers=N DataLoader workers. 0 uses PyTorch's standard + DataLoader in the main process (no worker processes). + > 0 switches to ThreadDataLoader with that many worker + threads (requires --method=1 or 2). Default: 0. -h, --help Show this help message and exit. Examples: diff --git a/examples/vae/vae-ddp.py b/examples/vae/vae-ddp.py index 809437e..9db3936 100644 --- a/examples/vae/vae-ddp.py +++ b/examples/vae/vae-ddp.py @@ -59,7 +59,7 @@ help="Allocate the DDStore get() destination buffer directly on the " "training device (GPUDirect RDMA, Phase 1), skipping the " "host->device copy. Requires DDSTORE_METHOD in (1, 2) and " - "DDSTORE_FABRIC=cxi (e.g. DDSTORE_METHOD=2 as in run-vae.sh).", + "DDSTORE_FABRIC=cxi (e.g. job-vae-single.sh --method=1 --gpudirect).", ) parser.add_argument( "--gpu-source", @@ -76,8 +76,9 @@ type=int, default=0, metavar="N", - help="Number of DataLoader workers. 0 uses the default (forked-process) " - "DataLoader, single-threaded. > 0 switches to ThreadDataLoader " + help="Number of DataLoader workers. 0 uses PyTorch's standard " + "DataLoader in the main process (no worker processes, no fork). " + "> 0 switches to ThreadDataLoader " "(examples/vae/ddstore_dataloader.py), with that many worker threads " "-- forked processes can't safely own GPU state or MPI's live state, " "so any --num-workers > 0 goes through threads, never a fork. " diff --git a/examples/vae/vae_extra_train.py b/examples/vae/vae_extra_train.py index 8183383..64b8153 100644 --- a/examples/vae/vae_extra_train.py +++ b/examples/vae/vae_extra_train.py @@ -100,8 +100,9 @@ type=int, default=0, metavar="N", - help="Number of DataLoader workers. 0 uses the default (forked-process) " - "DataLoader, single-threaded. > 0 switches to ThreadDataLoader " + help="Number of DataLoader workers. 0 uses PyTorch's standard " + "DataLoader in the main process (no worker processes, no fork). " + "> 0 switches to ThreadDataLoader " "(examples/vae/ddstore_dataloader.py), with that many worker threads " "-- forked processes can't safely own GPU state, so any " "--num-workers > 0 goes through threads, never a fork. Default: 0.", diff --git a/include/common.h b/include/common.h index 7a3d294..64b7a59 100644 --- a/include/common.h +++ b/include/common.h @@ -71,11 +71,9 @@ extern "C" * pointer that falls within this range with recv_data_len bytes * fitting inside it can reuse recv_mr without re-registration. * - * This covers both the single-buffer case (recv_data == recv_mr_base, - * recv_data_len == recv_mr_reg_len) and the pool-slice case, where - * Python pre-allocates a (POOL, disp) tensor and hands get() a - * different row-slice each call. All slices share one MR because - * they all lie within the same allocation. + * Hits when the same buffer (or a sub-range of it) is passed again -- + * e.g. PyTorch's caching allocator returning the same block for a + * same-shape torch.empty() -- so get() doesn't re-register every call. * * Initialised to NULL/0 so the first call always registers. */ char *recv_mr_base; @@ -114,11 +112,10 @@ extern "C" /* CXI (and some other providers) use FI_MR_ENDPOINT: after fi_mr_reg the * MR must be bound to the endpoint and enabled before it can be used, and * the key is only valid after fi_mr_enable(). - * On Perlmutter, fi_getinfo with NULL hints returns mr_mode=0 even for - * CXI, so we detect by provider name instead of mr_mode flags. False - * (no-op) for every provider dev-file2 already supports (hsn/verbs/ - * gni/psm2), since none of those set mr_mode & FI_MR_ENDPOINT and none - * are named "cxi". */ + * With NULL hints fi_getinfo returns mr_mode=0 even for CXI (seen on + * Perlmutter), so we detect by provider name instead of mr_mode flags. + * False (no-op) for hsn/verbs/gni/psm2, since none of those set + * mr_mode & FI_MR_ENDPOINT and none are named "cxi". */ static bool is_mr_endpoint(struct fabric_state *f) { return (f->info->domain_attr->mr_mode & FI_MR_ENDPOINT) != 0 || @@ -129,16 +126,15 @@ extern "C" /* With FI_MR_VIRT_ADDR the fi_read remote addr is the virtual address. * CXI does NOT use virtual addresses — offset is 0-based from MR base. * - * NOTE: this is deliberately NOT a mr_mode bit check. dev-file2's - * init_fabric_hsn() sets mr_mode to the legacy FI_MR_BASIC sentinel, + * NOTE: this is deliberately NOT a mr_mode bit check. init_fabric_hsn() + * sets mr_mode to the legacy FI_MR_BASIC sentinel, * which on this system's libfabric (2.3.1) is bit 0 (value 1) — a * completely different bit than FI_MR_VIRT_ADDR (bit 4). A `mr_mode & * FI_MR_VIRT_ADDR` check would therefore silently resolve to false for * hsn, breaking address exchange for the already-proven path. Before - * this helper existed, dev-file2 unconditionally used the real pointer - * for every provider it supported (hsn/verbs/gni/psm2) — no virt-addr/ - * prov-key distinction existed at all — so preserve that unconditional - * behavior for anything that isn't cxi. */ + * cxi support, the real pointer was used unconditionally for every + * provider (hsn/verbs/gni/psm2), so preserve that for anything that + * isn't cxi. */ static bool is_virt_addr(struct fabric_state *f) { return !(f->info->fabric_attr->prov_name && diff --git a/include/ddstore.hpp b/include/ddstore.hpp index 72451fa..ef942de 100644 --- a/include/ddstore.hpp +++ b/include/ddstore.hpp @@ -21,6 +21,10 @@ struct VarInfo bool active; bool fence_active; void *base; + /* true if base came from MPI_Alloc_mem() in add()/init() and free() + * must release it; false for a caller-owned GPU buffer (add() with + * hmem_iface != 0) or a joined variable (base == NULL). */ + bool owns_base; struct fabric_state *fabric_state; }; typedef struct VarInfo VarInfo_t; @@ -228,6 +232,7 @@ class DDStore var.active = true; var.fence_active = false; var.base = base; + var.owns_base = (hmem_iface == 0); var.fabric_state = fabric_state; this->varlist.insert(std::pair(name, var)); return; /* lenlist already stored; skip the MPI_Allgather block below */ @@ -258,6 +263,7 @@ class DDStore var.active = true; var.fence_active = false; var.base = base; + var.owns_base = (hmem_iface == 0); var.fabric_state = fabric_state; this->varlist.insert(std::pair(name, var)); @@ -365,6 +371,7 @@ class DDStore var.active = true; var.fence_active = false; var.base = base; + var.owns_base = true; var.fabric_state = fabric_state; this->varlist.insert(std::pair(name, var)); return; @@ -395,6 +402,7 @@ class DDStore var.active = true; var.fence_active = false; var.base = base; + var.owns_base = true; var.fabric_state = fabric_state; this->varlist.insert(std::pair(name, var)); diff --git a/src/common.cxx b/src/common.cxx index 6d210ba..3e6a345 100644 --- a/src/common.cxx +++ b/src/common.cxx @@ -15,8 +15,8 @@ #include #include -/* hsn (tcp;ofi_rxm over Slingshot) path — Frontier. Unchanged from the - * already-proven dev-file2 implementation; only renamed (was init_fabric). */ +/* hsn (tcp;ofi_rxm over Slingshot) path — DDSTORE_FABRIC=hsn (default). + * The original init_fabric(), unchanged apart from the rename. */ static void init_fabric_hsn(struct fabric_state *fabric) { struct fi_info *hints, *info, *originfo, *useinfo; @@ -257,9 +257,10 @@ static void init_fabric_hsn(struct fabric_state *fabric) fi_freeinfo(originfo); } -/* cxi path — Perlmutter. Ported from dev-cxi@7cb110b (confirmed working on - * Perlmutter). Kept structurally separate from init_fabric_hsn() above - * rather than unified, so cxi support cannot change hsn's behavior. */ +/* cxi (native Slingshot) path — DDSTORE_FABRIC=cxi. First written for + * Perlmutter; also used on Frontier, where it is required for GPUDirect + * RDMA. Kept structurally separate from init_fabric_hsn() above rather + * than unified, so cxi support cannot change hsn's behavior. */ static void init_fabric_cxi(struct fabric_state *fabric) { struct fi_info *info, *originfo, *useinfo; @@ -667,37 +668,12 @@ int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset return 1; } - /* Cache the recv MR by registered region rather than exact pointer. - * - * The cache hits when recv_data falls within the previously registered - * region [recv_mr_base, recv_mr_base + recv_mr_reg_len) AND the transfer - * length fits within it. This covers two cases: - * - * 1. Single buffer reused across batches (recv_data == recv_mr_base): - * exact match, always hits after first call. - * - * 2. Pool of slices from one contiguous allocation: Python pre-allocates - * a (POOL, disp) tensor; each __getitem__ call takes a different row - * slice. All slices share the same base allocation, so their pointers - * lie within [recv_mr_base, recv_mr_base + recv_mr_reg_len). On the - * first call we register the full allocation (recv_data_len covers one - * row; we extend the registration to the full pool via the stored - * recv_mr_reg_len). Actually for pool slices the caller sets - * recv_data_len to the row size, and recv_data to a row pointer -- - * we register only that row on the first call, then on subsequent - * calls we check whether the new pointer falls in the same region. - * Since pool rows are contiguous and fixed-size, consecutive pointers - * differ by exactly recv_data_len, so they are NOT in the same region - * unless we register the whole pool. - * - * To handle the pool case efficiently, the Python side now passes the - * full pool allocation as recv_data/recv_data_len on the first call - * (via the pool base pointer and total size), and the slices are handled - * by the range check below. - * - * Simpler model: register recv_data..recv_data+recv_data_len on miss, - * and on subsequent calls re-use the MR if the new buffer is a subset of - * the cached region. */ + /* Cache the recv MR by registered region rather than exact pointer: + * register recv_data..recv_data+recv_data_len on a miss, and reuse the + * MR while later buffers lie inside [recv_mr_base, recv_mr_base + + * recv_mr_reg_len). Hits when the caller passes the same buffer again -- + * e.g. PyTorch's caching allocator returning the same block for a + * same-shape torch.empty() -- so each get() needn't re-register. */ char *cur_base = fabric_state->recv_data; size_t cur_len = fabric_state->recv_data_len; bool in_cached_region = diff --git a/src/cpu_nic_map.py b/src/cpu_nic_map.py index 4682b2c..7446b1f 100644 --- a/src/cpu_nic_map.py +++ b/src/cpu_nic_map.py @@ -12,17 +12,17 @@ (src/pyddstore.pyx) for method=1/2 to set FABRIC_IFACE if not already set. Kernel NIC names are always hsnN under /sys/class/net, on Frontier and -Perlmutter alike -- there is no per-system glob pattern to choose. Perlmutter -just exposes each hsnN NIC's libfabric domain under a different name (cxiN); -pass --fabric cxi (or set DDSTORE_FABRIC=cxi) to see that -translated name instead of the raw kernel one. +Perlmutter alike -- there is no per-system glob pattern to choose. The cxi +libfabric provider names each hsnN NIC's domain cxiN instead; pass --fabric +cxi (or set DDSTORE_FABRIC=cxi) to see that translated name instead of the +raw kernel one. CLI: cpu_nic_map.py print the full CPU -> nearest HSN NIC table cpu_nic_map.py 42 print only the nearest HSN NIC for cpu 42 cpu_nic_map.py --env print the compact DDSTORE_NIC_MAP env-var value cpu_nic_map.py --allocated print this process's allocated CPUs and nearest NIC(s) - cpu_nic_map.py --env --fabric cxi show the Perlmutter-translated (cxiN) names + cpu_nic_map.py --env --fabric cxi show the cxi-provider (cxiN) names export DDSTORE_NIC_MAP=$(python3 cpu_nic_map.py --env) srun --threads-per-core=2 -n8 -c14 python cpu_nic_map.py --allocated @@ -134,8 +134,8 @@ def build_map(pattern): for nic, part in zip(sorted(group), partitions): nic_closest[nic] = part - # multiple NICs can share a NUMA node; pick the numerically/PCI-closest - # NIC as the "same-NUMA fallback owner" for cores not exactly local to any NIC + # multiple NICs can share a NUMA node; the first one by name is the + # "same-NUMA fallback owner" for cores not exactly local to any NIC numa_to_nics = {} for n, numa in nic_numa.items(): numa_to_nics.setdefault(numa, []).append(n) @@ -172,8 +172,8 @@ def compress_ranges(values): def translate_iface(name, provider="hsn"): """Translate a kernel NIC name (hsnN) to the libfabric domain name for - `provider`. 'cxi' -> cxiN (Perlmutter exposes hsnN's libfabric domain - under this name); 'hsn' (default) or anything else -> unchanged.""" + `provider`. 'cxi' -> cxiN (the cxi provider's domain name for hsnN); + 'hsn' (default) or anything else -> unchanged.""" if provider == "cxi": m = re.match(r"hsn(\d+)$", name) if m: @@ -261,11 +261,10 @@ def select_fabric_iface(nic_map=None): than the process environment. DDSTORE_FABRIC selects hsn (default) or cxi: - - hsn: Frontier's unchanged, already-proven behavior -- the kernel NIC - name (hsnN) is used as-is. - - cxi: Perlmutter's behavior, ported from dev-cxi@7cb110b. The kernel - NIC names are hsn0-hsn3 there too, but libfabric only exposes them - as cxi0-cxi3, so the result is translated hsnN -> cxiN. Also adds a + - hsn: the kernel NIC name (hsnN) is used as-is (tcp;ofi_rxm). + - cxi: the kernel NIC names are hsn0-hsn3, but the cxi provider + exposes them as cxi0-cxi3, so the result is translated hsnN -> cxiN + (Frontier and Perlmutter alike). Also adds a SLURM_LOCALID round-robin fallback for when hwloc can't map this rank's CPU affinity to a NIC (common inside srun tasks with limited PCI visibility). @@ -330,7 +329,7 @@ def main(): " cpu_nic_map.py 42 print only the nearest HSN NIC for cpu 42\n" " export DDSTORE_NIC_MAP=$(cpu_nic_map.py --env) compute once, share via env\n" " srun ... python cpu_nic_map.py --allocated show this task's allocated CPUs + nearest NIC(s)\n" - " cpu_nic_map.py --env --fabric cxi show the Perlmutter-translated (cxiN) names\n" + " cpu_nic_map.py --env --fabric cxi show the cxi-provider (cxiN) names\n" ), ) parser.add_argument( @@ -345,7 +344,7 @@ def main(): choices=["hsn", "cxi"], help="translate printed NIC names to this fabric's libfabric " "domain name (default: $DDSTORE_FABRIC, or hsn if unset) " - "-- hsn: unchanged (e.g. hsn0); cxi: hsnN -> cxiN (Perlmutter)", + "-- hsn: unchanged (e.g. hsn0); cxi: hsnN -> cxiN", ) parser.add_argument( "--env", diff --git a/src/ddstore.cxx b/src/ddstore.cxx index 5e91ae2..4703c12 100644 --- a/src/ddstore.cxx +++ b/src/ddstore.cxx @@ -118,8 +118,9 @@ void DDStore::epoch_end() /* -------------------------------------------------------------------------- * join() — extra member: discover a variable published by core members. * - * Calls handshake_join() which polls for all CoreRecord files, then - * populates a fabric_state and builds the lenlist for get() calls. + * Calls handshake_join(), which polls for the combined {name}.bin record + * file written by core rank 0, then populates a fabric_state and builds + * the lenlist for get() calls. * -------------------------------------------------------------------------- */ void DDStore::join(std::string name) { @@ -130,6 +131,7 @@ void DDStore::join(std::string name) struct fabric_state *fs = (struct fabric_state *)calloc(1, sizeof(struct fabric_state)); + pthread_mutex_init(&fs->recv_lock, NULL); fs->world_size = this->n_core; fs->rank = -1; /* extra members have no core rank */ @@ -137,9 +139,9 @@ void DDStore::join(std::string name) if (!fs->info) throw std::runtime_error("init_fabric failed for extra member"); - /* Extra member has no send buffer to register as MR — set a dummy - * zero-length registration so handshake_join doesn't need special-casing. - * We only need fi_read capability, not FI_REMOTE_READ on our side. */ + /* Extra member has no send buffer, so nothing is registered for remote + * access (mr stays NULL, key 0). It only issues fi_read()s; get() + * registers each destination buffer as usual in read_from_remote(). */ fs->send_data = NULL; fs->send_data_len = 0; fs->mr = NULL; @@ -171,51 +173,58 @@ void DDStore::join(std::string name) var.active = true; var.fence_active = false; var.base = NULL; /* extra member owns no data */ + var.owns_base = false; var.fabric_state = fs; this->varlist.insert(std::pair(name, var)); } /* -------------------------------------------------------------------------- * free() — release all resources. + * + * Per variable: the MPI window (method 0) or the libfabric objects (methods + * 1/2) first, since they reference the buffer, then the buffer itself if + * DDStore allocated it (owns_base). MPI_Win_free/MPI_Free_mem are skipped + * after MPI_Finalize (no longer callable). Idempotent via `active`. * -------------------------------------------------------------------------- */ void DDStore::free() { - int flag; - MPI_Finalized(&flag); - if (!this->method && !flag) + int finalized; + MPI_Finalized(&finalized); + for (auto &x : this->varlist) { - for (auto &x : this->varlist) + VarInfo_t &var = x.second; + if (!var.active) + continue; + + if (this->method == 0) { - if (x.second.active) - { - MPI_Win_free(&x.second.win); - } - x.second.active = false; + if (!finalized) + MPI_Win_free(&var.win); } - } - else if (this->method == 1 || this->method == 2) - { - for (auto &x : this->varlist) + else if (var.fabric_state) { - if (x.second.active && x.second.fabric_state) - { - struct fabric_state *fs = x.second.fabric_state; - if (fs->recv_mr) fi_close(&fs->recv_mr->fid); - if (fs->mr) fi_close(&fs->mr->fid); - if (fs->signal) fi_close(&fs->signal->fid); - if (fs->cq_signal) fi_close(&fs->cq_signal->fid); - if (fs->av) fi_close(&fs->av->fid); - if (fs->domain) fi_close(&fs->domain->fid); - if (fs->fabric) fi_close(&fs->fabric->fid); - if (fs->info) fi_freeinfo(fs->info); - if (fs->ctx) ::free(fs->ctx); - ::free(fs->comm_partner); - ::free(fs->remote_key); - ::free(fs->remote_address); - ::free(fs); - x.second.fabric_state = NULL; - } - x.second.active = false; + struct fabric_state *fs = var.fabric_state; + if (fs->recv_mr) fi_close(&fs->recv_mr->fid); + if (fs->mr) fi_close(&fs->mr->fid); + if (fs->signal) fi_close(&fs->signal->fid); + if (fs->cq_signal) fi_close(&fs->cq_signal->fid); + if (fs->av) fi_close(&fs->av->fid); + if (fs->domain) fi_close(&fs->domain->fid); + if (fs->fabric) fi_close(&fs->fabric->fid); + if (fs->info) fi_freeinfo(fs->info); + if (fs->ctx) ::free(fs->ctx); + ::free(fs->comm_partner); + ::free(fs->remote_key); + ::free(fs->remote_address); + pthread_mutex_destroy(&fs->recv_lock); + ::free(fs); + var.fabric_state = NULL; } + + if (var.owns_base && var.base && !finalized) + MPI_Free_mem(var.base); + var.base = NULL; + var.owns_base = false; + var.active = false; } } diff --git a/src/pyddstore.pyx b/src/pyddstore.pyx index a799103..2b36b83 100644 --- a/src/pyddstore.pyx +++ b/src/pyddstore.pyx @@ -292,8 +292,9 @@ cdef class PyDDStore: def update(self, str name, arr, long offset): if _is_cuda_tensor(arr): raise NotImplementedError( - "GPU source buffers are not yet supported by update() " - "(GPU-to-GPU is a future phase); pass arr.cpu().numpy() instead") + "update() only supports host (numpy) buffers -- the " + "init()/update() path is host-only; pass arr.cpu().numpy() " + "instead, or add() the GPU tensor directly") cdef np.ndarray np_arr = arr assert np_arr.flags.c_contiguous cdef long nrows = np_arr.shape[0] diff --git a/test/test_gpu_rdma.py b/test/test_gpu_rdma.py index 7286be1..efc1fda 100644 --- a/test/test_gpu_rdma.py +++ b/test/test_gpu_rdma.py @@ -1,9 +1,10 @@ """ -GPUDirect RDMA tests (Phase 1: host source -> GPU destination). +GPUDirect RDMA tests: GPU destination (Phase 1), GPU source (Phase 2), both, +negative paths, and concurrent get() from multiple threads. Positive path — run with: DDSTORE_FABRIC=cxi mpirun -n 2 pytest test/test_gpu_rdma.py -v -requires a live cxi/Slingshot fabric and at least one visible GPU per rank -(see run-test-gpu.sh). Negative-path tests need neither and always run. +requires a live cxi/Slingshot fabric and at least one visible GPU per rank. +Negative-path tests need neither and always run. """ import threading @@ -130,11 +131,12 @@ def test_get_into_gpu_tensor_cxi_matrix(comm, monkeypatch): allocation method (torch.empty, uninitialized vs torch.full, poisoned via a GPU compute-kernel write) crossed with readback method (.cpu() DMA copy vs GPU compute-kernel read + torch.cuda.synchronize()). - test_get_into_gpu_tensor_cxi (poison + .cpu()) reliably fails on - Frontier; test_get_into_gpu_tensor_cxi_compute_kernel_read (empty + - compute-kernel read) just passed cleanly, 200/200 iterations, diff=0.0. - This runs all 4 combinations back-to-back in one job to find out which - axis (allocation vs readback) actually matters, rather than guessing. + Written when test_get_into_gpu_tensor_cxi (poison + .cpu()) reliably + failed on Frontier while test_get_into_gpu_tensor_cxi_compute_kernel_read + (empty + compute-kernel read) passed; runs all 4 combinations to show + which axis (allocation vs readback) matters. Both now pass, since + PyDDStore.get() synchronizes the device before every GPU-destination + transfer (see test_get_into_gpu_tensor_cxi_sync_before_get). """ monkeypatch.setenv("DDSTORE_FABRIC", "cxi") rank = comm.Get_rank() @@ -356,9 +358,7 @@ def test_gpu_source_rejected_on_hsn(comm, monkeypatch): # Phase 2: GPU-resident producer (add()) -- host/GPU destination, over cxi # --------------------------------------------------------------------------- # -# Frontier will still fail these (the open, unrelated OLCF ROCm+CXI driver -# issue affects get(), which every one of these tests also exercises to -# check correctness) -- validation target is Perlmutter, same as Phase 1. +# These pass on Frontier (ROCm + cxi). @gpu_required diff --git a/test/test_multirank.py b/test/test_multirank.py index ad89109..e62efcc 100644 --- a/test/test_multirank.py +++ b/test/test_multirank.py @@ -2,7 +2,8 @@ Multi-rank tests — run with: mpirun -n 4 pytest test/test_multirank.py -v Each rank stores a distinct value; tests verify cross-rank remote reads. -Requires at least 2 ranks; some tests require exactly 4. +Requires at least 2 ranks; the ddstore_width test needs at least 4 +(skipped otherwise). """ import numpy as np From 533666cc5433d012c85b93618f563ba2c7bba0b6 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 13:37:59 -0400 Subject: [PATCH 24/56] job-vae-core-extra.sh: fix cross-step RDMA on Frontier, add --num-workers On Slingshot every srun step gets its own VNI, and endpoints only talk on the same VNI. The core/extra split therefore needs: - #SBATCH --network=single_node_vni,job_vni: job_vni adds a job-wide VNI to each step (SLINGSHOT_VNIS=,); single_node_vni gives single-node steps a CXI service at all (else fi_domain fails with -38). - a per-task wrapper keeping only the job VNI (last entry), since the cxi provider uses only the first VNI listed (else VNI_NOT_FOUND). Verified on Frontier, method=2, cxi, split-node: 1 core + 1 extra node and 2 core + 4 extra nodes both train 3 epochs and shut down cleanly; removing either piece reproduces the failure. Colocate (both steps on the same nodes) still fails to launch the second step with job_vni ("Error configuring interconnect"); without job_vni the steps can't share a VNI. Also add --num-workers (extra step) and document the fix in README. Correction to 80c3c65's message: test_single/test_multirank always use method=0 (DDSTORE_METHOD is ignored), so its "method 0/1" results were method 0 only; methods 1/2 were covered by test_gpu_rdma and VAE runs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- README.md | 13 ++++++++---- examples/vae/script/job-vae-core-extra.sh | 25 ++++++++++++++++++++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index df34fdc..1a8b5b6 100644 --- a/README.md +++ b/README.md @@ -276,9 +276,14 @@ The sync in `get()` was re-checked by removing it: every `--gpu-dest` run of `va ## Known Limitations -### Multiple `srun` steps in one job (`method=2`, `cxi`) +### Multiple `srun` steps in one job (`method=2`, `cxi`, Frontier) -On Frontier, `method=2`'s separate core/extra `srun` steps within one job have shown intermittent RDMA connectivity issues between steps, and a later step in a job with several sequential steps can occasionally fail to start. The `--network=job_vni`/`single_node_vni` `sbatch` options have not reliably fixed this. The cause isn't fully understood. If you hit this, use fewer sequential steps per job, or use `method=1` (single job step), which doesn't have this issue. +Core and extra run as separate `srun` steps, and on Slingshot every step gets its own VNI (network isolation ID); two endpoints can only communicate on the same VNI. Two things are needed, both handled by [job-vae-core-extra.sh](examples/vae/script/job-vae-core-extra.sh): + +1. `#SBATCH --network=single_node_vni,job_vni`. `job_vni` adds a job-wide VNI to every step (`SLINGSHOT_VNIS=,`); `single_node_vni` makes single-node steps (e.g. `--layout=split-node`) get a CXI service at all — without it `fi_domain()` fails with `-38 (Function not implemented)`. +2. In each task, keep only the job VNI: `export SLINGSHOT_VNIS=${SLINGSHOT_VNIS##*,}` before starting Python. libfabric's cxi provider uses only the first VNI listed, i.e. the per-step one, so without this the extra side's reads fail with `fi_cq_read ... prov_errno=25 (VNI_NOT_FOUND)`. + +Verified on Frontier (2 nodes, split-node, `method=2`, `cxi`): with both, the extra step trains against the core step's data and both shut down cleanly; with either missing, it fails as above. MPI and RCCL inside each step work on the job VNI too. ### `get()` has no GPU destination-buffer pool @@ -304,7 +309,7 @@ On Frontier, the GPU synchronization performed before each RDMA call (needed for ### Troubleshooting: RDMA fails to connect (`cxi`, Frontier) -If a `cxi` job fails to connect over RDMA, try adding `#SBATCH --network=single_node_vni` — this has been needed specifically for **single-node** (`-N 1`) jobs on Frontier. For jobs spanning multiple nodes, or multiple `srun` steps in one job, this hasn't reliably helped (see above); if you hit connection issues there, reducing the number of sequential steps or using `method=1` is more likely to help. +If `fi_domain()` fails with `-38 (Function not implemented)` on `cxi`, the step has no CXI service: add `#SBATCH --network=single_node_vni` — needed whenever a step runs on a single node (a `-N 1` job, or a one-node step inside a larger job). If ranks in different `srun` steps can't reach each other (`VNI_NOT_FOUND`), see [Multiple `srun` steps](#multiple-srun-steps-in-one-job-method2-cxi-frontier) above. ## Partitioned / Sub-communicator Usage @@ -379,7 +384,7 @@ sbatch examples/vae/script/job-vae-core-extra.sh # method=2, c sbatch examples/vae/script/job-vae-core-extra.sh --gpudirect --layout=split-node --core-nnodes=2 ``` -Run `--help` on either script for the full option list. `job-vae-single.sh` additionally has `--method` and `--num-workers` (default 0; `> 0` switches to `ThreadDataLoader`, see above). `job-vae-core-extra.sh` additionally has `--layout=colocate|split-node` and `--core-nnodes`. Note `--layout=colocate` together with `--gpudirect` will over-request GPUs per node (core and extra each ask for a full node's worth of GPUs on the same nodes) — use `--layout=split-node` when testing GPUDirect on `job-vae-core-extra.sh`. +Run `--help` on either script for the full option list. `job-vae-single.sh` additionally has `--method` and `--num-workers` (default 0; `> 0` switches to `ThreadDataLoader`, see above). `job-vae-core-extra.sh` additionally has `--layout=colocate|split-node`, `--core-nnodes`, and `--num-workers` (for the extra/training step). Note `--layout=colocate` together with `--gpudirect` will over-request GPUs per node (core and extra each ask for a full node's worth of GPUs on the same nodes) — use `--layout=split-node` when testing GPUDirect on `job-vae-core-extra.sh`. ## Testing diff --git a/examples/vae/script/job-vae-core-extra.sh b/examples/vae/script/job-vae-core-extra.sh index 479481c..4b54b33 100755 --- a/examples/vae/script/job-vae-core-extra.sh +++ b/examples/vae/script/job-vae-core-extra.sh @@ -6,8 +6,17 @@ #SBATCH -N 4 #SBATCH -t 30:00 #SBATCH -q debug +#SBATCH --network=single_node_vni,job_vni # # Core/extra split VAE DDP run (two independent srun steps). +# +# Slingshot networking (Frontier): every srun step gets its own VNI, and two +# endpoints can only talk on the same VNI. --network=job_vni adds a job-wide +# VNI to every step (SLINGSHOT_VNIS=,), and single_node_vni +# makes single-node steps (split-node layout) get a CXI service at all -- +# without it fi_domain() fails with -38 (ENOSYS). libfabric's cxi provider +# uses only the FIRST VNI listed, so each task below restricts +# SLINGSHOT_VNIS to the job VNI (last entry) so core and extra share it. usage() { cat < 0 + switches to ThreadDataLoader with that many worker threads. + Default: 0. -h, --help Show this help message and exit. EOF } @@ -45,6 +58,7 @@ FABRIC= GPUDIRECT=0 LAYOUT= CORE_NNODES_OPT= +NUM_WORKERS= for arg in "$@"; do case "$arg" in --method=*) METHOD="${arg#--method=}" ;; @@ -52,9 +66,11 @@ for arg in "$@"; do --gpudirect) GPUDIRECT=1 ;; --layout=*) LAYOUT="${arg#--layout=}" ;; --core-nnodes=*) CORE_NNODES_OPT="${arg#--core-nnodes=}" ;; + --num-workers=*) NUM_WORKERS="${arg#--num-workers=}" ;; esac done LAYOUT="${LAYOUT:-colocate}" +NUM_WORKERS="${NUM_WORKERS:-0}" export DDSTORE_FABRIC="${FABRIC:-cxi}" export DDSTORE_METHOD="${METHOD:-2}" @@ -69,6 +85,9 @@ else EXTRA_EXTRA_ARGS="" fi +# Per-task wrapper: use only the job VNI (see the header comment). +JOB_VNI_WRAP='export SLINGSHOT_VNIS=${SLINGSHOT_VNIS##*,}; exec "$@"' + rm -rf ddstore_hs_vae mkdir -p results sleep 2 @@ -87,15 +106,15 @@ EXTRA_NTASKS=$((EXTRA_NNODES * EXTRA_NR)) echo "DDSTORE_METHOD=$DDSTORE_METHOD DDSTORE_FABRIC=$DDSTORE_FABRIC LAYOUT=$LAYOUT GPUDIRECT=$GPUDIRECT" echo "CORE_NNODES=$CORE_NNODES CORE_NTASKS=$CORE_NTASKS CORE_GPUS_PER_TASK=$CORE_GPUS_PER_TASK CORE_EXTRA_ARGS=\"$CORE_EXTRA_ARGS\"" -echo "EXTRA_NNODES=$EXTRA_NNODES EXTRA_NTASKS=$EXTRA_NTASKS EXTRA_EXTRA_ARGS=\"$EXTRA_EXTRA_ARGS\"" +echo "EXTRA_NNODES=$EXTRA_NNODES EXTRA_NTASKS=$EXTRA_NTASKS EXTRA_EXTRA_ARGS=\"$EXTRA_EXTRA_ARGS\" NUM_WORKERS=$NUM_WORKERS" MASTER_PORT=8889 srun -N$CORE_NNODES -n$CORE_NTASKS -c1 --gpus-per-task=$CORE_GPUS_PER_TASK --cpu-bind=verbose,core -l \ - python -u examples/vae/vae_core_server.py ddstore_hs_vae $CORE_EXTRA_ARGS \ + bash -c "$JOB_VNI_WRAP" _ python -u examples/vae/vae_core_server.py ddstore_hs_vae $CORE_EXTRA_ARGS \ > >(sed 's/^/[core] /') 2> >(sed 's/^/[core] /') & sleep 5 MASTER_PORT=8891 DDSTORE_HANDSHAKE_TIMEOUT_S=60 srun -N$EXTRA_NNODES -n$EXTRA_NTASKS -c6 --gpus-per-task=1 --cpu-bind=verbose,core -l \ - python -u examples/vae/vae_extra_train.py --handshake-dir ddstore_hs_vae --n-core $CORE_NTASKS --epochs 3 $EXTRA_EXTRA_ARGS \ + bash -c "$JOB_VNI_WRAP" _ python -u examples/vae/vae_extra_train.py --handshake-dir ddstore_hs_vae --n-core $CORE_NTASKS --epochs 3 --num-workers=$NUM_WORKERS $EXTRA_EXTRA_ARGS \ > >(sed 's/^/[extr] /') 2> >(sed 's/^/[extr] /') sleep 5 From 3dadb328349e56a1f08e9d3a6f6c23b473cc193a Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 14:22:01 -0400 Subject: [PATCH 25/56] Add get() profiling, bench_get.py, and VAE --replicate/--image-scale - DDSTORE_PROFILE=1: per-variable C++ counters in fabric_state (calls, lock wait, recv-MR check/registration + misses, fi_read post, CQ wait), updated under recv_lock; DDStore::profile() and PyDDStore.get_profile(), which adds Python-side whole-call and GPU sync time. Off by default. - vae-ddp.py prints an all-rank get() breakdown at the end when enabled. - examples/scripts/bench_get.py: single-row get() latency/throughput vs row size, host / fresh GPU / reused GPU destination, threads, with the same breakdown. - VAE: --replicate R (ConcatDataset, longer epochs) and --image-scale S ((28*S)^2 images, hidden 400*S); core server and both job scripts pass them through, extra side follows the published row width. Defaults reproduce the original example (epoch-8 loss 8.8960 at R=1/S=1). - README: new options, profiling, and Frontier measurements. Findings (Frontier, cxi, 2x8 ranks): GPU-destination get() inside the VAE is dominated by the per-call device sync once a worker thread runs (130-430 us/sample vs ~4 us without workers); lock wait and MR registration are negligible. GPUDirect beats host between 12.5 KB and 200 KB per row (1 MB: 134 vs 206-276 us). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- README.md | 21 ++++ examples/scripts/bench_get.py | 132 ++++++++++++++++++++++ examples/vae/distdataset.py | 10 +- examples/vae/script/job-vae-core-extra.sh | 14 ++- examples/vae/script/job-vae-single.sh | 12 +- examples/vae/vae-ddp.py | 61 +++++++++- examples/vae/vae_core_server.py | 29 ++++- examples/vae/vae_extra_train.py | 20 ++-- examples/vae/vae_model.py | 27 +++-- include/common.h | 34 ++++++ include/ddstore.hpp | 27 +++++ src/common.cxx | 20 ++++ src/pyddstore.pyx | 41 ++++++- 13 files changed, 415 insertions(+), 33 deletions(-) create mode 100644 examples/scripts/bench_get.py diff --git a/README.md b/README.md index 1a8b5b6..f455fdc 100644 --- a/README.md +++ b/README.md @@ -386,6 +386,27 @@ sbatch examples/vae/script/job-vae-core-extra.sh --gpudirect --layout=split-node Run `--help` on either script for the full option list. `job-vae-single.sh` additionally has `--method` and `--num-workers` (default 0; `> 0` switches to `ThreadDataLoader`, see above). `job-vae-core-extra.sh` additionally has `--layout=colocate|split-node`, `--core-nnodes`, and `--num-workers` (for the extra/training step). Note `--layout=colocate` together with `--gpudirect` will over-request GPUs per node (core and extra each ask for a full node's worth of GPUs on the same nodes) — use `--layout=split-node` when testing GPUDirect on `job-vae-core-extra.sh`. +### Larger VAE cases + +`vae-ddp.py` (and `vae_core_server.py` for the core/extra split; the extra side follows the published data) takes two size knobs, also exposed by both job scripts: + +- `--replicate R` — repeat the MNIST training set `R` times: longer epochs, same per-sample cost. +- `--image-scale S` — upscale images to (28·S)×(28·S): each row is S² larger (3 KB at S=1, 12.5 KB at S=2); the VAE hidden layer grows to 400·S. + +Both default to 1, which reproduces the original example exactly. + +### Profiling `get()` (`DDSTORE_PROFILE=1`) + +With `DDSTORE_PROFILE=1`, `PyDDStore.get_profile(name)` returns where `get()` time goes for a variable: lock wait, receive-MR check/registration (and miss count), posting `fi_read`, waiting for completion (C++, methods 1/2), plus the GPU-destination `torch.cuda.synchronize()` and whole-call time (Python). `vae-ddp.py` prints an all-rank summary at the end when it is set. Off by default. + +[examples/scripts/bench_get.py](examples/scripts/bench_get.py) measures single-row `get()` latency/throughput vs row size, host vs GPU destination, and thread count, with the same breakdown. + +Measured on Frontier (`method=1`, `cxi`, 2 nodes × 8 ranks): + +- Inside the VAE, GPU-destination `get()` is dominated by the per-call device sync once a worker thread runs alongside training: ~4 µs per sample with no workers, but 130–190 µs with 1 worker and 290–430 µs with 2 (S=1/S=2), because the worker's sync waits for the training kernels. Lock wait (≤0.2 µs) and MR registration (<1 µs, even at 100% misses) are negligible; the RDMA itself is ~4–5 µs. +- `bench_get.py`, one thread, µs per single-row `get()`: 3 KB — host 8.6, GPU 19.5 (12.2 reusing the buffer); 12.5 KB — host 9.9, GPU 20.5; 200 KB — host 53, GPU 37; 1 MB — host 206–276, GPU 134. GPUDirect wins from somewhere between 12.5 KB and 200 KB per row; below that its fixed per-call overhead (sync, allocation) dominates. +- A second thread adds no per-rank throughput: the per-variable lock serializes the transfers (lock wait ≈ transfer time at large rows). + ## Testing ### Unit tests (pytest) diff --git a/examples/scripts/bench_get.py b/examples/scripts/bench_get.py new file mode 100644 index 0000000..2b544cd --- /dev/null +++ b/examples/scripts/bench_get.py @@ -0,0 +1,132 @@ +"""Microbenchmark for DDStore get(): per-call latency and throughput vs row size. + +Each rank adds a shard of random float32 rows, then issues --nget single-row +get()s of uniformly random global rows (most of them remote) and reports the +average per-get latency and per-rank throughput, plus the DDSTORE_PROFILE +breakdown (lock wait / MR / fi_read post / CQ wait / GPU sync). + +Run (method 1, cxi), e.g. on 2 nodes: + DDSTORE_PROFILE=1 DDSTORE_FABRIC=cxi srun -N2 -n16 -c7 --gpus-per-task=1 \\ + python examples/scripts/bench_get.py --row-floats 784,3136 --dest host,gpu + +Destinations: "host" reads into a reused numpy row; "gpu" allocates a fresh +torch.empty() per call, like DistDataset.get() with --gpu-dest; "gpu-reuse" +reuses one GPU row (no allocator churn). +""" + +import argparse +import os +import threading +import time + +## torch must load before mpi4py triggers MPI_Init (see vae-ddp.py). +import torch +import numpy as np +from mpi4py import MPI + +import pyddstore as dds + +parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) +parser.add_argument("--row-floats", default="784,3136", + help="comma-separated row widths in float32 (784 = MNIST " + "28x28 / --image-scale 1, 3136 = 56x56 / --image-scale 2)") +parser.add_argument("--rows-per-rank", type=int, default=4096) +parser.add_argument("--nget", type=int, default=4000, + help="get() calls per rank per configuration") +parser.add_argument("--dest", default="host,gpu", + help="comma-separated: host, gpu, gpu-reuse") +parser.add_argument("--gpu-source", action="store_true", + help="add() the shard as a GPU tensor instead of numpy") +parser.add_argument("--threads", default="1", + help="comma-separated thread counts issuing get()s concurrently") +parser.add_argument("--method", type=int, default=int(os.environ.get("DDSTORE_METHOD", "1"))) +args = parser.parse_args() + +comm = MPI.COMM_WORLD +rank, size = comm.Get_rank(), comm.Get_size() +ngpu = torch.cuda.device_count() +device = torch.device(f"cuda:{int(os.environ.get('SLURM_LOCALID', 0)) % ngpu}") if ngpu else None +if device is not None: + torch.cuda.set_device(device) + +row_floats = [int(x) for x in args.row_floats.split(",")] +dests = args.dest.split(",") +thread_counts = [int(x) for x in args.threads.split(",")] +total_rows = args.rows_per_rank * size + +if rank == 0: + print(f"ranks={size} rows/rank={args.rows_per_rank} nget/rank={args.nget} " + f"method={args.method} fabric={os.environ.get('DDSTORE_FABRIC', 'hsn')} " + f"gpu_source={args.gpu_source} profile={os.environ.get('DDSTORE_PROFILE', '0')}", + flush=True) + print(f"{'row_B':>8} {'dest':>9} {'thr':>3} {'us/get':>8} {'MB/s/rank':>9} | " + f"{'sync':>6} {'lock':>6} {'mr':>6} {'miss%':>6} {'read':>6} {'cq':>6} {'other':>6} (us/get)", + flush=True) + +for nf in row_floats: + for dest in dests: + if dest != "host" and device is None: + continue + for nthr in thread_counts: + store = dds.PyDDStore(comm, method=args.method) + rng = np.random.default_rng(rank) + shard = np.full((args.rows_per_rank, nf), float(rank), dtype=np.float32) + if args.gpu_source: + shard = torch.from_numpy(shard).to(device) + store.add("x", shard) + comm.Barrier() + store.epoch_begin() + + idx = rng.integers(0, total_rows, size=args.nget) + chunks = np.array_split(idx, nthr) + + def worker(ids): + reuse_host = np.empty((1, nf), dtype=np.float32) + reuse_gpu = (torch.empty((1, nf), dtype=torch.float32, device=device) + if device is not None else None) + for g in ids: + if dest == "host": + store.get("x", reuse_host, int(g)) + elif dest == "gpu": + out = torch.empty((1, nf), dtype=torch.float32, device=device) + store.get("x", out, int(g)) + else: + store.get("x", reuse_gpu, int(g)) + + # One warm-up get so first-call registration isn't timed as typical. + worker(idx[:1]) + comm.Barrier() + p0 = store.get_profile("x") + comm.Barrier() + t0 = time.perf_counter() + ths = [threading.Thread(target=worker, args=(c,)) for c in chunks] + for t in ths: + t.start() + for t in ths: + t.join() + if device is not None: + torch.cuda.synchronize() + dt = time.perf_counter() - t0 + p1 = store.get_profile("x") + store.epoch_end() + + d = {k: p1[k] - p0[k] for k in p1} + vals = comm.gather((dt, d), root=0) + if rank == 0: + n = sum(v[1]["calls"] for v in vals) or 1 + ngets = args.nget * size + us_get = 1e6 * sum(v[0] for v in vals) / ngets * nthr + mbps = nf * 4 * args.nget / (sum(v[0] for v in vals) / size) / 1e6 + s = lambda k: 1e6 * sum(v[1][k] for v in vals) / n + other = s("py_get") - s("py_sync") - s("lock_wait") - s("mr") - s("read") - s("cq") + miss = 100.0 * sum(v[1]["mr_miss"] for v in vals) / n + print(f"{nf * 4:>8} {dest:>9} {nthr:>3} {us_get:>8.1f} {mbps:>9.1f} | " + f"{s('py_sync'):>6.1f} {s('lock_wait'):>6.1f} {s('mr'):>6.1f} " + f"{miss:>6.1f} {s('read'):>6.1f} {s('cq'):>6.1f} {other:>6.1f}", + flush=True) + # Every rank must finish reading before any rank tears down its + # endpoint (gather() doesn't synchronize non-root ranks). + comm.Barrier() + store.free() + del store + comm.Barrier() diff --git a/examples/vae/distdataset.py b/examples/vae/distdataset.py index bcc29af..fa02a4b 100644 --- a/examples/vae/distdataset.py +++ b/examples/vae/distdataset.py @@ -118,6 +118,10 @@ def __init__( self.ddstore.add(f"{self.label}data", self.data) self.ddstore.add(f"{self.label}labels", self.labels) + # Row width and image side, from the data (28*28 for plain MNIST, + # larger with vae-ddp.py --image-scale). + self.data_disp = int(self.data.shape[1]) + self.side = int(round(self.data_disp**0.5)) # get() allocates a fresh GPU tensor per call on the GPU path (no # buffer pool) -- simpler, at the cost of a fresh fi_mr_regattr per @@ -143,16 +147,16 @@ def get(self, idx, device=None): # complexity for no benefit. label = np.zeros(1, dtype=np.int32) if device is not None: - val = torch.empty((1, 28 * 28), dtype=torch.float32, device=device) + val = torch.empty((1, self.data_disp), dtype=torch.float32, device=device) else: - val = np.zeros((1, 28 * 28), dtype=np.float32) + val = np.zeros((1, self.data_disp), dtype=np.float32) val = np.ascontiguousarray(val) assert val.data.contiguous self.ddstore.get(f"{self.label}data", val, idx) self.ddstore.get(f"{self.label}labels", label, idx) if device is None: val = torch.tensor(val) - val = torch.reshape(val, (1, 28, 28)) + val = torch.reshape(val, (1, self.side, self.side)) return (val, label[0]) def __getitem__(self, idx): diff --git a/examples/vae/script/job-vae-core-extra.sh b/examples/vae/script/job-vae-core-extra.sh index 4b54b33..f745ad9 100755 --- a/examples/vae/script/job-vae-core-extra.sh +++ b/examples/vae/script/job-vae-core-extra.sh @@ -43,6 +43,10 @@ Options: PyTorch's standard DataLoader in the main process. > 0 switches to ThreadDataLoader with that many worker threads. Default: 0. + --replicate=R Repeat the MNIST training set R times on the core side + (longer epochs, same per-sample cost). Default: 1. + --image-scale=S Upscale images to (28*S)x(28*S) on the core side (the + extra side follows the published size). Default: 1. -h, --help Show this help message and exit. EOF } @@ -59,6 +63,8 @@ GPUDIRECT=0 LAYOUT= CORE_NNODES_OPT= NUM_WORKERS= +REPLICATE= +IMAGE_SCALE= for arg in "$@"; do case "$arg" in --method=*) METHOD="${arg#--method=}" ;; @@ -67,10 +73,14 @@ for arg in "$@"; do --layout=*) LAYOUT="${arg#--layout=}" ;; --core-nnodes=*) CORE_NNODES_OPT="${arg#--core-nnodes=}" ;; --num-workers=*) NUM_WORKERS="${arg#--num-workers=}" ;; + --replicate=*) REPLICATE="${arg#--replicate=}" ;; + --image-scale=*) IMAGE_SCALE="${arg#--image-scale=}" ;; esac done LAYOUT="${LAYOUT:-colocate}" NUM_WORKERS="${NUM_WORKERS:-0}" +REPLICATE="${REPLICATE:-1}" +IMAGE_SCALE="${IMAGE_SCALE:-1}" export DDSTORE_FABRIC="${FABRIC:-cxi}" export DDSTORE_METHOD="${METHOD:-2}" @@ -105,11 +115,11 @@ CORE_NTASKS=$((CORE_NNODES * CORE_NR)) EXTRA_NTASKS=$((EXTRA_NNODES * EXTRA_NR)) echo "DDSTORE_METHOD=$DDSTORE_METHOD DDSTORE_FABRIC=$DDSTORE_FABRIC LAYOUT=$LAYOUT GPUDIRECT=$GPUDIRECT" -echo "CORE_NNODES=$CORE_NNODES CORE_NTASKS=$CORE_NTASKS CORE_GPUS_PER_TASK=$CORE_GPUS_PER_TASK CORE_EXTRA_ARGS=\"$CORE_EXTRA_ARGS\"" +echo "CORE_NNODES=$CORE_NNODES CORE_NTASKS=$CORE_NTASKS CORE_GPUS_PER_TASK=$CORE_GPUS_PER_TASK CORE_EXTRA_ARGS=\"$CORE_EXTRA_ARGS\" REPLICATE=$REPLICATE IMAGE_SCALE=$IMAGE_SCALE" echo "EXTRA_NNODES=$EXTRA_NNODES EXTRA_NTASKS=$EXTRA_NTASKS EXTRA_EXTRA_ARGS=\"$EXTRA_EXTRA_ARGS\" NUM_WORKERS=$NUM_WORKERS" MASTER_PORT=8889 srun -N$CORE_NNODES -n$CORE_NTASKS -c1 --gpus-per-task=$CORE_GPUS_PER_TASK --cpu-bind=verbose,core -l \ - bash -c "$JOB_VNI_WRAP" _ python -u examples/vae/vae_core_server.py ddstore_hs_vae $CORE_EXTRA_ARGS \ + bash -c "$JOB_VNI_WRAP" _ python -u examples/vae/vae_core_server.py ddstore_hs_vae --replicate=$REPLICATE --image-scale=$IMAGE_SCALE $CORE_EXTRA_ARGS \ > >(sed 's/^/[core] /') 2> >(sed 's/^/[core] /') & sleep 5 diff --git a/examples/vae/script/job-vae-single.sh b/examples/vae/script/job-vae-single.sh index 3ea6827..1fa49e1 100755 --- a/examples/vae/script/job-vae-single.sh +++ b/examples/vae/script/job-vae-single.sh @@ -25,6 +25,10 @@ Options: DataLoader in the main process (no worker processes). > 0 switches to ThreadDataLoader with that many worker threads (requires --method=1 or 2). Default: 0. + --replicate=R Repeat the MNIST training set R times (longer epochs, + same per-sample cost). Default: 1. + --image-scale=S Upscale images to (28*S)x(28*S): S^2 larger samples. + Default: 1. -h, --help Show this help message and exit. Examples: @@ -53,20 +57,26 @@ METHOD= FABRIC= GPUDIRECT_ARGS="" NUM_WORKERS= +REPLICATE= +IMAGE_SCALE= for arg in "$@"; do case "$arg" in --method=*) METHOD="${arg#--method=}" ;; --fabric=*) FABRIC="${arg#--fabric=}" ;; --gpudirect) GPUDIRECT_ARGS="--gpu-dest --gpu-source" ;; --num-workers=*) NUM_WORKERS="${arg#--num-workers=}" ;; + --replicate=*) REPLICATE="${arg#--replicate=}" ;; + --image-scale=*) IMAGE_SCALE="${arg#--image-scale=}" ;; esac done export DDSTORE_FABRIC="${FABRIC:-cxi}" METHOD="${METHOD:-0}" NUM_WORKERS="${NUM_WORKERS:-0}" +REPLICATE="${REPLICATE:-1}" +IMAGE_SCALE="${IMAGE_SCALE:-1}" -EXTRA_ARGS="$GPUDIRECT_ARGS --num-workers=$NUM_WORKERS" +EXTRA_ARGS="$GPUDIRECT_ARGS --num-workers=$NUM_WORKERS --replicate=$REPLICATE --image-scale=$IMAGE_SCALE" echo "DDSTORE_METHOD=$METHOD DDSTORE_FABRIC=$DDSTORE_FABRIC EXTRA_ARGS=\"$EXTRA_ARGS\"" diff --git a/examples/vae/vae-ddp.py b/examples/vae/vae-ddp.py index 9db3936..bb228a9 100644 --- a/examples/vae/vae-ddp.py +++ b/examples/vae/vae-ddp.py @@ -19,7 +19,7 @@ from ddstore_dataloader import ThreadDataLoader from ddp_utils import setup_ddp, get_local_rank -from vae_model import VAE, loss_function +from vae_model import VAE, loss_function, mnist_transform parser = argparse.ArgumentParser(description="VAE MNIST Example") parser.add_argument( @@ -84,6 +84,20 @@ "so any --num-workers > 0 goes through threads, never a fork. " "Requires DDSTORE_METHOD 1 or 2 when > 0. Default: 0.", ) +parser.add_argument( + "--replicate", + type=int, + default=1, + metavar="R", + help="Repeat the MNIST training set this many times (torch ConcatDataset), for longer epochs with the same per-sample cost. Default: 1.", +) +parser.add_argument( + "--image-scale", + type=int, + default=1, + metavar="S", + help="Upscale MNIST images to (28*S)x(28*S) (bilinear), so each sample is S^2 times larger; the VAE hidden layer grows to 400*S. Default: 1 (plain 28x28).", +) args = parser.parse_args() args.cuda = not args.no_cuda and torch.cuda.is_available() use_mps = not args.no_mps and torch.backends.mps.is_available() @@ -127,12 +141,16 @@ os.makedirs("results", exist_ok=True) comm.Barrier() -model = VAE().to(device) +side = 28 * args.image_scale +model = VAE(input_dim=side * side, hidden=400 * args.image_scale).to(device) model = torch.nn.parallel.DistributedDataParallel(model) optimizer = optim.Adam(model.parameters(), lr=1e-3) +mnist_train = datasets.MNIST( + "data", train=True, download=True, transform=mnist_transform(args.image_scale) +) trainset = DistDataset( - datasets.MNIST("data", train=True, download=True, transform=transforms.ToTensor()), + torch.utils.data.ConcatDataset([mnist_train] * args.replicate), "train", comm, device=device if args.gpu_dest else None, @@ -162,7 +180,7 @@ ) testset = datasets.MNIST( - "data", train=False, download=True, transform=transforms.ToTensor() + "data", train=False, download=True, transform=mnist_transform(args.image_scale) ) test_loader = torch.utils.data.DataLoader(testset, batch_size=args.batch_size, shuffle=False) @@ -256,7 +274,7 @@ def test(epoch): if i == 0: n = min(data.size(0), 8) comparison = torch.cat( - [data[:n], recon_batch.view(args.batch_size, 1, 28, 28)[:n]] + [data[:n], recon_batch.view(-1, 1, side, side)[:n]] ) save_image( comparison.cpu(), @@ -278,7 +296,38 @@ def test(epoch): sample = torch.randn(64, 20).to(device) sample = model.module.decode(sample).cpu() save_image( - sample.view(64, 1, 28, 28), "results/sample_" + str(epoch) + ".png" + sample.view(64, 1, side, side), "results/sample_" + str(epoch) + ".png" + ) + + # DDSTORE_PROFILE=1: where get() time goes, summed over all ranks and + # both variables (data + labels), all epochs. + if os.environ.get("DDSTORE_PROFILE", "0") not in ("", "0"): + ds = trainset.ddstore + tot = {} + for var in ("traindata", "trainlabels"): + for k, v in ds.get_profile(var).items(): + if not k.startswith("py_"): + tot[k] = tot.get(k, 0) + v + prof = ds.get_profile("traindata") + for k in ("py_gets", "py_get", "py_sync"): + tot[k] = prof[k] + tot = {k: comm.allreduce(v) for k, v in tot.items()} + if rank == 0: + n = max(tot["calls"], 1) + us = lambda x: 1e6 * x / n + other = tot["py_get"] - tot["py_sync"] - tot["lock_wait"] - tot["mr"] - tot["read"] - tot["cq"] + print( + "[ddstore-profile] all ranks: gets={} py_gets={} mr_miss={} ({:.1%})".format( + tot["calls"], tot["py_gets"], tot["mr_miss"], tot["mr_miss"] / n ) + ) + print( + "[ddstore-profile] per get (us): total={:.1f} sync={:.1f} lock_wait={:.1f} " + "mr={:.1f} read={:.1f} cq={:.1f} other={:.1f}".format( + us(tot["py_get"]), us(tot["py_sync"]), us(tot["lock_wait"]), + us(tot["mr"]), us(tot["read"]), us(tot["cq"]), us(other) + ), + flush=True, + ) dist.destroy_process_group() diff --git a/examples/vae/vae_core_server.py b/examples/vae/vae_core_server.py index 0b6b4b2..b8e45b5 100644 --- a/examples/vae/vae_core_server.py +++ b/examples/vae/vae_core_server.py @@ -8,7 +8,7 @@ is done. Usage: - srun -n python examples/vae/vae_core_server.py [handshake_dir] [--gpu-source] + srun -n python examples/vae/vae_core_server.py [handshake_dir] [--gpu-source] [--replicate=R] [--image-scale=S] --gpu-source Stack this rank's shard directly on the GPU and add() it in place (GPUDirect RDMA source, Phase 2), skipping the host @@ -16,6 +16,11 @@ and DDSTORE_FABRIC=cxi, and one visible GPU per rank (--gpus-per-task=1, unlike the --gpus-per-task=0 this script normally runs with). + --replicate=R Repeat the MNIST training set R times (default 1), for + longer epochs with the same per-sample cost. The extra side + picks up the row count from the published variable. + --image-scale=S Upscale images to (28*S)x(28*S) (default 1). The extra + side derives S from the published row width. Environment: DDSTORE_HANDSHAKE_DIR overrides handshake_dir positional arg @@ -40,6 +45,7 @@ from mpi4py import MPI from distdataset import DistDataset +from vae_model import mnist_transform def _resolve_dir(arg): @@ -49,6 +55,13 @@ def _resolve_dir(arg): gpu_source = "--gpu-source" in sys.argv +replicate = 1 +image_scale = 1 +for a in sys.argv[1:]: + if a.startswith("--replicate="): + replicate = int(a.split("=", 1)[1]) + elif a.startswith("--image-scale="): + image_scale = int(a.split("=", 1)[1]) positional_args = [a for a in sys.argv[1:] if not a.startswith("--")] hs_dir = _resolve_dir(positional_args[0] if positional_args else "") os.environ["DDSTORE_METHOD"] = "2" @@ -72,13 +85,21 @@ def _resolve_dir(arg): comm.Barrier() trainset = datasets.MNIST( - "data", train=True, download=True, transform=transforms.ToTensor() + "data", train=True, download=True, transform=mnist_transform(image_scale) +) +dds_trainset = DistDataset( + torch.utils.data.ConcatDataset([trainset] * replicate), + "train", + comm, + add_device=add_device, ) -dds_trainset = DistDataset(trainset, "train", comm, add_device=add_device) comm.Barrier() if rank == 0: - print("gpu_source:", gpu_source, flush=True) + print( + "gpu_source:", gpu_source, "replicate:", replicate, + "image_scale:", image_scale, flush=True, + ) if rank == 0: print( diff --git a/examples/vae/vae_extra_train.py b/examples/vae/vae_extra_train.py index 64b8153..70bbac9 100644 --- a/examples/vae/vae_extra_train.py +++ b/examples/vae/vae_extra_train.py @@ -41,7 +41,7 @@ from ddp_utils import setup_ddp, get_local_rank from distdataset import DistDatasetReader from ddstore_dataloader import ThreadDataLoader -from vae_model import VAE, loss_function +from vae_model import VAE, loss_function, mnist_transform parser = argparse.ArgumentParser(description="VAE MNIST Example - extra (reader) group") parser.add_argument( @@ -135,16 +135,20 @@ print("DDP setup:", comm_size, rank, device, "gpu_dest:", args.gpu_dest) -model = VAE().to(device) -model = torch.nn.parallel.DistributedDataParallel(model) -optimizer = optim.Adam(model.parameters(), lr=1e-3) - trainset = DistDatasetReader( "train", args.handshake_dir, args.n_core, device=device if args.gpu_dest else None, ) + +# Image size comes from the core side's published data (vae_core_server.py +# --image-scale); the model and test set must match it. +side = trainset.side +image_scale = side // 28 +model = VAE(input_dim=side * side, hidden=400 * image_scale).to(device) +model = torch.nn.parallel.DistributedDataParallel(model) +optimizer = optim.Adam(model.parameters(), lr=1e-3) sampler = torch.utils.data.distributed.DistributedSampler(trainset) if args.num_workers > 0: @@ -167,7 +171,7 @@ ) testset = datasets.MNIST( - "data", train=False, download=True, transform=transforms.ToTensor() + "data", train=False, download=True, transform=mnist_transform(image_scale) ) test_loader = torch.utils.data.DataLoader(testset, batch_size=args.batch_size, shuffle=False) @@ -217,7 +221,7 @@ def test(epoch): if i == 0: n = min(data.size(0), 8) comparison = torch.cat( - [data[:n], recon_batch.view(args.batch_size, 1, 28, 28)[:n]] + [data[:n], recon_batch.view(-1, 1, side, side)[:n]] ) save_image( comparison.cpu(), @@ -237,7 +241,7 @@ def test(epoch): sample = torch.randn(64, 20).to(device) sample = model.module.decode(sample).cpu() save_image( - sample.view(64, 1, 28, 28), + sample.view(64, 1, side, side), "results/extra_sample_" + str(epoch) + ".png", ) diff --git a/examples/vae/vae_model.py b/examples/vae/vae_model.py index b8f435e..f785d41 100644 --- a/examples/vae/vae_model.py +++ b/examples/vae/vae_model.py @@ -1,17 +1,19 @@ import torch from torch import nn from torch.nn import functional as F +from torchvision import transforms class VAE(nn.Module): - def __init__(self): + def __init__(self, input_dim=784, hidden=400): super(VAE, self).__init__() - self.fc1 = nn.Linear(784, 400) - self.fc21 = nn.Linear(400, 20) - self.fc22 = nn.Linear(400, 20) - self.fc3 = nn.Linear(20, 400) - self.fc4 = nn.Linear(400, 784) + self.input_dim = input_dim + self.fc1 = nn.Linear(input_dim, hidden) + self.fc21 = nn.Linear(hidden, 20) + self.fc22 = nn.Linear(hidden, 20) + self.fc3 = nn.Linear(20, hidden) + self.fc4 = nn.Linear(hidden, input_dim) def encode(self, x): h1 = F.relu(self.fc1(x)) @@ -27,14 +29,16 @@ def decode(self, z): return torch.sigmoid(self.fc4(h3)) def forward(self, x): - mu, logvar = self.encode(x.view(-1, 784)) + mu, logvar = self.encode(x.view(-1, self.input_dim)) z = self.reparameterize(mu, logvar) return self.decode(z), mu, logvar def loss_function(recon_x, x, mu, logvar): # Reconstruction + KL divergence losses summed over all elements and batch - BCE = F.binary_cross_entropy(recon_x, x.view(-1, 784), reduction="sum") + BCE = F.binary_cross_entropy( + recon_x, x.view(-1, recon_x.shape[1]), reduction="sum" + ) # see Appendix B from VAE paper: # Kingma and Welling. Auto-Encoding Variational Bayes. ICLR, 2014 @@ -43,3 +47,10 @@ def loss_function(recon_x, x, mu, logvar): KLD = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp()) return BCE + KLD + + +def mnist_transform(scale): + """ToTensor(), preceded by a bilinear upscale to (28*scale)^2 if scale > 1.""" + if scale == 1: + return transforms.ToTensor() + return transforms.Compose([transforms.Resize(28 * scale), transforms.ToTensor()]) diff --git a/include/common.h b/include/common.h index 64b7a59..e3b8e4a 100644 --- a/include/common.h +++ b/include/common.h @@ -4,7 +4,10 @@ #include #include #include +#include +#include #include +#include #include #define DP_AV_DEF_SIZE 512 @@ -102,8 +105,39 @@ extern "C" * PTHREAD_MUTEX_INITIALIZER is a common but implementation-defined * assumption; init explicitly instead. */ pthread_mutex_t recv_lock; + + /* DDSTORE_PROFILE=1: cumulative get() timing for this variable, all + * updated while recv_lock is held (see ddstore_profile_enabled()). + * prof_lock_wait_ns is the time spent waiting to acquire recv_lock; + * mr = recv-MR cache check / (re)registration; read = posting + * fi_read(); cq = polling the CQ until the read completes. */ + uint64_t prof_calls; + uint64_t prof_lock_wait_ns; + uint64_t prof_mr_ns; + uint64_t prof_mr_miss; + uint64_t prof_read_ns; + uint64_t prof_cq_ns; }; + static inline uint64_t ddstore_now_ns(void) + { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec; + } + + /* True if DDSTORE_PROFILE is set to a non-"0" value (read once). */ + static inline bool ddstore_profile_enabled(void) + { + static int enabled = -1; + if (enabled < 0) + { + const char *e = getenv("DDSTORE_PROFILE"); + enabled = (e && e[0] && strcmp(e, "0") != 0) ? 1 : 0; + } + return enabled == 1; + } + static bool is_local_mr_req(struct fabric_state *f) { return (f->info->mode & FI_LOCAL_MR) != 0; diff --git a/include/ddstore.hpp b/include/ddstore.hpp index ef942de..ef005ec 100644 --- a/include/ddstore.hpp +++ b/include/ddstore.hpp @@ -67,6 +67,26 @@ class DDStore /* Method 2 extra member: discover variable published by core members. */ void join(std::string name); + /* DDSTORE_PROFILE=1 counters for `name` (methods 1/2), as + * {calls, lock_wait_ns, mr_ns, mr_miss, read_ns, cq_ns}; all zero for + * method 0 or when profiling is off. Takes the variable's lock. */ + void profile(std::string name, unsigned long long out[6]) + { + for (int i = 0; i < 6; i++) + out[i] = 0; + const VarInfo_t &varinfo = this->varlist.at(name); + struct fabric_state *fs = varinfo.fabric_state; + if (!fs) + return; + fabric_state_lock_guard lock(fs); + out[0] = fs->prof_calls; + out[1] = fs->prof_lock_wait_ns; + out[2] = fs->prof_mr_ns; + out[3] = fs->prof_mr_miss; + out[4] = fs->prof_read_ns; + out[5] = fs->prof_cq_ns; + } + /* hmem_iface: 0 (FI_HMEM_SYSTEM) for a host buffer, or an fi_hmem_iface * value (FI_HMEM_CUDA, FI_HMEM_ROCR, ...) identifying what kind of GPU * memory `buffer` is. Mirrors get()'s hmem_iface parameter. @@ -481,7 +501,14 @@ class DDStore * concurrent get() calls on this variable, not just the * read_from_remote() call that follows them -- see recv_lock's * comment in common.h. */ + const bool prof = ddstore_profile_enabled(); + uint64_t t_wait = prof ? ddstore_now_ns() : 0; fabric_state_lock_guard lock(varinfo.fabric_state); + if (prof) + { + varinfo.fabric_state->prof_calls++; + varinfo.fabric_state->prof_lock_wait_ns += ddstore_now_ns() - t_wait; + } if (hmem_iface != 0 && !is_hmem_capable(varinfo.fabric_state)) throw std::runtime_error( "GPU destination buffer requires DDSTORE_FABRIC=cxi " diff --git a/src/common.cxx b/src/common.cxx index 3e6a345..395cab3 100644 --- a/src/common.cxx +++ b/src/common.cxx @@ -674,6 +674,8 @@ int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset * recv_mr_reg_len). Hits when the caller passes the same buffer again -- * e.g. PyTorch's caching allocator returning the same block for a * same-shape torch.empty() -- so each get() needn't re-register. */ + const bool prof = ddstore_profile_enabled(); + uint64_t t_mr = prof ? ddstore_now_ns() : 0; char *cur_base = fabric_state->recv_data; size_t cur_len = fabric_state->recv_data_len; bool in_cached_region = @@ -683,6 +685,8 @@ int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset if (!in_cached_region) { + if (prof) + fabric_state->prof_mr_miss++; /* Close the stale registration before creating a new one. */ if (fabric_state->recv_mr) fi_close(&fabric_state->recv_mr->fid); @@ -758,6 +762,13 @@ int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset memory_descriptor = fi_mr_desc(fabric_state->recv_mr); } + uint64_t t_read = 0; + if (prof) + { + t_read = ddstore_now_ns(); + fabric_state->prof_mr_ns += t_read - t_mr; + } + size_t rc; // fprintf(stderr, "fabric_state->remote_address: %llu\n", fabric_state->remote_address[src]); do @@ -787,6 +798,13 @@ int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset // return 1; // } + uint64_t t_cq = 0; + if (prof) + { + t_cq = ddstore_now_ns(); + fabric_state->prof_read_ns += t_cq - t_read; + } + /* This loop blocks until the transfer completes — read_from_remote() does * not return until it does. For a GPU (recv_hmem_iface != FI_HMEM_SYSTEM) * destination, this is load-bearing: it's what keeps the caller's device @@ -800,6 +818,8 @@ int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset rc = fi_cq_read(fabric_state->cq_signal, &CQEntry, 1); if (rc == 1) { + if (prof) + fabric_state->prof_cq_ns += ddstore_now_ns() - t_cq; /* NOTE: CQEntry.len is NOT a reliable success signal on this * provider/CQ format — it reads 0 even for host-to-host * transfers independently verified to deliver correct data, so diff --git a/src/pyddstore.pyx b/src/pyddstore.pyx index 2b36b83..59823da 100644 --- a/src/pyddstore.pyx +++ b/src/pyddstore.pyx @@ -3,6 +3,7 @@ # cython: language=c++ import os +import time import mpi4py.MPI as MPI cimport mpi4py.MPI as MPI @@ -116,6 +117,7 @@ cdef extern from "ddstore.hpp": void join(string name) except + void query(string name, VarInfo &varinfo) except + long size(string name) except + + void profile(string name, unsigned long long *out) except + cdef class PyDDstoreVarinfo: cdef VarInfo c_varinfo @@ -131,6 +133,11 @@ cdef class PyDDStore: # lifetime-contract comment) -- this dict keeps the Python reference # alive for as long as the variable stays registered. cdef dict _gpu_owned_buffers + # DDSTORE_PROFILE=1: Python-side get() timing (see get_profile()). + cdef bint _prof + cdef double _prof_get_s + cdef double _prof_sync_s + cdef long _prof_gets def __cinit__(self, comm_or_none=None, int method=0, str handshake_dir="", int n_core=0, nic_map=None): @@ -152,6 +159,10 @@ cdef class PyDDStore: cdef MPI.Comm mpi_comm self.method = method self._gpu_owned_buffers = {} + self._prof = os.environ.get("DDSTORE_PROFILE", "0") not in ("", "0") + self._prof_get_s = 0.0 + self._prof_sync_s = 0.0 + self._prof_gets = 0 if method != 0: cpu_nic_map.select_fabric_iface(nic_map=nic_map) if method == 2: @@ -241,6 +252,8 @@ cdef class PyDDStore: self._gpu_owned_buffers[name] = arr def get(self, str name, arr, long start=0): + cdef double t_get = time.perf_counter() if self._prof else 0.0 + cdef double t_sync cdef long count = arr.shape[0] cdef size_t ptr cdef int itemsize @@ -252,7 +265,12 @@ cdef class PyDDStore: assert arr.is_contiguous() import torch # See the matching comment in add() for what this guards against. - torch.cuda.synchronize(device=arr.device) + if self._prof: + t_sync = time.perf_counter() + torch.cuda.synchronize(device=arr.device) + self._prof_sync_s += time.perf_counter() - t_sync + else: + torch.cuda.synchronize(device=arr.device) ptr = arr.data_ptr() itemsize = arr.element_size() iface = _hmem_iface_for(arr) @@ -275,6 +293,27 @@ cdef class PyDDStore: self.c_ddstore.get(cname, start, count, ptr, iface) else: self.c_ddstore.get(cname, start, count, ptr, iface) + if self._prof: + self._prof_get_s += time.perf_counter() - t_get + self._prof_gets += 1 + + def get_profile(self, str name): + """DDSTORE_PROFILE=1 timing for `name` (methods 1/2), in seconds. + + C++ counters for this variable: calls, lock_wait, mr (recv-MR cache + check/registration), mr_miss (re-registrations), read (posting + fi_read), cq (waiting for completion). Python counters for this + store, across all variables: py_gets, py_get (whole get() calls), + py_sync (torch.cuda.synchronize on the GPU-destination path). + """ + cdef unsigned long long c[6] + self.c_ddstore.profile(s2b(name), c) + return { + "calls": c[0], "lock_wait": c[1] * 1e-9, "mr": c[2] * 1e-9, + "mr_miss": c[3], "read": c[4] * 1e-9, "cq": c[5] * 1e-9, + "py_gets": self._prof_gets, "py_get": self._prof_get_s, + "py_sync": self._prof_sync_s, + } def epoch_begin(self): self.c_ddstore.epoch_begin() From ddcadd398d1c565ff6d709c864cfa86661f85ad3 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 14:33:14 -0400 Subject: [PATCH 26/56] Add plan for batched get (get_batch) Step-by-step plan with the measured per-sample baseline (DDSTORE_PROFILE, bench_get.py) it is meant to beat. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- docs/plan-batched-get.md | 137 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 docs/plan-batched-get.md diff --git a/docs/plan-batched-get.md b/docs/plan-batched-get.md new file mode 100644 index 0000000..d6f9689 --- /dev/null +++ b/docs/plan-batched-get.md @@ -0,0 +1,137 @@ +# Plan: batched `get` (`get_batch`) + +Status: planned, not started (updated 2026-10-04, branch `check-thread`, after +`3dadb32`, which added `DDSTORE_PROFILE`, `bench_get.py` and the VAE +`--replicate`/`--image-scale` options used below). + +## Motivation (measured) + +Today `DistDataset.get()` makes one `PyDDStore.get()` per sample, and each call +does: allocate a 1-row tensor, whole-device `torch.cuda.synchronize()` (GPU +destination only), take the per-variable `recv_lock`, check/register the +recv MR, post one `fi_read`, busy-poll the CQ, return to Python. + +`DDSTORE_PROFILE=1` on Frontier (`method=1`, cxi, 2 nodes x 8 ranks), µs per +`get()`: + +| VAE run | total | GPU sync | lock wait | MR | read post | CQ wait | other | +|---|---|---|---|---|---|---|---| +| host, 0/1 workers (S=1) | 7.9 / 9.3 | – | 0.0 | 0.1 | 0.6 | 3.6 | 3.6 / 5.0 | +| GPU, 0 workers (S=1 / S=2) | 13.6 / 13.6 | 3.8 / 3.7 | 0.1 | 0.3 | 0.6 | 3.7 / 4.0 | ~5 | +| GPU, 1 worker (S=1 / S=2) | 142.7 / 198.7 | **131.6 / 187.3** | 0.1 | 0.4 | 0.6 | 3.7 / 4.0 | ~6 | +| GPU, 2 workers (S=1 / S=2) | 310.7 / 452.8 | **287.2 / 429.3** | 0.2 | 0.7 | 0.7 | 3.7 / 4.1 | ~18 | + +`bench_get.py`, single-row `get()`, 1 thread, µs: 3 KB host 8.6 / GPU 19.5 +(12.2 reusing the buffer); 12.5 KB host 9.9 / GPU 20.5; 200 KB host 53 / GPU 37; +1 MB host 206–276 / GPU 134. A second thread adds no per-rank throughput: lock +wait ≈ transfer time at large rows. + +What this says about the design: +- **The per-call GPU sync is the cost to remove.** With a worker running next + to training it waits for the training kernels (130–430 µs per sample); + with no worker the GPU is idle and it costs ~4 µs — still as much as the RDMA. +- **Fixed per-call overhead dominates small rows:** the RDMA round trip is + ~4–5 µs, while sync + allocation + Python add ~10 µs per GPU `get()`. +- **Reads are serialized** by `recv_lock`, so more threads don't add bandwidth. +- **Not worth fixing:** lock contention (≤0.2 µs) and MR registration (<1 µs + even at 100% misses — libfabric caches registrations). No multi-entry MR cache. + +A batched read pays the sync, lock, MR check and Python call once per batch +instead of once per sample, and posts all of a batch's `fi_read`s before +waiting, so the round trips overlap on the network. + +Checked facts: +- torch 2.14 `_MapDatasetFetcher.fetch()` calls `dataset.__getitems__(indices)` + if defined, so the standard DataLoader picks up a batch hook without changes. +- cxi (`fi_info -p cxi -v`, login node): tx `size: 1024`, `rma_iov_limit: 1` + — up to 1024 outstanding ops per endpoint, one contiguous region per + `fi_read`. Re-check on a compute node. + +## 1. C++ (`include/ddstore.hpp`, `src/common.cxx`) + +- New `DDStore::get_batch(name, const long *idx, long n, T *buffer, int hmem_iface)`: + row `i` of the contiguous `n`-row `buffer` receives global row `idx[i]`. + Same per-row validation as `get()` (range, `sortedsearch` target, itemsize), + done for all rows before anything is posted. +- method 0: loop the existing per-row `MPI_Get` path (unchanged behavior). +- methods 1/2, holding `recv_lock` once for the whole batch: + 1. Register the whole batch buffer once (`n * row_bytes`) through the existing + recv-MR region cache. + 2. Post all `n` `fi_read`s back to back (target `comm_partner[t]`, + `remote_address[t] + offset`, `remote_key[t]`, local `buffer + i*row_bytes`, + same MR desc). On `-FI_EAGAIN`, drain completions with `fi_cq_read` and retry. + Optional: coalesce runs of consecutive indices on the same target into one read. + 3. Wait for exactly `n` completions. On any error, keep draining the + completions of reads already posted before returning — a stale completion + left in the CQ would be consumed by the next call. +- Refactor `read_from_remote()` into `ensure_recv_mr()` + `post_read()` + + `wait_completions(k)`; single-row `get()` = post 1 + wait 1 (same behavior). +- Extend the `DDSTORE_PROFILE` counters: count batches and rows separately so + per-row and per-batch costs can both be reported. + +## 2. Cython (`src/pyddstore.pyx`) + +- `get_batch(name, arr, indices)`: `arr` shape `(n, ...)` (numpy or CUDA/HIP + tensor), `indices` converted to a contiguous int64 array of length `n`. +- Same checks as `get()` (`_check_dtype`, contiguity, GPU fabric preconditions). +- One `torch.cuda.synchronize(device)` per batch on the GPU path (keep it + device-wide for now: removing it entirely caused GPU faults; see Follow-ups). +- C++ call under `with nogil:`, item-size dispatch like `get()`. +- Python-side profiling as in `get()` (whole-call and sync time). + +## 3. Dataset (`examples/vae/distdataset.py`, `ddstore_dataloader.py`) + +- `DistDataset.__getitems__(indices)` / `DistDatasetReader.__getitems__`: + allocate one `(n, data_disp)` buffer (numpy, or `torch.empty(..., device=...)`), + `get_batch` data and labels, return + `[(row_i.reshape(1, side, side), label_i) for i]` so `collate_fn` works unchanged. +- `ThreadDataLoader.fetch()`: use `dataset.__getitems__(index)` when present, + else the per-sample list (mirror torch's fetcher). +- `DDSTORE_BATCH_GET=0` env var to fall back to per-sample `get()` for A/B. + +## 4. Tests + +Note: `test_single.py` / `test_multirank.py` always use `method=0`, so they +only cover `get_batch`'s MPI fallback. Add method 1 coverage explicitly. + +- `test_single` / `test_multirank`: `get_batch` with shuffled indices spanning + all ranks, duplicates, `n == 1`; compare row by row with per-row `get()`. + Out-of-range index raises and leaves the store usable (a later `get()` works). +- Method 1 (cxi) multi-rank: the same cases, in `test_gpu_rdma.py` or a new + libfabric test file (host destination too, not only GPU). +- `test_gpu_rdma`: same into a GPU tensor; concurrent `get_batch` from threads. + +## 5. Measure (`method=1`, cxi, 2 nodes x 8 ranks, `DDSTORE_PROFILE=1`) + +- `bench_get.py --batch N` (new option): rows per call 1/32/128, row sizes + 3 KB, 12.5 KB, 200 KB, 1 MB, host / GPU destination, 1–2 threads. +- `vae-ddp.py --image-scale 1` and `2` (optionally `--replicate 4`), host path and + `--gpu-dest --gpu-source`, `--num-workers` 0/1/2, `DDSTORE_BATCH_GET` on/off. + Check the epoch-8 loss is unchanged (8.8960 at S=1; 30.0178 at S=2). +- Baselines (per-sample `get`, epochs 2–8 avg, s/epoch): + S=1 — host w0 0.25, host w1 0.16, GPU w0 0.30, GPU w1 0.26, GPU w2 0.40; + S=2 — host w0 0.39, host w1 0.28, GPU w0 0.42, GPU w1 0.42, GPU w2 0.55. +- Expected: GPU path loses most of its gap (128 syncs per batch -> 1, overlapped + reads); host path gains a little; extra workers matter even less. +- Update README (`get_batch` API, results). + +## Follow-ups (after batching) + +- The one remaining sync per batch still waits for training kernels when a + worker runs. Options: a per-worker CUDA/HIP stream + stream-only sync (raise + `GPU_MAX_HW_QUEUES`, see README), or a per-worker pre-registered receive + ring with events. Must be re-verified against the GPU fault seen when the + sync was removed. +- Block-shuffling sampler so consecutive rows on the same rank coalesce into + one larger read. + +## Risks / open questions + +- Batch indices span many targets: 128 concurrent reads to up to 16 ranks is + within the 1024 tx queue, but receiver-side limits are unmeasured. +- Partial-post failure paths must drain in-flight reads before the buffer is + handed back (the subtle part). +- cxi `threading` level irrelevant here because the lock is kept. + +Size estimate: C++ ~120 lines (mostly the refactor), Cython ~40, dataset ~30, +tests ~100, bench ~20. From cb1761b07eec2a998e9983ad5b1c1cfa6a3b3b33 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 14:58:44 -0400 Subject: [PATCH 27/56] Add batched get: get_batch() and DistDataset.__getitems__ One call per training batch instead of one get() per sample. - C++: read_from_remote() split into ensure_recv_mr(), reap_one() and read_batch_from_remote(), which posts all of a batch's fi_read()s (reaping completions when the TX queue is full) before waiting, and always waits for every posted read, even after an error, so no stale completion is left for the next call. Single-row get() is a 1-row batch. DDStore::get_batch() validates every index first, then takes the per-variable lock once; method 0 loops the per-row MPI_Get path. DDSTORE_PROFILE also counts rows. - Cython: PyDDStore.get_batch(name, arr, indices): one GPU sync per batch, GIL released, ValueError on rows/indices mismatch. - DistDataset / DistDatasetReader.__getitems__ (used by DataLoader and ThreadDataLoader); DDSTORE_BATCH_GET=0 falls back to per-sample get(). - bench_get.py --batch N (per-row reporting); vae-ddp profile summary per call and per row. - test/test_get_batch.py: method 0 and method 1 (cxi), GPU destination, concurrent threads, error recovery. Frontier (cxi, 2 nodes x 8 ranks): new tests 20/20 on 4 ranks; existing suites pass. VAE epoch time with batching, S=1: host w1 0.156 -> 0.126 s, GPU w1 0.259 -> 0.127 s, GPU w2 0.403 -> 0.134 s (S=2: GPU w1 0.416 -> 0.264 s); losses unchanged (8.8960 / 30.0178). Per-row GPU sync drops from 129-383 us to ~0.1 us. bench: 3 KB rows 9.0 -> 0.68 us/row (host), 20.9 -> 0.78 (GPU); GPUDirect now beats host from 12.5 KB rows up. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- README.md | 30 +++ examples/scripts/bench_get.py | 51 +++-- examples/vae/ddstore_dataloader.py | 8 +- examples/vae/distdataset.py | 43 ++++ examples/vae/vae-ddp.py | 14 +- include/common.h | 8 +- include/ddstore.hpp | 75 ++++++- src/common.cxx | 345 +++++++++++++++++------------ src/pyddstore.pyx | 64 +++++- test/test_get_batch.py | 189 ++++++++++++++++ 10 files changed, 648 insertions(+), 179 deletions(-) create mode 100644 test/test_get_batch.py diff --git a/README.md b/README.md index f455fdc..1ad798c 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,22 @@ Read `arr.shape[0]` consecutive rows starting at global index `start` into `arr` --- +### `get_batch(name, arr, indices)` + +Read rows `indices` (global row ids; any order, any ranks, repeats allowed) into `arr`: row `i` of `arr` receives row `indices[i]`, so `arr.shape[0]` must equal `len(indices)` (else `ValueError`). Same buffer rules as `get()` (NumPy array or CUDA/HIP tensor). Every index is checked before anything is read; an out-of-range one raises `IndexError` and leaves the store usable. + +For `method=1`/`2` the whole batch is one call: one lock acquisition, one memory registration, and (GPU destination) one device sync, with all of the batch's `fi_read`s posted before any is waited for, so the reads overlap on the network. It is still one `fi_read` per row. `method=0` loops the per-row `MPI_Get` path. + +```python +idx = np.array([2048, 7, 4096, 7]) +out = np.zeros((len(idx), 64), dtype=np.float32) +store.get_batch("features", out, idx) +``` + +`DistDataset`/`DistDatasetReader` use it through `__getitems__`, which PyTorch's `DataLoader` (and `ThreadDataLoader`) calls with a whole batch's indices; `DDSTORE_BATCH_GET=0` falls back to one `get()` per sample. + +--- + ### `join(name)` `method=2` extra member only. Discovers a variable published by the core group by polling the handshake directory until the combined record file (`{name}.bin`) written by core rank 0 reaches its expected size (up to `DDSTORE_HANDSHAKE_TIMEOUT_S` seconds), then registers it for `get()`. @@ -361,6 +377,8 @@ Measured with `vae-ddp.py`, `method=1`, `cxi`, 2 Frontier nodes × 8 ranks, `VAE On the host path one worker thread (background prefetch) cuts epoch time ~30%; more workers make it steadily worse as they contend for the per-variable lock and the GIL. On the GPU path threading doesn't help, because of the per-call whole-device sync. All runs finished with the same final loss. Recommended: `--num-workers=1` on the host path, `0` on the GPU path. +These numbers use one `get()` per sample. With batched get (the default now; see [get_batch](#get_batchname-arr-indices) and the measurements under [Profiling](#profiling-get-ddstore_profile1)), every configuration is faster and the GPU path no longer suffers from workers. + The table predates moving batch collation into the worker thread (`ThreadDataLoader.fetch()`); with that change, the host path measured fetch ≈ 0.006 s / total ≈ 0.15 s per epoch with 1 worker and 0.044 / 0.19 with 2 (GPU path with 1 worker unchanged at 0.09 / 0.25). ```bash @@ -407,6 +425,17 @@ Measured on Frontier (`method=1`, `cxi`, 2 nodes × 8 ranks): - `bench_get.py`, one thread, µs per single-row `get()`: 3 KB — host 8.6, GPU 19.5 (12.2 reusing the buffer); 12.5 KB — host 9.9, GPU 20.5; 200 KB — host 53, GPU 37; 1 MB — host 206–276, GPU 134. GPUDirect wins from somewhere between 12.5 KB and 200 KB per row; below that its fixed per-call overhead (sync, allocation) dominates. - A second thread adds no per-rank throughput: the per-variable lock serializes the transfers (lock wait ≈ transfer time at large rows). +With `get_batch()` (`bench_get.py --batch 128`, µs per row, 1 thread): 3 KB — host 0.68, GPU 0.78 (from 9.0 / 20.9 with one row per call); 12.5 KB — host 1.82, GPU 1.38; 200 KB — host 27.5, GPU 19.6; 1 MB — host 189, GPU 99 (~10.6 GB/s per rank). The GPU sync drops to ~0.06 µs per row, and GPUDirect now beats host from 12.5 KB rows up. (Host-destination batches of 1 MB rows are slower than single reads; not investigated.) + +VAE (`vae-ddp.py`, epochs 2–8 average, s/epoch), per-sample `get()` → batched (`DDSTORE_BATCH_GET` 0 → 1); losses identical (8.8960 at S=1, 30.0178 at S=2): + +| `--image-scale` | host, 0 workers | host, 1 worker | GPU, 0 workers | GPU, 1 worker | GPU, 2 workers | +|---|---|---|---|---|---| +| 1 | 0.247 → 0.132 | 0.156 → 0.126 | 0.263 → 0.135 | 0.259 → 0.127 | 0.403 → 0.134 | +| 2 | 0.395 → 0.286 | 0.293 → 0.265 | 0.428 → 0.277 | 0.416 → 0.264 | 0.518 → 0.297 | + +The per-row GPU sync falls from 129–383 µs (with workers) to ~0.1 µs, and training compute time recovers because it no longer waits behind the workers' syncs. + ## Testing ### Unit tests (pytest) @@ -440,6 +469,7 @@ DDSTORE_FABRIC=cxi mpirun -n 2 python -m pytest test/test_gpu_rdma.py -v | `test/test_single.py` | 1 | All dtypes, `add`/`get`, `init`/`update`/`get`, error handling, double `free()` | | `test/test_multirank.py` | 2 (4 recommended) | Remote reads, shard boundaries, multiple variables, `ddstore_width` grouping | | `test/test_gpu_rdma.py` | 2 | GPU-resident `add()`/`get()` in both directions, both libfabric methods, negative/error cases | +| `test/test_get_batch.py` | 2 (4 recommended) | `get_batch()`: shuffled indices across ranks with repeats, single row, dtypes, error recovery, GPU destination, concurrent threads; method 0, plus method 1 over `cxi` inside a Slurm step | ### Integration scripts diff --git a/examples/scripts/bench_get.py b/examples/scripts/bench_get.py index 2b544cd..c03bebf 100644 --- a/examples/scripts/bench_get.py +++ b/examples/scripts/bench_get.py @@ -1,9 +1,10 @@ """Microbenchmark for DDStore get(): per-call latency and throughput vs row size. -Each rank adds a shard of random float32 rows, then issues --nget single-row -get()s of uniformly random global rows (most of them remote) and reports the -average per-get latency and per-rank throughput, plus the DDSTORE_PROFILE -breakdown (lock wait / MR / fi_read post / CQ wait / GPU sync). +Each rank adds a shard of float32 rows, then reads --nget uniformly random +global rows (most of them remote), either one get() per row (--batch 1) or +get_batch() of --batch rows per call, and reports time per row, per-rank +throughput, and the DDSTORE_PROFILE breakdown per row (lock wait / MR / +fi_read post / CQ wait / GPU sync). Run (method 1, cxi), e.g. on 2 nodes: DDSTORE_PROFILE=1 DDSTORE_FABRIC=cxi srun -N2 -n16 -c7 --gpus-per-task=1 \\ @@ -37,6 +38,8 @@ help="comma-separated: host, gpu, gpu-reuse") parser.add_argument("--gpu-source", action="store_true", help="add() the shard as a GPU tensor instead of numpy") +parser.add_argument("--batch", default="1", + help="comma-separated rows per call: 1 = get(), >1 = get_batch()") parser.add_argument("--threads", default="1", help="comma-separated thread counts issuing get()s concurrently") parser.add_argument("--method", type=int, default=int(os.environ.get("DDSTORE_METHOD", "1"))) @@ -52,6 +55,7 @@ row_floats = [int(x) for x in args.row_floats.split(",")] dests = args.dest.split(",") thread_counts = [int(x) for x in args.threads.split(",")] +batches = [int(x) for x in args.batch.split(",")] total_rows = args.rows_per_rank * size if rank == 0: @@ -59,15 +63,15 @@ f"method={args.method} fabric={os.environ.get('DDSTORE_FABRIC', 'hsn')} " f"gpu_source={args.gpu_source} profile={os.environ.get('DDSTORE_PROFILE', '0')}", flush=True) - print(f"{'row_B':>8} {'dest':>9} {'thr':>3} {'us/get':>8} {'MB/s/rank':>9} | " - f"{'sync':>6} {'lock':>6} {'mr':>6} {'miss%':>6} {'read':>6} {'cq':>6} {'other':>6} (us/get)", + print(f"{'row_B':>8} {'dest':>9} {'batch':>5} {'thr':>3} {'us/row':>8} {'MB/s/rank':>9} | " + f"{'sync':>6} {'lock':>6} {'mr':>6} {'miss%':>6} {'read':>6} {'cq':>6} {'other':>6} (us/row)", flush=True) for nf in row_floats: for dest in dests: if dest != "host" and device is None: continue - for nthr in thread_counts: + for B, nthr in [(b, t) for b in batches for t in thread_counts]: store = dds.PyDDStore(comm, method=args.method) rng = np.random.default_rng(rank) shard = np.full((args.rows_per_rank, nf), float(rank), dtype=np.float32) @@ -81,20 +85,25 @@ chunks = np.array_split(idx, nthr) def worker(ids): - reuse_host = np.empty((1, nf), dtype=np.float32) - reuse_gpu = (torch.empty((1, nf), dtype=torch.float32, device=device) + reuse_host = np.empty((B, nf), dtype=np.float32) + reuse_gpu = (torch.empty((B, nf), dtype=torch.float32, device=device) if device is not None else None) - for g in ids: + for s0 in range(0, len(ids), B): + chunk = ids[s0:s0 + B] + k = len(chunk) if dest == "host": - store.get("x", reuse_host, int(g)) + out = reuse_host[:k] elif dest == "gpu": - out = torch.empty((1, nf), dtype=torch.float32, device=device) - store.get("x", out, int(g)) + out = torch.empty((k, nf), dtype=torch.float32, device=device) else: - store.get("x", reuse_gpu, int(g)) + out = reuse_gpu[:k] + if B == 1: + store.get("x", out, int(chunk[0])) + else: + store.get_batch("x", out, chunk) - # One warm-up get so first-call registration isn't timed as typical. - worker(idx[:1]) + # One warm-up call so first-call registration isn't timed as typical. + worker(idx[:B]) comm.Barrier() p0 = store.get_profile("x") comm.Barrier() @@ -113,16 +122,16 @@ def worker(ids): d = {k: p1[k] - p0[k] for k in p1} vals = comm.gather((dt, d), root=0) if rank == 0: - n = sum(v[1]["calls"] for v in vals) or 1 + n = sum(v[1]["rows"] for v in vals) or 1 ngets = args.nget * size - us_get = 1e6 * sum(v[0] for v in vals) / ngets * nthr + us_get = 1e6 * sum(v[0] for v in vals) / ngets mbps = nf * 4 * args.nget / (sum(v[0] for v in vals) / size) / 1e6 s = lambda k: 1e6 * sum(v[1][k] for v in vals) / n other = s("py_get") - s("py_sync") - s("lock_wait") - s("mr") - s("read") - s("cq") miss = 100.0 * sum(v[1]["mr_miss"] for v in vals) / n - print(f"{nf * 4:>8} {dest:>9} {nthr:>3} {us_get:>8.1f} {mbps:>9.1f} | " - f"{s('py_sync'):>6.1f} {s('lock_wait'):>6.1f} {s('mr'):>6.1f} " - f"{miss:>6.1f} {s('read'):>6.1f} {s('cq'):>6.1f} {other:>6.1f}", + print(f"{nf * 4:>8} {dest:>9} {B:>5} {nthr:>3} {us_get:>8.2f} {mbps:>9.1f} | " + f"{s('py_sync'):>6.2f} {s('lock_wait'):>6.2f} {s('mr'):>6.2f} " + f"{miss:>6.1f} {s('read'):>6.2f} {s('cq'):>6.2f} {other:>6.2f}", flush=True) # Every rank must finish reading before any rank tears down its # endpoint (gather() doesn't synchronize non-root ranks). diff --git a/examples/vae/ddstore_dataloader.py b/examples/vae/ddstore_dataloader.py index c2ada2a..ab42560 100644 --- a/examples/vae/ddstore_dataloader.py +++ b/examples/vae/ddstore_dataloader.py @@ -69,8 +69,12 @@ def worker_init(counter): def fetch(dataset, ibatch, index, collate_fn=None, pin_memory=False): # Collate here, in the worker, before pinning: pinning per-sample # tensors and collating afterwards would just torch.stack them into - # a new, unpinned tensor. - batch = [dataset[i] for i in index] + # a new, unpinned tensor. Use the dataset's whole-batch fetch when it + # has one, like torch's own map-style fetcher. + if getattr(dataset, "__getitems__", None): + batch = dataset.__getitems__(index) + else: + batch = [dataset[i] for i in index] if collate_fn is not None: batch = collate_fn(batch) if pin_memory: diff --git a/examples/vae/distdataset.py b/examples/vae/distdataset.py index fa02a4b..24bb243 100644 --- a/examples/vae/distdataset.py +++ b/examples/vae/distdataset.py @@ -7,6 +7,9 @@ import pyddstore as dds +# DDSTORE_BATCH_GET=0: __getitems__ falls back to one get() per sample. +_BATCH_GET = os.environ.get("DDSTORE_BATCH_GET", "1") != "0" + def nsplit(a, n): k, m = divmod(len(a), n) @@ -162,6 +165,26 @@ def get(self, idx, device=None): def __getitem__(self, idx): return self.get(idx, device=self.device) + def __getitems__(self, indices): + """Whole-batch fetch, called by DataLoader (and ThreadDataLoader) with + a batch's indices: one PyDDStore.get_batch() per variable instead of + one get() per sample. DDSTORE_BATCH_GET=0 falls back to per-sample + get() (for A/B comparison).""" + if not _BATCH_GET: + return [self.get(i, device=self.device) for i in indices] + n = len(indices) + label = np.zeros(n, dtype=np.int32) + if self.device is not None: + val = torch.empty((n, self.data_disp), dtype=torch.float32, device=self.device) + else: + val = np.zeros((n, self.data_disp), dtype=np.float32) + self.ddstore.get_batch(f"{self.label}data", val, indices) + self.ddstore.get_batch(f"{self.label}labels", label, indices) + if self.device is None: + val = torch.from_numpy(val) + val = val.reshape(n, 1, self.side, self.side) + return [(val[i], label[i]) for i in range(n)] + class DistDatasetReader(Dataset): """Distributed dataset class — extra (read-only) member. @@ -224,3 +247,23 @@ def get(self, idx, device=None): def __getitem__(self, idx): return self.get(idx, device=self.device) + + def __getitems__(self, indices): + """Whole-batch fetch, called by DataLoader (and ThreadDataLoader) with + a batch's indices: one PyDDStore.get_batch() per variable instead of + one get() per sample. DDSTORE_BATCH_GET=0 falls back to per-sample + get() (for A/B comparison).""" + if not _BATCH_GET: + return [self.get(i, device=self.device) for i in indices] + n = len(indices) + label = np.zeros(n, dtype=np.int32) + if self.device is not None: + val = torch.empty((n, self.data_disp), dtype=torch.float32, device=self.device) + else: + val = np.zeros((n, self.data_disp), dtype=np.float32) + self.ddstore.get_batch(f"{self.label}data", val, indices) + self.ddstore.get_batch(f"{self.label}labels", label, indices) + if self.device is None: + val = torch.from_numpy(val) + val = val.reshape(n, 1, self.side, self.side) + return [(val[i], label[i]) for i in range(n)] diff --git a/examples/vae/vae-ddp.py b/examples/vae/vae-ddp.py index bb228a9..b59a4e9 100644 --- a/examples/vae/vae-ddp.py +++ b/examples/vae/vae-ddp.py @@ -315,19 +315,27 @@ def test(epoch): if rank == 0: n = max(tot["calls"], 1) us = lambda x: 1e6 * x / n + us_row = lambda x: 1e6 * x / max(tot["rows"], 1) other = tot["py_get"] - tot["py_sync"] - tot["lock_wait"] - tot["mr"] - tot["read"] - tot["cq"] print( - "[ddstore-profile] all ranks: gets={} py_gets={} mr_miss={} ({:.1%})".format( - tot["calls"], tot["py_gets"], tot["mr_miss"], tot["mr_miss"] / n + "[ddstore-profile] all ranks: calls={} rows={} py_calls={} mr_miss={} ({:.1%})".format( + tot["calls"], tot["rows"], tot["py_gets"], tot["mr_miss"], tot["mr_miss"] / n ) ) print( - "[ddstore-profile] per get (us): total={:.1f} sync={:.1f} lock_wait={:.1f} " + "[ddstore-profile] per call (us): total={:.1f} sync={:.1f} lock_wait={:.1f} " "mr={:.1f} read={:.1f} cq={:.1f} other={:.1f}".format( us(tot["py_get"]), us(tot["py_sync"]), us(tot["lock_wait"]), us(tot["mr"]), us(tot["read"]), us(tot["cq"]), us(other) ), flush=True, ) + print( + "[ddstore-profile] per row (us): total={:.2f} sync={:.2f} read+cq={:.2f}".format( + us_row(tot["py_get"]), us_row(tot["py_sync"]), + us_row(tot["read"] + tot["cq"]) + ), + flush=True, + ) dist.destroy_process_group() diff --git a/include/common.h b/include/common.h index e3b8e4a..8ab18e8 100644 --- a/include/common.h +++ b/include/common.h @@ -111,7 +111,8 @@ extern "C" * prof_lock_wait_ns is the time spent waiting to acquire recv_lock; * mr = recv-MR cache check / (re)registration; read = posting * fi_read(); cq = polling the CQ until the read completes. */ - uint64_t prof_calls; + uint64_t prof_calls; /* get() / get_batch() calls */ + uint64_t prof_rows; /* rows read by those calls */ uint64_t prof_lock_wait_ns; uint64_t prof_mr_ns; uint64_t prof_mr_miss; @@ -190,6 +191,11 @@ extern "C" void init_fabric(struct fabric_state *fabric); int handshake(struct fabric_state *fabric_state, MPI_Comm comm); int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset); + /* n rows of row_len bytes into recv_data (recv_data_len == n * row_len); + * row i from rank src[i] at byte offset offset[i]. All reads are posted + * before any is waited for. 0 on success. See common.cxx. */ + int read_batch_from_remote(struct fabric_state *fabric_state, long n, + const int *src, const uint64_t *offset, size_t row_len); /* --- Method 2: file-based handshake ---------------------------------- */ diff --git a/include/ddstore.hpp b/include/ddstore.hpp index ef005ec..e771f0d 100644 --- a/include/ddstore.hpp +++ b/include/ddstore.hpp @@ -68,11 +68,11 @@ class DDStore void join(std::string name); /* DDSTORE_PROFILE=1 counters for `name` (methods 1/2), as - * {calls, lock_wait_ns, mr_ns, mr_miss, read_ns, cq_ns}; all zero for - * method 0 or when profiling is off. Takes the variable's lock. */ - void profile(std::string name, unsigned long long out[6]) + * {calls, lock_wait_ns, mr_ns, mr_miss, read_ns, cq_ns, rows}; all zero + * for method 0 or when profiling is off. Takes the variable's lock. */ + void profile(std::string name, unsigned long long out[7]) { - for (int i = 0; i < 6; i++) + for (int i = 0; i < 7; i++) out[i] = 0; const VarInfo_t &varinfo = this->varlist.at(name); struct fabric_state *fs = varinfo.fabric_state; @@ -85,6 +85,7 @@ class DDStore out[3] = fs->prof_mr_miss; out[4] = fs->prof_read_ns; out[5] = fs->prof_cq_ns; + out[6] = fs->prof_rows; } /* hmem_iface: 0 (FI_HMEM_SYSTEM) for a host buffer, or an fi_hmem_iface @@ -524,6 +525,72 @@ class DDStore } } + /* Batched get: row i of `buffer` (n contiguous rows) receives global + * row idx[i]; rows may come from any ranks, in any order, with repeats. + * Every index is validated before anything is read. Methods 1/2 take the + * variable's lock once and post all n fi_read()s before waiting for any + * (read_batch_from_remote()); method 0 loops the per-row MPI_Get path. + * hmem_iface as in get(). */ + template + void get_batch(std::string name, const long *idx, long n, T *buffer, int hmem_iface = 0) + { + const VarInfo_t& varinfo = this->varlist.at(name); + + if (varinfo.itemsize != sizeof(T)) + throw std::invalid_argument("Invalid data type"); + if (n <= 0) + return; + + size_t row_bytes = (size_t)varinfo.disp * varinfo.itemsize; + std::vector target(n); + std::vector offset(n); + for (long i = 0; i < n; i++) + { + int t = sortedsearch(varinfo.lenlist, idx[i]); /* throws if out of range */ + long first = t > 0 ? varinfo.lenlist[t - 1] : 0; + target[i] = t; + offset[i] = (uint64_t)(idx[i] - first) * row_bytes; + } + + if (this->method == 0) + { + if (hmem_iface != 0) + throw std::runtime_error("GPU destination buffer is not supported with method=0 (MPI_Win)"); + MPI_Win win = varinfo.win; + for (long i = 0; i < n; i++) + { + MPI_Win_lock(MPI_LOCK_SHARED, target[i], 0, win); + MPI_Get((char *)buffer + i * row_bytes, (int)row_bytes, MPI_BYTE, + target[i], (MPI_Aint)(offset[i] / row_bytes), + (int)row_bytes, MPI_BYTE, win); + MPI_Win_unlock(target[i], win); + } + return; + } + + /* Methods 1 and 2: one lock acquisition for the whole batch. */ + const bool prof = ddstore_profile_enabled(); + uint64_t t_wait = prof ? ddstore_now_ns() : 0; + fabric_state_lock_guard lock(varinfo.fabric_state); + if (prof) + { + varinfo.fabric_state->prof_calls++; + varinfo.fabric_state->prof_lock_wait_ns += ddstore_now_ns() - t_wait; + } + if (hmem_iface != 0 && !is_hmem_capable(varinfo.fabric_state)) + throw std::runtime_error( + "GPU destination buffer requires DDSTORE_FABRIC=cxi " + "(current fabric does not support FI_HMEM)"); + varinfo.fabric_state->recv_data = (char *)buffer; + varinfo.fabric_state->recv_data_len = n * row_bytes; + varinfo.fabric_state->recv_hmem_iface = hmem_iface; + int rc = read_batch_from_remote(varinfo.fabric_state, n, target.data(), + offset.data(), row_bytes); + if (rc != 0) + throw std::runtime_error( + "read_batch_from_remote failed with code " + std::to_string(rc) + + " (" + std::to_string(n) + " rows)"); + } private: int method; // 0: MPI, 1: libfabric, 2: file-based handshake (libfabric transport) diff --git a/src/common.cxx b/src/common.cxx index 395cab3..a8d49cb 100644 --- a/src/common.cxx +++ b/src/common.cxx @@ -659,23 +659,18 @@ int handshake(struct fabric_state *fabric_state, MPI_Comm comm) return 0; } -int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset) +/* Make sure recv_data..recv_data+recv_data_len is covered by a registered + * recv MR. Returns 0, or 1 after printing the libfabric error. + * + * The recv MR is cached by registered region rather than exact pointer: + * register recv_data..recv_data+recv_data_len on a miss, and reuse the + * MR while later buffers lie inside [recv_mr_base, recv_mr_base + + * recv_mr_reg_len). Hits when the caller passes the same buffer again -- + * e.g. PyTorch's caching allocator returning the same block for a + * same-shape torch.empty() -- so each get() needn't re-register. */ +static int ensure_recv_mr(struct fabric_state *fabric_state) { bool recv_is_hmem = fabric_state->recv_hmem_iface != FI_HMEM_SYSTEM; - if (recv_is_hmem && !is_hmem_capable(fabric_state)) - { - fprintf(stderr, "GPU (HMEM) recv buffer requested but fabric is not cxi\n"); - return 1; - } - - /* Cache the recv MR by registered region rather than exact pointer: - * register recv_data..recv_data+recv_data_len on a miss, and reuse the - * MR while later buffers lie inside [recv_mr_base, recv_mr_base + - * recv_mr_reg_len). Hits when the caller passes the same buffer again -- - * e.g. PyTorch's caching allocator returning the same block for a - * same-shape torch.empty() -- so each get() needn't re-register. */ - const bool prof = ddstore_profile_enabled(); - uint64_t t_mr = prof ? ddstore_now_ns() : 0; char *cur_base = fabric_state->recv_data; size_t cur_len = fabric_state->recv_data_len; bool in_cached_region = @@ -683,72 +678,147 @@ int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset (cur_base >= fabric_state->recv_mr_base) && (cur_base + cur_len <= fabric_state->recv_mr_base + fabric_state->recv_mr_reg_len); - if (!in_cached_region) + if (in_cached_region) + return 0; + + if (ddstore_profile_enabled()) + fabric_state->prof_mr_miss++; + /* Close the stale registration before creating a new one. */ + if (fabric_state->recv_mr) { - if (prof) - fabric_state->prof_mr_miss++; - /* Close the stale registration before creating a new one. */ - if (fabric_state->recv_mr) - fi_close(&fabric_state->recv_mr->fid); + fi_close(&fabric_state->recv_mr->fid); + fabric_state->recv_mr = NULL; + } - int mr_rc; - if (recv_is_hmem) - { - /* GPU destination buffer (ROCr on AMD, CUDA on NVIDIA -- whichever - * iface the caller set). No host-staged fallback exists: either - * fi_mr_regattr succeeds and fi_read() below DMAs straight into - * device memory, or it fails loudly here (checked below). */ - struct iovec iov = {cur_base, cur_len}; - struct fi_mr_attr attr; - memset(&attr, 0, sizeof(attr)); - attr.mr_iov = &iov; - attr.iov_count = 1; - attr.access = FI_READ; - attr.iface = (enum fi_hmem_iface)fabric_state->recv_hmem_iface; - attr.device.reserved = 0; /* ROCr/CUDA both resolve the device from the pointer */ - mr_rc = fi_mr_regattr(fabric_state->domain, &attr, 0, &fabric_state->recv_mr); - } - else - { - mr_rc = fi_mr_reg( - fabric_state->domain, - cur_base, - cur_len, - FI_READ, - 0, 0, 0, - &fabric_state->recv_mr, - NULL); - } - if (mr_rc != FI_SUCCESS) + int mr_rc; + if (recv_is_hmem) + { + /* GPU destination buffer (ROCr on AMD, CUDA on NVIDIA -- whichever + * iface the caller set). No host-staged fallback exists: either + * fi_mr_regattr succeeds and fi_read() DMAs straight into device + * memory, or it fails loudly here (checked below). */ + struct iovec iov = {cur_base, cur_len}; + struct fi_mr_attr attr; + memset(&attr, 0, sizeof(attr)); + attr.mr_iov = &iov; + attr.iov_count = 1; + attr.access = FI_READ; + attr.iface = (enum fi_hmem_iface)fabric_state->recv_hmem_iface; + attr.device.reserved = 0; /* ROCr/CUDA both resolve the device from the pointer */ + mr_rc = fi_mr_regattr(fabric_state->domain, &attr, 0, &fabric_state->recv_mr); + } + else + { + mr_rc = fi_mr_reg( + fabric_state->domain, + cur_base, + cur_len, + FI_READ, + 0, 0, 0, + &fabric_state->recv_mr, + NULL); + } + if (mr_rc != FI_SUCCESS) + { + fprintf(stderr, "%s failed: %s\n", + recv_is_hmem ? "fi_mr_regattr" : "fi_mr_reg", + fi_strerror(mr_rc)); + fabric_state->recv_mr = NULL; + return 1; + } + + /* CXI (FI_MR_ENDPOINT): bind and enable recv MR before use. No-op for + * hsn/verbs/gni/psm2 (is_mr_endpoint() is false for those). */ + if (is_mr_endpoint(fabric_state)) + { + int rc_mr = fi_mr_bind(fabric_state->recv_mr, &fabric_state->signal->fid, 0); + if (rc_mr == FI_SUCCESS) + rc_mr = fi_mr_enable(fabric_state->recv_mr); + if (rc_mr != FI_SUCCESS) { - fprintf(stderr, "%s failed: %s\n", - recv_is_hmem ? "fi_mr_regattr" : "fi_mr_reg", - fi_strerror(mr_rc)); + fprintf(stderr, "fi_mr_bind/fi_mr_enable (recv) failed: %s\n", fi_strerror(rc_mr)); + fi_close(&fabric_state->recv_mr->fid); + fabric_state->recv_mr = NULL; return 1; } + } - /* CXI (FI_MR_ENDPOINT): bind and enable recv MR before use. No-op for - * hsn/verbs/gni/psm2 (is_mr_endpoint() is false for those). */ - if (is_mr_endpoint(fabric_state)) - { - int rc_mr = fi_mr_bind(fabric_state->recv_mr, &fabric_state->signal->fid, 0); - if (rc_mr != FI_SUCCESS) - { - fprintf(stderr, "fi_mr_bind (recv) failed: %s\n", fi_strerror(rc_mr)); - return 1; - } - rc_mr = fi_mr_enable(fabric_state->recv_mr); - if (rc_mr != FI_SUCCESS) - { - fprintf(stderr, "fi_mr_enable (recv) failed: %s\n", fi_strerror(rc_mr)); - return 1; - } - } + /* Record the registered region for future range checks. */ + fabric_state->recv_mr_base = cur_base; + fabric_state->recv_mr_reg_len = cur_len; + return 0; +} + +/* Reap at most one completion from the CQ. Returns 1 for a successful + * completion, -1 for an error completion (printed and consumed, so it + * still counts as one finished read), 0 if none is ready yet, and -2 if + * the CQ itself failed (nothing consumed; the caller cannot keep waiting). + * + * NOTE: CQEntry.len is NOT a reliable success signal on this provider/CQ + * format — it reads 0 even for host-to-host transfers independently + * verified to deliver correct data, so it can't be used to distinguish a + * real silent-no-op (observed once, for an HMEM/ROCr destination) from a + * normal completion. A hard check on it was tried and reverted: it + * false-positived on the working host path. Left unchecked deliberately. */ +static int reap_one(struct fabric_state *fabric_state) +{ + struct fi_cq_data_entry CQEntry = {0}; + ssize_t rc = fi_cq_read(fabric_state->cq_signal, &CQEntry, 1); + if (rc == 1) + return 1; + if (rc == -FI_EAGAIN) + return 0; + if (rc == -FI_EAVAIL) + { + struct fi_cq_err_entry ee = {0}; + fi_cq_readerr(fabric_state->cq_signal, &ee, 0); + /* prov_errno is provider-specific; fi_strerror() is only valid + * for generic fi_errno values. Use fi_cq_strerror() to get the + * correct provider-aware error string (provider-agnostic fix, + * applies to every provider, not just cxi). */ + char errbuf[256]; + const char *errstr = fi_cq_strerror(fabric_state->cq_signal, + ee.prov_errno, ee.err_data, + errbuf, sizeof(errbuf)); + fprintf(stderr, + "fi_cq_read failed: err=%d (%s) prov_errno=%d (%s)\n", + ee.err, fi_strerror(ee.err), ee.prov_errno, + errstr ? errstr : "(unknown)"); + return -1; + } + fprintf(stderr, "fi_cq_read failed: %zd (%s)\n", rc, fi_strerror((int)-rc)); + return -2; +} + +/* Read n rows of row_len bytes into recv_data (recv_data_len must be + * n * row_len): row i comes from rank src[i] at byte offset offset[i] of + * its registered buffer, into recv_data + i * row_len. + * + * All n fi_read()s are posted back to back and only then waited for, so + * the round trips overlap on the network; one recv MR covers the whole + * buffer. Returns 0, or non-zero if any read failed. + * + * Every read that was posted is waited for before returning, even after + * an error: a completion left in the CQ would be taken as the next call's. + * This also makes the call blocking, which is load-bearing for a GPU + * (recv_hmem_iface != FI_HMEM_SYSTEM) destination: it keeps the caller's + * device buffer alive (still referenced on the Python stack, so PyTorch's + * caching allocator cannot reuse its storage) for the whole in-flight RDMA + * window. If this is ever made asynchronous, GPU buffer safety must be + * re-examined. */ +int read_batch_from_remote(struct fabric_state *fabric_state, long n, + const int *src, const uint64_t *offset, size_t row_len) +{ + if (fabric_state->recv_hmem_iface != FI_HMEM_SYSTEM && !is_hmem_capable(fabric_state)) + { + fprintf(stderr, "GPU (HMEM) recv buffer requested but fabric is not cxi\n"); + return 1; + } - /* Record the registered region for future range checks. */ - fabric_state->recv_mr_base = cur_base; - fabric_state->recv_mr_reg_len = cur_len; - } /* end !in_cached_region */ + const bool prof = ddstore_profile_enabled(); + uint64_t t_mr = prof ? ddstore_now_ns() : 0; + if (ensure_recv_mr(fabric_state) != 0) + return 1; void *memory_descriptor = NULL; /* HMEM (device) buffers need their local descriptor passed to fi_read() @@ -757,10 +827,8 @@ int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset * build (is_local_mr_req() is always false here), so without this the * descriptor stayed NULL for HMEM too and fi_read() silently no-op'd * instead of DMAing into the GPU buffer. */ - if (is_local_mr_req(fabric_state) || recv_is_hmem) - { + if (is_local_mr_req(fabric_state) || fabric_state->recv_hmem_iface != FI_HMEM_SYSTEM) memory_descriptor = fi_mr_desc(fabric_state->recv_mr); - } uint64_t t_read = 0; if (prof) @@ -769,35 +837,43 @@ int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset fabric_state->prof_mr_ns += t_read - t_mr; } - size_t rc; - // fprintf(stderr, "fabric_state->remote_address: %llu\n", fabric_state->remote_address[src]); - do + long posted = 0, done = 0; + int failed = 0; + for (long i = 0; i < n && !failed; i++) { - rc = fi_read( - fabric_state->signal, - fabric_state->recv_data, - fabric_state->recv_data_len, - memory_descriptor, - fabric_state->comm_partner[src], - fabric_state->remote_address[src] + offset, - fabric_state->remote_key[src], - NULL); - } while (rc == -EAGAIN); - if (rc != 0) - { - fprintf(stderr, "fi_read failed with code %zu.\n", rc); - return (rc); + for (;;) + { + ssize_t rc = fi_read( + fabric_state->signal, + fabric_state->recv_data + (size_t)i * row_len, + row_len, + memory_descriptor, + fabric_state->comm_partner[src[i]], + fabric_state->remote_address[src[i]] + offset[i], + fabric_state->remote_key[src[i]], + NULL); + if (rc == 0) + { + posted++; + break; + } + if (rc != -FI_EAGAIN) + { + fprintf(stderr, "fi_read failed: %zd (%s)\n", rc, fi_strerror((int)-rc)); + failed = 1; + break; + } + /* Transmit queue full: make progress by reaping a completion. */ + int r = reap_one(fabric_state); + if (r == -2) + return 1; /* CQ broken: cannot account for in-flight reads */ + if (r != 0) + done++; + if (r < 0) + failed = 1; + } } - // (2025/09) segfault when using providers other than sockets - // struct fi_cq_data_entry CQEntry = {0}; - // rc = fi_cq_sread(fabric_state->cq_signal, &CQEntry, 1, NULL, -1); - // if (rc < 1) - // { - // fprintf(stderr, "Received no completion event for remote read\n"); - // return 1; - // } - uint64_t t_cq = 0; if (prof) { @@ -805,51 +881,32 @@ int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset fabric_state->prof_read_ns += t_cq - t_read; } - /* This loop blocks until the transfer completes — read_from_remote() does - * not return until it does. For a GPU (recv_hmem_iface != FI_HMEM_SYSTEM) - * destination, this is load-bearing: it's what keeps the caller's device - * buffer alive (still - * referenced on the Python stack, so PyTorch's caching allocator cannot - * reuse its storage) for the entire in-flight RDMA window. If this call - * is ever made asynchronous, GPU buffer safety must be re-examined. */ - for (;;) + /* Wait for every posted read, successful or not. */ + while (done < posted) { - struct fi_cq_data_entry CQEntry = {0}; - rc = fi_cq_read(fabric_state->cq_signal, &CQEntry, 1); - if (rc == 1) - { - if (prof) - fabric_state->prof_cq_ns += ddstore_now_ns() - t_cq; - /* NOTE: CQEntry.len is NOT a reliable success signal on this - * provider/CQ format — it reads 0 even for host-to-host - * transfers independently verified to deliver correct data, so - * it can't be used to distinguish a real silent-no-op (observed - * once, for an HMEM/ROCr destination) from a normal completion. - * A hard check on it was tried and reverted: it false-positived - * on the working host path. Left unchecked deliberately. */ - break; - } - if (rc == -FI_EAVAIL) - { - struct fi_cq_err_entry ee = {0}; - fi_cq_readerr(fabric_state->cq_signal, &ee, 0); - /* prov_errno is provider-specific; fi_strerror() is only valid - * for generic fi_errno values. Use fi_cq_strerror() to get the - * correct provider-aware error string (provider-agnostic fix, - * applies to every provider, not just cxi). */ - char errbuf[256]; - const char *errstr = fi_cq_strerror(fabric_state->cq_signal, - ee.prov_errno, ee.err_data, - errbuf, sizeof(errbuf)); - fprintf(stderr, - "fi_cq_read failed: err=%d (%s) prov_errno=%d (%s)\n", - ee.err, fi_strerror(ee.err), ee.prov_errno, - errstr ? errstr : "(unknown)"); + int r = reap_one(fabric_state); + if (r == -2) return 1; - } + if (r != 0) + done++; + if (r < 0) + failed = 1; } - return 0; + if (prof) + { + fabric_state->prof_cq_ns += ddstore_now_ns() - t_cq; + fabric_state->prof_rows += n; + } + return failed; +} + +/* Single-row read into recv_data/recv_data_len from rank src at byte + * offset `offset`: a one-row batch. */ +int read_from_remote(struct fabric_state *fabric_state, int src, uint64_t offset) +{ + return read_batch_from_remote(fabric_state, 1, &src, &offset, + fabric_state->recv_data_len); } /* ========================================================================= diff --git a/src/pyddstore.pyx b/src/pyddstore.pyx index 59823da..ba9b713 100644 --- a/src/pyddstore.pyx +++ b/src/pyddstore.pyx @@ -109,6 +109,7 @@ cdef extern from "ddstore.hpp": DDStore(int method, string handshake_dir, int n_core) void add[T](string name, T* buffer, long nrows, int disp, int hmem_iface) except + void get[T](string name, long start, long count, T* buffer, int hmem_iface) except + nogil + void get_batch[T](string name, const long *idx, long n, T* buffer, int hmem_iface) except + nogil void epoch_begin() void epoch_end() void free() @@ -297,20 +298,75 @@ cdef class PyDDStore: self._prof_get_s += time.perf_counter() - t_get self._prof_gets += 1 + def get_batch(self, str name, arr, indices): + """Read rows `indices` (global row ids, any order, repeats allowed) + into `arr`, whose first dimension must equal len(indices): row i of + `arr` receives row indices[i]. Same buffer rules as get(); for + method 1/2 all reads of the batch are in flight together, under one + lock acquisition and (GPU destination) one device sync.""" + cdef double t_get = time.perf_counter() if self._prof else 0.0 + cdef double t_sync + cdef np.ndarray idx = np.ascontiguousarray(indices, dtype=np.int64) + if idx.ndim != 1: + raise ValueError("indices must be one-dimensional") + cdef long n = idx.shape[0] + if arr.shape[0] != n: + raise ValueError( + "arr has %d rows but %d indices were given" % (arr.shape[0], n)) + cdef const long *cidx = idx.data + cdef size_t ptr + cdef int itemsize + cdef int iface + cdef bint is_gpu = _is_cuda_tensor(arr) + _check_dtype(arr, is_gpu) + if is_gpu: + _check_gpu_fabric_preconditions(self.method, "GPU destination buffer") + assert arr.is_contiguous() + import torch + # Same reason as get(), once per batch. + if self._prof: + t_sync = time.perf_counter() + torch.cuda.synchronize(device=arr.device) + self._prof_sync_s += time.perf_counter() - t_sync + else: + torch.cuda.synchronize(device=arr.device) + ptr = arr.data_ptr() + itemsize = arr.element_size() + iface = _hmem_iface_for(arr) + else: + assert arr.flags.c_contiguous + ptr = arr.ctypes.data + itemsize = arr.itemsize + iface = 0 + + cdef string cname = s2b(name) + with nogil: + if itemsize == 1: + self.c_ddstore.get_batch(cname, cidx, n, ptr, iface) + elif itemsize == 4: + self.c_ddstore.get_batch(cname, cidx, n, ptr, iface) + else: + self.c_ddstore.get_batch(cname, cidx, n, ptr, iface) + if self._prof: + self._prof_get_s += time.perf_counter() - t_get + self._prof_gets += 1 + def get_profile(self, str name): """DDSTORE_PROFILE=1 timing for `name` (methods 1/2), in seconds. - C++ counters for this variable: calls, lock_wait, mr (recv-MR cache - check/registration), mr_miss (re-registrations), read (posting - fi_read), cq (waiting for completion). Python counters for this + C++ counters for this variable: calls (get + get_batch), rows, + lock_wait, mr (recv-MR cache check/registration), mr_miss + (re-registrations), read (posting fi_read), cq (waiting for + completion). Python counters for this store, across all variables: py_gets, py_get (whole get() calls), py_sync (torch.cuda.synchronize on the GPU-destination path). """ - cdef unsigned long long c[6] + cdef unsigned long long c[7] self.c_ddstore.profile(s2b(name), c) return { "calls": c[0], "lock_wait": c[1] * 1e-9, "mr": c[2] * 1e-9, "mr_miss": c[3], "read": c[4] * 1e-9, "cq": c[5] * 1e-9, + "rows": c[6], "py_gets": self._prof_gets, "py_get": self._prof_get_s, "py_sync": self._prof_sync_s, } diff --git a/test/test_get_batch.py b/test/test_get_batch.py new file mode 100644 index 0000000..d3ed2e5 --- /dev/null +++ b/test/test_get_batch.py @@ -0,0 +1,189 @@ +""" +Batched get (PyDDStore.get_batch) tests — run with 2+ ranks, e.g.: + mpirun -n 4 pytest test/test_get_batch.py -v + +Each case runs with method 0 (MPI RMA) and, where a CXI device is present, +method 1 over cxi. Every rank fills its shard with values that encode the +global row id, so any misplaced or missing row is caught exactly. +""" + +import glob +import os +import threading + +import numpy as np +import pytest +from mpi4py import MPI + +import pyddstore as dds + +# A CXI device alone isn't enough (login nodes have one but can't open the +# fabric); also require running inside a Slurm job step. +HAVE_CXI = bool(glob.glob("/dev/cxi*")) and "SLURM_STEP_ID" in os.environ +METHODS = [0, pytest.param(1, marks=pytest.mark.skipif(not HAVE_CXI, reason="no CXI device"))] + +try: + import torch + + HAVE_GPU = torch.cuda.is_available() +except ImportError: # pragma: no cover + torch = None + HAVE_GPU = False + +NROWS, NCOLS = 16, 5 + + +def all_passed(comm, local_ok): + return comm.allreduce(int(local_ok), op=MPI.LAND) + + +def make_store(comm, method, monkeypatch, dtype=np.float32): + if method != 0: + monkeypatch.setenv("DDSTORE_FABRIC", "cxi") + rank = comm.Get_rank() + store = dds.PyDDStore(comm, method=method) + # row r (global) holds r*100 + column, so every element is identifiable + first = rank * NROWS + data = (np.arange(first, first + NROWS)[:, None] * 100 + np.arange(NCOLS)).astype(dtype) + store.add("x", data) + store.epoch_begin() + return store + + +def expected_rows(idx, dtype=np.float32): + idx = np.asarray(idx) + return (idx[:, None] * 100 + np.arange(NCOLS)).astype(dtype) + + +def finish(store): + store.epoch_end() + store.free() + + +@pytest.mark.parametrize("method", METHODS) +def test_batch_all_ranks_shuffled_with_repeats(comm, monkeypatch, method): + size = comm.Get_size() + if size < 2: + pytest.skip("requires at least 2 ranks") + store = make_store(comm, method, monkeypatch) + rng = np.random.default_rng(comm.Get_rank()) + idx = rng.integers(0, NROWS * size, size=200) # spans every rank, with repeats + out = np.zeros((len(idx), NCOLS), dtype=np.float32) + store.get_batch("x", out, idx) + ok = np.array_equal(out, expected_rows(idx)) + comm.Barrier() + finish(store) + assert all_passed(comm, ok) + + +@pytest.mark.parametrize("method", METHODS) +def test_batch_matches_per_row_get(comm, monkeypatch, method): + size = comm.Get_size() + store = make_store(comm, method, monkeypatch) + idx = list(range(NROWS * size))[::-1] + out = np.zeros((len(idx), NCOLS), dtype=np.float32) + store.get_batch("x", out, idx) + row = np.zeros((1, NCOLS), dtype=np.float32) + ok = True + for i, g in enumerate(idx): + store.get("x", row, g) + ok &= np.array_equal(row[0], out[i]) + comm.Barrier() + finish(store) + assert all_passed(comm, ok) + + +@pytest.mark.parametrize("method", METHODS) +def test_batch_single_row(comm, monkeypatch, method): + size = comm.Get_size() + store = make_store(comm, method, monkeypatch) + g = (comm.Get_rank() + 1) % size * NROWS + 3 + out = np.zeros((1, NCOLS), dtype=np.float32) + store.get_batch("x", out, [g]) + ok = np.array_equal(out, expected_rows([g])) + comm.Barrier() + finish(store) + assert all_passed(comm, ok) + + +@pytest.mark.parametrize("method", METHODS) +@pytest.mark.parametrize("dtype", [np.uint8, np.int32, np.float32, np.int64, np.float64]) +def test_batch_dtypes(comm, monkeypatch, method, dtype): + size = comm.Get_size() + store = make_store(comm, method, monkeypatch, dtype=dtype) + # row r holds r*100 + col: only rows 0..2 fit in uint8 + idx = [2, 0, 1] if dtype == np.uint8 else [NROWS * size - 1, 0, NROWS * size // 2] + out = np.zeros((len(idx), NCOLS), dtype=dtype) + store.get_batch("x", out, idx) + ok = np.array_equal(out, expected_rows(idx, dtype)) + comm.Barrier() + finish(store) + assert all_passed(comm, ok) + + +@pytest.mark.parametrize("method", METHODS) +def test_batch_errors_leave_store_usable(comm, monkeypatch, method): + size = comm.Get_size() + store = make_store(comm, method, monkeypatch) + out = np.zeros((2, NCOLS), dtype=np.float32) + with pytest.raises(IndexError): + store.get_batch("x", out, [0, NROWS * size]) # second index out of range + with pytest.raises(ValueError): + store.get_batch("x", out, [0, 1, 2]) # 3 indices for 2 rows + with pytest.raises(Exception): + store.get_batch("x", out.astype(np.float64), [0, 1]) # wrong item size + idx = [NROWS * size - 1, 0] + store.get_batch("x", out, idx) + ok = np.array_equal(out, expected_rows(idx)) + comm.Barrier() + finish(store) + assert all_passed(comm, ok) + + +@pytest.mark.skipif(not (HAVE_CXI and HAVE_GPU), reason="requires cxi and a GPU") +def test_batch_into_gpu_tensor(comm, monkeypatch): + size = comm.Get_size() + store = make_store(comm, 1, monkeypatch) + rng = np.random.default_rng(100 + comm.Get_rank()) + ok = True + for _ in range(20): + idx = rng.integers(0, NROWS * size, size=64) + out = torch.empty((len(idx), NCOLS), dtype=torch.float32, device="cuda") + store.get_batch("x", out, idx) + # compute-kernel read, like a training step would do + diff = (out - torch.from_numpy(expected_rows(idx)).cuda()).abs().sum().item() + ok &= diff == 0.0 + comm.Barrier() + finish(store) + assert all_passed(comm, ok) + + +@pytest.mark.parametrize("method", [pytest.param(1, marks=pytest.mark.skipif(not HAVE_CXI, reason="no CXI device"))]) +def test_batch_concurrent_threads(comm, monkeypatch, method): + """get_batch from several threads at once on one variable: the + per-variable lock must keep each batch's rows and completions apart.""" + size = comm.Get_size() + store = make_store(comm, method, monkeypatch) + errors, bad = [], [] + + def worker(seed): + rng = np.random.default_rng(seed) + try: + for _ in range(50): + idx = rng.integers(0, NROWS * size, size=32) + out = np.zeros((len(idx), NCOLS), dtype=np.float32) + store.get_batch("x", out, idx) + if not np.array_equal(out, expected_rows(idx)): + bad.append(seed) + except Exception as exc: # noqa: BLE001 - surface any thread exception + errors.append(exc) + + threads = [threading.Thread(target=worker, args=(1000 * comm.Get_rank() + t,)) for t in range(4)] + for t in threads: + t.start() + for t in threads: + t.join() + comm.Barrier() + finish(store) + assert not errors, f"worker thread(s) raised: {errors}" + assert all_passed(comm, not bad) From ca77463b45cd178806c41fbc76f275808352aebe Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 15:01:50 -0400 Subject: [PATCH 28/56] README: state that batched reads are the default Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ad798c..e72ad06 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ out = np.zeros((len(idx), 64), dtype=np.float32) store.get_batch("features", out, idx) ``` -`DistDataset`/`DistDatasetReader` use it through `__getitems__`, which PyTorch's `DataLoader` (and `ThreadDataLoader`) calls with a whole batch's indices; `DDSTORE_BATCH_GET=0` falls back to one `get()` per sample. +`DistDataset`/`DistDatasetReader` use it by default through `__getitems__`, which PyTorch's `DataLoader` (and `ThreadDataLoader`) calls with a whole batch's indices, so the VAE examples and job scripts read in batches with no extra flag. Set `DDSTORE_BATCH_GET=0` to fall back to one `get()` per sample. --- From 46e006e0fe5787bfd805528fc5270d2efb316b8f Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 15:24:03 -0400 Subject: [PATCH 29/56] Method 0 get_batch: MDLoader-style collective (Allgatherv + Alltoallv) For method 0, DDStore::get_batch() now all-gathers every rank's batch indices, packs the rows each rank owns per requester, and delivers them with one MPI_Alltoallv (rows as a contiguous datatype), instead of a per-row MPI_Win_lock/MPI_Get/unlock loop. Runs on a private duplicate of the store's communicator (freed in free()); indices are validated on the gathered list so every rank raises together. get_batch() is therefore collective for method 0: every rank calls it the same number of times, in the same order, from one thread at a time. __getitems__, loaders, methods 1/2 and single-row get() are unchanged. Also: README environment-variable section and get_batch notes; bench_get.py per-row normalization for method 0 (no C++ counters there). Frontier (2 nodes): test_get_batch 20/20 on 4 ranks, 19/19 on 16; other suites pass. VAE method 0, per-row -> collective: S=1 0.421 -> 0.142 s/epoch, S=2 0.597 -> 0.315 (losses unchanged). bench (16 ranks, us/row): 3 KB 30 -> 2.95, 12.5 KB 35 -> 7.4; large rows lose at batch 128 (200 KB 272, 1 MB 1765 vs 166 / 723 per-row) - to be addressed by bounded rounds. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- README.md | 47 ++++++++++--- examples/scripts/bench_get.py | 4 +- include/ddstore.hpp | 125 ++++++++++++++++++++++++++++------ src/ddstore.cxx | 2 + src/pyddstore.pyx | 7 +- 5 files changed, 154 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index e72ad06..c01460d 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,9 @@ Read `arr.shape[0]` consecutive rows starting at global index `start` into `arr` Read rows `indices` (global row ids; any order, any ranks, repeats allowed) into `arr`: row `i` of `arr` receives row `indices[i]`, so `arr.shape[0]` must equal `len(indices)` (else `ValueError`). Same buffer rules as `get()` (NumPy array or CUDA/HIP tensor). Every index is checked before anything is read; an out-of-range one raises `IndexError` and leaves the store usable. -For `method=1`/`2` the whole batch is one call: one lock acquisition, one memory registration, and (GPU destination) one device sync, with all of the batch's `fi_read`s posted before any is waited for, so the reads overlap on the network. It is still one `fi_read` per row. `method=0` loops the per-row `MPI_Get` path. +For `method=1`/`2` the whole batch is one call: one lock acquisition, one memory registration, and (GPU destination) one device sync, with all of the batch's `fi_read`s posted before any is waited for, so the reads overlap on the network. It is still one `fi_read` per row. + +For `method=0`, `get_batch()` is **collective**, after the collective module of [MDLoader](https://ieeexplore.ieee.org/document/10596438/): every rank all-gathers all ranks' indices (`MPI_Allgatherv`), packs the rows it owns for each requester, and one `MPI_Alltoallv` delivers them, on a private duplicate of the store's communicator. So every rank must call it for the variable the same number of times, in the same order, from one thread at a time; the number of indices may differ per rank (including 0). Indices are checked on the gathered list, so a bad index raises on every rank together. `DistributedSampler` gives every rank the same number of batches, and `vae-ddp.py` allows no worker threads with `method=0`, so the data loaders meet this automatically. ```python idx = np.array([2048, 7, 4096, 7]) @@ -194,6 +196,40 @@ Open and close an MPI RMA access epoch (calls `MPI_Win_fence`). **Collective**. Release every variable's MPI window (`method=0`) or libfabric endpoints and memory registrations (`method=1`/`2`), then the host buffer DDStore allocated for it in `add()`/`init()` (a GPU tensor passed to `add()` is the caller's and is not freed). Safe to call more than once. After `MPI_Finalize` the MPI window and buffer can no longer be released and are skipped. +## Environment variables + +**Read by DDStore itself** (the C++ library, `pyddstore`, `cpu_nic_map`): + +| Variable | Default | Effect | +|---|---|---| +| `DDSTORE_FABRIC` | `hsn` | libfabric provider for `method=1`/`2`: `hsn` (`tcp;ofi_rxm`) or `cxi` (native Slingshot; required for [GPUDirect RDMA](#gpudirect-rdma-gpu-resident-buffers)). See [libfabric RDMA](#libfabric-rdma-method1). | +| `FABRIC_IFACE` | auto | Network interface (libfabric domain, e.g. `cxi0`, `hsn0`) for `method=1`/`2`. Set it to force one; otherwise picked from the rank's CPU affinity. | +| `DDSTORE_NIC_MAP` | unset | Precomputed CPU→NIC map used for that automatic pick instead of a live hwloc query (`python3 -m cpu_nic_map --env`). The constructor's `nic_map=` argument takes priority. | +| `DDSTORE_HANDSHAKE_DIR` | `./ddstore_hs` | `method=2` handshake directory when none is given (C++ API; `PyDDStore` requires `handshake_dir`, and the examples fill it from this variable). Must be on a shared filesystem. | +| `DDSTORE_HANDSHAKE_TIMEOUT_S` | `300` | Seconds a `method=2` extra member's `join()` polls for the core group's record file. | +| `DDSTORE_PROFILE` | off | `1` turns on `get()`/`get_batch()` timing counters, read with `get_profile(name)`. See [Profiling](#profiling-get-ddstore_profile1). | + +The backend itself is not an environment variable in the library: pass `method=` to `PyDDStore` (`DDSTORE_METHOD` below is how the examples choose it). + +**Read by the examples** (`examples/vae/`, `examples/scripts/`, job scripts): + +| Variable | Default | Effect | +|---|---|---| +| `DDSTORE_METHOD` | `0` (`bench_get.py`: `1`) | Backend passed as `method=`: `0` MPI RMA, `1` libfabric, `2` file-based handshake. `--num-workers > 0` in `vae-ddp.py` needs `1` or `2`. | +| `DDSTORE_BATCH_GET` | `1` | `DistDataset.__getitems__` reads a whole batch with one `get_batch()`; `0` falls back to one `get()` per sample. | +| `DDSTORE_N_CORE` | `4` | `vae_extra_train.py`, `test_method2_*.py`: number of core ranks that published the data (`--n-core` overrides). | +| `DDSTORE_AFFINITY_WIDTH` / `DDSTORE_AFFINITY_OFFSET` | `0` / `0` | `ThreadDataLoader`: pin worker thread *i* to CPUs `[offset + i·width, offset + (i+1)·width)` of the process's affinity; width `0` = no pinning. | +| `DDSTORE_BACKEND` | auto | `torch.distributed` backend for the examples' DDP setup (`nccl`, `gloo`, `xccl`). | +| `VAE_PROFILE` | off | `1`: `vae-ddp.py` prints per-epoch fetch vs compute time. | +| `MASTER_PORT` | `2345` | DDP rendezvous port; the core/extra job script gives each step its own. | + +**System settings that matter on Frontier:** + +| Variable | Effect | +|---|---| +| `SLINGSHOT_VNIS` | Set by Slurm per step. With `--network=job_vni`, keep only the last (job-wide) entry before starting Python so separate `srun` steps can reach each other — see [Multiple `srun` steps](#multiple-srun-steps-in-one-job-method2-cxi-frontier). | +| `GPU_MAX_HW_QUEUES` | ROCm hardware queues per GPU per process (default 4); raise it if data-loading threads use their own streams — see [HIP streams](#hip-streams-and-hardware-queues-amdrocm). | + ## Backends ### MPI RMA (`method=0`, default) @@ -252,14 +288,7 @@ store.get("x", out, start=global_idx) store.free() ``` -Environment variables: - -| Variable | Default | Description | -|---|---|---| -| `DDSTORE_HANDSHAKE_DIR` | `./ddstore_hs` | Shared directory for handshake record files | -| `DDSTORE_HANDSHAKE_TIMEOUT_S` | `300` | Seconds to poll for core records / a join before raising a timeout | -| `DDSTORE_NIC_MAP` | unset | CPU→NIC map for `FABRIC_IFACE` auto-selection — see [libfabric RDMA](#libfabric-rdma-method1) above | -| `DDSTORE_FABRIC` | `hsn` | `hsn` (`tcp;ofi_rxm`) or `cxi` (native CXI) — see [libfabric RDMA](#libfabric-rdma-method1) above | +Environment variables: `DDSTORE_HANDSHAKE_DIR`, `DDSTORE_HANDSHAKE_TIMEOUT_S`, `DDSTORE_FABRIC` and `DDSTORE_NIC_MAP` — see [Environment variables](#environment-variables). See [test/test_method2_core.py](test/test_method2_core.py) / [test/test_method2_extra.py](test/test_method2_extra.py) for a minimal runnable pair, and [examples/vae/vae_core_server.py](examples/vae/vae_core_server.py) / [examples/vae/vae_extra_train.py](examples/vae/vae_extra_train.py) for a full DDP training example using this split. diff --git a/examples/scripts/bench_get.py b/examples/scripts/bench_get.py index c03bebf..8b68b87 100644 --- a/examples/scripts/bench_get.py +++ b/examples/scripts/bench_get.py @@ -122,8 +122,10 @@ def worker(ids): d = {k: p1[k] - p0[k] for k in p1} vals = comm.gather((dt, d), root=0) if rank == 0: - n = sum(v[1]["rows"] for v in vals) or 1 ngets = args.nget * size + # rows comes from the C++ counters (methods 1/2 only); method 0 + # has none, so fall back to the rows this run requested. + n = sum(v[1]["rows"] for v in vals) or ngets us_get = 1e6 * sum(v[0] for v in vals) / ngets mbps = nf * 4 * args.nget / (sum(v[0] for v in vals) / size) / 1e6 s = lambda k: 1e6 * sum(v[1][k] for v in vals) / n diff --git a/include/ddstore.hpp b/include/ddstore.hpp index e771f0d..4edac8a 100644 --- a/include/ddstore.hpp +++ b/include/ddstore.hpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include #include @@ -527,10 +529,15 @@ class DDStore /* Batched get: row i of `buffer` (n contiguous rows) receives global * row idx[i]; rows may come from any ranks, in any order, with repeats. - * Every index is validated before anything is read. Methods 1/2 take the - * variable's lock once and post all n fi_read()s before waiting for any - * (read_batch_from_remote()); method 0 loops the per-row MPI_Get path. - * hmem_iface as in get(). */ + * Every index is validated before anything is read. hmem_iface as in get(). + * + * Methods 1/2 (one-sided): take the variable's lock once and post all n + * fi_read()s before waiting for any (read_batch_from_remote()). + * + * Method 0 (collective, MDLoader-style): COLLECTIVE over this store's + * communicator — every rank must call get_batch() for the same variable + * the same number of times, in the same order (n may differ per rank, + * including 0). See get_batch_alltoall(). */ template void get_batch(std::string name, const long *idx, long n, T *buffer, int hmem_iface = 0) { @@ -538,6 +545,15 @@ class DDStore if (varinfo.itemsize != sizeof(T)) throw std::invalid_argument("Invalid data type"); + + if (this->method == 0) + { + if (hmem_iface != 0) + throw std::runtime_error("GPU destination buffer is not supported with method=0 (MPI_Win)"); + this->get_batch_alltoall(varinfo, idx, n, (char *)buffer); + return; + } + if (n <= 0) return; @@ -552,22 +568,6 @@ class DDStore offset[i] = (uint64_t)(idx[i] - first) * row_bytes; } - if (this->method == 0) - { - if (hmem_iface != 0) - throw std::runtime_error("GPU destination buffer is not supported with method=0 (MPI_Win)"); - MPI_Win win = varinfo.win; - for (long i = 0; i < n; i++) - { - MPI_Win_lock(MPI_LOCK_SHARED, target[i], 0, win); - MPI_Get((char *)buffer + i * row_bytes, (int)row_bytes, MPI_BYTE, - target[i], (MPI_Aint)(offset[i] / row_bytes), - (int)row_bytes, MPI_BYTE, win); - MPI_Win_unlock(target[i], win); - } - return; - } - /* Methods 1 and 2: one lock acquisition for the whole batch. */ const bool prof = ddstore_profile_enabled(); uint64_t t_wait = prof ? ddstore_now_ns() : 0; @@ -593,7 +593,92 @@ class DDStore } private: + /* Method 0 batched get, after MDLoader's collective module (Bae et al., + * IPDPSW 2024): every rank all-gathers the batch indices of all ranks, + * packs the rows it owns for each requester, and one MPI_Alltoallv + * delivers them; each rank then puts its rows in request order. Uses a + * private duplicate of the store's communicator (coll_comm), so it never + * matches the caller's own collectives or the windows' fences. The + * indices are validated after the gather, on the global list, so every + * rank throws the same error together instead of one rank leaving the + * others blocked in the exchange. */ + void get_batch_alltoall(const VarInfo_t &varinfo, const long *idx, long n, char *out) + { + std::lock_guard guard(this->coll_mutex); + if (this->coll_comm == MPI_COMM_NULL) + MPI_Comm_dup(this->comm, &this->coll_comm); + + const int P = this->comm_size; + const int me = this->rank; + const size_t row = (size_t)varinfo.disp * varinfo.itemsize; + + /* 1. Every rank learns every rank's requests. */ + int nloc = (int)n; + std::vector nreq(P), rbase(P + 1, 0); + MPI_Allgather(&nloc, 1, MPI_INT, nreq.data(), 1, MPI_INT, this->coll_comm); + for (int p = 0; p < P; p++) + rbase[p + 1] = rbase[p] + nreq[p]; + std::vector all(rbase[P] > 0 ? rbase[P] : 1); + MPI_Allgatherv(idx, nloc, MPI_LONG, all.data(), nreq.data(), rbase.data(), + MPI_LONG, this->coll_comm); + + const long total_rows = varinfo.lenlist.empty() ? 0 : varinfo.lenlist.back(); + for (int j = 0; j < rbase[P]; j++) + if (all[j] < 0 || all[j] >= total_rows) + throw std::out_of_range( + "Global index " + std::to_string(all[j]) + + " is out of range [0, " + std::to_string(total_rows) + ")"); + + /* 2. Rows this rank owns, packed per requester in request order. */ + const long my_first = me > 0 ? varinfo.lenlist[me - 1] : 0; + const long my_end = varinfo.lenlist[me]; + std::vector scount(P, 0), sdispl(P, 0); + for (int p = 0; p < P; p++) + for (int j = rbase[p]; j < rbase[p + 1]; j++) + if (all[j] >= my_first && all[j] < my_end) + scount[p]++; + for (int p = 1; p < P; p++) + sdispl[p] = sdispl[p - 1] + scount[p - 1]; + const int nsend = sdispl[P - 1] + scount[P - 1]; + std::vector sendbuf((size_t)(nsend > 0 ? nsend : 1) * row); + { + size_t k = 0; + for (int p = 0; p < P; p++) + for (int j = rbase[p]; j < rbase[p + 1]; j++) + if (all[j] >= my_first && all[j] < my_end) + memcpy(sendbuf.data() + (k++) * row, + (char *)varinfo.base + (size_t)(all[j] - my_first) * row, row); + } + + /* 3. Where each of this rank's rows comes from. */ + std::vector owner(n > 0 ? n : 1), rcount(P, 0), rdispl(P, 0); + for (long i = 0; i < n; i++) + { + owner[i] = sortedsearch(varinfo.lenlist, idx[i]); + rcount[owner[i]]++; + } + for (int p = 1; p < P; p++) + rdispl[p] = rdispl[p - 1] + rcount[p - 1]; + std::vector recvbuf((size_t)(n > 0 ? n : 1) * row); + + /* 4. One exchange, counted in rows. */ + MPI_Datatype rowtype; + MPI_Type_contiguous((int)row, MPI_BYTE, &rowtype); + MPI_Type_commit(&rowtype); + MPI_Alltoallv(sendbuf.data(), scount.data(), sdispl.data(), rowtype, + recvbuf.data(), rcount.data(), rdispl.data(), rowtype, this->coll_comm); + MPI_Type_free(&rowtype); + + /* 5. Received rows are grouped by owner, each group in request + * order: put them back in this rank's request order. */ + std::vector next(rdispl); + for (long i = 0; i < n; i++) + memcpy(out + (size_t)i * row, recvbuf.data() + (size_t)(next[owner[i]]++) * row, row); + } + int method; // 0: MPI, 1: libfabric, 2: file-based handshake (libfabric transport) + MPI_Comm coll_comm = MPI_COMM_NULL; /* method 0 get_batch; see above */ + std::mutex coll_mutex; /* one get_batch_alltoall at a time */ MPI_Comm comm; int comm_size; diff --git a/src/ddstore.cxx b/src/ddstore.cxx index 4703c12..3aa9e6b 100644 --- a/src/ddstore.cxx +++ b/src/ddstore.cxx @@ -227,4 +227,6 @@ void DDStore::free() var.owns_base = false; var.active = false; } + if (this->coll_comm != MPI_COMM_NULL && !finalized) + MPI_Comm_free(&this->coll_comm); } diff --git a/src/pyddstore.pyx b/src/pyddstore.pyx index ba9b713..6e1614d 100644 --- a/src/pyddstore.pyx +++ b/src/pyddstore.pyx @@ -303,7 +303,12 @@ cdef class PyDDStore: into `arr`, whose first dimension must equal len(indices): row i of `arr` receives row indices[i]. Same buffer rules as get(); for method 1/2 all reads of the batch are in flight together, under one - lock acquisition and (GPU destination) one device sync.""" + lock acquisition and (GPU destination) one device sync. + + Method 0 is COLLECTIVE (MDLoader-style Allgatherv of indices + + Alltoallv of rows): every rank of the store must call get_batch() + for the variable the same number of times, in the same order, from + one thread at a time (len(indices) may differ, including 0).""" cdef double t_get = time.perf_counter() if self._prof else 0.0 cdef double t_sync cdef np.ndarray idx = np.ascontiguousarray(indices, dtype=np.int64) From 3e5af49d23f38516149121f82f3bdf0c5637a6a4 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 15:30:03 -0400 Subject: [PATCH 30/56] Method 0 get_batch: bound each Alltoallv round (DDSTORE_ALLTOALL_MAX_BYTES) One exchange of a whole batch of large rows was slower than per-row reads (e.g. 1 MB rows at batch 128: 1765 vs 739 us/row). Split it into rounds of at most DDSTORE_ALLTOALL_MAX_BYTES received per rank (default 2 MiB); every rank derives the same round count from the gathered request counts. Frontier, 16 ranks, us/row at batch 128 (per-row get -> no cap -> 2 MiB): 200 KB 170 -> 272 -> 111, 1 MB 739 -> 1765 -> 685; 3 KB / 12.5 KB unchanged (one round). 32 MiB reproduces the slowdown; 2 and 8 MiB are similar. Tests pass on 4 ranks and on 16 ranks with a 50-byte cap (many rounds); VAE losses unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- README.md | 5 +- include/ddstore.hpp | 111 ++++++++++++++++++++++++++++++-------------- 2 files changed, 80 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index c01460d..9858130 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ Read rows `indices` (global row ids; any order, any ranks, repeats allowed) into For `method=1`/`2` the whole batch is one call: one lock acquisition, one memory registration, and (GPU destination) one device sync, with all of the batch's `fi_read`s posted before any is waited for, so the reads overlap on the network. It is still one `fi_read` per row. -For `method=0`, `get_batch()` is **collective**, after the collective module of [MDLoader](https://ieeexplore.ieee.org/document/10596438/): every rank all-gathers all ranks' indices (`MPI_Allgatherv`), packs the rows it owns for each requester, and one `MPI_Alltoallv` delivers them, on a private duplicate of the store's communicator. So every rank must call it for the variable the same number of times, in the same order, from one thread at a time; the number of indices may differ per rank (including 0). Indices are checked on the gathered list, so a bad index raises on every rank together. `DistributedSampler` gives every rank the same number of batches, and `vae-ddp.py` allows no worker threads with `method=0`, so the data loaders meet this automatically. +For `method=0`, `get_batch()` is **collective**, after the collective module of [MDLoader](https://ieeexplore.ieee.org/document/10596438/): every rank all-gathers all ranks' indices (`MPI_Allgatherv`), packs the rows it owns for each requester, and one `MPI_Alltoallv` delivers them, on a private duplicate of the store's communicator, in rounds of at most `DDSTORE_ALLTOALL_MAX_BYTES` (default 2 MiB) received per rank so large rows don't turn into one huge exchange. So every rank must call it for the variable the same number of times, in the same order, from one thread at a time; the number of indices may differ per rank (including 0). Indices are checked on the gathered list, so a bad index raises on every rank together. `DistributedSampler` gives every rank the same number of batches, and `vae-ddp.py` allows no worker threads with `method=0`, so the data loaders meet this automatically. ```python idx = np.array([2048, 7, 4096, 7]) @@ -208,6 +208,7 @@ Release every variable's MPI window (`method=0`) or libfabric endpoints and memo | `DDSTORE_HANDSHAKE_DIR` | `./ddstore_hs` | `method=2` handshake directory when none is given (C++ API; `PyDDStore` requires `handshake_dir`, and the examples fill it from this variable). Must be on a shared filesystem. | | `DDSTORE_HANDSHAKE_TIMEOUT_S` | `300` | Seconds a `method=2` extra member's `join()` polls for the core group's record file. | | `DDSTORE_PROFILE` | off | `1` turns on `get()`/`get_batch()` timing counters, read with `get_profile(name)`. See [Profiling](#profiling-get-ddstore_profile1). | +| `DDSTORE_ALLTOALL_MAX_BYTES` | `2097152` (2 MiB) | `method=0` `get_batch()`: bytes each rank receives per exchange round. Must be equal on all ranks. | The backend itself is not an environment variable in the library: pass `method=` to `PyDDStore` (`DDSTORE_METHOD` below is how the examples choose it). @@ -465,6 +466,8 @@ VAE (`vae-ddp.py`, epochs 2–8 average, s/epoch), per-sample `get()` → batche The per-row GPU sync falls from 129–383 µs (with workers) to ~0.1 µs, and training compute time recovers because it no longer waits behind the workers' syncs. +`method=0` collective `get_batch()` (`bench_get.py`, host, 16 ranks, µs per row, per-row `get()` → batch 128): 3 KB 28.8 → 3.0, 12.5 KB 34.9 → 7.5, 200 KB 170 → 111, 1 MB 739 → 685. Without the 2 MiB round cap, 200 KB and 1 MB rows were 1.6–2.4× *slower* than per-row reads. In the VAE (no workers), `method=0` goes from 0.421 → 0.144 s/epoch at S=1 and 0.597 → 0.310 at S=2, close to `method=1` batched (0.135 / 0.296); losses unchanged. + ## Testing ### Unit tests (pytest) diff --git a/include/ddstore.hpp b/include/ddstore.hpp index 4edac8a..49e6b0a 100644 --- a/include/ddstore.hpp +++ b/include/ddstore.hpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -629,51 +630,91 @@ class DDStore "Global index " + std::to_string(all[j]) + " is out of range [0, " + std::to_string(total_rows) + ")"); - /* 2. Rows this rank owns, packed per requester in request order. */ - const long my_first = me > 0 ? varinfo.lenlist[me - 1] : 0; - const long my_end = varinfo.lenlist[me]; - std::vector scount(P, 0), sdispl(P, 0); + /* 2. Split the exchange into rounds of at most `cap` bytes received + * per rank (DDSTORE_ALLTOALL_MAX_BYTES, default 2 MiB): one huge + * Alltoallv of large rows is slower than per-row reads. Every rank + * derives the same round count from the gathered request counts; + * round k moves each rank's k-th slice of `per` requests. */ + const long cap = alltoall_max_bytes(); + const long per = row >= (size_t)cap ? 1 : (long)(cap / (long)row); + long max_req = 0; for (int p = 0; p < P; p++) - for (int j = rbase[p]; j < rbase[p + 1]; j++) - if (all[j] >= my_first && all[j] < my_end) - scount[p]++; - for (int p = 1; p < P; p++) - sdispl[p] = sdispl[p - 1] + scount[p - 1]; - const int nsend = sdispl[P - 1] + scount[P - 1]; - std::vector sendbuf((size_t)(nsend > 0 ? nsend : 1) * row); - { - size_t k = 0; - for (int p = 0; p < P; p++) - for (int j = rbase[p]; j < rbase[p + 1]; j++) - if (all[j] >= my_first && all[j] < my_end) - memcpy(sendbuf.data() + (k++) * row, - (char *)varinfo.base + (size_t)(all[j] - my_first) * row, row); - } + max_req = nreq[p] > max_req ? nreq[p] : max_req; + const long rounds = (max_req + per - 1) / per; - /* 3. Where each of this rank's rows comes from. */ - std::vector owner(n > 0 ? n : 1), rcount(P, 0), rdispl(P, 0); + const long my_first = me > 0 ? varinfo.lenlist[me - 1] : 0; + const long my_end = varinfo.lenlist[me]; + std::vector owner(n > 0 ? n : 1); for (long i = 0; i < n; i++) - { owner[i] = sortedsearch(varinfo.lenlist, idx[i]); - rcount[owner[i]]++; - } - for (int p = 1; p < P; p++) - rdispl[p] = rdispl[p - 1] + rcount[p - 1]; - std::vector recvbuf((size_t)(n > 0 ? n : 1) * row); - /* 4. One exchange, counted in rows. */ MPI_Datatype rowtype; MPI_Type_contiguous((int)row, MPI_BYTE, &rowtype); MPI_Type_commit(&rowtype); - MPI_Alltoallv(sendbuf.data(), scount.data(), sdispl.data(), rowtype, - recvbuf.data(), rcount.data(), rdispl.data(), rowtype, this->coll_comm); + std::vector scount(P), sdispl(P), rcount(P), rdispl(P), next(P); + std::vector sendbuf, recvbuf; + for (long k = 0; k < rounds; k++) + { + /* Rows this rank owns from every requester's slice, packed per + * requester in request order. */ + for (int p = 0; p < P; p++) + { + scount[p] = 0; + long lo = rbase[p] + k * per, hi = rbase[p] + std::min((long)nreq[p], (k + 1) * per); + for (long j = lo; j < hi; j++) + if (all[j] >= my_first && all[j] < my_end) + scount[p]++; + } + sdispl[0] = 0; + for (int p = 1; p < P; p++) + sdispl[p] = sdispl[p - 1] + scount[p - 1]; + const long nsend = sdispl[P - 1] + scount[P - 1]; + sendbuf.resize((size_t)(nsend > 0 ? nsend : 1) * row); + size_t ks = 0; + for (int p = 0; p < P; p++) + { + long lo = rbase[p] + k * per, hi = rbase[p] + std::min((long)nreq[p], (k + 1) * per); + for (long j = lo; j < hi; j++) + if (all[j] >= my_first && all[j] < my_end) + memcpy(sendbuf.data() + (ks++) * row, + (char *)varinfo.base + (size_t)(all[j] - my_first) * row, row); + } + + /* Where this rank's own slice comes from. */ + const long ilo = std::min(n, k * per), ihi = std::min(n, (k + 1) * per); + std::fill(rcount.begin(), rcount.end(), 0); + for (long i = ilo; i < ihi; i++) + rcount[owner[i]]++; + rdispl[0] = 0; + for (int p = 1; p < P; p++) + rdispl[p] = rdispl[p - 1] + rcount[p - 1]; + recvbuf.resize((size_t)(ihi > ilo ? ihi - ilo : 1) * row); + + MPI_Alltoallv(sendbuf.data(), scount.data(), sdispl.data(), rowtype, + recvbuf.data(), rcount.data(), rdispl.data(), rowtype, this->coll_comm); + + /* Received rows are grouped by owner, each group in request + * order: put them back in this rank's request order. */ + next = rdispl; + for (long i = ilo; i < ihi; i++) + memcpy(out + (size_t)i * row, recvbuf.data() + (size_t)(next[owner[i]]++) * row, row); + } MPI_Type_free(&rowtype); + } - /* 5. Received rows are grouped by owner, each group in request - * order: put them back in this rank's request order. */ - std::vector next(rdispl); - for (long i = 0; i < n; i++) - memcpy(out + (size_t)i * row, recvbuf.data() + (size_t)(next[owner[i]]++) * row, row); + /* DDSTORE_ALLTOALL_MAX_BYTES: method 0 get_batch() bytes received per + * rank per exchange round (default 2 MiB; read once). Must be the same + * on every rank: the round count is derived from it. */ + static long alltoall_max_bytes() + { + static long cap = -1; + if (cap < 0) + { + const char *e = getenv("DDSTORE_ALLTOALL_MAX_BYTES"); + long v = e ? atol(e) : 0; + cap = v > 0 ? v : 2L * 1024 * 1024; + } + return cap; } int method; // 0: MPI, 1: libfabric, 2: file-based handshake (libfabric transport) From 8c035692d38303d36ea5cfd2fab4d71e08bd5b0d Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 16:03:22 -0400 Subject: [PATCH 31/56] Tests follow DDSTORE_FABRIC; add Perlmutter check script and checklist - test_get_batch.py: method-1 cases use DDSTORE_FABRIC when set (cxi otherwise); the GPU-destination case runs on cxi only. On Frontier with DDSTORE_FABRIC=hsn: 19 passed, 1 skipped (4 ranks); test_gpu_rdma 14/14; VAE method 1 over hsn, batched on/off x 0/2 workers, loss 8.8960; GPU destination refused with the hsn error; method-2 split over hsn trains. - examples/scripts/perlmutter-check.sh + docs/perlmutter-checklist.md: what Frontier could not cover (CUDA GPUDirect, Perlmutter Slingshot VNIs, colocate), as one 2-node debug job writing pm-check-/summary.txt. Syntax-checked only; not yet run on Perlmutter. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- docs/perlmutter-checklist.md | 50 +++++++++++ examples/scripts/perlmutter-check.sh | 124 +++++++++++++++++++++++++++ test/test_get_batch.py | 9 +- 3 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 docs/perlmutter-checklist.md create mode 100755 examples/scripts/perlmutter-check.sh diff --git a/docs/perlmutter-checklist.md b/docs/perlmutter-checklist.md new file mode 100644 index 0000000..6c764a0 --- /dev/null +++ b/docs/perlmutter-checklist.md @@ -0,0 +1,50 @@ +# Perlmutter checklist for `check-thread` + +Everything on this branch was verified on Frontier (AMD MI250X, ROCm, cxi). +These are the things Frontier could not cover, and how to check them on +Perlmutter (NVIDIA A100, CUDA, cxi, 4 GPUs and 64 cores per node). + +## Run it + +From the repo root on Perlmutter, with your environment loaded (modules, a +venv with CUDA torch + mpi4py) and the branch built: + +```bash +git fetch origin && git checkout check-thread && git pull +CC=cc CXX=CC pip install --no-build-isolation --no-deps -e . +sbatch -A examples/scripts/perlmutter-check.sh +``` + +About 15–25 min on 2 debug nodes. Send back `pm-check-/summary.txt` +(logs are in the same directory). + +If the build fails with `No module named 'distutils.msvccompiler'`, prefix it +with `SETUPTOOLS_USE_DISTUTILS=stdlib`. + +## What it checks, and what "good" looks like + +| # | Check | Why it matters | Expect | +|---|---|---|---| +| 1 | Slurm switch config; `SLINGSHOT_VNIS` per step | The core/extra fix keeps the **last** VNI (job VNI) because cxi uses the first. Perlmutter's order must match. | Each step: `,`, job VNI identical in both, last | +| 2 | NIC picked per rank (`cpu_nic_map`) | 4 NICs per node, `hsnN` → `cxiN` | Ranks on different NICs, names `cxi0..cxi3` | +| 3 | `test_single`, `test_multirank` | method 0 basics | all pass | +| 4 | `test_get_batch` on 8 ranks | batched get, method 0 collective + method 1 cxi, threads | all pass (the GPU case runs here) | +| 5 | `test_gpu_rdma` on 2 ranks | **CUDA GPUDirect (`FI_HMEM_CUDA`) was never run on this branch** | all pass | +| 6 | VAE, method 1, host/GPU, 0 and 2 workers, batched on/off | batched vs per-sample on CUDA | rc=0, 0 errors, **same loss8 for B0 and B1** within each pair | +| 7 | VAE, method 0 per-row vs collective | MDLoader-style collective | same loss8 for B0 and B1 | +| 8 | core/extra **split-node**, host and `--gpu-dest` | VNI wrapper + single-node steps (`single_node_vni`) | extra epoch3 printed, `core_done=4/4`, 0 errors | +| 9 | core/extra **colocate** | fails on Frontier with `job_vni` (`Error configuring interconnect`) | your test: does it launch and train? | +| 10 | `bench_get.py` | sanity of batched speedup on A100 | batch 128 much faster per row than batch 1 | + +Notes: +- If `sbatch` rejects `--network=single_node_vni,job_vni`, delete that line + from the script and note it: the split-node checks then show whether + Perlmutter needs it. +- Losses on A100 will differ from Frontier's (8.8960 / 30.0178); compare + batched vs per-sample on Perlmutter itself. +- The repo's `job-vae-single.sh` / `job-vae-core-extra.sh` assume Frontier + (8 ranks × 7 cores per node, `-A FUS184`); this check script uses `srun` + directly with 4 ranks per node (`RANKS_PER_NODE`, `CPUS_PER_TASK`). +- If colocate fails the same way as on Frontier, the README's limitation + applies to both machines; if it works, note which `SwitchParameters` + Perlmutter uses. diff --git a/examples/scripts/perlmutter-check.sh b/examples/scripts/perlmutter-check.sh new file mode 100755 index 0000000..2c9618c --- /dev/null +++ b/examples/scripts/perlmutter-check.sh @@ -0,0 +1,124 @@ +#!/bin/bash +#SBATCH -J ddstore-pm-check +#SBATCH -C gpu +#SBATCH -q debug +#SBATCH -N 2 +#SBATCH -t 30:00 +#SBATCH --gpus-per-node=4 +#SBATCH --network=single_node_vni,job_vni +# +# Correctness check of the check-thread branch on Perlmutter (NERSC). +# +# Usage, from the repo root with your Perlmutter Python env loaded (modules, +# venv with torch + mpi4py, pyddstore built with +# CC=cc CXX=CC pip install --no-build-isolation --no-deps -e . ): +# sbatch -A examples/scripts/perlmutter-check.sh +# Results: pm-check-/summary.txt (send that back), logs alongside. +# +# Knobs: RANKS_PER_NODE (4), CPUS_PER_TASK (32). + +set -u +cd "$SLURM_SUBMIT_DIR" +NR=${RANKS_PER_NODE:-4} +CPT=${CPUS_PER_TASK:-32} +N=$SLURM_NNODES +NT=$((N * NR)) +OUT=pm-check-$SLURM_JOB_ID +mkdir -p "$OUT" +SUM=$OUT/summary.txt +export DDSTORE_FABRIC=cxi +SRUN="srun -N$N -n$NT -c$CPT --gpus-per-task=1" +NOISE='Tip:|DDSTORE_NIC_MAP=|using interface|endpoint max_msg|DDStore\]|Step created' + +say() { echo "$*" | tee -a "$SUM"; } +section() { say ""; say "=== $*"; } + +# ---------------------------------------------------------------- system +section "system" +scontrol show config | grep -iE "SwitchType|SwitchParameters" | tee -a "$SUM" +python - <<'EOF' 2>&1 | tee -a "$SUM" +import torch, numpy, mpi4py +print("torch", torch.__version__, "cuda", torch.version.cuda, "hip", torch.version.hip, + "gpus/node", torch.cuda.device_count(), "| numpy", numpy.__version__, "| mpi4py", mpi4py.__version__) +EOF +(fi_info --version 2>/dev/null | head -2) | tee -a "$SUM" +say "-- per-step Slingshot env (each step should show its own VNI, then the job VNI, last)" +for r in 0 1; do + srun -N1 -n1 --relative=$r bash -c 'echo " step $SLURM_STEP_ID node $(hostname): SLINGSHOT_VNIS=$SLINGSHOT_VNIS SVC_IDS=$SLINGSHOT_SVC_IDS DEVICES=$SLINGSHOT_DEVICES"' 2>&1 | tee -a "$SUM" +done +say "-- NIC picked per rank (cpu_nic_map, cxi)" +$SRUN python -c " +import os, cpu_nic_map as m +iface = m.select_fabric_iface() +print(' rank', os.environ.get('SLURM_PROCID'), 'cpus', sorted(os.sched_getaffinity(0))[:3], '... ->', iface) +" 2>&1 | grep -vE "$NOISE" | sort -k2 -n | tee -a "$SUM" + +# ---------------------------------------------------------------- tests +section "unit tests" +srun -N1 -n1 -c$CPT --gpus-per-task=1 python -m pytest -q -p no:cacheprovider test/test_single.py > $OUT/t_single.log 2>&1 +say "test_single (1 rank): $(tail -1 $OUT/t_single.log)" +srun -N1 -n$NR -c$CPT --gpus-per-task=1 python -m pytest -q -p no:cacheprovider test/test_multirank.py > $OUT/t_multirank.log 2>&1 +say "test_multirank ($NR ranks): $(grep -E 'passed|failed' $OUT/t_multirank.log | sort | uniq -c | tr -s ' ' | tr '\n' ' ')" +$SRUN python -m pytest -q -p no:cacheprovider -rs test/test_get_batch.py > $OUT/t_get_batch.log 2>&1 +say "test_get_batch ($NT ranks, cxi): $(grep -E 'passed|failed' $OUT/t_get_batch.log | sort | uniq -c | tr -s ' ' | tr '\n' ' ')" +srun -N2 -n2 -c$CPT --gpus-per-task=1 python -m pytest -q -p no:cacheprovider test/test_gpu_rdma.py > $OUT/t_gpu_rdma.log 2>&1 +say "test_gpu_rdma (2 ranks, CUDA): $(grep -E 'passed|failed' $OUT/t_gpu_rdma.log | sort | uniq -c | tr -s ' ' | tr '\n' ' ')" +grep -E "^FAILED|Error" $OUT/t_*.log | sort | uniq | head -20 | tee -a "$SUM" + +# ---------------------------------------------------------------- VAE +section "vae-ddp ($NT ranks, 8 epochs; batched vs per-sample must give the same loss)" +export VAE_PROFILE=1 +vae() { # tag, env..., -- args... + local tag=$1; shift + local envs=() + while [ "$1" != "--" ]; do envs+=("$1"); shift; done; shift + rm -rf ddstore_hs* + env "${envs[@]}" $SRUN -l python -u examples/vae/vae-ddp.py --epochs 8 "$@" > $OUT/vae-$tag.log 2>&1 + local rc=$? + local loss=$(grep -h ' 0: ====> Epoch: 8 Average' $OUT/vae-$tag.log | awk '{print $NF}') + local ep=$(grep -h '\[profile\]' $OUT/vae-$tag.log | awk '{f=$5;c=$6;sub("fetch=","",f);sub("s","",f);sub("compute=","",c);sub("s","",c);F+=f;C+=c;n++} END{if(n)printf "fetch=%.3f compute=%.3f total=%.3f",F/n,C/n,(F+C)/n}') + say "$(printf '%-22s' $tag) rc=$rc loss8=$loss $ep errors=$(grep -ciE 'traceback|error|abort' $OUT/vae-$tag.log)" +} +for B in 0 1; do + vae m1_host_w0_B$B DDSTORE_METHOD=1 DDSTORE_BATCH_GET=$B -- --num-workers=0 + vae m1_host_w2_B$B DDSTORE_METHOD=1 DDSTORE_BATCH_GET=$B -- --num-workers=2 + vae m1_gpu_w0_B$B DDSTORE_METHOD=1 DDSTORE_BATCH_GET=$B -- --num-workers=0 --gpu-dest --gpu-source + vae m1_gpu_w2_B$B DDSTORE_METHOD=1 DDSTORE_BATCH_GET=$B -- --num-workers=2 --gpu-dest --gpu-source + vae m0_host_w0_B$B DDSTORE_METHOD=0 DDSTORE_BATCH_GET=$B -- --num-workers=0 +done +vae m1_gpu_w0_B1_S2 DDSTORE_METHOD=1 DDSTORE_BATCH_GET=1 -- --num-workers=0 --gpu-dest --gpu-source --image-scale=2 +vae m1_host_w0_B1_S2 DDSTORE_METHOD=1 DDSTORE_BATCH_GET=1 -- --num-workers=0 --image-scale=2 + +# ---------------------------------------------------------------- core/extra +section "method 2 core/extra (separate srun steps, job VNI)" +# libfabric cxi uses the FIRST VNI in SLINGSHOT_VNIS; keep only the job VNI +# (last entry) so both steps share it. See README "Multiple srun steps". +WRAP='export SLINGSHOT_VNIS=${SLINGSHOT_VNIS##*,}; exec "$@"' +corextra() { # tag, core srun opts, extra srun opts, extra-args + local tag=$1 copts=$2 eopts=$3 eargs=$4 hs=ddstore_hs_pm_$1 + rm -rf "$hs" + DDSTORE_METHOD=2 DDSTORE_HANDSHAKE_TIMEOUT_S=120 MASTER_PORT=8889 timeout 600 srun $copts -l \ + bash -c "$WRAP" _ python -u examples/vae/vae_core_server.py "$hs" > $OUT/ce-$tag-core.log 2>&1 & + local cpid=$! + sleep 5 + DDSTORE_METHOD=2 DDSTORE_HANDSHAKE_TIMEOUT_S=120 MASTER_PORT=8891 timeout 600 srun $eopts -l \ + bash -c "$WRAP" _ python -u examples/vae/vae_extra_train.py --handshake-dir "$hs" --n-core $NR --epochs 3 $eargs \ + > $OUT/ce-$tag-extra.log 2>&1 + local erc=$? + wait $cpid; local crc=$? + say "$(printf '%-22s' $tag) extra rc=$erc epoch3=$(grep -h ' 0: ====> Epoch: 3 Average' $OUT/ce-$tag-extra.log | awk '{print $NF}') core rc=$crc core_done=$(grep -c 'core rank [0-9]*\] done' $OUT/ce-$tag-core.log)/$NR errors=$(cat $OUT/ce-$tag-*.log | grep -ciE 'traceback|error|abort|VNI_NOT|PTLTE|interconnect')" + grep -hoE "Error configuring interconnect|VNI_NOT_FOUND|fi_domain\(\) has failed" $OUT/ce-$tag-*.log | sort | uniq -c | sed 's/^/ /' | tee -a "$SUM" +} +corextra split_host "-N1 -n$NR --relative=0 -c$CPT --gpus-per-task=0" "-N1 -n$NR --relative=1 -c$CPT --gpus-per-task=1" "--num-workers=1" +corextra split_gpudest "-N1 -n$NR --relative=0 -c$CPT --gpus-per-task=0" "-N1 -n$NR --relative=1 -c$CPT --gpus-per-task=1" "--num-workers=1 --gpu-dest" +# colocate: both steps on both nodes at once (fails on Frontier with job_vni). +# Core ranks split as NR/2 per node so --n-core still equals NR. +corextra colocate_host "-N2 -n$NR -c8 --gpus-per-task=0" "-N2 -n$NR -c16 --gpus-per-task=1" "--num-workers=1" + +# ---------------------------------------------------------------- bench +section "bench_get.py (method 1, host/GPU, 1 row vs 128 rows per call; us/row)" +DDSTORE_PROFILE=1 DDSTORE_METHOD=1 $SRUN python -u examples/scripts/bench_get.py \ + --row-floats 784,3136,262144 --dest host,gpu --batch 1,128 --nget 2048 2>&1 | grep -vE "$NOISE" | tee -a "$SUM" + +section "done" +say "Logs: $OUT/" diff --git a/test/test_get_batch.py b/test/test_get_batch.py index d3ed2e5..94629a3 100644 --- a/test/test_get_batch.py +++ b/test/test_get_batch.py @@ -3,7 +3,8 @@ mpirun -n 4 pytest test/test_get_batch.py -v Each case runs with method 0 (MPI RMA) and, where a CXI device is present, -method 1 over cxi. Every rank fills its shard with values that encode the +method 1 over libfabric: cxi by default, or the provider named by +DDSTORE_FABRIC if it is set (e.g. DDSTORE_FABRIC=hsn). Every rank fills its shard with values that encode the global row id, so any misplaced or missing row is caught exactly. """ @@ -20,6 +21,7 @@ # A CXI device alone isn't enough (login nodes have one but can't open the # fabric); also require running inside a Slurm job step. HAVE_CXI = bool(glob.glob("/dev/cxi*")) and "SLURM_STEP_ID" in os.environ +FABRIC = os.environ.get("DDSTORE_FABRIC", "cxi") METHODS = [0, pytest.param(1, marks=pytest.mark.skipif(not HAVE_CXI, reason="no CXI device"))] try: @@ -39,7 +41,7 @@ def all_passed(comm, local_ok): def make_store(comm, method, monkeypatch, dtype=np.float32): if method != 0: - monkeypatch.setenv("DDSTORE_FABRIC", "cxi") + monkeypatch.setenv("DDSTORE_FABRIC", FABRIC) rank = comm.Get_rank() store = dds.PyDDStore(comm, method=method) # row r (global) holds r*100 + column, so every element is identifiable @@ -140,7 +142,8 @@ def test_batch_errors_leave_store_usable(comm, monkeypatch, method): assert all_passed(comm, ok) -@pytest.mark.skipif(not (HAVE_CXI and HAVE_GPU), reason="requires cxi and a GPU") +@pytest.mark.skipif(not (HAVE_CXI and HAVE_GPU and FABRIC == "cxi"), + reason="requires the cxi provider and a GPU") def test_batch_into_gpu_tensor(comm, monkeypatch): size = comm.Get_size() store = make_store(comm, 1, monkeypatch) From eec4fafcea04852670d3cf80690737bf31b2474a Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 17:55:44 -0400 Subject: [PATCH 32/56] Perlmutter results: colocate needs --overlap; platform notes Perlmutter check (job 59332134, 2 nodes x 4 A100): all tests pass incl. CUDA GPUDirect (test_gpu_rdma 14/14, test_get_batch 20/20 on 8 ranks); VAE losses identical for every variant, batched 1.9-3.9x faster per epoch; core/extra split-node passes; VNI order , as on Frontier. - Colocate works on Perlmutter only with job_vni + the VNI wrapper + srun --overlap; job-vae-core-extra.sh now runs colocate steps with --overlap. On Frontier the same setup still fails ("Error configuring interconnect", re-tested with --overlap): use split-node there. - Perlmutter single-node steps work without single_node_vni (default CXI service); Frontier needs it. - GPUDirect vs host crossover is platform-dependent: host destinations win at every row size on Perlmutter (1 MB: 75 vs 142 us/row at batch 128). - perlmutter-check.sh: match rank 0 with or without srun -l padding (loss columns were empty with < 10 ranks); colocate case uses --overlap. - README and checklist updated with the above. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- README.md | 23 ++++++++++++++++++----- docs/perlmutter-checklist.md | 17 +++++++++++++++++ examples/scripts/perlmutter-check.sh | 10 ++++++---- examples/vae/script/job-vae-core-extra.sh | 13 +++++++++---- 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 9858130..93fc881 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,7 @@ The backend itself is not an environment variable in the library: pass `method=` | Variable | Effect | |---|---| -| `SLINGSHOT_VNIS` | Set by Slurm per step. With `--network=job_vni`, keep only the last (job-wide) entry before starting Python so separate `srun` steps can reach each other — see [Multiple `srun` steps](#multiple-srun-steps-in-one-job-method2-cxi-frontier). | +| `SLINGSHOT_VNIS` | Set by Slurm per step. With `--network=job_vni`, keep only the last (job-wide) entry before starting Python so separate `srun` steps can reach each other — see [Multiple `srun` steps](#multiple-srun-steps-in-one-job-method2-cxi). | | `GPU_MAX_HW_QUEUES` | ROCm hardware queues per GPU per process (default 4); raise it if data-loading threads use their own streams — see [HIP streams](#hip-streams-and-hardware-queues-amdrocm). | ## Backends @@ -322,7 +322,7 @@ The sync in `get()` was re-checked by removing it: every `--gpu-dest` run of `va ## Known Limitations -### Multiple `srun` steps in one job (`method=2`, `cxi`, Frontier) +### Multiple `srun` steps in one job (`method=2`, `cxi`) Core and extra run as separate `srun` steps, and on Slingshot every step gets its own VNI (network isolation ID); two endpoints can only communicate on the same VNI. Two things are needed, both handled by [job-vae-core-extra.sh](examples/vae/script/job-vae-core-extra.sh): @@ -331,6 +331,17 @@ Core and extra run as separate `srun` steps, and on Slingshot every step gets it Verified on Frontier (2 nodes, split-node, `method=2`, `cxi`): with both, the extra step trains against the core step's data and both shut down cleanly; with either missing, it fails as above. MPI and RCCL inside each step work on the job VNI too. +On Perlmutter the VNI order is the same (`,`), so the same wrapper works. Single-node steps there run even without `single_node_vni` (they get no `SLINGSHOT_*` variables but cxi falls back to a default CXI service), so split-node works with or without the flags; with them it behaves as on Frontier. + +**Colocate** (core and extra steps on the same nodes at once): + +| | `job_vni` + wrapper | `job_vni` + wrapper + `srun --overlap` | no `--network` | +|---|---|---|---| +| Frontier | second step fails to launch: `Error configuring interconnect` | same failure | steps launch, but each has only its own VNI: extra cannot reach core | +| Perlmutter | second step does not start (timeout) | **works** | extra cannot reach core (timeout) | + +`job-vae-core-extra.sh --layout=colocate` therefore runs both steps with `--overlap`; on Frontier use `--layout=split-node`. + ### `get()` has no GPU destination-buffer pool `DistDataset`/`DistDatasetReader`'s `get()` allocates a fresh GPU tensor per call on the GPU path (`--gpu-dest`), rather than reusing a pre-allocated pool. An earlier pooled design (round-robin slices of one pre-registered buffer, to amortize `fi_mr_regattr` cost) was removed after it was confirmed by direct experiment to corrupt data under `ThreadDataLoader` with `--num-workers > 1`: multiple worker threads raced for pool slots at per-sample granularity, and bounding how many batches could be in flight at once didn't bound which physical slots got overwritten, since slot-write order was determined by lock-acquisition order, not batch order. Removing the pool removes that race entirely — each call's destination is privately owned, nothing to reuse. The tradeoff: a fresh `fi_mr_regattr` whenever the new tensor isn't inside the previously registered range (the receive-MR cache in `read_from_remote()` reuses the registration when PyTorch's allocator hands back the same block, which is common for same-shape `torch.empty()`), instead of one registration shared across many. Revisit with a pool later if that registration cost matters (`--num-workers > 0` is otherwise known to work per the next section). @@ -353,9 +364,9 @@ HIP maps streams onto a small pool of hardware queues per GPU per process — `G On Frontier, the GPU synchronization performed before each RDMA call (needed for correctness) can outweigh the benefit of skipping the host copy for small, per-sample transfers — GPU-to-GPU has not shown a speed advantage there yet, though results are correct either way. Larger, batched transfers should benefit more; that usage pattern isn't built yet. -### Troubleshooting: RDMA fails to connect (`cxi`, Frontier) +### Troubleshooting: RDMA fails to connect (`cxi`) -If `fi_domain()` fails with `-38 (Function not implemented)` on `cxi`, the step has no CXI service: add `#SBATCH --network=single_node_vni` — needed whenever a step runs on a single node (a `-N 1` job, or a one-node step inside a larger job). If ranks in different `srun` steps can't reach each other (`VNI_NOT_FOUND`), see [Multiple `srun` steps](#multiple-srun-steps-in-one-job-method2-cxi-frontier) above. +If `fi_domain()` fails with `-38 (Function not implemented)` on `cxi`, the step has no CXI service: add `#SBATCH --network=single_node_vni` — needed on Frontier whenever a step runs on a single node (a `-N 1` job, or a one-node step inside a larger job). If ranks in different `srun` steps can't reach each other (`VNI_NOT_FOUND`), see [Multiple `srun` steps](#multiple-srun-steps-in-one-job-method2-cxi) above. ## Partitioned / Sub-communicator Usage @@ -455,7 +466,9 @@ Measured on Frontier (`method=1`, `cxi`, 2 nodes × 8 ranks): - `bench_get.py`, one thread, µs per single-row `get()`: 3 KB — host 8.6, GPU 19.5 (12.2 reusing the buffer); 12.5 KB — host 9.9, GPU 20.5; 200 KB — host 53, GPU 37; 1 MB — host 206–276, GPU 134. GPUDirect wins from somewhere between 12.5 KB and 200 KB per row; below that its fixed per-call overhead (sync, allocation) dominates. - A second thread adds no per-rank throughput: the per-variable lock serializes the transfers (lock wait ≈ transfer time at large rows). -With `get_batch()` (`bench_get.py --batch 128`, µs per row, 1 thread): 3 KB — host 0.68, GPU 0.78 (from 9.0 / 20.9 with one row per call); 12.5 KB — host 1.82, GPU 1.38; 200 KB — host 27.5, GPU 19.6; 1 MB — host 189, GPU 99 (~10.6 GB/s per rank). The GPU sync drops to ~0.06 µs per row, and GPUDirect now beats host from 12.5 KB rows up. (Host-destination batches of 1 MB rows are slower than single reads; not investigated.) +With `get_batch()` (`bench_get.py --batch 128`, µs per row, 1 thread): 3 KB — host 0.68, GPU 0.78 (from 9.0 / 20.9 with one row per call); 12.5 KB — host 1.82, GPU 1.38; 200 KB — host 27.5, GPU 19.6; 1 MB — host 189, GPU 99 (~10.6 GB/s per rank). The GPU sync drops to ~0.06 µs per row, and GPUDirect now beats host from 12.5 KB rows up on Frontier. (Host-destination batches of 1 MB rows are slower than single reads there; not investigated.) + +The crossover is platform-dependent. On Perlmutter (A100, 2 nodes × 4 ranks, batch 128, µs per row): 3 KB — host 0.82, GPU 0.99; 12.5 KB — host 1.19, GPU 1.80; 1 MB — host 75, GPU 142 (~14 vs ~7.4 GB/s per rank). There host destinations are faster at every size; with batching the two are close for small rows on both machines. VAE on Perlmutter (8 ranks, s/epoch, per-sample → batched): method 1 host 0.398 → 0.208, method 1 GPU 0.508 → 0.204, GPU with 2 workers 0.678 → 0.183, method 0 0.849 → 0.219; losses identical across all variants. VAE (`vae-ddp.py`, epochs 2–8 average, s/epoch), per-sample `get()` → batched (`DDSTORE_BATCH_GET` 0 → 1); losses identical (8.8960 at S=1, 30.0178 at S=2): diff --git a/docs/perlmutter-checklist.md b/docs/perlmutter-checklist.md index 6c764a0..85753d9 100644 --- a/docs/perlmutter-checklist.md +++ b/docs/perlmutter-checklist.md @@ -48,3 +48,20 @@ Notes: - If colocate fails the same way as on Frontier, the README's limitation applies to both machines; if it works, note which `SwitchParameters` Perlmutter uses. + +## Results (job 59332134, 2026-10-04) + +- VNIs per step: `,` with the job VNI last; the wrapper rule holds. +- NICs: ranks spread over `cxi0`–`cxi3`. +- Tests: test_single 14/14, test_multirank 5/5, test_get_batch 20/20 on 8 + ranks, test_gpu_rdma 14/14 (first CUDA GPUDirect run of this branch). +- VAE: identical losses for every variant (S=1 15.3781, S=2 54.7914); + batched reads 1.9–3.9x faster per epoch (method 0 0.849 → 0.219 s). +- core/extra split-node (host and `--gpu-dest`): pass, with or without the + `--network` flags (single-node steps fall back to a default CXI service). +- colocate: works only with `job_vni` + the VNI wrapper + `srun --overlap`. + (Frontier fails the same setup with `Error configuring interconnect`.) +- bench: host beats GPU destinations at every row size on Perlmutter + (1 MB rows: 75 vs 142 us/row at batch 128); on Frontier GPU wins from 12.5 KB. +- The first version of the script printed empty loss columns (`srun -l` + pads rank labels only with >= 10 ranks); fixed. diff --git a/examples/scripts/perlmutter-check.sh b/examples/scripts/perlmutter-check.sh index 2c9618c..bc3e4b7 100755 --- a/examples/scripts/perlmutter-check.sh +++ b/examples/scripts/perlmutter-check.sh @@ -75,7 +75,8 @@ vae() { # tag, env..., -- args... rm -rf ddstore_hs* env "${envs[@]}" $SRUN -l python -u examples/vae/vae-ddp.py --epochs 8 "$@" > $OUT/vae-$tag.log 2>&1 local rc=$? - local loss=$(grep -h ' 0: ====> Epoch: 8 Average' $OUT/vae-$tag.log | awk '{print $NF}') + # srun -l pads the rank label only with >= 10 ranks: match ' 0:' and '0:' + local loss=$(grep -hE '^ *0: ====> Epoch: 8 Average' $OUT/vae-$tag.log | awk '{print $NF}') local ep=$(grep -h '\[profile\]' $OUT/vae-$tag.log | awk '{f=$5;c=$6;sub("fetch=","",f);sub("s","",f);sub("compute=","",c);sub("s","",c);F+=f;C+=c;n++} END{if(n)printf "fetch=%.3f compute=%.3f total=%.3f",F/n,C/n,(F+C)/n}') say "$(printf '%-22s' $tag) rc=$rc loss8=$loss $ep errors=$(grep -ciE 'traceback|error|abort' $OUT/vae-$tag.log)" } @@ -106,14 +107,15 @@ corextra() { # tag, core srun opts, extra srun opts, extra-args > $OUT/ce-$tag-extra.log 2>&1 local erc=$? wait $cpid; local crc=$? - say "$(printf '%-22s' $tag) extra rc=$erc epoch3=$(grep -h ' 0: ====> Epoch: 3 Average' $OUT/ce-$tag-extra.log | awk '{print $NF}') core rc=$crc core_done=$(grep -c 'core rank [0-9]*\] done' $OUT/ce-$tag-core.log)/$NR errors=$(cat $OUT/ce-$tag-*.log | grep -ciE 'traceback|error|abort|VNI_NOT|PTLTE|interconnect')" + say "$(printf '%-22s' $tag) extra rc=$erc epoch3=$(grep -hE '^ *0: ====> Epoch: 3 Average' $OUT/ce-$tag-extra.log | awk '{print $NF}') core rc=$crc core_done=$(grep -c 'core rank [0-9]*\] done' $OUT/ce-$tag-core.log)/$NR errors=$(cat $OUT/ce-$tag-*.log | grep -ciE 'traceback|error|abort|VNI_NOT|PTLTE|interconnect')" grep -hoE "Error configuring interconnect|VNI_NOT_FOUND|fi_domain\(\) has failed" $OUT/ce-$tag-*.log | sort | uniq -c | sed 's/^/ /' | tee -a "$SUM" } corextra split_host "-N1 -n$NR --relative=0 -c$CPT --gpus-per-task=0" "-N1 -n$NR --relative=1 -c$CPT --gpus-per-task=1" "--num-workers=1" corextra split_gpudest "-N1 -n$NR --relative=0 -c$CPT --gpus-per-task=0" "-N1 -n$NR --relative=1 -c$CPT --gpus-per-task=1" "--num-workers=1 --gpu-dest" -# colocate: both steps on both nodes at once (fails on Frontier with job_vni). +# colocate: both steps on both nodes at once. Needs --overlap on Perlmutter +# (works there with job_vni + the wrapper); fails on Frontier either way. # Core ranks split as NR/2 per node so --n-core still equals NR. -corextra colocate_host "-N2 -n$NR -c8 --gpus-per-task=0" "-N2 -n$NR -c16 --gpus-per-task=1" "--num-workers=1" +corextra colocate_host "--overlap -N2 -n$NR -c8 --gpus-per-task=0" "--overlap -N2 -n$NR -c16 --gpus-per-task=1" "--num-workers=1" # ---------------------------------------------------------------- bench section "bench_get.py (method 1, host/GPU, 1 row vs 128 rows per call; us/row)" diff --git a/examples/vae/script/job-vae-core-extra.sh b/examples/vae/script/job-vae-core-extra.sh index f745ad9..1aef005 100755 --- a/examples/vae/script/job-vae-core-extra.sh +++ b/examples/vae/script/job-vae-core-extra.sh @@ -35,8 +35,11 @@ Options: (needed for --gpu-source; the baseline core step doesn't use one). --layout=X Process distribution: colocate (core and extra both span - every allocated node) or split-node (core gets 1 node, - extra gets the rest). Default: colocate. + every allocated node, steps run with --overlap) or + split-node (core gets --core-nnodes nodes, extra the rest). + Colocate works on Perlmutter but not on Frontier, where the + second step fails with "Error configuring interconnect"; + use split-node there. Default: colocate. --core-nnodes=N Number of nodes for the core step in split-node layout. Ignored in colocate layout. Default: 1. --num-workers=N DataLoader workers for the extra (training) step. 0 uses @@ -105,6 +108,8 @@ sleep 2 if [ "$LAYOUT" == "colocate" ]; then CORE_NNODES=$SLURM_NNODES EXTRA_NNODES=$SLURM_NNODES + # Both steps share the nodes; Perlmutter needs --overlap for that. + OVERLAP=--overlap else CORE_NNODES="${CORE_NNODES_OPT:-1}" EXTRA_NNODES=$((SLURM_NNODES - CORE_NNODES)) @@ -118,12 +123,12 @@ echo "DDSTORE_METHOD=$DDSTORE_METHOD DDSTORE_FABRIC=$DDSTORE_FABRIC LAYOUT=$LAYO echo "CORE_NNODES=$CORE_NNODES CORE_NTASKS=$CORE_NTASKS CORE_GPUS_PER_TASK=$CORE_GPUS_PER_TASK CORE_EXTRA_ARGS=\"$CORE_EXTRA_ARGS\" REPLICATE=$REPLICATE IMAGE_SCALE=$IMAGE_SCALE" echo "EXTRA_NNODES=$EXTRA_NNODES EXTRA_NTASKS=$EXTRA_NTASKS EXTRA_EXTRA_ARGS=\"$EXTRA_EXTRA_ARGS\" NUM_WORKERS=$NUM_WORKERS" -MASTER_PORT=8889 srun -N$CORE_NNODES -n$CORE_NTASKS -c1 --gpus-per-task=$CORE_GPUS_PER_TASK --cpu-bind=verbose,core -l \ +MASTER_PORT=8889 srun ${OVERLAP:-} -N$CORE_NNODES -n$CORE_NTASKS -c1 --gpus-per-task=$CORE_GPUS_PER_TASK --cpu-bind=verbose,core -l \ bash -c "$JOB_VNI_WRAP" _ python -u examples/vae/vae_core_server.py ddstore_hs_vae --replicate=$REPLICATE --image-scale=$IMAGE_SCALE $CORE_EXTRA_ARGS \ > >(sed 's/^/[core] /') 2> >(sed 's/^/[core] /') & sleep 5 -MASTER_PORT=8891 DDSTORE_HANDSHAKE_TIMEOUT_S=60 srun -N$EXTRA_NNODES -n$EXTRA_NTASKS -c6 --gpus-per-task=1 --cpu-bind=verbose,core -l \ +MASTER_PORT=8891 DDSTORE_HANDSHAKE_TIMEOUT_S=60 srun ${OVERLAP:-} -N$EXTRA_NNODES -n$EXTRA_NTASKS -c6 --gpus-per-task=1 --cpu-bind=verbose,core -l \ bash -c "$JOB_VNI_WRAP" _ python -u examples/vae/vae_extra_train.py --handshake-dir ddstore_hs_vae --n-core $CORE_NTASKS --epochs 3 --num-workers=$NUM_WORKERS $EXTRA_EXTRA_ARGS \ > >(sed 's/^/[extr] /') 2> >(sed 's/^/[extr] /') sleep 5 From 7f37bcc0e4582ebcef679d67f0debd800c44665c Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 17:56:59 -0400 Subject: [PATCH 33/56] job-vae-core-extra.sh: default to --layout=split-node Colocate fails on Frontier (the script's #SBATCH defaults target it) and works on Perlmutter only with --overlap; split-node works on both. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- README.md | 4 ++-- examples/vae/script/job-vae-core-extra.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 93fc881..c868f71 100644 --- a/README.md +++ b/README.md @@ -439,11 +439,11 @@ sbatch examples/vae/script/job-vae-single.sh # method=0, c sbatch examples/vae/script/job-vae-single.sh --method=1 --gpudirect sbatch examples/vae/script/job-vae-single.sh --method=1 --num-workers=4 # ThreadDataLoader, 4 worker threads sbatch examples/vae/script/job-vae-single.sh --method=1 --gpudirect --num-workers=4 # ThreadDataLoader + GPUDirect -sbatch examples/vae/script/job-vae-core-extra.sh # method=2, cxi, colocate layout +sbatch examples/vae/script/job-vae-core-extra.sh # method=2, cxi, split-node layout (1 core node) sbatch examples/vae/script/job-vae-core-extra.sh --gpudirect --layout=split-node --core-nnodes=2 ``` -Run `--help` on either script for the full option list. `job-vae-single.sh` additionally has `--method` and `--num-workers` (default 0; `> 0` switches to `ThreadDataLoader`, see above). `job-vae-core-extra.sh` additionally has `--layout=colocate|split-node`, `--core-nnodes`, and `--num-workers` (for the extra/training step). Note `--layout=colocate` together with `--gpudirect` will over-request GPUs per node (core and extra each ask for a full node's worth of GPUs on the same nodes) — use `--layout=split-node` when testing GPUDirect on `job-vae-core-extra.sh`. +Run `--help` on either script for the full option list. `job-vae-single.sh` additionally has `--method` and `--num-workers` (default 0; `> 0` switches to `ThreadDataLoader`, see above). `job-vae-core-extra.sh` additionally has `--layout=split-node|colocate` (default split-node; colocate works on Perlmutter only, see [Multiple `srun` steps](#multiple-srun-steps-in-one-job-method2-cxi)), `--core-nnodes`, and `--num-workers` (for the extra/training step). Note `--layout=colocate` together with `--gpudirect` will over-request GPUs per node (core and extra each ask for a full node's worth of GPUs on the same nodes) — use `--layout=split-node` when testing GPUDirect on `job-vae-core-extra.sh`. ### Larger VAE cases diff --git a/examples/vae/script/job-vae-core-extra.sh b/examples/vae/script/job-vae-core-extra.sh index 1aef005..5f70bf5 100755 --- a/examples/vae/script/job-vae-core-extra.sh +++ b/examples/vae/script/job-vae-core-extra.sh @@ -39,7 +39,7 @@ Options: split-node (core gets --core-nnodes nodes, extra the rest). Colocate works on Perlmutter but not on Frontier, where the second step fails with "Error configuring interconnect"; - use split-node there. Default: colocate. + use split-node there. Default: split-node. --core-nnodes=N Number of nodes for the core step in split-node layout. Ignored in colocate layout. Default: 1. --num-workers=N DataLoader workers for the extra (training) step. 0 uses @@ -80,7 +80,7 @@ for arg in "$@"; do --image-scale=*) IMAGE_SCALE="${arg#--image-scale=}" ;; esac done -LAYOUT="${LAYOUT:-colocate}" +LAYOUT="${LAYOUT:-split-node}" NUM_WORKERS="${NUM_WORKERS:-0}" REPLICATE="${REPLICATE:-1}" IMAGE_SCALE="${IMAGE_SCALE:-1}" From f38669081fedd80530c0571560f1367250c67438 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 18:16:22 -0400 Subject: [PATCH 34/56] README: usage-focused rewrite; move measurements to docs/results.md; cite MDLoader - README second half reorganized around usage: GPUDirect (rules, not the investigation), PyTorch integration (batched by default, recommended settings), job scripts, sub-communicators, a short Performance section, Known Limitations (multi-step Slingshot + colocate table, Concurrency, troubleshooting). Removes the stale "GPU-to-GPU performance" section. - docs/results.md: all measurements and experiments (batched vs per-sample on Frontier 2/4 nodes and Perlmutter, DDSTORE_PROFILE breakdown, bench_get.py, method-0 collective and round sizes, worker threads, GPU sync / lock / pool / MPI thread level experiments, HIP streams). - Citation: add MDLoader (SC24-W, doi 10.1109/SCW63240.2024.00145), which the method-0 batched path follows. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- README.md | 213 ++++++++++++++++-------------------------------- docs/results.md | 141 ++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+), 145 deletions(-) create mode 100644 docs/results.md diff --git a/README.md b/README.md index c868f71..e04e03b 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ Read rows `indices` (global row ids; any order, any ranks, repeats allowed) into For `method=1`/`2` the whole batch is one call: one lock acquisition, one memory registration, and (GPU destination) one device sync, with all of the batch's `fi_read`s posted before any is waited for, so the reads overlap on the network. It is still one `fi_read` per row. -For `method=0`, `get_batch()` is **collective**, after the collective module of [MDLoader](https://ieeexplore.ieee.org/document/10596438/): every rank all-gathers all ranks' indices (`MPI_Allgatherv`), packs the rows it owns for each requester, and one `MPI_Alltoallv` delivers them, on a private duplicate of the store's communicator, in rounds of at most `DDSTORE_ALLTOALL_MAX_BYTES` (default 2 MiB) received per rank so large rows don't turn into one huge exchange. So every rank must call it for the variable the same number of times, in the same order, from one thread at a time; the number of indices may differ per rank (including 0). Indices are checked on the gathered list, so a bad index raises on every rank together. `DistributedSampler` gives every rank the same number of batches, and `vae-ddp.py` allows no worker threads with `method=0`, so the data loaders meet this automatically. +For `method=0`, `get_batch()` is **collective**, after the collective module of [MDLoader](https://ieeexplore.ieee.org/abstract/document/10820758) (see [Citation](#citation)): every rank all-gathers all ranks' indices (`MPI_Allgatherv`), packs the rows it owns for each requester, and one `MPI_Alltoallv` delivers them, on a private duplicate of the store's communicator, in rounds of at most `DDSTORE_ALLTOALL_MAX_BYTES` (default 2 MiB) received per rank so large rows don't turn into one huge exchange. So every rank must call it for the variable the same number of times, in the same order, from one thread at a time; the number of indices may differ per rank (including 0). Indices are checked on the gathered list, so a bad index raises on every rank together. `DistributedSampler` gives every rank the same number of batches, and `vae-ddp.py` allows no worker threads with `method=0`, so the data loaders meet this automatically. ```python idx = np.array([2048, 7, 4096, 7]) @@ -207,7 +207,7 @@ Release every variable's MPI window (`method=0`) or libfabric endpoints and memo | `DDSTORE_NIC_MAP` | unset | Precomputed CPU→NIC map used for that automatic pick instead of a live hwloc query (`python3 -m cpu_nic_map --env`). The constructor's `nic_map=` argument takes priority. | | `DDSTORE_HANDSHAKE_DIR` | `./ddstore_hs` | `method=2` handshake directory when none is given (C++ API; `PyDDStore` requires `handshake_dir`, and the examples fill it from this variable). Must be on a shared filesystem. | | `DDSTORE_HANDSHAKE_TIMEOUT_S` | `300` | Seconds a `method=2` extra member's `join()` polls for the core group's record file. | -| `DDSTORE_PROFILE` | off | `1` turns on `get()`/`get_batch()` timing counters, read with `get_profile(name)`. See [Profiling](#profiling-get-ddstore_profile1). | +| `DDSTORE_PROFILE` | off | `1` turns on `get()`/`get_batch()` timing counters, read with `get_profile(name)`. See [Performance](#performance). | | `DDSTORE_ALLTOALL_MAX_BYTES` | `2097152` (2 MiB) | `method=0` `get_batch()`: bytes each rank receives per exchange round. Must be equal on all ranks. | The backend itself is not an environment variable in the library: pass `method=` to `PyDDStore` (`DDSTORE_METHOD` below is how the examples choose it). @@ -229,7 +229,7 @@ The backend itself is not an environment variable in the library: pass `method=` | Variable | Effect | |---|---| | `SLINGSHOT_VNIS` | Set by Slurm per step. With `--network=job_vni`, keep only the last (job-wide) entry before starting Python so separate `srun` steps can reach each other — see [Multiple `srun` steps](#multiple-srun-steps-in-one-job-method2-cxi). | -| `GPU_MAX_HW_QUEUES` | ROCm hardware queues per GPU per process (default 4); raise it if data-loading threads use their own streams — see [HIP streams](#hip-streams-and-hardware-queues-amdrocm). | +| `GPU_MAX_HW_QUEUES` | ROCm hardware queues per GPU per process (default 4); raise it if data-loading threads use their own streams — see [HIP streams](docs/results.md#hip-streams-and-hardware-queues-frontier-rocm-72). | ## Backends @@ -297,90 +297,55 @@ See [test/test_method2_core.py](test/test_method2_core.py) / [test/test_method2_ ## GPUDirect RDMA (GPU-resident buffers) -`add()` and `get()` accept a CUDA/HIP `torch.Tensor` in place of a NumPy array, letting RDMA read from or write directly into GPU memory — no host staging buffer, no `.cpu()`/`.to(device)` copy. Requires `method=1` or `2` and a CUDA- or ROCm/HIP-enabled PyTorch build. - -**Requires `DDSTORE_FABRIC=cxi`. The `hsn` provider does not support GPUDirect RDMA at all** — passing a GPU tensor to `add()`/`get()` while `DDSTORE_FABRIC=hsn` (the default) raises a clear error rather than silently falling back to a host copy. +`add()`, `get()` and `get_batch()` accept a CUDA/HIP `torch.Tensor` in place of a NumPy array, so RDMA reads from or writes directly into GPU memory, with no `.cpu()`/`.to(device)` copy. Requires `method=1` or `2`, **`DDSTORE_FABRIC=cxi`**, and a CUDA- or ROCm-enabled PyTorch. A GPU tensor with `DDSTORE_FABRIC=hsn` (the default) or `method=0` raises a clear error instead of silently copying through the host. ```python import torch data = torch.rand(1024, 64, dtype=torch.float32, device="cuda") -store.add("features", data) # GPU source -- no host copy +store.add("features", data) # GPU source, no host copy out = torch.empty((1, 64), dtype=torch.float32, device="cuda") -store.get("features", out, start=2048) # GPU destination -- no host copy +store.get("features", out, start=2048) # GPU destination, no host copy ``` -Passing a GPU tensor to `add()` registers a **raw pointer into your tensor's own storage — no copy is made.** You must keep that tensor alive (not garbage-collected, not reused) for as long as the variable stays registered, i.e. until `free()`. `PyDDStore` holds its own reference internally as a safety net, but calling `add()` again for the same variable name with a GPU tensor is rejected outright rather than silently dropping the earlier reference. This differs from the NumPy path, where `add()` always makes a private copy and the caller's array can be freed or reused immediately after the call returns. `get()`'s destination buffer has no such caveat — it's yours as usual. - -Not supported: `init()`/`update()` (the incremental-fill path) remain host-only; `method=0` (MPI RMA) does not support GPU buffers on either `add()` or `get()`. Both raise a clear error naming the actual requirement if you try. - -See [test/test_gpu_rdma.py](test/test_gpu_rdma.py) for runnable examples covering both directions, both libfabric methods, and the negative/error cases, and the `--gpu-dest`/`--gpu-source` flags on [examples/vae/vae-ddp.py](examples/vae/vae-ddp.py) / [examples/vae/vae_extra_train.py](examples/vae/vae_extra_train.py) / [examples/vae/vae_core_server.py](examples/vae/vae_core_server.py) for a full DDP training example using it. - -GPU kernels execute asynchronously: a compute kernel that just wrote to (or is about to read) a buffer may not have fully retired by the time that buffer is handed to RDMA. On at least one ROCm+CXI build, this produced a real, confirmed bug: the RDMA transfer reported success, but the destination buffer could still show stale, pre-transfer content, because the GPU's cache hadn't been reconciled with the external NIC write. Under sustained, real-workload conditions (not just short unit tests) this showed up as hard GPU faults, not just wrong data. To guard against this, `PyDDStore` always synchronizes the GPU device (`torch.cuda.synchronize()`) before registering a buffer for RDMA in `add()`/`get()`. This is a blocking, whole-device sync, which can serialize GPU compute against RDMA transfers when called at high frequency (e.g. once per sample in a data loader) — see the performance note below. - -The sync in `get()` was re-checked by removing it: every `--gpu-dest` run of `vae-ddp.py` (`method=1`, `cxi`, 2 Frontier nodes, any `--num-workers` including 0) aborted during the first epoch with `HSA_STATUS_ERROR_EXCEPTION ... code: 0x1016` (GPU memory fault) on every rank, while host-path and `--gpu-source`-only runs were unaffected. With the sync restored the same runs complete normally. Keep it. - -## Known Limitations - -### Multiple `srun` steps in one job (`method=2`, `cxi`) - -Core and extra run as separate `srun` steps, and on Slingshot every step gets its own VNI (network isolation ID); two endpoints can only communicate on the same VNI. Two things are needed, both handled by [job-vae-core-extra.sh](examples/vae/script/job-vae-core-extra.sh): - -1. `#SBATCH --network=single_node_vni,job_vni`. `job_vni` adds a job-wide VNI to every step (`SLINGSHOT_VNIS=,`); `single_node_vni` makes single-node steps (e.g. `--layout=split-node`) get a CXI service at all — without it `fi_domain()` fails with `-38 (Function not implemented)`. -2. In each task, keep only the job VNI: `export SLINGSHOT_VNIS=${SLINGSHOT_VNIS##*,}` before starting Python. libfabric's cxi provider uses only the first VNI listed, i.e. the per-step one, so without this the extra side's reads fail with `fi_cq_read ... prov_errno=25 (VNI_NOT_FOUND)`. - -Verified on Frontier (2 nodes, split-node, `method=2`, `cxi`): with both, the extra step trains against the core step's data and both shut down cleanly; with either missing, it fails as above. MPI and RCCL inside each step work on the job VNI too. - -On Perlmutter the VNI order is the same (`,`), so the same wrapper works. Single-node steps there run even without `single_node_vni` (they get no `SLINGSHOT_*` variables but cxi falls back to a default CXI service), so split-node works with or without the flags; with them it behaves as on Frontier. - -**Colocate** (core and extra steps on the same nodes at once): - -| | `job_vni` + wrapper | `job_vni` + wrapper + `srun --overlap` | no `--network` | -|---|---|---|---| -| Frontier | second step fails to launch: `Error configuring interconnect` | same failure | steps launch, but each has only its own VNI: extra cannot reach core | -| Perlmutter | second step does not start (timeout) | **works** | extra cannot reach core (timeout) | - -`job-vae-core-extra.sh --layout=colocate` therefore runs both steps with `--overlap`; on Frontier use `--layout=split-node`. - -### `get()` has no GPU destination-buffer pool +- **`add()` with a GPU tensor registers your tensor's own memory; no copy is made.** Keep it alive and unmodified until `free()`. `PyDDStore` holds a reference as a safety net, and adding the same name again with a GPU tensor is rejected. (With NumPy, `add()` copies and the array can be reused right away.) +- **The device is synchronized before each GPU transfer** (`torch.cuda.synchronize()`, once per `get()` / `get_batch()` call): the NIC writes outside PyTorch's stream ordering, and without the sync training hit GPU memory faults. Prefer `get_batch()` on the GPU path so this costs one sync per batch, not per sample. +- `init()`/`update()` stay host-only. +- Whether GPU destinations are faster than host ones depends on the machine: on Frontier they win from ~12.5 KB rows up, on Perlmutter host destinations win at every size ([results](docs/results.md#bench_getpy-µs-per-row)). -`DistDataset`/`DistDatasetReader`'s `get()` allocates a fresh GPU tensor per call on the GPU path (`--gpu-dest`), rather than reusing a pre-allocated pool. An earlier pooled design (round-robin slices of one pre-registered buffer, to amortize `fi_mr_regattr` cost) was removed after it was confirmed by direct experiment to corrupt data under `ThreadDataLoader` with `--num-workers > 1`: multiple worker threads raced for pool slots at per-sample granularity, and bounding how many batches could be in flight at once didn't bound which physical slots got overwritten, since slot-write order was determined by lock-acquisition order, not batch order. Removing the pool removes that race entirely — each call's destination is privately owned, nothing to reuse. The tradeoff: a fresh `fi_mr_regattr` whenever the new tensor isn't inside the previously registered range (the receive-MR cache in `read_from_remote()` reuses the registration when PyTorch's allocator hands back the same block, which is common for same-shape `torch.empty()`), instead of one registration shared across many. Revisit with a pool later if that registration cost matters (`--num-workers > 0` is otherwise known to work per the next section). +Examples: [test/test_gpu_rdma.py](test/test_gpu_rdma.py), and `--gpu-dest`/`--gpu-source` on [vae-ddp.py](examples/vae/vae-ddp.py), [vae_extra_train.py](examples/vae/vae_extra_train.py) and [vae_core_server.py](examples/vae/vae_core_server.py). -### Thread-safety of concurrent `get()` calls - -`get()` is safe to call from multiple threads. For `method=1`/`2`, `DDStore::get()` serializes calls on the same variable with a per-variable mutex (`fabric_state::recv_lock` in `include/common.h`, taken via `fabric_state_lock_guard`), held for the whole RDMA read. It is required: `get()` writes per-variable fields (`recv_data`, the cached receive MR) that concurrent calls would otherwise race on, and the libfabric objects aren't opened thread-safe either (`hsn` requests `FI_THREAD_DOMAIN`, i.e. the application serializes access; `cxi` takes the provider's default from a NULL-hints `fi_getinfo()`) — without the lock, concurrent `get()` calls crashed with `double free or corruption`. Different variables have separate domains/endpoints/CQs and don't contend. `method=0` doesn't use the lock. - -`get()` releases the GIL for the transfer on both the host and the GPU-destination path. The GPU-destination path first does a whole-device `torch.cuda.synchronize()` per call (see [GPUDirect RDMA](#gpudirect-rdma-gpu-resident-buffers)), and that sync still dominates: releasing the GIL there made no measurable difference to `--gpu-dest` epoch time. `add()` keeps the GIL (one-time collective setup). - -### MPI thread level +## PyTorch Dataset Integration -DDStore and the examples use mpi4py's default initialization (`MPI_Init_thread` requesting `MPI_THREAD_MULTIPLE`). Only the main thread calls MPI — `add()`/`init()`/`join()` at setup, plus `epoch_begin()`/`epoch_end()` and `get()` for `method=0` — while `ThreadDataLoader` worker threads only call `get()` with `method=1`/`2`, which makes no MPI calls. So `MPI_THREAD_FUNNELED` is the minimum strictly required. On Frontier (Cray MPICH, 2 nodes × 8 ranks), `vae-ddp.py` and the pytest suites gave identical results and timing with `SINGLE`, `FUNNELED` and `MULTIPLE`. If you call MPI from your own worker threads with `method=0`, keep the default `MULTIPLE`. +[examples/vae/distdataset.py](examples/vae/distdataset.py) wraps a store as a `torch.utils.data.Dataset`, and [examples/vae/vae-ddp.py](examples/vae/vae-ddp.py) trains a VAE with DDP on top of it: -### HIP streams and hardware queues (AMD/ROCm) +```bash +DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --num-workers=1 +DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --num-workers=1 --gpu-dest --gpu-source +``` -HIP maps streams onto a small pool of hardware queues per GPU per process — `GPU_MAX_HW_QUEUES`, 4 by default — and streams beyond that share a queue round-robin. Work in a shared queue runs in order, so a sync on a "separate" stream can still wait behind another stream's kernels. Measured on Frontier (ROCm 7.2): with the default stream kept busy, 12 of 16 new streams were independent of it by default (every 4th collided, including the first one created), 14 of 16 with `GPU_MAX_HW_QUEUES=8`, 15 of 16 with `16`. If you give data-loading threads their own streams, raise `GPU_MAX_HW_QUEUES` and remember the training stream and RCCL already occupy queues. +- **Batched by default.** `DistDataset.__getitems__` reads each training batch with one [`get_batch()`](#get_batchname-arr-indices) call; `DDSTORE_BATCH_GET=0` falls back to one `get()` per sample. +- **`--num-workers`** (default 0): `0` uses PyTorch's standard `DataLoader` in the main process; `> 0` uses [`ThreadDataLoader`](examples/vae/ddstore_dataloader.py), which fetches and collates batches in worker *threads* (no fork, so it is safe with MPI and GPU buffers). It needs `method=1`/`2`. One or two workers are enough: reads on one variable are serialized by its lock, and a single batched call already keeps the network busy. +- **`--gpu-dest` / `--gpu-source`**: fetched batches land directly on the training GPU / each rank's shard is stored on its GPU (see [GPUDirect](#gpudirect-rdma-gpu-resident-buffers)). +- **`--replicate R`** repeats the training set R times (longer epochs); **`--image-scale S`** upscales images to (28·S)² so each row is S² larger. Both default to 1, the original example. +- The [method=2 split](#file-based-handshake-method2) variant is [vae_core_server.py](examples/vae/vae_core_server.py) (holds the data) + [vae_extra_train.py](examples/vae/vae_extra_train.py) (trains), with the same options. -### GPU-to-GPU RDMA performance on AMD/ROCm +### Slurm job scripts -On Frontier, the GPU synchronization performed before each RDMA call (needed for correctness) can outweigh the benefit of skipping the host copy for small, per-sample transfers — GPU-to-GPU has not shown a speed advantage there yet, though results are correct either way. Larger, batched transfers should benefit more; that usage pattern isn't built yet. +[job-vae-single.sh](examples/vae/script/job-vae-single.sh) runs `vae-ddp.py` as one `srun` step; [job-vae-core-extra.sh](examples/vae/script/job-vae-core-extra.sh) runs the core/extra split as two steps. Run either with `--help` for all options. Their `#SBATCH` lines target Frontier (`-A FUS184`, 8 ranks × 7 cores per node); adjust for other machines. -### Troubleshooting: RDMA fails to connect (`cxi`) +```bash +sbatch examples/vae/script/job-vae-single.sh --method=1 --num-workers=1 +sbatch examples/vae/script/job-vae-single.sh --method=1 --gpudirect --image-scale=2 +sbatch examples/vae/script/job-vae-core-extra.sh # split-node: 1 core node, the rest extra +sbatch examples/vae/script/job-vae-core-extra.sh --gpudirect --core-nnodes=2 +``` -If `fi_domain()` fails with `-38 (Function not implemented)` on `cxi`, the step has no CXI service: add `#SBATCH --network=single_node_vni` — needed on Frontier whenever a step runs on a single node (a `-N 1` job, or a one-node step inside a larger job). If ranks in different `srun` steps can't reach each other (`VNI_NOT_FOUND`), see [Multiple `srun` steps](#multiple-srun-steps-in-one-job-method2-cxi) above. +`job-vae-core-extra.sh` sets up Slingshot networking for its two steps (see [Multiple `srun` steps](#multiple-srun-steps-in-one-job-method2-cxi)). `--layout=colocate` (both steps on the same nodes) works on Perlmutter only. On Perlmutter, [examples/scripts/perlmutter-check.sh](examples/scripts/perlmutter-check.sh) runs the whole check-list in one job ([docs/perlmutter-checklist.md](docs/perlmutter-checklist.md)). ## Partitioned / Sub-communicator Usage -`PyDDStore` itself always spans the full communicator you pass it — there is no built-in "ranks per group" option. To run several independent stores side by side (e.g. one per node), split `comm` yourself before constructing `PyDDStore`, giving each group its own sub-communicator. Each group then holds a full replica of the dataset, partitioned across its own members. - -**Example — 16 ranks split into groups of 4:** -``` -ranks 0– 3 → DDStore group 0 -ranks 4– 7 → DDStore group 1 -ranks 8–11 → DDStore group 2 -ranks 12–15 → DDStore group 3 -``` - -This is useful when you want one store per node (e.g. 4 GPUs per node), limiting cross-node RDMA traffic to the dataset replication step at startup rather than every sample fetch. +`PyDDStore` always spans the whole communicator you pass it. To run several independent stores side by side (e.g. one per node), split `comm` first; each group then holds a full replica of the dataset, partitioned across its own members: ```python width = 4 # ranks per group, e.g. GPUs per node @@ -388,98 +353,43 @@ sub_comm = comm.Split(rank // width, rank) store = dds.PyDDStore(sub_comm) # one independent store per group ``` -`DistDataset` in [examples/vae/distdataset.py](examples/vae/distdataset.py) wraps exactly this pattern behind a `ddstore_width` constructor argument — pass `ddstore_width=None` (default) for a single store across all ranks in `comm`, or an integer to split into groups of that size. - -## PyTorch Dataset Integration - -See [examples/vae/distdataset.py](examples/vae/distdataset.py) for a `torch.utils.data.Dataset` wrapper and [examples/vae/vae-ddp.py](examples/vae/vae-ddp.py) for a full DDP training example. - -```bash -mpirun -n 4 python examples/vae/vae-ddp.py -``` - -`vae-ddp.py` and `vae_extra_train.py`/`vae_core_server.py` (the [method=2 split](#file-based-handshake-method2) variant) also accept `--gpu-dest`/`--gpu-source` to exercise [GPUDirect RDMA](#gpudirect-rdma-gpu-resident-buffers) end-to-end in a real training loop — `--gpu-dest` allocates the fetched batch directly on the training device, `--gpu-source` stores the local shard GPU-resident too: - -```bash -DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --gpu-dest --gpu-source -``` - -`--num-workers` (default 0) controls the training `DataLoader`'s parallelism. `0` uses PyTorch's normal `DataLoader`, single-threaded (no forked worker processes at all, so no MPI-after-`MPI_Init`-fork hazard). Any `--num-workers > 0` switches to [examples/vae/ddstore_dataloader.py](examples/vae/ddstore_dataloader.py)'s `ThreadDataLoader` instead — real threads, no fork, so it's safe together with `--gpu-dest`/`--gpu-source` too. The applied loader and worker count are printed at startup: `train_loader: DataLoader, num_workers=N` or `train_loader: ThreadDataLoader, num_workers=N`. `ThreadDataLoader` requires `DDSTORE_METHOD` 1 or 2 (libfabric) in `vae-ddp.py`. Concurrent `get()` calls from multiple threads are serialized inside `DDStore::get()` by a per-variable lock (see [Thread-safety](#thread-safety-of-concurrent-get-calls)), so threads gain overlap on everything except the RDMA call itself. `vae_extra_train.py` has the same `--num-workers` flag and behavior. +This keeps sample fetches inside a node at the cost of replicating the data per group. `DistDataset` exposes it as `ddstore_width` (`None` = one store across all ranks). Not supported with `method=2`. -Measured with `vae-ddp.py`, `method=1`, `cxi`, 2 Frontier nodes × 8 ranks, `VAE_PROFILE=1`, average per-epoch time over epochs 2–8 (epochs are short, ~0.2 s, so treat as trends): - -| `--num-workers` | host path: fetch / total (s) | `--gpu-dest --gpu-source`: fetch / total (s) | -|---|---|---| -| 0 (`DataLoader`) | 0.11 / 0.22 | 0.15 / 0.25 | -| 1 | 0.02 / 0.14–0.16 | 0.09 / 0.26 | -| 2 | 0.03 / 0.16 | 0.11 / 0.31 | -| 4 | 0.04 / 0.17 | 0.12 / 0.30 | -| 8 | 0.08 / 0.21 | 0.15 / 0.35–0.38 | - -On the host path one worker thread (background prefetch) cuts epoch time ~30%; more workers make it steadily worse as they contend for the per-variable lock and the GIL. On the GPU path threading doesn't help, because of the per-call whole-device sync. All runs finished with the same final loss. Recommended: `--num-workers=1` on the host path, `0` on the GPU path. - -These numbers use one `get()` per sample. With batched get (the default now; see [get_batch](#get_batchname-arr-indices) and the measurements under [Profiling](#profiling-get-ddstore_profile1)), every configuration is faster and the GPU path no longer suffers from workers. - -The table predates moving batch collation into the worker thread (`ThreadDataLoader.fetch()`); with that change, the host path measured fetch ≈ 0.006 s / total ≈ 0.15 s per epoch with 1 worker and 0.044 / 0.19 with 2 (GPU path with 1 worker unchanged at 0.09 / 0.25). - -```bash -# ThreadDataLoader, 1 worker thread, host path (no GPU buffers) -- the recommended setting -DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --num-workers=1 - -# ThreadDataLoader + GPUDirect together -DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --num-workers=1 --gpu-dest --gpu-source -``` +## Performance -### Slurm job scripts (Frontier) +- Use **batched reads** (the default with `DistDataset`, or `get_batch()` directly). They cut per-sample cost by 10–27× for small rows and make the GPU path insensitive to worker threads; in the VAE every configuration got 1.2–3.9× faster per epoch. +- `method=1` (one-sided `fi_read`) is the fastest backend; `method=0` with batching (collective) comes close for small rows. +- `DDSTORE_PROFILE=1` + `get_profile(name)` shows where `get()`/`get_batch()` time goes: lock wait, memory registration, posting and completing `fi_read`, GPU sync. `vae-ddp.py` prints an all-rank summary when it is set. [examples/scripts/bench_get.py](examples/scripts/bench_get.py) measures per-row latency and throughput vs row size, destination, batch size and threads. -[examples/vae/script/job-vae-single.sh](examples/vae/script/job-vae-single.sh) and [examples/vae/script/job-vae-core-extra.sh](examples/vae/script/job-vae-core-extra.sh) wrap the same VAE example for `sbatch` on Frontier — `job-vae-single.sh` runs plain DDP (one `srun` step), `job-vae-core-extra.sh` runs the [method=2 core/extra split](#file-based-handshake-method2) (two independent `srun` steps). Both take `--fabric`/`--gpudirect`; `job-vae-single.sh` also takes `--method` (the core/extra split is always `method=2` — `vae_core_server.py` sets it and the extra side always joins via method 2): +Measurements, profiles and the experiments behind these choices: [docs/results.md](docs/results.md). -```bash -sbatch examples/vae/script/job-vae-single.sh # method=0, cxi -sbatch examples/vae/script/job-vae-single.sh --method=1 --gpudirect -sbatch examples/vae/script/job-vae-single.sh --method=1 --num-workers=4 # ThreadDataLoader, 4 worker threads -sbatch examples/vae/script/job-vae-single.sh --method=1 --gpudirect --num-workers=4 # ThreadDataLoader + GPUDirect -sbatch examples/vae/script/job-vae-core-extra.sh # method=2, cxi, split-node layout (1 core node) -sbatch examples/vae/script/job-vae-core-extra.sh --gpudirect --layout=split-node --core-nnodes=2 -``` - -Run `--help` on either script for the full option list. `job-vae-single.sh` additionally has `--method` and `--num-workers` (default 0; `> 0` switches to `ThreadDataLoader`, see above). `job-vae-core-extra.sh` additionally has `--layout=split-node|colocate` (default split-node; colocate works on Perlmutter only, see [Multiple `srun` steps](#multiple-srun-steps-in-one-job-method2-cxi)), `--core-nnodes`, and `--num-workers` (for the extra/training step). Note `--layout=colocate` together with `--gpudirect` will over-request GPUs per node (core and extra each ask for a full node's worth of GPUs on the same nodes) — use `--layout=split-node` when testing GPUDirect on `job-vae-core-extra.sh`. - -### Larger VAE cases - -`vae-ddp.py` (and `vae_core_server.py` for the core/extra split; the extra side follows the published data) takes two size knobs, also exposed by both job scripts: - -- `--replicate R` — repeat the MNIST training set `R` times: longer epochs, same per-sample cost. -- `--image-scale S` — upscale images to (28·S)×(28·S): each row is S² larger (3 KB at S=1, 12.5 KB at S=2); the VAE hidden layer grows to 400·S. - -Both default to 1, which reproduces the original example exactly. - -### Profiling `get()` (`DDSTORE_PROFILE=1`) +## Known Limitations -With `DDSTORE_PROFILE=1`, `PyDDStore.get_profile(name)` returns where `get()` time goes for a variable: lock wait, receive-MR check/registration (and miss count), posting `fi_read`, waiting for completion (C++, methods 1/2), plus the GPU-destination `torch.cuda.synchronize()` and whole-call time (Python). `vae-ddp.py` prints an all-rank summary at the end when it is set. Off by default. +### Multiple `srun` steps in one job (`method=2`, `cxi`) -[examples/scripts/bench_get.py](examples/scripts/bench_get.py) measures single-row `get()` latency/throughput vs row size, host vs GPU destination, and thread count, with the same breakdown. +On Slingshot every `srun` step gets its own VNI (network isolation ID), and two endpoints can only talk on the same VNI. For core and extra running as separate steps, [job-vae-core-extra.sh](examples/vae/script/job-vae-core-extra.sh) does both of these: -Measured on Frontier (`method=1`, `cxi`, 2 nodes × 8 ranks): +1. `#SBATCH --network=single_node_vni,job_vni`: `job_vni` adds a job-wide VNI to every step (`SLINGSHOT_VNIS=,`); on Frontier, `single_node_vni` is also what gives single-node steps a CXI service at all (without it `fi_domain()` fails with `-38`). +2. In each task, before Python starts: `export SLINGSHOT_VNIS=${SLINGSHOT_VNIS##*,}`. libfabric's cxi provider uses only the first VNI listed (the step's own), so without this reads fail with `VNI_NOT_FOUND`. -- Inside the VAE, GPU-destination `get()` is dominated by the per-call device sync once a worker thread runs alongside training: ~4 µs per sample with no workers, but 130–190 µs with 1 worker and 290–430 µs with 2 (S=1/S=2), because the worker's sync waits for the training kernels. Lock wait (≤0.2 µs) and MR registration (<1 µs, even at 100% misses) are negligible; the RDMA itself is ~4–5 µs. -- `bench_get.py`, one thread, µs per single-row `get()`: 3 KB — host 8.6, GPU 19.5 (12.2 reusing the buffer); 12.5 KB — host 9.9, GPU 20.5; 200 KB — host 53, GPU 37; 1 MB — host 206–276, GPU 134. GPUDirect wins from somewhere between 12.5 KB and 200 KB per row; below that its fixed per-call overhead (sync, allocation) dominates. -- A second thread adds no per-rank throughput: the per-variable lock serializes the transfers (lock wait ≈ transfer time at large rows). +Colocating both steps on the same nodes: -With `get_batch()` (`bench_get.py --batch 128`, µs per row, 1 thread): 3 KB — host 0.68, GPU 0.78 (from 9.0 / 20.9 with one row per call); 12.5 KB — host 1.82, GPU 1.38; 200 KB — host 27.5, GPU 19.6; 1 MB — host 189, GPU 99 (~10.6 GB/s per rank). The GPU sync drops to ~0.06 µs per row, and GPUDirect now beats host from 12.5 KB rows up on Frontier. (Host-destination batches of 1 MB rows are slower than single reads there; not investigated.) +| | `job_vni` + wrapper | `job_vni` + wrapper + `srun --overlap` | no `--network` | +|---|---|---|---| +| Frontier | second step fails to launch: `Error configuring interconnect` | same failure | each step has only its own VNI: extra cannot reach core | +| Perlmutter | second step does not start | **works** | extra cannot reach core | -The crossover is platform-dependent. On Perlmutter (A100, 2 nodes × 4 ranks, batch 128, µs per row): 3 KB — host 0.82, GPU 0.99; 12.5 KB — host 1.19, GPU 1.80; 1 MB — host 75, GPU 142 (~14 vs ~7.4 GB/s per rank). There host destinations are faster at every size; with batching the two are close for small rows on both machines. VAE on Perlmutter (8 ranks, s/epoch, per-sample → batched): method 1 host 0.398 → 0.208, method 1 GPU 0.508 → 0.204, GPU with 2 workers 0.678 → 0.183, method 0 0.849 → 0.219; losses identical across all variants. +So the script defaults to `--layout=split-node`; `--layout=colocate` (with `--overlap`) is for Perlmutter. Perlmutter's single-node steps also work without the `--network` flags. -VAE (`vae-ddp.py`, epochs 2–8 average, s/epoch), per-sample `get()` → batched (`DDSTORE_BATCH_GET` 0 → 1); losses identical (8.8960 at S=1, 30.0178 at S=2): +### Concurrency -| `--image-scale` | host, 0 workers | host, 1 worker | GPU, 0 workers | GPU, 1 worker | GPU, 2 workers | -|---|---|---|---|---|---| -| 1 | 0.247 → 0.132 | 0.156 → 0.126 | 0.263 → 0.135 | 0.259 → 0.127 | 0.403 → 0.134 | -| 2 | 0.395 → 0.286 | 0.293 → 0.265 | 0.428 → 0.277 | 0.416 → 0.264 | 0.518 → 0.297 | +- `get()` and `get_batch()` are thread-safe. For `method=1`/`2` a per-variable lock in `DDStore::get()` serializes calls on one variable (it guards shared receive state; without it concurrent calls crashed). Both release the GIL during the transfer. +- `method=0` `get_batch()` is **collective**: every rank calls it the same number of times, in the same order, from one thread. `vae-ddp.py` therefore allows no worker threads with `method=0`. +- Only the main thread calls MPI (setup, `epoch_begin`/`epoch_end`, `method=0` reads); mpi4py's default `MPI_THREAD_MULTIPLE` is fine, `FUNNELED` is the minimum. If you call MPI from your own worker threads, keep `MULTIPLE`. -The per-row GPU sync falls from 129–383 µs (with workers) to ~0.1 µs, and training compute time recovers because it no longer waits behind the workers' syncs. +### Troubleshooting: RDMA fails to connect (`cxi`) -`method=0` collective `get_batch()` (`bench_get.py`, host, 16 ranks, µs per row, per-row `get()` → batch 128): 3 KB 28.8 → 3.0, 12.5 KB 34.9 → 7.5, 200 KB 170 → 111, 1 MB 739 → 685. Without the 2 MiB round cap, 200 KB and 1 MB rows were 1.6–2.4× *slower* than per-row reads. In the VAE (no workers), `method=0` goes from 0.421 → 0.144 s/epoch at S=1 and 0.597 → 0.310 at S=2, close to `method=1` batched (0.135 / 0.296); losses unchanged. +If `fi_domain()` fails with `-38 (Function not implemented)`, the step has no CXI service: add `#SBATCH --network=single_node_vni` (needed on Frontier for any single-node step, i.e. a `-N 1` job or a one-node step inside a larger job). If ranks in different `srun` steps can't reach each other (`VNI_NOT_FOUND`), see [Multiple `srun` steps](#multiple-srun-steps-in-one-job-method2-cxi) above. ## Testing @@ -561,6 +471,19 @@ If you use DDStore in your research, please cite: } ``` +The batched `method=0` path (`get_batch()` with `MPI_Allgatherv` + `MPI_Alltoallv`) follows the collective data loader of MDLoader; if you use it, please also cite: + +```bibtex +@inproceedings{bae2024mdloader, + title={MDLoader: A Hybrid Model-Driven Data Loader for Distributed Graph Neural Network Training}, + author={Bae, Jonghyun and Choi, Jong Youl and Lupo Pasini, Massimiliano and Mehta, Kshitij and Zhang, Pei and Ibrahim, Khaled}, + booktitle={SC24-W: Workshops of the International Conference for High Performance Computing, Networking, Storage and Analysis}, + year={2024}, + month={nov}, + doi={10.1109/SCW63240.2024.00145} +} +``` + ## License See [LICENSE](LICENSE). diff --git a/docs/results.md b/docs/results.md new file mode 100644 index 0000000..f3b48b0 --- /dev/null +++ b/docs/results.md @@ -0,0 +1,141 @@ +# DDStore measurements and findings + +Measurements behind the recommendations in the [README](../README.md), +collected on the `check-thread` branch in October 2026. Unless noted: +`method=1`, `DDSTORE_FABRIC=cxi`, `vae-ddp.py` with `VAE_PROFILE=1`, epoch +times averaged over all epochs but the first. Epochs are short (0.1–0.6 s), +so treat differences under ~10% as noise; numbers from different jobs vary by +up to ~2× (different nodes), comparisons within one table are from one job. + +Machines: **Frontier** (AMD MI250X, ROCm 7.2, 8 ranks × 7 cores per node) and +**Perlmutter** (NVIDIA A100, CUDA 13, 4 ranks × 32 cores per node). + +## Batched reads (`get_batch`) vs one `get()` per sample + +VAE, seconds per epoch, per-sample (`DDSTORE_BATCH_GET=0`) → batched (`1`). +Losses are identical within every row. + +**Frontier, 2 nodes × 8 ranks, 8 epochs** (loss 8.8960 at S=1, 30.0178 at S=2): + +| `--image-scale` | host, 0 workers | host, 1 worker | GPU, 0 workers | GPU, 1 worker | GPU, 2 workers | +|---|---|---|---|---|---| +| 1 | 0.247 → 0.132 | 0.156 → 0.126 | 0.263 → 0.135 | 0.259 → 0.127 | 0.403 → 0.134 | +| 2 | 0.395 → 0.286 | 0.293 → 0.265 | 0.428 → 0.277 | 0.416 → 0.264 | 0.518 → 0.297 | + +"GPU" = `--gpu-dest --gpu-source`. + +**Frontier, 4 nodes × 8 ranks, `job-vae-single.sh` (3 epochs)** (loss 6.7155 / 24.9987): + +| | S=1 | S=2 | +|---|---|---| +| method 0, 0 workers | 0.241 → 0.088 | 0.356 → 0.203 | +| method 1, 0 workers | 0.128 → 0.088 | 0.254 → 0.185 | +| method 1, 2 workers | 0.096 → 0.072 | 0.218 → 0.175 | +| method 1, 4 workers | 0.113 → 0.067 | 0.193 → 0.174 | + +**Perlmutter, 2 nodes × 4 ranks, 8 epochs** (loss 15.3781 at S=1): + +| | per-sample → batched | +|---|---| +| method 1, host, 0 workers | 0.398 → 0.208 | +| method 1, host, 2 workers | 0.380 → 0.181 | +| method 1, GPU, 0 workers | 0.508 → 0.204 | +| method 1, GPU, 2 workers | 0.678 → 0.183 | +| method 0, host, 0 workers | 0.849 → 0.219 | + +## Where `get()` time goes (`DDSTORE_PROFILE=1`) + +Frontier, 2 nodes × 8 ranks, VAE, µs per call, one `get()` per sample: + +| run | total | GPU sync | lock wait | MR | read post | CQ wait | other | +|---|---|---|---|---|---|---|---| +| host, 0/1 workers (S=1) | 7.9 / 9.3 | – | 0.0 | 0.1 | 0.6 | 3.6 | 3.6 / 5.0 | +| GPU, 0 workers (S=1 / S=2) | 13.6 / 13.6 | 3.8 / 3.7 | 0.1 | 0.3 | 0.6 | 3.7 / 4.0 | ~5 | +| GPU, 1 worker (S=1 / S=2) | 142.7 / 198.7 | **131.6 / 187.3** | 0.1 | 0.4 | 0.6 | 3.7 / 4.0 | ~6 | +| GPU, 2 workers (S=1 / S=2) | 310.7 / 452.8 | **287.2 / 429.3** | 0.2 | 0.7 | 0.7 | 3.7 / 4.1 | ~18 | + +- The per-call whole-device `torch.cuda.synchronize()` dominates the GPU path + once a worker thread runs next to training: the worker's sync waits for the + training kernels. With no workers the GPU is idle and the sync costs ~4 µs. +- Lock contention (≤0.2 µs) and MR registration (<1 µs, even at 100% cache + misses; libfabric caches registrations) are negligible. The RDMA round trip + is ~4–5 µs. +- With `get_batch()` the per-row sync drops to ~0.1 µs and compute time + recovers (training no longer waits behind the workers' syncs). + +## `bench_get.py`: µs per row + +Single-row `get()` vs `get_batch()` of 128 rows, 1 thread, host vs fresh GPU +destination. + +| row | Frontier host, 1 / 128 | Frontier GPU, 1 / 128 | Perlmutter host, 1 / 128 | Perlmutter GPU, 1 / 128 | +|---|---|---|---|---| +| 3 KB | 9.0 / 0.68 | 20.9 / 0.78 | 8.6 / 0.82 | 21.6 / 0.99 | +| 12.5 KB | 10.1 / 1.82 | 21.9 / 1.38 | 9.3 / 1.19 | 22.4 / 1.80 | +| 200 KB | 32.5 / 27.5 | 37.9 / 19.6 | – | – | +| 1 MB | 160 / 189 | 134 / 99 | 88 / 75 | 142 / 142 | + +- Batching makes small rows 10–27× cheaper per row on both machines. +- Host vs GPU destination is platform-dependent: on Frontier GPUDirect wins + from ~12.5 KB rows (up to ~10.6 GB/s per rank); on Perlmutter host + destinations win at every size (1 MB: ~14 vs ~7.4 GB/s per rank). +- On Frontier, host-destination batches of 1 MB rows are slower than single + reads (not investigated). +- A second thread adds no per-rank throughput: the per-variable lock + serializes transfers on one variable (lock wait ≈ transfer time at large + rows). One batch already keeps up to 128 reads in flight. + +## `method=0`: per-row `MPI_Get` vs collective `get_batch` + +Frontier, 16 ranks, host, µs per row (per-row `get()` → batch 128, 2 MiB rounds): +3 KB 28.8 → 3.0; 12.5 KB 34.9 → 7.5; 200 KB 170 → 111; 1 MB 739 → 685. + +Round size (`DDSTORE_ALLTOALL_MAX_BYTES`) at batch 128: 200 KB rows — no cap +272, 2 MiB 111, 8 MiB 125, 32 MiB 267; 1 MB rows — no cap 1765, 2 MiB 685, +8 MiB 696, 32 MiB 1262. One unbounded exchange of large rows is slower than +per-row reads; 2–8 MiB rounds fix it. One-sided `method=1` batching is still +faster at every size (3 KB: 1.6 µs/row in the same job). + +## Worker threads (`ThreadDataLoader`), before batching + +Frontier, 2 nodes × 8 ranks, one `get()` per sample, s/epoch (fetch / total): + +| `--num-workers` | host | GPU (`--gpu-dest --gpu-source`) | +|---|---|---| +| 0 (`DataLoader`) | 0.11 / 0.22 | 0.15 / 0.25 | +| 1 | 0.02 / 0.14–0.16 | 0.09 / 0.26 | +| 2 | 0.03 / 0.16 | 0.11 / 0.31 | +| 4 | 0.04 / 0.17 | 0.12 / 0.30 | +| 8 | 0.08 / 0.21 | 0.15 / 0.35–0.38 | + +One worker hid the fetch on the host path; more workers only contended for +the lock and the GIL. On the GPU path threads did not help (per-call device +sync). Moving collation into the worker thread later brought host, 1 worker +to fetch ≈ 0.006 s / total ≈ 0.15 s. Pinned memory with +`.to(device, non_blocking=True)` gave no gain (and was ~3× slower with 0 +workers, where pinning runs on the training thread). + +## Correctness experiments + +- **GPU sync in `get()`**: removing it made every `--gpu-dest` VAE run abort + in the first epoch with `HSA_STATUS_ERROR_EXCEPTION ... code: 0x1016` (GPU + memory fault) on every rank (Frontier); host and `--gpu-source`-only runs + were unaffected. The sync stays. +- **Per-variable lock**: without it, concurrent `get()` calls crashed with + `double free or corruption`; it protects the shared recv fields and MR cache. +- **GPU destination-buffer pool** (removed): round-robin slices of one + pre-registered buffer corrupted data under `ThreadDataLoader` with more than + one worker (slot order followed lock acquisition, not batch order). Each + `get()` now uses its own fresh tensor; MR registration is cheap enough. +- **MPI thread level**: only the main thread calls MPI. `vae-ddp.py` and the + test suites gave identical results and timing with `MPI_THREAD_SINGLE`, + `FUNNELED` and `MULTIPLE` (Frontier, Cray MPICH). + +## HIP streams and hardware queues (Frontier, ROCm 7.2) + +HIP maps streams onto `GPU_MAX_HW_QUEUES` hardware queues per GPU per process +(default 4); extra streams share a queue round-robin, and work in a shared +queue runs in order. With the default stream kept busy, 12 of 16 new streams +were independent of it by default (every 4th collided, including the first +created), 14 of 16 with `GPU_MAX_HW_QUEUES=8`, 15 of 16 with `16`. Relevant if +data-loading threads get their own streams. From fffaeddcc6454b2ace6ec26fbe60fc6f81f5cc6e Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 19:10:53 -0400 Subject: [PATCH 35/56] README: drop the sentence introducing the MDLoader citation Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index e04e03b..60ff761 100644 --- a/README.md +++ b/README.md @@ -471,8 +471,6 @@ If you use DDStore in your research, please cite: } ``` -The batched `method=0` path (`get_batch()` with `MPI_Allgatherv` + `MPI_Alltoallv`) follows the collective data loader of MDLoader; if you use it, please also cite: - ```bibtex @inproceedings{bae2024mdloader, title={MDLoader: A Hybrid Model-Driven Data Loader for Distributed Graph Neural Network Training}, From c12c5db7148c3afd865a9d1930a9a6d844866c28 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 19:13:46 -0400 Subject: [PATCH 36/56] Set version to 3.0 v2.0 is already tagged (2025-09-18, PR #4); this release adds GPUDirect RDMA, batched reads (get_batch) and thread-safe get(), and changes behavior (collective method-0 get_batch, free() releases memory, core/extra script default layout). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- pyproject.toml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9e40331..bcb0677 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "PyDDStore" -version = "0.1" +version = "3.0" description = "Distributed Data Store" requires-python = ">=3.6" dependencies = ["numpy", "mpi4py"] \ No newline at end of file diff --git a/setup.py b/setup.py index f91dcac..49eb7c7 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ setup( name="PyDDStore", - version="0.1", + version="3.0", description="Distributed Data Store", package_dir={"": "src"}, py_modules=["cpu_nic_map"], From 2d5017e63d022ba0ac957e7918eaa439ccf8279f Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 17:55:33 -0700 Subject: [PATCH 37/56] VAE job scripts: run on both Frontier and Perlmutter Pick the rank layout from NERSC_HOST/LMOD_SYSTEM_NAME. On Perlmutter, DDP ranks see all 4 GPUs of their node (--gpus-per-node=4) and pick cuda:$SLURM_LOCALID: with --gpus-per-task=1, NCCL 2.29 (pytorch/2.13.0) fails in DDP init with "Cuda failure 101 'invalid device ordinal'". Frontier keeps 8 ranks/node with --gpus-per-task=1. job-vae-single.sh: add --ranks-per-node and --cpus-per-task. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016wo6ZbZfw9fvsZooyVL3j2 --- examples/vae/script/job-vae-core-extra.sh | 29 +++++++++++++++++--- examples/vae/script/job-vae-single.sh | 32 +++++++++++++++++++++-- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/examples/vae/script/job-vae-core-extra.sh b/examples/vae/script/job-vae-core-extra.sh index 5f70bf5..e324458 100755 --- a/examples/vae/script/job-vae-core-extra.sh +++ b/examples/vae/script/job-vae-core-extra.sh @@ -17,6 +17,11 @@ # without it fi_domain() fails with -38 (ENOSYS). libfabric's cxi provider # uses only the FIRST VNI listed, so each task below restricts # SLINGSHOT_VNIS to the job VNI (last entry) so core and extra share it. +# +# Runs on Frontier and Perlmutter; the rank layout is picked from the machine +# (see PLATFORM below). The #SBATCH lines above are for Frontier. On +# Perlmutter, override them on the command line: +# sbatch -A -C gpu --gpus-per-node=4 examples/vae/script/job-vae-core-extra.sh usage() { cat < >(sed 's/^/[core] /') 2> >(sed 's/^/[core] /') & sleep 5 -MASTER_PORT=8891 DDSTORE_HANDSHAKE_TIMEOUT_S=60 srun ${OVERLAP:-} -N$EXTRA_NNODES -n$EXTRA_NTASKS -c6 --gpus-per-task=1 --cpu-bind=verbose,core -l \ +MASTER_PORT=8891 DDSTORE_HANDSHAKE_TIMEOUT_S=60 srun ${OVERLAP:-} -N$EXTRA_NNODES -n$EXTRA_NTASKS -c$EXTRA_CPUS_PER_TASK $EXTRA_GPU_ARGS --cpu-bind=verbose,core -l \ bash -c "$JOB_VNI_WRAP" _ python -u examples/vae/vae_extra_train.py --handshake-dir ddstore_hs_vae --n-core $CORE_NTASKS --epochs 3 --num-workers=$NUM_WORKERS $EXTRA_EXTRA_ARGS \ > >(sed 's/^/[extr] /') 2> >(sed 's/^/[extr] /') sleep 5 diff --git a/examples/vae/script/job-vae-single.sh b/examples/vae/script/job-vae-single.sh index 1fa49e1..18848c4 100755 --- a/examples/vae/script/job-vae-single.sh +++ b/examples/vae/script/job-vae-single.sh @@ -7,7 +7,10 @@ #SBATCH -t 30:00 #SBATCH -q debug # -# Baseline VAE DDP run. +# Baseline VAE DDP run. Runs on Frontier and Perlmutter; the rank layout is +# picked from the machine (see PLATFORM below). The #SBATCH lines above are +# for Frontier. On Perlmutter, override them on the command line: +# sbatch -A -C gpu --gpus-per-node=4 examples/vae/script/job-vae-single.sh usage() { cat < >(sed 's/^/[core] /') 2> >(sed 's/^/[core] /') From 47910277c665196f7e609fa8c7cca33ef8bf79a7 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 18:33:28 -0700 Subject: [PATCH 38/56] ThreadDataLoader: fix clean() hang; match DataLoader RNG and batch_size=None - clean(): drain the future queue without blocking. The end marker is queued only once the sampler is exhausted, so after an epoch that stopped early the old iter(fs.get, None) blocked forever. - __iter__(): draw a base seed from the generator every epoch, as torch's DataLoader iterator does, so later random draws match DataLoader. - fetch(): with batch_size=None (no auto-collation) pass the sampler's index to dataset[index] as is, as torch's map-style fetcher does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016wo6ZbZfw9fvsZooyVL3j2 --- examples/vae/ddstore_dataloader.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/examples/vae/ddstore_dataloader.py b/examples/vae/ddstore_dataloader.py index ab42560..28bb916 100644 --- a/examples/vae/ddstore_dataloader.py +++ b/examples/vae/ddstore_dataloader.py @@ -66,12 +66,17 @@ def worker_init(counter): return 0 @staticmethod - def fetch(dataset, ibatch, index, collate_fn=None, pin_memory=False): + def fetch(dataset, ibatch, index, collate_fn=None, pin_memory=False, + auto_collation=True): # Collate here, in the worker, before pinning: pinning per-sample # tensors and collating afterwards would just torch.stack them into # a new, unpinned tensor. Use the dataset's whole-batch fetch when it - # has one, like torch's own map-style fetcher. - if getattr(dataset, "__getitems__", None): + # has one, like torch's own map-style fetcher. With batch_size=None + # (no auto-collation) the sampler's index goes to dataset[index] as + # is, also as torch does (e.g. samplers that yield whole batches). + if not auto_collation: + batch = dataset[index] + elif getattr(dataset, "__getitems__", None): batch = dataset.__getitems__(index) else: batch = [dataset[i] for i in index] @@ -82,12 +87,15 @@ def fetch(dataset, ibatch, index, collate_fn=None, pin_memory=False): return (ibatch, batch) def __iter__(self): - if self.fs.qsize() > 0: - for future in iter(self.fs.get, None): - future.cancel() + # Drop what an earlier epoch left behind (it may have stopped early) + self.clean() self._num_yielded = 0 self._sampler_iter = iter(self._index_sampler) + # torch's DataLoader iterator draws a base seed from the global RNG + # here, every epoch; draw it too, so the training loop's later random + # draws are the same as with DataLoader + torch.empty((), dtype=torch.int64).random_(generator=self.generator) self.fs_iter = iter(self.fs.get, None) self._next_batch_i = 0 self._inflight = 0 @@ -117,6 +125,7 @@ def _refill(self): index, collate_fn=self.collate_fn, pin_memory=self.pin_memory, + auto_collation=self._auto_collation, ) self.fs.put(future) self._next_batch_i += 1 @@ -139,8 +148,13 @@ def __next__(self): return data def clean(self): - if self.fs.qsize() > 0: - for future in iter(self.fs.get, None): + # Without blocking: the end marker (None) is queued only once the + # sampler is exhausted, so an epoch that stopped early has none, and + # waiting for it (iter(self.fs.get, None)) would block forever. Only + # this thread puts into fs, so qsize() is exact here. + while self.fs.qsize() > 0: + future = self.fs.get_nowait() + if future is not None: future.cancel() def __del__(self): From 407935b708cabcd36a57169ebab6961df7854d30 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 21:42:56 -0400 Subject: [PATCH 39/56] perlmutter-check.sh: DDP steps see all 4 GPUs (--gpus-per-node=4) Match 2d5017e: with --gpus-per-task=1, NCCL 2.29 fails in DDP init on Perlmutter ("invalid device ordinal"). VAE runs and the extra (training) step of the core/extra checks now use --gpus-per-node=4; pytest and bench_get.py steps keep --gpus-per-task=1 (no NCCL). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- examples/scripts/perlmutter-check.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/scripts/perlmutter-check.sh b/examples/scripts/perlmutter-check.sh index bc3e4b7..56d4604 100755 --- a/examples/scripts/perlmutter-check.sh +++ b/examples/scripts/perlmutter-check.sh @@ -28,6 +28,10 @@ mkdir -p "$OUT" SUM=$OUT/summary.txt export DDSTORE_FABRIC=cxi SRUN="srun -N$N -n$NT -c$CPT --gpus-per-task=1" +# DDP (NCCL) steps: every rank sees its node's 4 GPUs and picks +# cuda:$SLURM_LOCALID; with --gpus-per-task=1 NCCL 2.29 fails in DDP init +# ("Cuda failure 101 'invalid device ordinal'"). See job-vae-single.sh. +DDP_SRUN="srun -N$N -n$NT -c$CPT --gpus-per-node=4" NOISE='Tip:|DDSTORE_NIC_MAP=|using interface|endpoint max_msg|DDStore\]|Step created' say() { echo "$*" | tee -a "$SUM"; } @@ -73,7 +77,7 @@ vae() { # tag, env..., -- args... local envs=() while [ "$1" != "--" ]; do envs+=("$1"); shift; done; shift rm -rf ddstore_hs* - env "${envs[@]}" $SRUN -l python -u examples/vae/vae-ddp.py --epochs 8 "$@" > $OUT/vae-$tag.log 2>&1 + env "${envs[@]}" $DDP_SRUN -l python -u examples/vae/vae-ddp.py --epochs 8 "$@" > $OUT/vae-$tag.log 2>&1 local rc=$? # srun -l pads the rank label only with >= 10 ranks: match ' 0:' and '0:' local loss=$(grep -hE '^ *0: ====> Epoch: 8 Average' $OUT/vae-$tag.log | awk '{print $NF}') @@ -110,12 +114,12 @@ corextra() { # tag, core srun opts, extra srun opts, extra-args say "$(printf '%-22s' $tag) extra rc=$erc epoch3=$(grep -hE '^ *0: ====> Epoch: 3 Average' $OUT/ce-$tag-extra.log | awk '{print $NF}') core rc=$crc core_done=$(grep -c 'core rank [0-9]*\] done' $OUT/ce-$tag-core.log)/$NR errors=$(cat $OUT/ce-$tag-*.log | grep -ciE 'traceback|error|abort|VNI_NOT|PTLTE|interconnect')" grep -hoE "Error configuring interconnect|VNI_NOT_FOUND|fi_domain\(\) has failed" $OUT/ce-$tag-*.log | sort | uniq -c | sed 's/^/ /' | tee -a "$SUM" } -corextra split_host "-N1 -n$NR --relative=0 -c$CPT --gpus-per-task=0" "-N1 -n$NR --relative=1 -c$CPT --gpus-per-task=1" "--num-workers=1" -corextra split_gpudest "-N1 -n$NR --relative=0 -c$CPT --gpus-per-task=0" "-N1 -n$NR --relative=1 -c$CPT --gpus-per-task=1" "--num-workers=1 --gpu-dest" +corextra split_host "-N1 -n$NR --relative=0 -c$CPT --gpus-per-task=0" "-N1 -n$NR --relative=1 -c$CPT --gpus-per-node=4" "--num-workers=1" +corextra split_gpudest "-N1 -n$NR --relative=0 -c$CPT --gpus-per-task=0" "-N1 -n$NR --relative=1 -c$CPT --gpus-per-node=4" "--num-workers=1 --gpu-dest" # colocate: both steps on both nodes at once. Needs --overlap on Perlmutter # (works there with job_vni + the wrapper); fails on Frontier either way. # Core ranks split as NR/2 per node so --n-core still equals NR. -corextra colocate_host "--overlap -N2 -n$NR -c8 --gpus-per-task=0" "--overlap -N2 -n$NR -c16 --gpus-per-task=1" "--num-workers=1" +corextra colocate_host "--overlap -N2 -n$NR -c8 --gpus-per-task=0" "--overlap -N2 -n$NR -c16 --gpus-per-node=4" "--num-workers=1" # ---------------------------------------------------------------- bench section "bench_get.py (method 1, host/GPU, 1 row vs 128 rows per call; us/row)" From 65049f3cd4c978b331ec6987b9ff093783b62db2 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 22:01:30 -0400 Subject: [PATCH 40/56] Remove perlmutter-check.sh and checklist; keep findings in docs/results.md The one-time Perlmutter acceptance check is done; the job scripts now run on Perlmutter themselves (2d5017e). Its findings (CUDA GPUDirect passes, VNI order, default CXI service for single-node steps, colocate needs --overlap, NCCL needs --gpus-per-node=4) move to docs/results.md; the README job-scripts section explains how to submit on Perlmutter. Recover the script with: git show 407935b:examples/scripts/perlmutter-check.sh Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- README.md | 10 ++- docs/perlmutter-checklist.md | 67 -------------- docs/results.md | 19 ++++ examples/scripts/perlmutter-check.sh | 130 --------------------------- 4 files changed, 27 insertions(+), 199 deletions(-) delete mode 100644 docs/perlmutter-checklist.md delete mode 100755 examples/scripts/perlmutter-check.sh diff --git a/README.md b/README.md index 60ff761..7468d34 100644 --- a/README.md +++ b/README.md @@ -332,7 +332,7 @@ DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py - ### Slurm job scripts -[job-vae-single.sh](examples/vae/script/job-vae-single.sh) runs `vae-ddp.py` as one `srun` step; [job-vae-core-extra.sh](examples/vae/script/job-vae-core-extra.sh) runs the core/extra split as two steps. Run either with `--help` for all options. Their `#SBATCH` lines target Frontier (`-A FUS184`, 8 ranks × 7 cores per node); adjust for other machines. +[job-vae-single.sh](examples/vae/script/job-vae-single.sh) runs `vae-ddp.py` as one `srun` step; [job-vae-core-extra.sh](examples/vae/script/job-vae-core-extra.sh) runs the core/extra split as two steps. Run either with `--help` for all options. Their `#SBATCH` lines target Frontier (`-A FUS184`, 8 ranks × 7 cores per node); see below for Perlmutter. ```bash sbatch examples/vae/script/job-vae-single.sh --method=1 --num-workers=1 @@ -341,7 +341,13 @@ sbatch examples/vae/script/job-vae-core-extra.sh # split-n sbatch examples/vae/script/job-vae-core-extra.sh --gpudirect --core-nnodes=2 ``` -`job-vae-core-extra.sh` sets up Slingshot networking for its two steps (see [Multiple `srun` steps](#multiple-srun-steps-in-one-job-method2-cxi)). `--layout=colocate` (both steps on the same nodes) works on Perlmutter only. On Perlmutter, [examples/scripts/perlmutter-check.sh](examples/scripts/perlmutter-check.sh) runs the whole check-list in one job ([docs/perlmutter-checklist.md](docs/perlmutter-checklist.md)). +`job-vae-core-extra.sh` sets up Slingshot networking for its two steps (see [Multiple `srun` steps](#multiple-srun-steps-in-one-job-method2-cxi)). `--layout=colocate` (both steps on the same nodes) works on Perlmutter only. + +Both scripts also run on Perlmutter: they detect the machine (`NERSC_HOST`) and use 4 ranks per node, with the training ranks seeing all 4 GPUs of their node (NCCL needs that there). Override the Frontier `#SBATCH` lines when submitting: + +```bash +sbatch -A -C gpu --gpus-per-node=4 examples/vae/script/job-vae-single.sh --method=1 +``` ## Partitioned / Sub-communicator Usage diff --git a/docs/perlmutter-checklist.md b/docs/perlmutter-checklist.md deleted file mode 100644 index 85753d9..0000000 --- a/docs/perlmutter-checklist.md +++ /dev/null @@ -1,67 +0,0 @@ -# Perlmutter checklist for `check-thread` - -Everything on this branch was verified on Frontier (AMD MI250X, ROCm, cxi). -These are the things Frontier could not cover, and how to check them on -Perlmutter (NVIDIA A100, CUDA, cxi, 4 GPUs and 64 cores per node). - -## Run it - -From the repo root on Perlmutter, with your environment loaded (modules, a -venv with CUDA torch + mpi4py) and the branch built: - -```bash -git fetch origin && git checkout check-thread && git pull -CC=cc CXX=CC pip install --no-build-isolation --no-deps -e . -sbatch -A examples/scripts/perlmutter-check.sh -``` - -About 15–25 min on 2 debug nodes. Send back `pm-check-/summary.txt` -(logs are in the same directory). - -If the build fails with `No module named 'distutils.msvccompiler'`, prefix it -with `SETUPTOOLS_USE_DISTUTILS=stdlib`. - -## What it checks, and what "good" looks like - -| # | Check | Why it matters | Expect | -|---|---|---|---| -| 1 | Slurm switch config; `SLINGSHOT_VNIS` per step | The core/extra fix keeps the **last** VNI (job VNI) because cxi uses the first. Perlmutter's order must match. | Each step: `,`, job VNI identical in both, last | -| 2 | NIC picked per rank (`cpu_nic_map`) | 4 NICs per node, `hsnN` → `cxiN` | Ranks on different NICs, names `cxi0..cxi3` | -| 3 | `test_single`, `test_multirank` | method 0 basics | all pass | -| 4 | `test_get_batch` on 8 ranks | batched get, method 0 collective + method 1 cxi, threads | all pass (the GPU case runs here) | -| 5 | `test_gpu_rdma` on 2 ranks | **CUDA GPUDirect (`FI_HMEM_CUDA`) was never run on this branch** | all pass | -| 6 | VAE, method 1, host/GPU, 0 and 2 workers, batched on/off | batched vs per-sample on CUDA | rc=0, 0 errors, **same loss8 for B0 and B1** within each pair | -| 7 | VAE, method 0 per-row vs collective | MDLoader-style collective | same loss8 for B0 and B1 | -| 8 | core/extra **split-node**, host and `--gpu-dest` | VNI wrapper + single-node steps (`single_node_vni`) | extra epoch3 printed, `core_done=4/4`, 0 errors | -| 9 | core/extra **colocate** | fails on Frontier with `job_vni` (`Error configuring interconnect`) | your test: does it launch and train? | -| 10 | `bench_get.py` | sanity of batched speedup on A100 | batch 128 much faster per row than batch 1 | - -Notes: -- If `sbatch` rejects `--network=single_node_vni,job_vni`, delete that line - from the script and note it: the split-node checks then show whether - Perlmutter needs it. -- Losses on A100 will differ from Frontier's (8.8960 / 30.0178); compare - batched vs per-sample on Perlmutter itself. -- The repo's `job-vae-single.sh` / `job-vae-core-extra.sh` assume Frontier - (8 ranks × 7 cores per node, `-A FUS184`); this check script uses `srun` - directly with 4 ranks per node (`RANKS_PER_NODE`, `CPUS_PER_TASK`). -- If colocate fails the same way as on Frontier, the README's limitation - applies to both machines; if it works, note which `SwitchParameters` - Perlmutter uses. - -## Results (job 59332134, 2026-10-04) - -- VNIs per step: `,` with the job VNI last; the wrapper rule holds. -- NICs: ranks spread over `cxi0`–`cxi3`. -- Tests: test_single 14/14, test_multirank 5/5, test_get_batch 20/20 on 8 - ranks, test_gpu_rdma 14/14 (first CUDA GPUDirect run of this branch). -- VAE: identical losses for every variant (S=1 15.3781, S=2 54.7914); - batched reads 1.9–3.9x faster per epoch (method 0 0.849 → 0.219 s). -- core/extra split-node (host and `--gpu-dest`): pass, with or without the - `--network` flags (single-node steps fall back to a default CXI service). -- colocate: works only with `job_vni` + the VNI wrapper + `srun --overlap`. - (Frontier fails the same setup with `Error configuring interconnect`.) -- bench: host beats GPU destinations at every row size on Perlmutter - (1 MB rows: 75 vs 142 us/row at batch 128); on Frontier GPU wins from 12.5 KB. -- The first version of the script printed empty loss columns (`srun -l` - pads rank labels only with >= 10 ranks); fixed. diff --git a/docs/results.md b/docs/results.md index f3b48b0..f22b5ca 100644 --- a/docs/results.md +++ b/docs/results.md @@ -139,3 +139,22 @@ queue runs in order. With the default stream kept busy, 12 of 16 new streams were independent of it by default (every 4th collided, including the first created), 14 of 16 with `GPU_MAX_HW_QUEUES=8`, 15 of 16 with `16`. Relevant if data-loading threads get their own streams. + +## Perlmutter validation (2 nodes × 4 A100, CUDA 13, cxi) + +One 2-node debug job covering what Frontier could not: + +- **Tests**: `test_single` 14/14, `test_multirank` 5/5, `test_get_batch` + 20/20 on 8 ranks, `test_gpu_rdma` 14/14 (CUDA GPUDirect, `FI_HMEM_CUDA`). +- **VAE**: identical losses for every variant (method 0/1, host/GPU, 0/2 + workers, per-sample/batched): 15.3781 at S=1, 54.7914 at S=2. +- **Slingshot**: each step's `SLINGSHOT_VNIS` is `,`, job + VNI last, as on Frontier, so the same wrapper works. Ranks spread over + `cxi0`–`cxi3`. Single-node steps work even without `--network` flags (no + `SLINGSHOT_*` variables; cxi falls back to a default CXI service). +- **core/extra**: split-node passes (host and `--gpu-dest`); colocate works + only with `job_vni` + the wrapper + `srun --overlap`. +- **DDP setup**: training ranks must see all 4 GPUs of their node + (`--gpus-per-node=4`, each picks `cuda:$SLURM_LOCALID`); with + `--gpus-per-task=1`, NCCL 2.29 fails in DDP setup with "Cuda failure 101 + 'invalid device ordinal'". The job scripts handle this. diff --git a/examples/scripts/perlmutter-check.sh b/examples/scripts/perlmutter-check.sh deleted file mode 100755 index 56d4604..0000000 --- a/examples/scripts/perlmutter-check.sh +++ /dev/null @@ -1,130 +0,0 @@ -#!/bin/bash -#SBATCH -J ddstore-pm-check -#SBATCH -C gpu -#SBATCH -q debug -#SBATCH -N 2 -#SBATCH -t 30:00 -#SBATCH --gpus-per-node=4 -#SBATCH --network=single_node_vni,job_vni -# -# Correctness check of the check-thread branch on Perlmutter (NERSC). -# -# Usage, from the repo root with your Perlmutter Python env loaded (modules, -# venv with torch + mpi4py, pyddstore built with -# CC=cc CXX=CC pip install --no-build-isolation --no-deps -e . ): -# sbatch -A examples/scripts/perlmutter-check.sh -# Results: pm-check-/summary.txt (send that back), logs alongside. -# -# Knobs: RANKS_PER_NODE (4), CPUS_PER_TASK (32). - -set -u -cd "$SLURM_SUBMIT_DIR" -NR=${RANKS_PER_NODE:-4} -CPT=${CPUS_PER_TASK:-32} -N=$SLURM_NNODES -NT=$((N * NR)) -OUT=pm-check-$SLURM_JOB_ID -mkdir -p "$OUT" -SUM=$OUT/summary.txt -export DDSTORE_FABRIC=cxi -SRUN="srun -N$N -n$NT -c$CPT --gpus-per-task=1" -# DDP (NCCL) steps: every rank sees its node's 4 GPUs and picks -# cuda:$SLURM_LOCALID; with --gpus-per-task=1 NCCL 2.29 fails in DDP init -# ("Cuda failure 101 'invalid device ordinal'"). See job-vae-single.sh. -DDP_SRUN="srun -N$N -n$NT -c$CPT --gpus-per-node=4" -NOISE='Tip:|DDSTORE_NIC_MAP=|using interface|endpoint max_msg|DDStore\]|Step created' - -say() { echo "$*" | tee -a "$SUM"; } -section() { say ""; say "=== $*"; } - -# ---------------------------------------------------------------- system -section "system" -scontrol show config | grep -iE "SwitchType|SwitchParameters" | tee -a "$SUM" -python - <<'EOF' 2>&1 | tee -a "$SUM" -import torch, numpy, mpi4py -print("torch", torch.__version__, "cuda", torch.version.cuda, "hip", torch.version.hip, - "gpus/node", torch.cuda.device_count(), "| numpy", numpy.__version__, "| mpi4py", mpi4py.__version__) -EOF -(fi_info --version 2>/dev/null | head -2) | tee -a "$SUM" -say "-- per-step Slingshot env (each step should show its own VNI, then the job VNI, last)" -for r in 0 1; do - srun -N1 -n1 --relative=$r bash -c 'echo " step $SLURM_STEP_ID node $(hostname): SLINGSHOT_VNIS=$SLINGSHOT_VNIS SVC_IDS=$SLINGSHOT_SVC_IDS DEVICES=$SLINGSHOT_DEVICES"' 2>&1 | tee -a "$SUM" -done -say "-- NIC picked per rank (cpu_nic_map, cxi)" -$SRUN python -c " -import os, cpu_nic_map as m -iface = m.select_fabric_iface() -print(' rank', os.environ.get('SLURM_PROCID'), 'cpus', sorted(os.sched_getaffinity(0))[:3], '... ->', iface) -" 2>&1 | grep -vE "$NOISE" | sort -k2 -n | tee -a "$SUM" - -# ---------------------------------------------------------------- tests -section "unit tests" -srun -N1 -n1 -c$CPT --gpus-per-task=1 python -m pytest -q -p no:cacheprovider test/test_single.py > $OUT/t_single.log 2>&1 -say "test_single (1 rank): $(tail -1 $OUT/t_single.log)" -srun -N1 -n$NR -c$CPT --gpus-per-task=1 python -m pytest -q -p no:cacheprovider test/test_multirank.py > $OUT/t_multirank.log 2>&1 -say "test_multirank ($NR ranks): $(grep -E 'passed|failed' $OUT/t_multirank.log | sort | uniq -c | tr -s ' ' | tr '\n' ' ')" -$SRUN python -m pytest -q -p no:cacheprovider -rs test/test_get_batch.py > $OUT/t_get_batch.log 2>&1 -say "test_get_batch ($NT ranks, cxi): $(grep -E 'passed|failed' $OUT/t_get_batch.log | sort | uniq -c | tr -s ' ' | tr '\n' ' ')" -srun -N2 -n2 -c$CPT --gpus-per-task=1 python -m pytest -q -p no:cacheprovider test/test_gpu_rdma.py > $OUT/t_gpu_rdma.log 2>&1 -say "test_gpu_rdma (2 ranks, CUDA): $(grep -E 'passed|failed' $OUT/t_gpu_rdma.log | sort | uniq -c | tr -s ' ' | tr '\n' ' ')" -grep -E "^FAILED|Error" $OUT/t_*.log | sort | uniq | head -20 | tee -a "$SUM" - -# ---------------------------------------------------------------- VAE -section "vae-ddp ($NT ranks, 8 epochs; batched vs per-sample must give the same loss)" -export VAE_PROFILE=1 -vae() { # tag, env..., -- args... - local tag=$1; shift - local envs=() - while [ "$1" != "--" ]; do envs+=("$1"); shift; done; shift - rm -rf ddstore_hs* - env "${envs[@]}" $DDP_SRUN -l python -u examples/vae/vae-ddp.py --epochs 8 "$@" > $OUT/vae-$tag.log 2>&1 - local rc=$? - # srun -l pads the rank label only with >= 10 ranks: match ' 0:' and '0:' - local loss=$(grep -hE '^ *0: ====> Epoch: 8 Average' $OUT/vae-$tag.log | awk '{print $NF}') - local ep=$(grep -h '\[profile\]' $OUT/vae-$tag.log | awk '{f=$5;c=$6;sub("fetch=","",f);sub("s","",f);sub("compute=","",c);sub("s","",c);F+=f;C+=c;n++} END{if(n)printf "fetch=%.3f compute=%.3f total=%.3f",F/n,C/n,(F+C)/n}') - say "$(printf '%-22s' $tag) rc=$rc loss8=$loss $ep errors=$(grep -ciE 'traceback|error|abort' $OUT/vae-$tag.log)" -} -for B in 0 1; do - vae m1_host_w0_B$B DDSTORE_METHOD=1 DDSTORE_BATCH_GET=$B -- --num-workers=0 - vae m1_host_w2_B$B DDSTORE_METHOD=1 DDSTORE_BATCH_GET=$B -- --num-workers=2 - vae m1_gpu_w0_B$B DDSTORE_METHOD=1 DDSTORE_BATCH_GET=$B -- --num-workers=0 --gpu-dest --gpu-source - vae m1_gpu_w2_B$B DDSTORE_METHOD=1 DDSTORE_BATCH_GET=$B -- --num-workers=2 --gpu-dest --gpu-source - vae m0_host_w0_B$B DDSTORE_METHOD=0 DDSTORE_BATCH_GET=$B -- --num-workers=0 -done -vae m1_gpu_w0_B1_S2 DDSTORE_METHOD=1 DDSTORE_BATCH_GET=1 -- --num-workers=0 --gpu-dest --gpu-source --image-scale=2 -vae m1_host_w0_B1_S2 DDSTORE_METHOD=1 DDSTORE_BATCH_GET=1 -- --num-workers=0 --image-scale=2 - -# ---------------------------------------------------------------- core/extra -section "method 2 core/extra (separate srun steps, job VNI)" -# libfabric cxi uses the FIRST VNI in SLINGSHOT_VNIS; keep only the job VNI -# (last entry) so both steps share it. See README "Multiple srun steps". -WRAP='export SLINGSHOT_VNIS=${SLINGSHOT_VNIS##*,}; exec "$@"' -corextra() { # tag, core srun opts, extra srun opts, extra-args - local tag=$1 copts=$2 eopts=$3 eargs=$4 hs=ddstore_hs_pm_$1 - rm -rf "$hs" - DDSTORE_METHOD=2 DDSTORE_HANDSHAKE_TIMEOUT_S=120 MASTER_PORT=8889 timeout 600 srun $copts -l \ - bash -c "$WRAP" _ python -u examples/vae/vae_core_server.py "$hs" > $OUT/ce-$tag-core.log 2>&1 & - local cpid=$! - sleep 5 - DDSTORE_METHOD=2 DDSTORE_HANDSHAKE_TIMEOUT_S=120 MASTER_PORT=8891 timeout 600 srun $eopts -l \ - bash -c "$WRAP" _ python -u examples/vae/vae_extra_train.py --handshake-dir "$hs" --n-core $NR --epochs 3 $eargs \ - > $OUT/ce-$tag-extra.log 2>&1 - local erc=$? - wait $cpid; local crc=$? - say "$(printf '%-22s' $tag) extra rc=$erc epoch3=$(grep -hE '^ *0: ====> Epoch: 3 Average' $OUT/ce-$tag-extra.log | awk '{print $NF}') core rc=$crc core_done=$(grep -c 'core rank [0-9]*\] done' $OUT/ce-$tag-core.log)/$NR errors=$(cat $OUT/ce-$tag-*.log | grep -ciE 'traceback|error|abort|VNI_NOT|PTLTE|interconnect')" - grep -hoE "Error configuring interconnect|VNI_NOT_FOUND|fi_domain\(\) has failed" $OUT/ce-$tag-*.log | sort | uniq -c | sed 's/^/ /' | tee -a "$SUM" -} -corextra split_host "-N1 -n$NR --relative=0 -c$CPT --gpus-per-task=0" "-N1 -n$NR --relative=1 -c$CPT --gpus-per-node=4" "--num-workers=1" -corextra split_gpudest "-N1 -n$NR --relative=0 -c$CPT --gpus-per-task=0" "-N1 -n$NR --relative=1 -c$CPT --gpus-per-node=4" "--num-workers=1 --gpu-dest" -# colocate: both steps on both nodes at once. Needs --overlap on Perlmutter -# (works there with job_vni + the wrapper); fails on Frontier either way. -# Core ranks split as NR/2 per node so --n-core still equals NR. -corextra colocate_host "--overlap -N2 -n$NR -c8 --gpus-per-task=0" "--overlap -N2 -n$NR -c16 --gpus-per-node=4" "--num-workers=1" - -# ---------------------------------------------------------------- bench -section "bench_get.py (method 1, host/GPU, 1 row vs 128 rows per call; us/row)" -DDSTORE_PROFILE=1 DDSTORE_METHOD=1 $SRUN python -u examples/scripts/bench_get.py \ - --row-floats 784,3136,262144 --dest host,gpu --batch 1,128 --nget 2048 2>&1 | grep -vE "$NOISE" | tee -a "$SUM" - -section "done" -say "Logs: $OUT/" From 36a2b9b38ea3d5c2edc36d9dca69405e4dc2adf3 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 22:03:14 -0400 Subject: [PATCH 41/56] Format Python with black (target py311) Formatting only (black's AST safety check passed); 8 files. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- examples/scripts/bench_get.py | 96 ++++++++++++++++++++---------- examples/vae/ddstore_dataloader.py | 9 ++- examples/vae/distdataset.py | 8 ++- examples/vae/vae-ddp.py | 36 ++++++++--- examples/vae/vae_core_server.py | 9 ++- examples/vae/vae_extra_train.py | 4 +- examples/vae/vae_model.py | 4 +- test/test_get_batch.py | 29 ++++++--- 8 files changed, 139 insertions(+), 56 deletions(-) diff --git a/examples/scripts/bench_get.py b/examples/scripts/bench_get.py index 8b68b87..d581901 100644 --- a/examples/scripts/bench_get.py +++ b/examples/scripts/bench_get.py @@ -28,27 +28,47 @@ import pyddstore as dds parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) -parser.add_argument("--row-floats", default="784,3136", - help="comma-separated row widths in float32 (784 = MNIST " - "28x28 / --image-scale 1, 3136 = 56x56 / --image-scale 2)") +parser.add_argument( + "--row-floats", + default="784,3136", + help="comma-separated row widths in float32 (784 = MNIST " + "28x28 / --image-scale 1, 3136 = 56x56 / --image-scale 2)", +) parser.add_argument("--rows-per-rank", type=int, default=4096) -parser.add_argument("--nget", type=int, default=4000, - help="get() calls per rank per configuration") -parser.add_argument("--dest", default="host,gpu", - help="comma-separated: host, gpu, gpu-reuse") -parser.add_argument("--gpu-source", action="store_true", - help="add() the shard as a GPU tensor instead of numpy") -parser.add_argument("--batch", default="1", - help="comma-separated rows per call: 1 = get(), >1 = get_batch()") -parser.add_argument("--threads", default="1", - help="comma-separated thread counts issuing get()s concurrently") -parser.add_argument("--method", type=int, default=int(os.environ.get("DDSTORE_METHOD", "1"))) +parser.add_argument( + "--nget", type=int, default=4000, help="get() calls per rank per configuration" +) +parser.add_argument( + "--dest", default="host,gpu", help="comma-separated: host, gpu, gpu-reuse" +) +parser.add_argument( + "--gpu-source", + action="store_true", + help="add() the shard as a GPU tensor instead of numpy", +) +parser.add_argument( + "--batch", + default="1", + help="comma-separated rows per call: 1 = get(), >1 = get_batch()", +) +parser.add_argument( + "--threads", + default="1", + help="comma-separated thread counts issuing get()s concurrently", +) +parser.add_argument( + "--method", type=int, default=int(os.environ.get("DDSTORE_METHOD", "1")) +) args = parser.parse_args() comm = MPI.COMM_WORLD rank, size = comm.Get_rank(), comm.Get_size() ngpu = torch.cuda.device_count() -device = torch.device(f"cuda:{int(os.environ.get('SLURM_LOCALID', 0)) % ngpu}") if ngpu else None +device = ( + torch.device(f"cuda:{int(os.environ.get('SLURM_LOCALID', 0)) % ngpu}") + if ngpu + else None +) if device is not None: torch.cuda.set_device(device) @@ -59,13 +79,17 @@ total_rows = args.rows_per_rank * size if rank == 0: - print(f"ranks={size} rows/rank={args.rows_per_rank} nget/rank={args.nget} " - f"method={args.method} fabric={os.environ.get('DDSTORE_FABRIC', 'hsn')} " - f"gpu_source={args.gpu_source} profile={os.environ.get('DDSTORE_PROFILE', '0')}", - flush=True) - print(f"{'row_B':>8} {'dest':>9} {'batch':>5} {'thr':>3} {'us/row':>8} {'MB/s/rank':>9} | " - f"{'sync':>6} {'lock':>6} {'mr':>6} {'miss%':>6} {'read':>6} {'cq':>6} {'other':>6} (us/row)", - flush=True) + print( + f"ranks={size} rows/rank={args.rows_per_rank} nget/rank={args.nget} " + f"method={args.method} fabric={os.environ.get('DDSTORE_FABRIC', 'hsn')} " + f"gpu_source={args.gpu_source} profile={os.environ.get('DDSTORE_PROFILE', '0')}", + flush=True, + ) + print( + f"{'row_B':>8} {'dest':>9} {'batch':>5} {'thr':>3} {'us/row':>8} {'MB/s/rank':>9} | " + f"{'sync':>6} {'lock':>6} {'mr':>6} {'miss%':>6} {'read':>6} {'cq':>6} {'other':>6} (us/row)", + flush=True, + ) for nf in row_floats: for dest in dests: @@ -86,10 +110,13 @@ def worker(ids): reuse_host = np.empty((B, nf), dtype=np.float32) - reuse_gpu = (torch.empty((B, nf), dtype=torch.float32, device=device) - if device is not None else None) + reuse_gpu = ( + torch.empty((B, nf), dtype=torch.float32, device=device) + if device is not None + else None + ) for s0 in range(0, len(ids), B): - chunk = ids[s0:s0 + B] + chunk = ids[s0 : s0 + B] k = len(chunk) if dest == "host": out = reuse_host[:k] @@ -129,12 +156,21 @@ def worker(ids): us_get = 1e6 * sum(v[0] for v in vals) / ngets mbps = nf * 4 * args.nget / (sum(v[0] for v in vals) / size) / 1e6 s = lambda k: 1e6 * sum(v[1][k] for v in vals) / n - other = s("py_get") - s("py_sync") - s("lock_wait") - s("mr") - s("read") - s("cq") + other = ( + s("py_get") + - s("py_sync") + - s("lock_wait") + - s("mr") + - s("read") + - s("cq") + ) miss = 100.0 * sum(v[1]["mr_miss"] for v in vals) / n - print(f"{nf * 4:>8} {dest:>9} {B:>5} {nthr:>3} {us_get:>8.2f} {mbps:>9.1f} | " - f"{s('py_sync'):>6.2f} {s('lock_wait'):>6.2f} {s('mr'):>6.2f} " - f"{miss:>6.1f} {s('read'):>6.2f} {s('cq'):>6.2f} {other:>6.2f}", - flush=True) + print( + f"{nf * 4:>8} {dest:>9} {B:>5} {nthr:>3} {us_get:>8.2f} {mbps:>9.1f} | " + f"{s('py_sync'):>6.2f} {s('lock_wait'):>6.2f} {s('mr'):>6.2f} " + f"{miss:>6.1f} {s('read'):>6.2f} {s('cq'):>6.2f} {other:>6.2f}", + flush=True, + ) # Every rank must finish reading before any rank tears down its # endpoint (gather() doesn't synchronize non-root ranks). comm.Barrier() diff --git a/examples/vae/ddstore_dataloader.py b/examples/vae/ddstore_dataloader.py index 28bb916..e9d387b 100644 --- a/examples/vae/ddstore_dataloader.py +++ b/examples/vae/ddstore_dataloader.py @@ -66,8 +66,9 @@ def worker_init(counter): return 0 @staticmethod - def fetch(dataset, ibatch, index, collate_fn=None, pin_memory=False, - auto_collation=True): + def fetch( + dataset, ibatch, index, collate_fn=None, pin_memory=False, auto_collation=True + ): # Collate here, in the worker, before pinning: pinning per-sample # tensors and collating afterwards would just torch.stack them into # a new, unpinned tensor. Use the dataset's whole-batch fetch when it @@ -105,7 +106,9 @@ def __iter__(self): # epoch up front -- keeps memory use (GPU tensors included) bounded # regardless of dataset size. Mirrors torch's own prefetch_factor # (default 2 per worker). - self._max_inflight = max(1, (self.num_workers or 1) * (self.prefetch_factor or 2)) + self._max_inflight = max( + 1, (self.num_workers or 1) * (self.prefetch_factor or 2) + ) self._refill() return self diff --git a/examples/vae/distdataset.py b/examples/vae/distdataset.py index 24bb243..baacd83 100644 --- a/examples/vae/distdataset.py +++ b/examples/vae/distdataset.py @@ -175,7 +175,9 @@ def __getitems__(self, indices): n = len(indices) label = np.zeros(n, dtype=np.int32) if self.device is not None: - val = torch.empty((n, self.data_disp), dtype=torch.float32, device=self.device) + val = torch.empty( + (n, self.data_disp), dtype=torch.float32, device=self.device + ) else: val = np.zeros((n, self.data_disp), dtype=np.float32) self.ddstore.get_batch(f"{self.label}data", val, indices) @@ -258,7 +260,9 @@ def __getitems__(self, indices): n = len(indices) label = np.zeros(n, dtype=np.int32) if self.device is not None: - val = torch.empty((n, self.data_disp), dtype=torch.float32, device=self.device) + val = torch.empty( + (n, self.data_disp), dtype=torch.float32, device=self.device + ) else: val = np.zeros((n, self.data_disp), dtype=np.float32) self.ddstore.get_batch(f"{self.label}data", val, indices) diff --git a/examples/vae/vae-ddp.py b/examples/vae/vae-ddp.py index b59a4e9..f222966 100644 --- a/examples/vae/vae-ddp.py +++ b/examples/vae/vae-ddp.py @@ -182,7 +182,9 @@ testset = datasets.MNIST( "data", train=False, download=True, transform=mnist_transform(args.image_scale) ) -test_loader = torch.utils.data.DataLoader(testset, batch_size=args.batch_size, shuffle=False) +test_loader = torch.utils.data.DataLoader( + testset, batch_size=args.batch_size, shuffle=False +) # VAE_PROFILE=1 splits each epoch's wall time into "fetch" (time spent @@ -296,7 +298,8 @@ def test(epoch): sample = torch.randn(64, 20).to(device) sample = model.module.decode(sample).cpu() save_image( - sample.view(64, 1, side, side), "results/sample_" + str(epoch) + ".png" + sample.view(64, 1, side, side), + "results/sample_" + str(epoch) + ".png", ) # DDSTORE_PROFILE=1: where get() time goes, summed over all ranks and @@ -316,24 +319,41 @@ def test(epoch): n = max(tot["calls"], 1) us = lambda x: 1e6 * x / n us_row = lambda x: 1e6 * x / max(tot["rows"], 1) - other = tot["py_get"] - tot["py_sync"] - tot["lock_wait"] - tot["mr"] - tot["read"] - tot["cq"] + other = ( + tot["py_get"] + - tot["py_sync"] + - tot["lock_wait"] + - tot["mr"] + - tot["read"] + - tot["cq"] + ) print( "[ddstore-profile] all ranks: calls={} rows={} py_calls={} mr_miss={} ({:.1%})".format( - tot["calls"], tot["rows"], tot["py_gets"], tot["mr_miss"], tot["mr_miss"] / n + tot["calls"], + tot["rows"], + tot["py_gets"], + tot["mr_miss"], + tot["mr_miss"] / n, ) ) print( "[ddstore-profile] per call (us): total={:.1f} sync={:.1f} lock_wait={:.1f} " "mr={:.1f} read={:.1f} cq={:.1f} other={:.1f}".format( - us(tot["py_get"]), us(tot["py_sync"]), us(tot["lock_wait"]), - us(tot["mr"]), us(tot["read"]), us(tot["cq"]), us(other) + us(tot["py_get"]), + us(tot["py_sync"]), + us(tot["lock_wait"]), + us(tot["mr"]), + us(tot["read"]), + us(tot["cq"]), + us(other), ), flush=True, ) print( "[ddstore-profile] per row (us): total={:.2f} sync={:.2f} read+cq={:.2f}".format( - us_row(tot["py_get"]), us_row(tot["py_sync"]), - us_row(tot["read"] + tot["cq"]) + us_row(tot["py_get"]), + us_row(tot["py_sync"]), + us_row(tot["read"] + tot["cq"]), ), flush=True, ) diff --git a/examples/vae/vae_core_server.py b/examples/vae/vae_core_server.py index b8e45b5..d0a11d9 100644 --- a/examples/vae/vae_core_server.py +++ b/examples/vae/vae_core_server.py @@ -97,8 +97,13 @@ def _resolve_dir(arg): if rank == 0: print( - "gpu_source:", gpu_source, "replicate:", replicate, - "image_scale:", image_scale, flush=True, + "gpu_source:", + gpu_source, + "replicate:", + replicate, + "image_scale:", + image_scale, + flush=True, ) if rank == 0: diff --git a/examples/vae/vae_extra_train.py b/examples/vae/vae_extra_train.py index 70bbac9..fc1602b 100644 --- a/examples/vae/vae_extra_train.py +++ b/examples/vae/vae_extra_train.py @@ -173,7 +173,9 @@ testset = datasets.MNIST( "data", train=False, download=True, transform=mnist_transform(image_scale) ) -test_loader = torch.utils.data.DataLoader(testset, batch_size=args.batch_size, shuffle=False) +test_loader = torch.utils.data.DataLoader( + testset, batch_size=args.batch_size, shuffle=False +) def train(epoch): diff --git a/examples/vae/vae_model.py b/examples/vae/vae_model.py index f785d41..1759c0b 100644 --- a/examples/vae/vae_model.py +++ b/examples/vae/vae_model.py @@ -36,9 +36,7 @@ def forward(self, x): def loss_function(recon_x, x, mu, logvar): # Reconstruction + KL divergence losses summed over all elements and batch - BCE = F.binary_cross_entropy( - recon_x, x.view(-1, recon_x.shape[1]), reduction="sum" - ) + BCE = F.binary_cross_entropy(recon_x, x.view(-1, recon_x.shape[1]), reduction="sum") # see Appendix B from VAE paper: # Kingma and Welling. Auto-Encoding Variational Bayes. ICLR, 2014 diff --git a/test/test_get_batch.py b/test/test_get_batch.py index 94629a3..8ef8db0 100644 --- a/test/test_get_batch.py +++ b/test/test_get_batch.py @@ -22,7 +22,10 @@ # fabric); also require running inside a Slurm job step. HAVE_CXI = bool(glob.glob("/dev/cxi*")) and "SLURM_STEP_ID" in os.environ FABRIC = os.environ.get("DDSTORE_FABRIC", "cxi") -METHODS = [0, pytest.param(1, marks=pytest.mark.skipif(not HAVE_CXI, reason="no CXI device"))] +METHODS = [ + 0, + pytest.param(1, marks=pytest.mark.skipif(not HAVE_CXI, reason="no CXI device")), +] try: import torch @@ -46,7 +49,9 @@ def make_store(comm, method, monkeypatch, dtype=np.float32): store = dds.PyDDStore(comm, method=method) # row r (global) holds r*100 + column, so every element is identifiable first = rank * NROWS - data = (np.arange(first, first + NROWS)[:, None] * 100 + np.arange(NCOLS)).astype(dtype) + data = (np.arange(first, first + NROWS)[:, None] * 100 + np.arange(NCOLS)).astype( + dtype + ) store.add("x", data) store.epoch_begin() return store @@ -109,7 +114,9 @@ def test_batch_single_row(comm, monkeypatch, method): @pytest.mark.parametrize("method", METHODS) -@pytest.mark.parametrize("dtype", [np.uint8, np.int32, np.float32, np.int64, np.float64]) +@pytest.mark.parametrize( + "dtype", [np.uint8, np.int32, np.float32, np.int64, np.float64] +) def test_batch_dtypes(comm, monkeypatch, method, dtype): size = comm.Get_size() store = make_store(comm, method, monkeypatch, dtype=dtype) @@ -142,8 +149,10 @@ def test_batch_errors_leave_store_usable(comm, monkeypatch, method): assert all_passed(comm, ok) -@pytest.mark.skipif(not (HAVE_CXI and HAVE_GPU and FABRIC == "cxi"), - reason="requires the cxi provider and a GPU") +@pytest.mark.skipif( + not (HAVE_CXI and HAVE_GPU and FABRIC == "cxi"), + reason="requires the cxi provider and a GPU", +) def test_batch_into_gpu_tensor(comm, monkeypatch): size = comm.Get_size() store = make_store(comm, 1, monkeypatch) @@ -161,7 +170,10 @@ def test_batch_into_gpu_tensor(comm, monkeypatch): assert all_passed(comm, ok) -@pytest.mark.parametrize("method", [pytest.param(1, marks=pytest.mark.skipif(not HAVE_CXI, reason="no CXI device"))]) +@pytest.mark.parametrize( + "method", + [pytest.param(1, marks=pytest.mark.skipif(not HAVE_CXI, reason="no CXI device"))], +) def test_batch_concurrent_threads(comm, monkeypatch, method): """get_batch from several threads at once on one variable: the per-variable lock must keep each batch's rows and completions apart.""" @@ -181,7 +193,10 @@ def worker(seed): except Exception as exc: # noqa: BLE001 - surface any thread exception errors.append(exc) - threads = [threading.Thread(target=worker, args=(1000 * comm.Get_rank() + t,)) for t in range(4)] + threads = [ + threading.Thread(target=worker, args=(1000 * comm.Get_rank() + t,)) + for t in range(4) + ] for t in threads: t.start() for t in threads: From 39fadd3055678b8eaf007a88750642ee6c6cbd6e Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Sun, 4 Oct 2026 22:03:36 -0400 Subject: [PATCH 42/56] Remove docs/plan-batched-get.md (implemented) All steps are done (cb1761b, 46e006e, 3e5af49); get_batch is documented in the README and the measurements are in docs/results.md. The plan remains in history (ddcadd3). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- docs/plan-batched-get.md | 137 --------------------------------------- 1 file changed, 137 deletions(-) delete mode 100644 docs/plan-batched-get.md diff --git a/docs/plan-batched-get.md b/docs/plan-batched-get.md deleted file mode 100644 index d6f9689..0000000 --- a/docs/plan-batched-get.md +++ /dev/null @@ -1,137 +0,0 @@ -# Plan: batched `get` (`get_batch`) - -Status: planned, not started (updated 2026-10-04, branch `check-thread`, after -`3dadb32`, which added `DDSTORE_PROFILE`, `bench_get.py` and the VAE -`--replicate`/`--image-scale` options used below). - -## Motivation (measured) - -Today `DistDataset.get()` makes one `PyDDStore.get()` per sample, and each call -does: allocate a 1-row tensor, whole-device `torch.cuda.synchronize()` (GPU -destination only), take the per-variable `recv_lock`, check/register the -recv MR, post one `fi_read`, busy-poll the CQ, return to Python. - -`DDSTORE_PROFILE=1` on Frontier (`method=1`, cxi, 2 nodes x 8 ranks), µs per -`get()`: - -| VAE run | total | GPU sync | lock wait | MR | read post | CQ wait | other | -|---|---|---|---|---|---|---|---| -| host, 0/1 workers (S=1) | 7.9 / 9.3 | – | 0.0 | 0.1 | 0.6 | 3.6 | 3.6 / 5.0 | -| GPU, 0 workers (S=1 / S=2) | 13.6 / 13.6 | 3.8 / 3.7 | 0.1 | 0.3 | 0.6 | 3.7 / 4.0 | ~5 | -| GPU, 1 worker (S=1 / S=2) | 142.7 / 198.7 | **131.6 / 187.3** | 0.1 | 0.4 | 0.6 | 3.7 / 4.0 | ~6 | -| GPU, 2 workers (S=1 / S=2) | 310.7 / 452.8 | **287.2 / 429.3** | 0.2 | 0.7 | 0.7 | 3.7 / 4.1 | ~18 | - -`bench_get.py`, single-row `get()`, 1 thread, µs: 3 KB host 8.6 / GPU 19.5 -(12.2 reusing the buffer); 12.5 KB host 9.9 / GPU 20.5; 200 KB host 53 / GPU 37; -1 MB host 206–276 / GPU 134. A second thread adds no per-rank throughput: lock -wait ≈ transfer time at large rows. - -What this says about the design: -- **The per-call GPU sync is the cost to remove.** With a worker running next - to training it waits for the training kernels (130–430 µs per sample); - with no worker the GPU is idle and it costs ~4 µs — still as much as the RDMA. -- **Fixed per-call overhead dominates small rows:** the RDMA round trip is - ~4–5 µs, while sync + allocation + Python add ~10 µs per GPU `get()`. -- **Reads are serialized** by `recv_lock`, so more threads don't add bandwidth. -- **Not worth fixing:** lock contention (≤0.2 µs) and MR registration (<1 µs - even at 100% misses — libfabric caches registrations). No multi-entry MR cache. - -A batched read pays the sync, lock, MR check and Python call once per batch -instead of once per sample, and posts all of a batch's `fi_read`s before -waiting, so the round trips overlap on the network. - -Checked facts: -- torch 2.14 `_MapDatasetFetcher.fetch()` calls `dataset.__getitems__(indices)` - if defined, so the standard DataLoader picks up a batch hook without changes. -- cxi (`fi_info -p cxi -v`, login node): tx `size: 1024`, `rma_iov_limit: 1` - — up to 1024 outstanding ops per endpoint, one contiguous region per - `fi_read`. Re-check on a compute node. - -## 1. C++ (`include/ddstore.hpp`, `src/common.cxx`) - -- New `DDStore::get_batch(name, const long *idx, long n, T *buffer, int hmem_iface)`: - row `i` of the contiguous `n`-row `buffer` receives global row `idx[i]`. - Same per-row validation as `get()` (range, `sortedsearch` target, itemsize), - done for all rows before anything is posted. -- method 0: loop the existing per-row `MPI_Get` path (unchanged behavior). -- methods 1/2, holding `recv_lock` once for the whole batch: - 1. Register the whole batch buffer once (`n * row_bytes`) through the existing - recv-MR region cache. - 2. Post all `n` `fi_read`s back to back (target `comm_partner[t]`, - `remote_address[t] + offset`, `remote_key[t]`, local `buffer + i*row_bytes`, - same MR desc). On `-FI_EAGAIN`, drain completions with `fi_cq_read` and retry. - Optional: coalesce runs of consecutive indices on the same target into one read. - 3. Wait for exactly `n` completions. On any error, keep draining the - completions of reads already posted before returning — a stale completion - left in the CQ would be consumed by the next call. -- Refactor `read_from_remote()` into `ensure_recv_mr()` + `post_read()` + - `wait_completions(k)`; single-row `get()` = post 1 + wait 1 (same behavior). -- Extend the `DDSTORE_PROFILE` counters: count batches and rows separately so - per-row and per-batch costs can both be reported. - -## 2. Cython (`src/pyddstore.pyx`) - -- `get_batch(name, arr, indices)`: `arr` shape `(n, ...)` (numpy or CUDA/HIP - tensor), `indices` converted to a contiguous int64 array of length `n`. -- Same checks as `get()` (`_check_dtype`, contiguity, GPU fabric preconditions). -- One `torch.cuda.synchronize(device)` per batch on the GPU path (keep it - device-wide for now: removing it entirely caused GPU faults; see Follow-ups). -- C++ call under `with nogil:`, item-size dispatch like `get()`. -- Python-side profiling as in `get()` (whole-call and sync time). - -## 3. Dataset (`examples/vae/distdataset.py`, `ddstore_dataloader.py`) - -- `DistDataset.__getitems__(indices)` / `DistDatasetReader.__getitems__`: - allocate one `(n, data_disp)` buffer (numpy, or `torch.empty(..., device=...)`), - `get_batch` data and labels, return - `[(row_i.reshape(1, side, side), label_i) for i]` so `collate_fn` works unchanged. -- `ThreadDataLoader.fetch()`: use `dataset.__getitems__(index)` when present, - else the per-sample list (mirror torch's fetcher). -- `DDSTORE_BATCH_GET=0` env var to fall back to per-sample `get()` for A/B. - -## 4. Tests - -Note: `test_single.py` / `test_multirank.py` always use `method=0`, so they -only cover `get_batch`'s MPI fallback. Add method 1 coverage explicitly. - -- `test_single` / `test_multirank`: `get_batch` with shuffled indices spanning - all ranks, duplicates, `n == 1`; compare row by row with per-row `get()`. - Out-of-range index raises and leaves the store usable (a later `get()` works). -- Method 1 (cxi) multi-rank: the same cases, in `test_gpu_rdma.py` or a new - libfabric test file (host destination too, not only GPU). -- `test_gpu_rdma`: same into a GPU tensor; concurrent `get_batch` from threads. - -## 5. Measure (`method=1`, cxi, 2 nodes x 8 ranks, `DDSTORE_PROFILE=1`) - -- `bench_get.py --batch N` (new option): rows per call 1/32/128, row sizes - 3 KB, 12.5 KB, 200 KB, 1 MB, host / GPU destination, 1–2 threads. -- `vae-ddp.py --image-scale 1` and `2` (optionally `--replicate 4`), host path and - `--gpu-dest --gpu-source`, `--num-workers` 0/1/2, `DDSTORE_BATCH_GET` on/off. - Check the epoch-8 loss is unchanged (8.8960 at S=1; 30.0178 at S=2). -- Baselines (per-sample `get`, epochs 2–8 avg, s/epoch): - S=1 — host w0 0.25, host w1 0.16, GPU w0 0.30, GPU w1 0.26, GPU w2 0.40; - S=2 — host w0 0.39, host w1 0.28, GPU w0 0.42, GPU w1 0.42, GPU w2 0.55. -- Expected: GPU path loses most of its gap (128 syncs per batch -> 1, overlapped - reads); host path gains a little; extra workers matter even less. -- Update README (`get_batch` API, results). - -## Follow-ups (after batching) - -- The one remaining sync per batch still waits for training kernels when a - worker runs. Options: a per-worker CUDA/HIP stream + stream-only sync (raise - `GPU_MAX_HW_QUEUES`, see README), or a per-worker pre-registered receive - ring with events. Must be re-verified against the GPU fault seen when the - sync was removed. -- Block-shuffling sampler so consecutive rows on the same rank coalesce into - one larger read. - -## Risks / open questions - -- Batch indices span many targets: 128 concurrent reads to up to 16 ranks is - within the 1024 tx queue, but receiver-side limits are unmeasured. -- Partial-post failure paths must drain in-flight reads before the buffer is - handed back (the subtle part). -- cxi `threading` level irrelevant here because the lock is kept. - -Size estimate: C++ ~120 lines (mostly the refactor), Cython ~40, dataset ~30, -tests ~100, bench ~20. From 5bf2e2a25d53b526011dc9ed1c4b3f3431885d7f Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Mon, 5 Oct 2026 08:49:23 -0400 Subject: [PATCH 43/56] Set version to 2.0 The v2.0 tag (2025-09-18) was never released; this branch is the 2.0 release instead of 3.0. The tag is to be moved to the release commit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- pyproject.toml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bcb0677..1dcac6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "PyDDStore" -version = "3.0" +version = "2.0" description = "Distributed Data Store" requires-python = ">=3.6" dependencies = ["numpy", "mpi4py"] \ No newline at end of file diff --git a/setup.py b/setup.py index 49eb7c7..9422a73 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ setup( name="PyDDStore", - version="3.0", + version="2.0", description="Distributed Data Store", package_dir={"": "src"}, py_modules=["cpu_nic_map"], From e3bd2f0cbabadeaede76198a10cb9b68854f7275 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Mon, 5 Oct 2026 09:10:36 -0400 Subject: [PATCH 44/56] Add pyddstore.torch: general DistDataset, DistDatasetReader, ThreadDataLoader pyddstore becomes a package: the extension is pyddstore._core (loaded on first use, so `from pyddstore.torch import ...` imports torch before MPI starts); `import pyddstore as dds; dds.PyDDStore` is unchanged. torch is an optional extra. pyddstore.torch (moved from examples/vae, generalized): - DistDataset(source, name, comm, ddstore_width, device, add_device, method, handshake_dir): any map-style dataset whose samples are a tensor / numpy array / number, a tuple or list of them, or a dict of them, with fixed shape and dtype per field. One DDStore variable per field (name/key); each rank loads its share. Samples come back with the source's structure, types, shapes and dtypes. Schema checked on every rank and errors raised on all ranks together. __getitems__ reads a batch with one get_batch() per field (DDSTORE_BATCH_GET=0: per sample). - DistDatasetReader(name, handshake_dir, n_core, device): method-2 extra member; reads the fields from {name}.meta.json written by the core group. - ThreadDataLoader: unchanged behavior (incl. today's fixes). The VAE examples use the library; their private distdataset.py and ddstore_dataloader.py are removed. test/test_torch.py covers structures, field kinds, loaders vs plain source, errors, ddstore_width, GPU, and the method-2 reader. Frontier (cxi, 2 nodes): test_torch 19/19 on 4 and 16 ranks; existing suites pass; vae-ddp loss 8.8960 (method 1 w0/w2/GPU, method 0) and core/extra split 19.9202 (host and --gpudirect), identical to before. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- .gitignore | 4 + README.md | 32 +- examples/vae/ddstore_dataloader.py | 165 ------ examples/vae/distdataset.py | 273 ---------- examples/vae/vae-ddp.py | 6 +- examples/vae/vae_core_server.py | 8 +- examples/vae/vae_extra_train.py | 7 +- pyproject.toml | 5 +- setup.py | 5 +- src/pyddstore/__init__.py | 29 ++ src/{pyddstore.pyx => pyddstore/_core.pyx} | 0 src/pyddstore/torch.py | 563 +++++++++++++++++++++ test/test_gpu_rdma.py | 2 +- test/test_torch.py | 236 +++++++++ 14 files changed, 875 insertions(+), 460 deletions(-) delete mode 100644 examples/vae/ddstore_dataloader.py delete mode 100644 examples/vae/distdataset.py create mode 100644 src/pyddstore/__init__.py rename src/{pyddstore.pyx => pyddstore/_core.pyx} (100%) create mode 100644 src/pyddstore/torch.py create mode 100644 test/test_torch.py diff --git a/.gitignore b/.gitignore index 22e6d26..f0f2345 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ # Cython build output build/ src/pyddstore.cpp +src/pyddstore/_core.cpp # built module PyDDStore.egg-info/ pyddstore.cpython-*.so +_core.cpython-*.so +__pycache__/ +*.pyc diff --git a/README.md b/README.md index 7468d34..c2bb562 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ PyDDStore(comm, method=2, handshake_dir="/path") # method 2, core member (n PyDDStore(None, method=2, handshake_dir="/path", n_core=N) # method 2, extra member (no comm) ``` -Note: grouping ranks into independent stores (the "sub-communicator" pattern below) is done by splitting `comm` yourself before constructing `PyDDStore` — there is no `ddstore_width` constructor parameter. `DistDataset` in [examples/vae/distdataset.py](examples/vae/distdataset.py) shows the pattern (`comm.Split()` then `PyDDStore(sub_comm)`). +Note: grouping ranks into independent stores (the "sub-communicator" pattern below) is done by splitting `comm` yourself before constructing `PyDDStore` — there is no `ddstore_width` constructor parameter. `pyddstore.torch.DistDataset` does this for you (`ddstore_width`) and shows the pattern (`comm.Split()` then `PyDDStore(sub_comm)`). --- @@ -317,18 +317,38 @@ Examples: [test/test_gpu_rdma.py](test/test_gpu_rdma.py), and `--gpu-dest`/`--gp ## PyTorch Dataset Integration -[examples/vae/distdataset.py](examples/vae/distdataset.py) wraps a store as a `torch.utils.data.Dataset`, and [examples/vae/vae-ddp.py](examples/vae/vae-ddp.py) trains a VAE with DDP on top of it: +`pyddstore.torch` (needs PyTorch: `pip install .[torch]` or an existing PyTorch) turns any map-style dataset into a distributed one: + +```python +import torch # import torch before MPI starts +from mpi4py import MPI +from pyddstore.torch import DistDataset, ThreadDataLoader + +trainset = DistDataset(my_dataset, "train", MPI.COMM_WORLD) # each rank loads only its share +sampler = torch.utils.data.distributed.DistributedSampler(trainset) +loader = ThreadDataLoader(trainset, batch_size=128, sampler=sampler, num_workers=1) +for x, y in loader: + ... +``` + +- **`DistDataset(source, name, comm=None, ddstore_width=None, device=None, add_device=None, method=None, handshake_dir=None)`**: each rank loads its contiguous share of `source` (anything with `len()` and `[i]`) into DDStore; every rank can then read every sample. Samples keep the source's structure (a tensor, numpy array or number; a tuple or list of them; or a dict of them), with each field's shape and dtype. Fields must have the same shape and dtype in every sample; supported dtypes are bool, uint8, int32, int64, float32 and float64. `ds.shapes` / `ds.dtypes` describe the fields, `ds.ddstore` is the underlying `PyDDStore`. + - `method` (default `DDSTORE_METHOD` or 0) picks the backend; `ddstore_width` splits `comm` into independent stores (see [Partitioned usage](#partitioned--sub-communicator-usage)). + - `device` puts tensor fields of read samples on a GPU and `add_device` keeps each rank's share there ([GPUDirect](#gpudirect-rdma-gpu-resident-buffers)). + - **Batched by default**: `__getitems__` reads a whole batch with one [`get_batch()`](#get_batchname-arr-indices) per field, which `DataLoader` and `ThreadDataLoader` call automatically; `DDSTORE_BATCH_GET=0` reads one sample at a time. With `method=0` batched reads are collective, so every rank must iterate the same number of batches from one thread (`DistributedSampler` does). +- **`DistDatasetReader(name, handshake_dir=None, n_core=None, device=None)`**: the same dataset read by a separate `method=2` extra job; it learns the fields from a `{name}.meta.json` file the core group writes next to the handshake records. +- **`ThreadDataLoader(dataset, **DataLoader args)`**: a `DataLoader` whose workers are threads, not forked processes, so it is safe with MPI and GPU buffers. Each batch is fetched, collated and optionally pinned in a worker thread; random draws match `DataLoader`'s. One or two workers are enough: reads on one variable are serialized by its lock, and one batched read already keeps the network busy. `DDSTORE_AFFINITY_WIDTH` / `DDSTORE_AFFINITY_OFFSET` pin worker threads to CPUs. + +[examples/vae/vae-ddp.py](examples/vae/vae-ddp.py) trains a VAE with DDP on top of it: ```bash DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --num-workers=1 DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --num-workers=1 --gpu-dest --gpu-source ``` -- **Batched by default.** `DistDataset.__getitems__` reads each training batch with one [`get_batch()`](#get_batchname-arr-indices) call; `DDSTORE_BATCH_GET=0` falls back to one `get()` per sample. -- **`--num-workers`** (default 0): `0` uses PyTorch's standard `DataLoader` in the main process; `> 0` uses [`ThreadDataLoader`](examples/vae/ddstore_dataloader.py), which fetches and collates batches in worker *threads* (no fork, so it is safe with MPI and GPU buffers). It needs `method=1`/`2`. One or two workers are enough: reads on one variable are serialized by its lock, and a single batched call already keeps the network busy. -- **`--gpu-dest` / `--gpu-source`**: fetched batches land directly on the training GPU / each rank's shard is stored on its GPU (see [GPUDirect](#gpudirect-rdma-gpu-resident-buffers)). +- **`--num-workers`** (default 0): `0` uses PyTorch's standard `DataLoader`; `> 0` uses `ThreadDataLoader` (needs `method=1`/`2`). +- **`--gpu-dest` / `--gpu-source`**: `DistDataset`'s `device` / `add_device`. - **`--replicate R`** repeats the training set R times (longer epochs); **`--image-scale S`** upscales images to (28·S)² so each row is S² larger. Both default to 1, the original example. -- The [method=2 split](#file-based-handshake-method2) variant is [vae_core_server.py](examples/vae/vae_core_server.py) (holds the data) + [vae_extra_train.py](examples/vae/vae_extra_train.py) (trains), with the same options. +- The [method=2 split](#file-based-handshake-method2) variant is [vae_core_server.py](examples/vae/vae_core_server.py) (a `DistDataset` core group) + [vae_extra_train.py](examples/vae/vae_extra_train.py) (a `DistDatasetReader`), with the same options. ### Slurm job scripts diff --git a/examples/vae/ddstore_dataloader.py b/examples/vae/ddstore_dataloader.py deleted file mode 100644 index e9d387b..0000000 --- a/examples/vae/ddstore_dataloader.py +++ /dev/null @@ -1,165 +0,0 @@ -import logging -import os -import socket -import queue -import multiprocessing as mp -from concurrent.futures import ThreadPoolExecutor - -import torch -from torch.utils.data import DataLoader - -logger = logging.getLogger(__name__) - - -class ThreadDataLoader(DataLoader): - """DataLoader that parallelizes __getitem__ across a thread pool instead - of forked worker processes. Threads share the parent's CUDA context and - Python objects directly, so GPU-resident buffers (DDStore's --gpu-dest/ - --gpu-source path) stay safe across workers -- the default DataLoader's - forked processes cannot own GPU state, which is why it's capped at - num_workers=0 for that path. - """ - - def __init__(self, dataset, **kwargs): - super().__init__(dataset, **kwargs) - - self.fs = queue.Queue() - # Persistent across epochs -- recreating the pool in every __iter__() - # would leak OS threads since the old pool is never shut down. - self._counter = mp.Value("i", 0) - self.executor = ThreadPoolExecutor( - max_workers=self.num_workers or 1, - initializer=self.worker_init, - initargs=(self._counter,), - ) - - logger.debug("num_workers: %s", self.num_workers) - logger.debug("len: %s", len(self._index_sampler)) - - @staticmethod - def worker_init(counter): - core_width = int(os.environ.get("DDSTORE_AFFINITY_WIDTH", "0")) - core_offset = int(os.environ.get("DDSTORE_AFFINITY_OFFSET", "0")) - if core_width <= 0 or not hasattr(os, "sched_getaffinity"): - return 0 - - with counter.get_lock(): - wid = counter.value - counter.value += 1 - - affinity = list(os.sched_getaffinity(0)) - affinity_mask = set( - affinity[ - core_width * wid + core_offset : core_width * (wid + 1) + core_offset - ] - ) - if affinity_mask: - os.sched_setaffinity(0, affinity_mask) - hostname = socket.gethostname() - logger.debug( - "Worker: pid=%s hostname=%s ID=%s affinity=%s", - os.getpid(), - hostname, - wid, - os.sched_getaffinity(0), - ) - return 0 - - @staticmethod - def fetch( - dataset, ibatch, index, collate_fn=None, pin_memory=False, auto_collation=True - ): - # Collate here, in the worker, before pinning: pinning per-sample - # tensors and collating afterwards would just torch.stack them into - # a new, unpinned tensor. Use the dataset's whole-batch fetch when it - # has one, like torch's own map-style fetcher. With batch_size=None - # (no auto-collation) the sampler's index goes to dataset[index] as - # is, also as torch does (e.g. samplers that yield whole batches). - if not auto_collation: - batch = dataset[index] - elif getattr(dataset, "__getitems__", None): - batch = dataset.__getitems__(index) - else: - batch = [dataset[i] for i in index] - if collate_fn is not None: - batch = collate_fn(batch) - if pin_memory: - batch = torch.utils.data._utils.pin_memory.pin_memory(batch) - return (ibatch, batch) - - def __iter__(self): - # Drop what an earlier epoch left behind (it may have stopped early) - self.clean() - - self._num_yielded = 0 - self._sampler_iter = iter(self._index_sampler) - # torch's DataLoader iterator draws a base seed from the global RNG - # here, every epoch; draw it too, so the training loop's later random - # draws are the same as with DataLoader - torch.empty((), dtype=torch.int64).random_(generator=self.generator) - self.fs_iter = iter(self.fs.get, None) - self._next_batch_i = 0 - self._inflight = 0 - self._sampler_exhausted = False - # Bound how many batches can be in flight (submitted but not yet - # consumed via __next__) at once, instead of submitting the whole - # epoch up front -- keeps memory use (GPU tensors included) bounded - # regardless of dataset size. Mirrors torch's own prefetch_factor - # (default 2 per worker). - self._max_inflight = max( - 1, (self.num_workers or 1) * (self.prefetch_factor or 2) - ) - self._refill() - return self - - def _refill(self): - while self._inflight < self._max_inflight: - try: - index = next(self._sampler_iter) - except StopIteration: - if not self._sampler_exhausted: - self._sampler_exhausted = True - self.fs.put(None) - return - future = self.executor.submit( - self.fetch, - self.dataset, - self._next_batch_i, - index, - collate_fn=self.collate_fn, - pin_memory=self.pin_memory, - auto_collation=self._auto_collation, - ) - self.fs.put(future) - self._next_batch_i += 1 - self._inflight += 1 - - def __next__(self): - # Refill *before* popping this call's batch, not after: refilling - # here only uses capacity freed by the *previous* call's batch, - # which -- by ordinary for-loop semantics -- the caller's loop body - # has already fully consumed by the time it asks for the next item - # (i.e. calls __next__ again). Bounds how far the executor can race - # ahead of consumption (memory, not correctness -- distdataset.py's - # get() allocates a fresh destination per call, nothing shared to - # race on). - self._refill() - future = next(self.fs_iter) - ibatch, data = future.result() - self._inflight -= 1 - self._num_yielded += 1 - return data - - def clean(self): - # Without blocking: the end marker (None) is queued only once the - # sampler is exhausted, so an epoch that stopped early has none, and - # waiting for it (iter(self.fs.get, None)) would block forever. Only - # this thread puts into fs, so qsize() is exact here. - while self.fs.qsize() > 0: - future = self.fs.get_nowait() - if future is not None: - future.cancel() - - def __del__(self): - self.clean() - self.executor.shutdown(wait=False) diff --git a/examples/vae/distdataset.py b/examples/vae/distdataset.py deleted file mode 100644 index baacd83..0000000 --- a/examples/vae/distdataset.py +++ /dev/null @@ -1,273 +0,0 @@ -from mpi4py import MPI -import numpy as np -import os - -import torch -from torch.utils.data import Dataset - -import pyddstore as dds - -# DDSTORE_BATCH_GET=0: __getitems__ falls back to one get() per sample. -_BATCH_GET = os.environ.get("DDSTORE_BATCH_GET", "1") != "0" - - -def nsplit(a, n): - k, m = divmod(len(a), n) - return (a[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)] for i in range(n)) - - -class DistDataset(Dataset): - """Distributed dataset class""" - - def __init__( - self, - data, - label, - comm=MPI.COMM_WORLD, - ddstore_width=None, - device=None, - add_device=None, - ): - super().__init__() - - self.dataset = list() - self.label = label - self.comm = comm - # None -> get() allocates host buffers (default, unchanged). - # torch.device/"cuda"/"cuda:N" -> get() allocates its destination - # buffer directly on that device (GPUDirect RDMA, Phase 1); requires - # DDSTORE_METHOD in (1, 2) and DDSTORE_FABRIC=cxi (pyddstore raises - # a clear error otherwise). - self.device = device - # None -> add() makes a private host copy of this rank's shard - # (default, unchanged). torch.device/"cuda"/"cuda:N" -> the shard is - # stacked directly on that device and add() registers it in place, - # no host copy (GPUDirect RDMA, Phase 2) -- same DDSTORE_METHOD/ - # DDSTORE_FABRIC requirements as `device` above. Independent of - # `device`: this controls the SOURCE side, `device` controls the - # DESTINATION side, so source and destination can be tested - # separately or together. - self.add_device = add_device - self.rank = self.comm.Get_rank() - self.comm_size = self.comm.Get_size() - print("init", self.rank, self.comm_size) - self.ddstore_width = ( - ddstore_width if ddstore_width is not None else self.comm_size - ) - self.ddstore_comm = self.comm.Split(self.rank // self.ddstore_width, self.rank) - self.ddstore_comm_rank = self.ddstore_comm.Get_rank() - self.ddstore_comm_size = self.ddstore_comm.Get_size() - - ddstore_method = int(os.getenv("DDSTORE_METHOD", "0")) - print("DDStore method:", ddstore_method) - handshake_dir = os.getenv("DDSTORE_HANDSHAKE_DIR", "./ddstore_hs") - - if ddstore_method == 2 and self.ddstore_width != self.comm_size: - # File-based handshake: each Split group would publish into the - # same shared {varname}.bin file, so more than one group sharing - # a handshake_dir would silently collide. - raise NotImplementedError( - "method=2 does not yet support ddstore_width < comm_size " - "(multiple core groups would collide on the same " - "handshake_dir)" - ) - - self.ddstore = dds.PyDDStore( - self.ddstore_comm, method=ddstore_method, handshake_dir=handshake_dir - ) - print("FABRIC_IFACE:", os.environ.get("FABRIC_IFACE", "n/a (method=0)")) - - ## set total before set subset - self.total_ns = len(data) - print("init", self.total_ns) - - # This rank's contiguous share of the whole dataset. - rx = list(nsplit(range(len(data)), self.ddstore_comm_size))[ - self.ddstore_comm_rank - ] - - for i in rx: - self.dataset.append(data[i]) - - print(self.rank, len(self.dataset)) - - # Label stays host-only regardless of add_device -- see get()'s - # matching comment; a GPU-resident int32 label buffer buys nothing. - self.labels = [label for _, label in self.dataset] - self.labels = np.array(self.labels, dtype=np.int32) - self.labels = np.ascontiguousarray(self.labels) - - if self.add_device is not None: - # GPUDirect RDMA source (Phase 2): stack directly on device, no - # host round-trip. torch.stack (not cat) keeps one row per image - # (nrows, 784) -- see the np.stack comment below for why that - # shape matters to ddstore.add()'s disp inference. - self.data = torch.stack([d.reshape(-1) for d, _ in self.dataset]).to( - self.add_device - ) - self.data = self.data.contiguous() - else: - data_list = list() - for data, _ in self.dataset: - val = data.cpu().numpy() - val = val.flatten() - data_list.append(val) - - # np.stack (not concatenate) keeps one row per image (nrows, 784) - # so ddstore.add() infers disp=784 instead of flattening into a - # single (nrows*784,) vector, which it would read back as disp=1. - self.data = np.stack(data_list) - self.data = np.ascontiguousarray(self.data) - - self.ddstore.add(f"{self.label}data", self.data) - self.ddstore.add(f"{self.label}labels", self.labels) - # Row width and image side, from the data (28*28 for plain MNIST, - # larger with vae-ddp.py --image-scale). - self.data_disp = int(self.data.shape[1]) - self.side = int(round(self.data_disp**0.5)) - - # get() allocates a fresh GPU tensor per call on the GPU path (no - # buffer pool) -- simpler, at the cost of a fresh fi_mr_regattr per - # distinct pointer on every call instead of one pre-registered - # region reused across calls. Revisit with a pool later if that - # registration cost matters. - # - # Thread-safety for concurrent get() calls (e.g. from ThreadDataLoader - # worker threads) is handled inside DDStore itself (a per-variable - # lock in include/common.h's fabric_state) -- no lock needed here. - - def len(self): - return self.total_ns - - def __len__(self): - return self.len() - - def get(self, idx, device=None): - ## first dim must be the row count (1), not the flattened feature - ## width, since ddstore.get() infers count from arr.shape[0] - # Label stays host-only regardless of `device` -- both training - # loops discard it, so a GPU-resident label buffer would add - # complexity for no benefit. - label = np.zeros(1, dtype=np.int32) - if device is not None: - val = torch.empty((1, self.data_disp), dtype=torch.float32, device=device) - else: - val = np.zeros((1, self.data_disp), dtype=np.float32) - val = np.ascontiguousarray(val) - assert val.data.contiguous - self.ddstore.get(f"{self.label}data", val, idx) - self.ddstore.get(f"{self.label}labels", label, idx) - if device is None: - val = torch.tensor(val) - val = torch.reshape(val, (1, self.side, self.side)) - return (val, label[0]) - - def __getitem__(self, idx): - return self.get(idx, device=self.device) - - def __getitems__(self, indices): - """Whole-batch fetch, called by DataLoader (and ThreadDataLoader) with - a batch's indices: one PyDDStore.get_batch() per variable instead of - one get() per sample. DDSTORE_BATCH_GET=0 falls back to per-sample - get() (for A/B comparison).""" - if not _BATCH_GET: - return [self.get(i, device=self.device) for i in indices] - n = len(indices) - label = np.zeros(n, dtype=np.int32) - if self.device is not None: - val = torch.empty( - (n, self.data_disp), dtype=torch.float32, device=self.device - ) - else: - val = np.zeros((n, self.data_disp), dtype=np.float32) - self.ddstore.get_batch(f"{self.label}data", val, indices) - self.ddstore.get_batch(f"{self.label}labels", label, indices) - if self.device is None: - val = torch.from_numpy(val) - val = val.reshape(n, 1, self.side, self.side) - return [(val[i], label[i]) for i in range(n)] - - -class DistDatasetReader(Dataset): - """Distributed dataset class — extra (read-only) member. - - Joins a variable published by a core group (see DistDataset) via - DDStore method=2's file-based handshake. Owns no MPI communicator and no - local copy of the data — every __getitem__ is an RDMA read against a - core rank's memory. - """ - - def __init__(self, label, handshake_dir, n_core, device=None): - super().__init__() - self.label = label - # See DistDataset.__init__ for what `device` does. - self.device = device - - self.ddstore = dds.PyDDStore( - None, method=2, handshake_dir=handshake_dir, n_core=n_core - ) - print("FABRIC_IFACE:", os.environ.get("FABRIC_IFACE", "n/a")) - self.ddstore.join(f"{label}data") - self.ddstore.join(f"{label}labels") - - self.total_ns, self.data_disp, self.data_itemsize = self.ddstore.info( - f"{label}data" - ) - self.side = int(round(self.data_disp**0.5)) - if self.side * self.side != self.data_disp: - raise ValueError( - f"joined '{label}data' has disp={self.data_disp}, " - "which is not a perfect square (expected a flattened square image)" - ) - - # See DistDataset.__init__ -- thread-safety lives inside DDStore - # itself, no lock needed here; and no buffer pool on the GPU path. - - def len(self): - return self.total_ns - - def __len__(self): - return self.len() - - def get(self, idx, device=None): - ## first dim must be the row count (1), not the flattened feature - ## width, since ddstore.get() infers count from arr.shape[0] - # Label stays host-only regardless of `device` -- see DistDataset.get(). - label = np.zeros(1, dtype=np.int32) - if device is not None: - val = torch.empty((1, self.data_disp), dtype=torch.float32, device=device) - else: - val = np.zeros((1, self.data_disp), dtype=np.float32) - val = np.ascontiguousarray(val) - assert val.data.contiguous - self.ddstore.get(f"{self.label}data", val, idx) - self.ddstore.get(f"{self.label}labels", label, idx) - if device is None: - val = torch.tensor(val) - val = torch.reshape(val, (1, self.side, self.side)) - return (val, label[0]) - - def __getitem__(self, idx): - return self.get(idx, device=self.device) - - def __getitems__(self, indices): - """Whole-batch fetch, called by DataLoader (and ThreadDataLoader) with - a batch's indices: one PyDDStore.get_batch() per variable instead of - one get() per sample. DDSTORE_BATCH_GET=0 falls back to per-sample - get() (for A/B comparison).""" - if not _BATCH_GET: - return [self.get(i, device=self.device) for i in indices] - n = len(indices) - label = np.zeros(n, dtype=np.int32) - if self.device is not None: - val = torch.empty( - (n, self.data_disp), dtype=torch.float32, device=self.device - ) - else: - val = np.zeros((n, self.data_disp), dtype=np.float32) - self.ddstore.get_batch(f"{self.label}data", val, indices) - self.ddstore.get_batch(f"{self.label}labels", label, indices) - if self.device is None: - val = torch.from_numpy(val) - val = val.reshape(n, 1, self.side, self.side) - return [(val[i], label[i]) for i in range(n)] diff --git a/examples/vae/vae-ddp.py b/examples/vae/vae-ddp.py index f222966..5fc2891 100644 --- a/examples/vae/vae-ddp.py +++ b/examples/vae/vae-ddp.py @@ -14,9 +14,7 @@ from mpi4py import MPI -import distdataset -from distdataset import DistDataset -from ddstore_dataloader import ThreadDataLoader +from pyddstore.torch import DistDataset, ThreadDataLoader from ddp_utils import setup_ddp, get_local_rank from vae_model import VAE, loss_function, mnist_transform @@ -79,7 +77,7 @@ help="Number of DataLoader workers. 0 uses PyTorch's standard " "DataLoader in the main process (no worker processes, no fork). " "> 0 switches to ThreadDataLoader " - "(examples/vae/ddstore_dataloader.py), with that many worker threads " + "(pyddstore.torch), with that many worker threads " "-- forked processes can't safely own GPU state or MPI's live state, " "so any --num-workers > 0 goes through threads, never a fork. " "Requires DDSTORE_METHOD 1 or 2 when > 0. Default: 0.", diff --git a/examples/vae/vae_core_server.py b/examples/vae/vae_core_server.py index d0a11d9..2728eaa 100644 --- a/examples/vae/vae_core_server.py +++ b/examples/vae/vae_core_server.py @@ -35,7 +35,7 @@ import sys import time -## torch (pulled in below via torchvision/distdataset) must finish loading +## torch (pulled in below via torchvision/pyddstore.torch) must finish loading ## before mpi4py triggers MPI_Init, or - if GPU/NCCL use is ever added here - ## their static destructors run in the wrong order at interpreter exit and ## corrupt the heap. Do not reorder these imports. @@ -44,7 +44,7 @@ from mpi4py import MPI -from distdataset import DistDataset +from pyddstore.torch import DistDataset from vae_model import mnist_transform @@ -79,7 +79,7 @@ def _resolve_dir(arg): if rank == 0: os.makedirs(hs_dir, exist_ok=True) for fname in os.listdir(hs_dir): - if fname.endswith(".bin") or fname == "done_extra": + if fname.endswith((".bin", ".meta.json")) or fname == "done_extra": os.remove(os.path.join(hs_dir, fname)) print(f"[core] handshake_dir={hs_dir}", flush=True) comm.Barrier() @@ -128,7 +128,7 @@ def _resolve_dir(arg): if rank == 0: for fname in os.listdir(hs_dir): - if fname.endswith(".bin"): + if fname.endswith((".bin", ".meta.json")): try: os.remove(os.path.join(hs_dir, fname)) except OSError: diff --git a/examples/vae/vae_extra_train.py b/examples/vae/vae_extra_train.py index fc1602b..967764a 100644 --- a/examples/vae/vae_extra_train.py +++ b/examples/vae/vae_extra_train.py @@ -39,8 +39,7 @@ from mpi4py import MPI from ddp_utils import setup_ddp, get_local_rank -from distdataset import DistDatasetReader -from ddstore_dataloader import ThreadDataLoader +from pyddstore.torch import DistDatasetReader, ThreadDataLoader from vae_model import VAE, loss_function, mnist_transform parser = argparse.ArgumentParser(description="VAE MNIST Example - extra (reader) group") @@ -103,7 +102,7 @@ help="Number of DataLoader workers. 0 uses PyTorch's standard " "DataLoader in the main process (no worker processes, no fork). " "> 0 switches to ThreadDataLoader " - "(examples/vae/ddstore_dataloader.py), with that many worker threads " + "(pyddstore.torch), with that many worker threads " "-- forked processes can't safely own GPU state, so any " "--num-workers > 0 goes through threads, never a fork. Default: 0.", ) @@ -144,7 +143,7 @@ # Image size comes from the core side's published data (vae_core_server.py # --image-scale); the model and test set must match it. -side = trainset.side +side = trainset.shapes[0][-1] # samples are (image (1, side, side), label) image_scale = side // 28 model = VAE(input_dim=side * side, hidden=400 * image_scale).to(device) model = torch.nn.parallel.DistributedDataParallel(model) diff --git a/pyproject.toml b/pyproject.toml index 1dcac6c..98b7461 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,4 +7,7 @@ name = "PyDDStore" version = "2.0" description = "Distributed Data Store" requires-python = ">=3.6" -dependencies = ["numpy", "mpi4py"] \ No newline at end of file +dependencies = ["numpy", "mpi4py"] + +[project.optional-dependencies] +torch = ["torch"] diff --git a/setup.py b/setup.py index 9422a73..be9d7d7 100644 --- a/setup.py +++ b/setup.py @@ -32,8 +32,8 @@ include_dirs.append("include") extending = Extension( - "pyddstore", - sources=["src/pyddstore.pyx", "src/ddstore.cxx", "src/common.cxx"], + "pyddstore._core", + sources=["src/pyddstore/_core.pyx", "src/ddstore.cxx", "src/common.cxx"], include_dirs=include_dirs, extra_compile_args=["-std=c++11"], define_macros=defs, @@ -50,6 +50,7 @@ version="2.0", description="Distributed Data Store", package_dir={"": "src"}, + packages=["pyddstore"], py_modules=["cpu_nic_map"], ext_modules=cythonize(extensions), ) diff --git a/src/pyddstore/__init__.py b/src/pyddstore/__init__.py new file mode 100644 index 0000000..870b071 --- /dev/null +++ b/src/pyddstore/__init__.py @@ -0,0 +1,29 @@ +"""DDStore: distributed in-memory data store for data-parallel training. + +The store itself is ``PyDDStore`` (compiled extension ``pyddstore._core``). +PyTorch helpers (``DistDataset``, ``DistDatasetReader``, ``ThreadDataLoader``) +live in ``pyddstore.torch`` and need PyTorch; importing ``pyddstore`` alone +does not. + +The extension (which initializes MPI through mpi4py) is loaded on first use +of ``PyDDStore``, not on ``import pyddstore``: ``from pyddstore.torch import +...`` then imports torch before MPI starts, the order torch + RCCL/NCCL need +to shut down cleanly. +""" + +try: + from importlib.metadata import version as _version + + __version__ = _version("PyDDStore") +except Exception: # pragma: no cover - not installed as a distribution + __version__ = "unknown" + +__all__ = ["PyDDStore", "PyDDstoreVarinfo", "__version__"] + + +def __getattr__(name): + if name in ("PyDDStore", "PyDDstoreVarinfo"): + from . import _core + + return getattr(_core, name) + raise AttributeError(f"module 'pyddstore' has no attribute {name!r}") diff --git a/src/pyddstore.pyx b/src/pyddstore/_core.pyx similarity index 100% rename from src/pyddstore.pyx rename to src/pyddstore/_core.pyx diff --git a/src/pyddstore/torch.py b/src/pyddstore/torch.py new file mode 100644 index 0000000..4907bb5 --- /dev/null +++ b/src/pyddstore/torch.py @@ -0,0 +1,563 @@ +"""PyTorch integration for DDStore. + +- ``DistDataset``: a map-style ``torch.utils.data.Dataset`` backed by DDStore. + Each rank loads its share of any map-style source dataset into the store; + every rank can then read any sample. Samples keep the source's structure + (a tensor/array/number, a tuple or list of them, or a dict of them) and + each field's shape and dtype. ``__getitems__`` reads a whole batch with one + ``PyDDStore.get_batch()`` per field, which PyTorch's ``DataLoader`` uses + automatically. +- ``DistDatasetReader``: the same, as a ``method=2`` extra member that joins a + dataset published by a ``DistDataset`` core group through a shared + handshake directory. +- ``ThreadDataLoader``: a ``DataLoader`` whose workers are threads instead of + forked processes (safe with MPI and GPU-resident buffers). + +Fields must have the same shape and dtype in every sample (fixed-shape). +Supported dtypes: bool, uint8, int32, int64, float32, float64. + +Import torch before mpi4py/MPI starts; ``from pyddstore.torch import ...`` +does that by itself. +""" + +import json +import logging +import multiprocessing as mp +import os +import queue +import socket +import time +from concurrent.futures import ThreadPoolExecutor + +import numpy as np +import torch +from torch.utils.data import DataLoader, Dataset + +logger = logging.getLogger(__name__) + +__all__ = ["DistDataset", "DistDatasetReader", "ThreadDataLoader"] + +_SUPPORTED = { + np.dtype(np.bool_), + np.dtype(np.uint8), + np.dtype(np.int32), + np.dtype(np.int64), + np.dtype(np.float32), + np.dtype(np.float64), +} +_TORCH_DTYPE = { + "bool": torch.bool, + "uint8": torch.uint8, + "int32": torch.int32, + "int64": torch.int64, + "float32": torch.float32, + "float64": torch.float64, +} + + +def _nsplit(n, parts): + """Contiguous index ranges [lo, hi) splitting range(n) into `parts`.""" + k, m = divmod(n, parts) + return [(i * k + min(i, m), (i + 1) * k + min(i + 1, m)) for i in range(parts)] + + +def _handshake_dir(handshake_dir): + return handshake_dir or os.environ.get("DDSTORE_HANDSHAKE_DIR") or "./ddstore_hs" + + +def _meta_path(handshake_dir, name): + # same sanitization as the C library's record files + safe = name.replace("/", "_").replace(".", "_") + return os.path.join(handshake_dir, f"{safe}.meta.json") + + +# --------------------------------------------------------------------------- +# sample structure <-> flat fields +# --------------------------------------------------------------------------- + + +def _field_spec(value, where): + """(kind, numpy dtype, shape) of one leaf value.""" + if isinstance(value, torch.Tensor): + kind, dtype, shape = ( + "torch", + np.dtype(str(value.dtype).replace("torch.", "")), + tuple(value.shape), + ) + elif isinstance(value, np.ndarray): + kind, dtype, shape = "numpy", value.dtype, value.shape + elif isinstance(value, np.generic): + kind, dtype, shape = "npscalar", value.dtype, () + elif isinstance(value, bool): + kind, dtype, shape = "py", np.dtype(np.bool_), () + elif isinstance(value, int): + kind, dtype, shape = "py", np.dtype(np.int64), () + elif isinstance(value, float): + kind, dtype, shape = "py", np.dtype(np.float64), () + else: + raise TypeError( + f"{where}: unsupported value of type {type(value).__name__}; " + "use tensors, numpy arrays or Python/numpy numbers" + ) + if dtype not in _SUPPORTED: + raise TypeError( + f"{where}: dtype {dtype} is not supported " + f"(supported: {', '.join(sorted(str(d) for d in _SUPPORTED))})" + ) + return {"kind": kind, "dtype": dtype.name, "shape": list(shape)} + + +def _flatten(sample): + """(structure, keys, leaf values) of one sample.""" + if isinstance(sample, dict): + keys = [str(k) for k in sample.keys()] + return "dict", keys, list(sample.values()) + if isinstance(sample, (tuple, list)): + structure = "tuple" if isinstance(sample, tuple) else "list" + return structure, [str(i) for i in range(len(sample))], list(sample) + return "single", ["0"], [sample] + + +def _schema_of(sample, where): + structure, keys, values = _flatten(sample) + for v in values: + if isinstance(v, (dict, list, tuple)): + raise TypeError(f"{where}: nested containers are not supported") + fields = [_field_spec(v, f"{where} field {k!r}") for k, v in zip(keys, values)] + return {"structure": structure, "keys": keys, "fields": fields} + + +def _to_numpy_row(value): + if isinstance(value, torch.Tensor): + return value.detach().cpu().numpy().reshape(-1) + return np.asarray(value).reshape(-1) + + +# --------------------------------------------------------------------------- +# datasets +# --------------------------------------------------------------------------- + + +class _StoreDataset(Dataset): + """Reads samples described by `self.schema` from `self.ddstore`.""" + + def _setup_fields(self, schema, name, device): + self.schema = schema + self.name = name + self.device = device + self._var = [f"{name}/{k}" for k in schema["keys"]] + self._size = [ + int(np.prod(f["shape"], dtype=np.int64)) for f in schema["fields"] + ] + self._batch_get = os.environ.get("DDSTORE_BATCH_GET", "1") != "0" + + # -- shapes as the user sees them (same structure as a sample) -------- + @property + def shapes(self): + return self._rebuild([tuple(f["shape"]) for f in self.schema["fields"]]) + + @property + def dtypes(self): + return self._rebuild([f["dtype"] for f in self.schema["fields"]]) + + def _rebuild(self, values): + s = self.schema["structure"] + if s == "single": + return values[0] + if s == "dict": + return dict(zip(self.schema["keys"], values)) + return tuple(values) if s == "tuple" else list(values) + + def _alloc(self, n, j): + f = self.schema["fields"][j] + if self.device is not None and f["kind"] == "torch": + return torch.empty( + (n, self._size[j]), dtype=_TORCH_DTYPE[f["dtype"]], device=self.device + ) + return np.empty((n, self._size[j]), dtype=f["dtype"]) + + def _value(self, row, j): + """One sample's field from its flat row (a view, no copy).""" + f = self.schema["fields"][j] + shape = tuple(f["shape"]) + if f["kind"] == "torch": + t = row if isinstance(row, torch.Tensor) else torch.from_numpy(row) + return t.reshape(shape) + if f["kind"] == "numpy": + return row.reshape(shape) + if f["kind"] == "npscalar": + return row[0] + return row[0].item() + + def __len__(self): + return self.total_ns + + def len(self): + return self.total_ns + + def get(self, idx): + values = [] + for j, var in enumerate(self._var): + buf = self._alloc(1, j) + self.ddstore.get(var, buf, int(idx)) + values.append(self._value(buf[0], j)) + return self._rebuild(values) + + def __getitem__(self, idx): + return self.get(idx) + + def __getitems__(self, indices): + """A whole batch: one get_batch() per field (DDSTORE_BATCH_GET=0: + one get() per sample). Called by DataLoader and ThreadDataLoader.""" + if not self._batch_get: + return [self.get(i) for i in indices] + idx = np.asarray(indices, dtype=np.int64) + columns = [] + for j, var in enumerate(self._var): + buf = self._alloc(len(idx), j) + self.ddstore.get_batch(var, buf, idx) + columns.append([self._value(buf[i], j) for i in range(len(idx))]) + return [self._rebuild([col[i] for col in columns]) for i in range(len(idx))] + + +class DistDataset(_StoreDataset): + """A map-style dataset stored in DDStore across the ranks of `comm`. + + Args: + source: any map-style dataset (``len(source)``, ``source[i]``). Each + rank loads only its contiguous share. Every sample must have the + same structure, and each field the same shape and dtype. + name: dataset name; field ``k`` is stored as variable ``name/k``. + comm: MPI communicator (default ``MPI.COMM_WORLD``). All its ranks + must construct the dataset together. + ddstore_width: ranks per independent store (default: all of + ``comm``); each group holds a full copy of the dataset. + device: put tensor fields of read samples on this device + (GPUDirect RDMA; needs ``method`` 1/2 and ``DDSTORE_FABRIC=cxi``). + add_device: keep this rank's share of tensor fields on this device. + method: DDStore backend (default ``DDSTORE_METHOD`` or 0). + handshake_dir: ``method=2`` directory (default + ``DDSTORE_HANDSHAKE_DIR`` or ``./ddstore_hs``). + + ``ds[i]`` returns a sample with the source's structure: tensors stay + tensors (on ``device`` if given), numpy arrays stay arrays, numbers stay + numbers. ``ds.ddstore`` is the underlying ``PyDDStore``. + + With ``method=0``, batched reads are collective: every rank must read the + same number of batches (DistributedSampler does that) from one thread. + """ + + def __init__( + self, + source, + name, + comm=None, + ddstore_width=None, + device=None, + add_device=None, + method=None, + handshake_dir=None, + ): + super().__init__() + from mpi4py import MPI + + from ._core import PyDDStore + + self.comm = comm if comm is not None else MPI.COMM_WORLD + self.rank = self.comm.Get_rank() + self.comm_size = self.comm.Get_size() + self.add_device = add_device + self.ddstore_width = ( + ddstore_width if ddstore_width is not None else self.comm_size + ) + self.method = ( + int(os.environ.get("DDSTORE_METHOD", "0")) + if method is None + else int(method) + ) + if self.method == 2 and self.ddstore_width != self.comm_size: + raise NotImplementedError( + "method=2 does not support ddstore_width < comm size (groups would " + "collide on the same handshake directory)" + ) + self.ddstore_comm = self.comm.Split(self.rank // self.ddstore_width, self.rank) + group_rank = self.ddstore_comm.Get_rank() + group_size = self.ddstore_comm.Get_size() + + # This rank's share of the source, and the schema every rank agrees on. + self.total_ns = len(source) + lo, hi = _nsplit(self.total_ns, group_size)[group_rank] + samples = [source[i] for i in range(lo, hi)] + # Check locally, but raise only after comparing with every rank, so a + # bad sample on some ranks raises on all of them instead of leaving + # the others blocked in the collective below. + schema, error = None, None + try: + if samples: + schema = _schema_of(samples[0], f"{name}[{lo}]") + for i, s in zip(range(lo + 1, hi), samples[1:]): + other = _schema_of(s, f"{name}[{i}]") + if other != schema: + raise ValueError( + f"{name}[{i}]: structure, shape or dtype differs from {name}[{lo}] " + f"({other} vs {schema}); DistDataset needs fixed-shape samples" + ) + except (TypeError, ValueError) as exc: + error = (type(exc).__name__, str(exc)) + gathered = self.comm.allgather((schema, error)) + errors = [e for _, e in gathered if e is not None] + if errors: + cls = TypeError if errors[0][0] == "TypeError" else ValueError + raise cls(errors[0][1]) + schemas = [s for s, _ in gathered] + if any(s is None for s in schemas): + raise ValueError( + f"{name}: every rank needs at least one sample (dataset has {self.total_ns})" + ) + if any(s != schemas[0] for s in schemas): + raise ValueError( + f"{name}: samples differ in structure, shape or dtype across ranks" + ) + self._setup_fields(schemas[0], name, device) + + hs = _handshake_dir(handshake_dir) + if self.method == 2: + self.ddstore = PyDDStore(self.ddstore_comm, method=2, handshake_dir=hs) + if group_rank == 0: + # published before the variables, so a reader that sees a + # variable's record file always finds the schema too + path = _meta_path(hs, name) + tmp = f"{path}.tmp.{os.getpid()}" + with open(tmp, "w") as fh: + json.dump({"total_ns": self.total_ns, **self.schema}, fh) + os.replace(tmp, path) + else: + self.ddstore = PyDDStore(self.ddstore_comm, method=self.method) + + for j, var in enumerate(self._var): + f = self.schema["fields"][j] + values = [_flatten(s)[2][j] for s in samples] + if add_device is not None and f["kind"] == "torch": + rows = ( + torch.stack([v.reshape(-1) for v in values]) + .to(add_device) + .contiguous() + ) + else: + rows = np.ascontiguousarray( + np.stack([_to_numpy_row(v) for v in values]).astype( + f["dtype"], copy=False + ) + ) + self.ddstore.add(var, rows) + logger.debug( + "DistDataset %s: rank %d holds [%d, %d) of %d", + name, + self.rank, + lo, + hi, + self.total_ns, + ) + + +class DistDatasetReader(_StoreDataset): + """A ``DistDataset`` published by a ``method=2`` core group, joined from a + separate job (no MPI communicator needed). + + Args: + name: the core group's dataset name. + handshake_dir: shared directory (default ``DDSTORE_HANDSHAKE_DIR`` or + ``./ddstore_hs``). + n_core: number of core ranks (default ``DDSTORE_N_CORE``). + device: put tensor fields of read samples on this device. + + Waits up to ``DDSTORE_HANDSHAKE_TIMEOUT_S`` (default 300 s) for the core + group to publish. + """ + + def __init__(self, name, handshake_dir=None, n_core=None, device=None): + super().__init__() + from ._core import PyDDStore + + hs = _handshake_dir(handshake_dir) + if n_core is None: + n_core = int(os.environ["DDSTORE_N_CORE"]) + timeout = float(os.environ.get("DDSTORE_HANDSHAKE_TIMEOUT_S", "300")) + path = _meta_path(hs, name) + t0 = time.monotonic() + while not os.path.exists(path): + if time.monotonic() - t0 > timeout: + raise TimeoutError( + f"no dataset {name!r} published in {hs} after {timeout:.0f} s" + ) + time.sleep(0.05) + with open(path) as fh: + meta = json.load(fh) + self.total_ns = meta.pop("total_ns") + self._setup_fields(meta, name, device) + self.ddstore = PyDDStore(None, method=2, handshake_dir=hs, n_core=n_core) + for var in self._var: + self.ddstore.join(var) + + +# --------------------------------------------------------------------------- +# loader +# --------------------------------------------------------------------------- + + +class ThreadDataLoader(DataLoader): + """A ``DataLoader`` that fetches batches in a thread pool instead of forked + worker processes. Threads share the process's MPI state, CUDA context and + Python objects, so it is safe with DDStore and GPU-resident buffers, where + forked workers are not. Takes the same arguments as ``DataLoader``; + ``num_workers`` is the number of threads (0 means 1). + + Each batch is fetched (via ``dataset.__getitems__`` when present), + collated and optionally pinned in a worker thread; at most + ``num_workers * prefetch_factor`` batches are in flight. Random draws + match ``DataLoader``'s. ``DDSTORE_AFFINITY_WIDTH`` / + ``DDSTORE_AFFINITY_OFFSET`` pin worker thread *i* to CPUs + ``[offset + i*width, offset + (i+1)*width)`` of the process's affinity. + """ + + def __init__(self, dataset, **kwargs): + super().__init__(dataset, **kwargs) + + self.fs = queue.Queue() + # Persistent across epochs -- recreating the pool in every __iter__() + # would leak OS threads since the old pool is never shut down. + self._counter = mp.Value("i", 0) + self.executor = ThreadPoolExecutor( + max_workers=self.num_workers or 1, + initializer=self.worker_init, + initargs=(self._counter,), + ) + + logger.debug("num_workers: %s", self.num_workers) + logger.debug("len: %s", len(self._index_sampler)) + + @staticmethod + def worker_init(counter): + core_width = int(os.environ.get("DDSTORE_AFFINITY_WIDTH", "0")) + core_offset = int(os.environ.get("DDSTORE_AFFINITY_OFFSET", "0")) + if core_width <= 0 or not hasattr(os, "sched_getaffinity"): + return 0 + + with counter.get_lock(): + wid = counter.value + counter.value += 1 + + affinity = list(os.sched_getaffinity(0)) + affinity_mask = set( + affinity[ + core_width * wid + core_offset : core_width * (wid + 1) + core_offset + ] + ) + if affinity_mask: + os.sched_setaffinity(0, affinity_mask) + logger.debug( + "Worker: pid=%s hostname=%s ID=%s affinity=%s", + os.getpid(), + socket.gethostname(), + wid, + os.sched_getaffinity(0), + ) + return 0 + + @staticmethod + def fetch( + dataset, ibatch, index, collate_fn=None, pin_memory=False, auto_collation=True + ): + # Collate here, in the worker, before pinning: pinning per-sample + # tensors and collating afterwards would just torch.stack them into + # a new, unpinned tensor. Use the dataset's whole-batch fetch when it + # has one, like torch's own map-style fetcher. With batch_size=None + # (no auto-collation) the sampler's index goes to dataset[index] as + # is, also as torch does (e.g. samplers that yield whole batches). + if not auto_collation: + batch = dataset[index] + elif getattr(dataset, "__getitems__", None): + batch = dataset.__getitems__(index) + else: + batch = [dataset[i] for i in index] + if collate_fn is not None: + batch = collate_fn(batch) + if pin_memory: + batch = torch.utils.data._utils.pin_memory.pin_memory(batch) + return (ibatch, batch) + + def __iter__(self): + # Drop what an earlier epoch left behind (it may have stopped early) + self.clean() + + self._num_yielded = 0 + self._sampler_iter = iter(self._index_sampler) + # torch's DataLoader iterator draws a base seed from the global RNG + # here, every epoch; draw it too, so the training loop's later random + # draws are the same as with DataLoader + torch.empty((), dtype=torch.int64).random_(generator=self.generator) + self.fs_iter = iter(self.fs.get, None) + self._next_batch_i = 0 + self._inflight = 0 + self._sampler_exhausted = False + # Bound how many batches can be in flight (submitted but not yet + # consumed via __next__) at once, instead of submitting the whole + # epoch up front -- keeps memory use (GPU tensors included) bounded + # regardless of dataset size. Mirrors torch's own prefetch_factor + # (default 2 per worker). + self._max_inflight = max( + 1, (self.num_workers or 1) * (self.prefetch_factor or 2) + ) + self._refill() + return self + + def _refill(self): + while self._inflight < self._max_inflight: + try: + index = next(self._sampler_iter) + except StopIteration: + if not self._sampler_exhausted: + self._sampler_exhausted = True + self.fs.put(None) + return + future = self.executor.submit( + self.fetch, + self.dataset, + self._next_batch_i, + index, + collate_fn=self.collate_fn, + pin_memory=self.pin_memory, + auto_collation=self._auto_collation, + ) + self.fs.put(future) + self._next_batch_i += 1 + self._inflight += 1 + + def __next__(self): + # Refill *before* popping this call's batch, not after: refilling + # here only uses capacity freed by the *previous* call's batch, + # which -- by ordinary for-loop semantics -- the caller's loop body + # has already fully consumed by the time it asks for the next item + # (i.e. calls __next__ again). Bounds how far the executor can race + # ahead of consumption (memory, not correctness -- each batch is read + # into its own freshly allocated buffers). + self._refill() + future = next(self.fs_iter) + ibatch, data = future.result() + self._inflight -= 1 + self._num_yielded += 1 + return data + + def clean(self): + # Without blocking: the end marker (None) is queued only once the + # sampler is exhausted, so an epoch that stopped early has none, and + # waiting for it (iter(self.fs.get, None)) would block forever. Only + # this thread puts into fs, so qsize() is exact here. + while self.fs.qsize() > 0: + future = self.fs.get_nowait() + if future is not None: + future.cancel() + + def __del__(self): + self.clean() + self.executor.shutdown(wait=False) diff --git a/test/test_gpu_rdma.py b/test/test_gpu_rdma.py index efc1fda..8fd24d3 100644 --- a/test/test_gpu_rdma.py +++ b/test/test_gpu_rdma.py @@ -429,7 +429,7 @@ def test_add_from_gpu_tensor_gpu_dest_cxi(comm, monkeypatch): def test_add_from_gpu_tensor_gpu_dest_cxi_method2(comm, monkeypatch, tmp_path): """Same as test_add_from_gpu_tensor_gpu_dest_cxi but method=2 (file-based handshake, core+extra split) -- the transport - examples/vae/distdataset.py's DistDatasetReader actually uses. Includes + pyddstore.torch's DistDatasetReader actually uses. Includes a self-read check (core rank both add()s and get()s its own data, mirroring test_method2_core.py's self-check pattern) to verify the independent send_hmem_iface/mr vs recv_hmem_iface/recv_mr fields don't diff --git a/test/test_torch.py b/test/test_torch.py new file mode 100644 index 0000000..27ea738 --- /dev/null +++ b/test/test_torch.py @@ -0,0 +1,236 @@ +""" +pyddstore.torch (DistDataset, DistDatasetReader, ThreadDataLoader) tests — +run with 2+ ranks, e.g.: + mpirun -n 4 pytest test/test_torch.py -v + +Method 0 always; method 1 and the method-2 reader where a CXI device is +present inside a Slurm step (provider: DDSTORE_FABRIC, default cxi). +""" + +import glob +import os + +import numpy as np +import pytest + +torch = pytest.importorskip("torch") +from torch.utils.data import DataLoader, Dataset # noqa: E402 + +from pyddstore.torch import ( + DistDataset, + DistDatasetReader, + ThreadDataLoader, +) # noqa: E402 + +HAVE_CXI = bool(glob.glob("/dev/cxi*")) and "SLURM_STEP_ID" in os.environ +FABRIC = os.environ.get("DDSTORE_FABRIC", "cxi") +HAVE_GPU = torch.cuda.is_available() +METHODS = [ + 0, + pytest.param(1, marks=pytest.mark.skipif(not HAVE_CXI, reason="no CXI device")), +] +N = 37 # not a multiple of the rank count + + +class TupleSource(Dataset): + """Every field kind, values derived from the index.""" + + def __len__(self): + return N + + def __getitem__(self, i): + return ( + torch.arange(12, dtype=torch.float32).reshape(3, 4) + + 100 * i, # torch tensor + i, # Python int + np.full(2, i / 3, dtype=np.float64), # numpy array + np.int32(-i), # numpy scalar + i % 2 == 0, # Python bool + float(i) * 0.5, # Python float + torch.tensor([i, i + 1], dtype=torch.uint8), # small dtype + ) + + +class DictSource(Dataset): + def __len__(self): + return N + + def __getitem__(self, i): + return { + "x": torch.full((2, 3), float(i)), + "y": torch.tensor(i, dtype=torch.int64), + } + + +class SingleSource(Dataset): + def __len__(self): + return N + + def __getitem__(self, i): + return np.full((5,), i, dtype=np.int32) + + +def same(a, b): + """Equal structure, types and values.""" + if type(a) is not type(b): + return False + if isinstance(a, (tuple, list)): + return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b)) + if isinstance(a, dict): + return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a) + if isinstance(a, torch.Tensor): + return ( + a.dtype == b.dtype + and a.shape == b.shape + and bool(torch.equal(a.cpu(), b.cpu())) + ) + if isinstance(a, np.ndarray): + return a.dtype == b.dtype and a.shape == b.shape and np.array_equal(a, b) + if isinstance(a, np.generic): + return a.dtype == b.dtype and a == b + return a == b + + +def all_ok(comm, ok): + return comm.allreduce(int(bool(ok)), op=__import__("mpi4py").MPI.LAND) + + +def make(comm, monkeypatch, source, method, **kw): + if method != 0: + monkeypatch.setenv("DDSTORE_FABRIC", FABRIC) + return DistDataset(source, f"t{method}", comm, method=method, **kw) + + +def finish(comm, ds): + comm.Barrier() # every rank done reading before any rank tears down + ds.ddstore.free() + + +@pytest.mark.parametrize("method", METHODS) +@pytest.mark.parametrize("source_cls", [TupleSource, DictSource, SingleSource]) +def test_items_match_source(comm, monkeypatch, method, source_cls): + src = source_cls() + ds = make(comm, monkeypatch, src, method) + rng = np.random.default_rng(comm.Get_rank()) + idx = rng.integers(0, N, size=20) + ok = len(ds) == N + ok &= all(same(ds[int(i)], src[int(i)]) for i in idx[:5]) # per-sample get() + batch = ds.__getitems__(idx) # one get_batch() per field (collective for method 0) + ok &= all(same(b, src[int(i)]) for b, i in zip(batch, idx)) + finish(comm, ds) + assert all_ok(comm, ok) + + +def test_shapes_and_dtypes(comm, monkeypatch): + ds = make(comm, monkeypatch, DictSource(), 0) + ok = ds.shapes == {"x": (2, 3), "y": ()} and ds.dtypes == { + "x": "float32", + "y": "int64", + } + finish(comm, ds) + assert all_ok(comm, ok) + + +@pytest.mark.parametrize("method", METHODS) +@pytest.mark.parametrize("loader", ["DataLoader", "ThreadDataLoader"]) +def test_loaders_match_plain_source(comm, monkeypatch, method, loader): + """A whole epoch through the loader equals the same loader over the plain + source (same order, same collation). Collective for method 0: every rank + iterates the same number of batches.""" + src = TupleSource() + ds = make(comm, monkeypatch, src, method) + cls = DataLoader if loader == "DataLoader" else ThreadDataLoader + kw = {} if cls is DataLoader else {"num_workers": 1 if method == 0 else 2} + got = list(cls(ds, batch_size=8, **kw)) + ref = list(DataLoader(src, batch_size=8)) + ok = len(got) == len(ref) and all(same(g, r) for g, r in zip(got, ref)) + finish(comm, ds) + assert all_ok(comm, ok) + + +def test_per_sample_fallback(comm, monkeypatch): + monkeypatch.setenv("DDSTORE_BATCH_GET", "0") + src = TupleSource() + ds = make(comm, monkeypatch, src, 0) + idx = list(range(N))[::-3] + ok = all(same(b, src[i]) for b, i in zip(ds.__getitems__(idx), idx)) + finish(comm, ds) + assert all_ok(comm, ok) + + +def test_ddstore_width_groups(comm, monkeypatch): + if comm.Get_size() < 4: + pytest.skip("requires at least 4 ranks") + src = SingleSource() + ds = make(comm, monkeypatch, src, 0, ddstore_width=2) + idx = list(range(N)) + ok = all(same(b, src[i]) for b, i in zip(ds.__getitems__(idx), idx)) + finish(comm, ds) + assert all_ok(comm, ok) + + +class _Bad(Dataset): + def __init__(self, kind): + self.kind = kind + + def __len__(self): + return N + + def __getitem__(self, i): + if self.kind == "shape": + return np.zeros(3 if i % 5 else 4, dtype=np.float32) + if self.kind == "dtype": + return torch.zeros(2, dtype=torch.float16) + if self.kind == "nested": + return (np.zeros(2), (1, 2)) + return object() + + +@pytest.mark.parametrize( + "kind,exc", + [ + ("shape", ValueError), + ("dtype", TypeError), + ("nested", TypeError), + ("object", TypeError), + ], +) +def test_unsupported_samples_raise(comm, kind, exc): + with pytest.raises(exc): + DistDataset(_Bad(kind), "bad", comm, method=0) + comm.Barrier() + + +@pytest.mark.skipif( + not (HAVE_CXI and HAVE_GPU and FABRIC == "cxi"), + reason="requires the cxi provider and a GPU", +) +def test_gpu_device_and_add_device(comm, monkeypatch): + src = TupleSource() + ds = make(comm, monkeypatch, src, 1, device="cuda", add_device="cuda") + idx = list(range(N))[::-1] + batch = ds.__getitems__(idx) + ok = batch[0][0].is_cuda and isinstance( + batch[0][2], np.ndarray + ) # tensors on GPU, numpy stays host + ok &= all(same(b, src[i]) for b, i in zip(batch, idx)) + finish(comm, ds) + assert all_ok(comm, ok) + + +@pytest.mark.skipif(not HAVE_CXI, reason="no CXI device") +def test_method2_reader(comm, monkeypatch, tmp_path): + monkeypatch.setenv("DDSTORE_FABRIC", FABRIC) + hs = comm.bcast(str(tmp_path / "hs") if comm.Get_rank() == 0 else None, root=0) + src = DictSource() + core = DistDataset(src, "rd", comm, method=2, handshake_dir=hs) + comm.Barrier() + ok = True + if comm.Get_rank() == 0: + reader = DistDatasetReader("rd", handshake_dir=hs, n_core=comm.Get_size()) + idx = list(range(N)) + ok = len(reader) == N and reader.shapes == core.shapes + ok &= all(same(b, src[i]) for b, i in zip(reader.__getitems__(idx), idx)) + reader.ddstore.free() + finish(comm, core) + assert all_ok(comm, ok) From 71c94aebdc6802db7857f446b46568076963a0c6 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Mon, 5 Oct 2026 09:20:57 -0400 Subject: [PATCH 45/56] DistDataset: chunk_size for chunked loading chunk_size=N loads each rank's share N samples at a time: every field is allocated with init() (collective), then filled chunk by chunk with update() (local), so only one chunk is held besides the store. Peak RSS for a 400 MiB share on 1 rank: 1202 MiB without chunking, 461 MiB with chunk_size=20. Host storage only (rejected with add_device for tensor fields). Default None keeps the whole-share path. Sample checks are now raised on every rank via one helper (_raise_on_all), before allocation (first sample's schema) and after loading (all samples), with no collectives in between. test_torch: chunk sizes 1 and 4 for every source/method, chunked error cases, chunk_size validation. Frontier: 36/36 on 4 and 16 ranks. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- README.md | 3 +- src/pyddstore/torch.py | 137 ++++++++++++++++++++++++++++++----------- test/test_torch.py | 18 ++++-- 3 files changed, 116 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index c2bb562..52e5522 100644 --- a/README.md +++ b/README.md @@ -331,9 +331,10 @@ for x, y in loader: ... ``` -- **`DistDataset(source, name, comm=None, ddstore_width=None, device=None, add_device=None, method=None, handshake_dir=None)`**: each rank loads its contiguous share of `source` (anything with `len()` and `[i]`) into DDStore; every rank can then read every sample. Samples keep the source's structure (a tensor, numpy array or number; a tuple or list of them; or a dict of them), with each field's shape and dtype. Fields must have the same shape and dtype in every sample; supported dtypes are bool, uint8, int32, int64, float32 and float64. `ds.shapes` / `ds.dtypes` describe the fields, `ds.ddstore` is the underlying `PyDDStore`. +- **`DistDataset(source, name, comm=None, ddstore_width=None, device=None, add_device=None, method=None, handshake_dir=None, chunk_size=None)`**: each rank loads its contiguous share of `source` (anything with `len()` and `[i]`) into DDStore; every rank can then read every sample. Samples keep the source's structure (a tensor, numpy array or number; a tuple or list of them; or a dict of them), with each field's shape and dtype. Fields must have the same shape and dtype in every sample; supported dtypes are bool, uint8, int32, int64, float32 and float64. `ds.shapes` / `ds.dtypes` describe the fields, `ds.ddstore` is the underlying `PyDDStore`. - `method` (default `DDSTORE_METHOD` or 0) picks the backend; `ddstore_width` splits `comm` into independent stores (see [Partitioned usage](#partitioned--sub-communicator-usage)). - `device` puts tensor fields of read samples on a GPU and `add_device` keeps each rank's share there ([GPUDirect](#gpudirect-rdma-gpu-resident-buffers)). + - `chunk_size` loads each rank's share that many samples at a time, writing each chunk into the store before reading the next: peak memory is about the share plus one chunk, instead of about three times the share (400 MiB share: 461 vs 1202 MiB). Host storage only (not with `add_device`). - **Batched by default**: `__getitems__` reads a whole batch with one [`get_batch()`](#get_batchname-arr-indices) per field, which `DataLoader` and `ThreadDataLoader` call automatically; `DDSTORE_BATCH_GET=0` reads one sample at a time. With `method=0` batched reads are collective, so every rank must iterate the same number of batches from one thread (`DistributedSampler` does). - **`DistDatasetReader(name, handshake_dir=None, n_core=None, device=None)`**: the same dataset read by a separate `method=2` extra job; it learns the fields from a `{name}.meta.json` file the core group writes next to the handshake records. - **`ThreadDataLoader(dataset, **DataLoader args)`**: a `DataLoader` whose workers are threads, not forked processes, so it is safe with MPI and GPU buffers. Each batch is fetched, collated and optionally pinned in a worker thread; random draws match `DataLoader`'s. One or two workers are enough: reads on one variable are serialized by its lock, and one batched read already keeps the network busy. `DDSTORE_AFFINITY_WIDTH` / `DDSTORE_AFFINITY_OFFSET` pin worker threads to CPUs. diff --git a/src/pyddstore/torch.py b/src/pyddstore/torch.py index 4907bb5..47b953d 100644 --- a/src/pyddstore/torch.py +++ b/src/pyddstore/torch.py @@ -220,6 +220,13 @@ def __getitems__(self, indices): return [self._rebuild([col[i] for col in columns]) for i in range(len(idx))] +def _rows(values, dtype): + """Stack per-sample values into contiguous (n, size) rows of `dtype`.""" + return np.ascontiguousarray( + np.stack([_to_numpy_row(v) for v in values]).astype(dtype, copy=False) + ) + + class DistDataset(_StoreDataset): """A map-style dataset stored in DDStore across the ranks of `comm`. @@ -238,6 +245,11 @@ class DistDataset(_StoreDataset): method: DDStore backend (default ``DDSTORE_METHOD`` or 0). handshake_dir: ``method=2`` directory (default ``DDSTORE_HANDSHAKE_DIR`` or ``./ddstore_hs``). + chunk_size: load this rank's share ``chunk_size`` samples at a time, + writing each chunk into the store before reading the next, so + only one chunk is held in memory besides the store (default: + read the whole share, then add it). Host storage only (no + ``add_device`` for tensor fields). ``ds[i]`` returns a sample with the source's structure: tensors stay tensors (on ``device`` if given), numpy arrays stay arrays, numbers stay @@ -257,6 +269,7 @@ def __init__( add_device=None, method=None, handshake_dir=None, + chunk_size=None, ): super().__init__() from mpi4py import MPI @@ -284,32 +297,20 @@ def __init__( group_rank = self.ddstore_comm.Get_rank() group_size = self.ddstore_comm.Get_size() - # This rank's share of the source, and the schema every rank agrees on. + # This rank's share of the source. Errors found locally are raised + # only after comparing with every rank (_raise_on_all), so a bad + # sample on some ranks raises on all of them instead of leaving the + # others blocked in a collective. self.total_ns = len(source) lo, hi = _nsplit(self.total_ns, group_size)[group_rank] - samples = [source[i] for i in range(lo, hi)] - # Check locally, but raise only after comparing with every rank, so a - # bad sample on some ranks raises on all of them instead of leaving - # the others blocked in the collective below. - schema, error = None, None + first, schema, error = None, None, None try: - if samples: - schema = _schema_of(samples[0], f"{name}[{lo}]") - for i, s in zip(range(lo + 1, hi), samples[1:]): - other = _schema_of(s, f"{name}[{i}]") - if other != schema: - raise ValueError( - f"{name}[{i}]: structure, shape or dtype differs from {name}[{lo}] " - f"({other} vs {schema}); DistDataset needs fixed-shape samples" - ) + if hi > lo: + first = source[lo] + schema = _schema_of(first, f"{name}[{lo}]") except (TypeError, ValueError) as exc: - error = (type(exc).__name__, str(exc)) - gathered = self.comm.allgather((schema, error)) - errors = [e for _, e in gathered if e is not None] - if errors: - cls = TypeError if errors[0][0] == "TypeError" else ValueError - raise cls(errors[0][1]) - schemas = [s for s, _ in gathered] + error = exc + schemas = self._raise_on_all(error, schema) if any(s is None for s in schemas): raise ValueError( f"{name}: every rank needs at least one sample (dataset has {self.total_ns})" @@ -319,6 +320,23 @@ def __init__( f"{name}: samples differ in structure, shape or dtype across ranks" ) self._setup_fields(schemas[0], name, device) + if chunk_size is not None: + if chunk_size < 1: + raise ValueError(f"chunk_size must be >= 1 (got {chunk_size})") + if add_device is not None and any( + f["kind"] == "torch" for f in self.schema["fields"] + ): + raise ValueError( + "chunk_size needs host storage: it can't be combined with add_device" + ) + + def check(i, sample): + other = _schema_of(sample, f"{name}[{i}]") + if other != self.schema: + raise ValueError( + f"{name}[{i}]: structure, shape or dtype differs from {name}[{lo}] " + f"({other} vs {self.schema}); DistDataset needs fixed-shape samples" + ) hs = _handshake_dir(handshake_dir) if self.method == 2: @@ -334,22 +352,56 @@ def __init__( else: self.ddstore = PyDDStore(self.ddstore_comm, method=self.method) - for j, var in enumerate(self._var): - f = self.schema["fields"][j] - values = [_flatten(s)[2][j] for s in samples] - if add_device is not None and f["kind"] == "torch": - rows = ( - torch.stack([v.reshape(-1) for v in values]) - .to(add_device) - .contiguous() - ) - else: - rows = np.ascontiguousarray( - np.stack([_to_numpy_row(v) for v in values]).astype( - f["dtype"], copy=False + fields = self.schema["fields"] + if chunk_size is None: + # Whole share at once: read, check, then add() each field. + samples, error = [first], None + try: + for i in range(lo + 1, hi): + samples.append(source[i]) + check(i, samples[-1]) + except (TypeError, ValueError) as exc: + error = exc + self._raise_on_all(error) + for j, var in enumerate(self._var): + values = [_flatten(s)[2][j] for s in samples] + if add_device is not None and fields[j]["kind"] == "torch": + rows = ( + torch.stack([v.reshape(-1) for v in values]) + .to(add_device) + .contiguous() ) - ) - self.ddstore.add(var, rows) + else: + rows = _rows(values, fields[j]["dtype"]) + self.ddstore.add(var, rows) + else: + # Chunked: allocate every field (init, collective), then copy the + # share in chunks of chunk_size samples (update, local), so at most + # one chunk is held in memory besides the store itself. + for j, var in enumerate(self._var): + itemsize = np.dtype(fields[j]["dtype"]).itemsize + self.ddstore.init(var, hi - lo, self._size[j], itemsize) + error = None + try: + for start in range(lo, hi, chunk_size): + stop = min(start + chunk_size, hi) + chunk = [ + first if i == lo else source[i] for i in range(start, stop) + ] + for i, sample in zip(range(start, stop), chunk): + if i != lo: + check(i, sample) + for j, var in enumerate(self._var): + values = [_flatten(sm)[2][j] for sm in chunk] + self.ddstore.update( + var, _rows(values, fields[j]["dtype"]), start - lo + ) + if start == lo: + first = None # held only for the first chunk + except (TypeError, ValueError) as exc: + error = exc + # also makes sure every rank has filled its share before any reads + self._raise_on_all(error) logger.debug( "DistDataset %s: rank %d holds [%d, %d) of %d", name, @@ -359,6 +411,17 @@ def __init__( self.total_ns, ) + def _raise_on_all(self, error, value=None): + """Allgather (value, error) over comm; if any rank had an error, + raise it on every rank. Returns the gathered values.""" + local = None if error is None else (type(error).__name__, str(error)) + gathered = self.comm.allgather((value, local)) + errors = [e for _, e in gathered if e is not None] + if errors: + cls = TypeError if errors[0][0] == "TypeError" else ValueError + raise cls(errors[0][1]) + return [v for v, _ in gathered] + class DistDatasetReader(_StoreDataset): """A ``DistDataset`` published by a ``method=2`` core group, joined from a diff --git a/test/test_torch.py b/test/test_torch.py index 27ea738..0d1cd34 100644 --- a/test/test_torch.py +++ b/test/test_torch.py @@ -106,11 +106,12 @@ def finish(comm, ds): ds.ddstore.free() +@pytest.mark.parametrize("chunk_size", [None, 1, 4]) @pytest.mark.parametrize("method", METHODS) @pytest.mark.parametrize("source_cls", [TupleSource, DictSource, SingleSource]) -def test_items_match_source(comm, monkeypatch, method, source_cls): +def test_items_match_source(comm, monkeypatch, method, source_cls, chunk_size): src = source_cls() - ds = make(comm, monkeypatch, src, method) + ds = make(comm, monkeypatch, src, method, chunk_size=chunk_size) rng = np.random.default_rng(comm.Get_rank()) idx = rng.integers(0, N, size=20) ok = len(ds) == N @@ -195,9 +196,18 @@ def __getitem__(self, i): ("object", TypeError), ], ) -def test_unsupported_samples_raise(comm, kind, exc): +@pytest.mark.parametrize("chunk_size", [None, 3]) +def test_unsupported_samples_raise(comm, kind, exc, chunk_size): with pytest.raises(exc): - DistDataset(_Bad(kind), "bad", comm, method=0) + DistDataset(_Bad(kind), "bad", comm, method=0, chunk_size=chunk_size) + comm.Barrier() + + +def test_chunk_size_needs_host_storage(comm): + with pytest.raises(ValueError): + DistDataset(TupleSource(), "c", comm, method=0, chunk_size=4, add_device="cpu") + with pytest.raises(ValueError): + DistDataset(TupleSource(), "c", comm, method=0, chunk_size=0) comm.Barrier() From 4b65e8271b128494fe8135951b9961d61d3c53aa Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Mon, 5 Oct 2026 09:28:30 -0400 Subject: [PATCH 46/56] DistDataset: accept numpy structured records as items or fields A structured record (np.void), structured ndarray or np.recarray of any shape is stored as raw bytes in a uint8 variable, as projects packing samples into record arrays did by hand; its dtype (incl. padded/aligned and nested layouts) goes into the schema, so DistDatasetReader rebuilds it from {name}.meta.json too. Reads return the same kind of object with the same layout; ds.dtypes reports the structured dtype. test_torch: np.void, 1-element structured array, recarray (2,), padded nested record, dict mixing a record and a tensor; per item, batched, ThreadDataLoader, chunked; method-2 reader with a record field. Frontier: 57/57 on 4 and 16 ranks. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- README.md | 2 +- src/pyddstore/torch.py | 58 +++++++++++++++++++++++++++- test/test_torch.py | 88 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 144 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 52e5522..55d21b5 100644 --- a/README.md +++ b/README.md @@ -331,7 +331,7 @@ for x, y in loader: ... ``` -- **`DistDataset(source, name, comm=None, ddstore_width=None, device=None, add_device=None, method=None, handshake_dir=None, chunk_size=None)`**: each rank loads its contiguous share of `source` (anything with `len()` and `[i]`) into DDStore; every rank can then read every sample. Samples keep the source's structure (a tensor, numpy array or number; a tuple or list of them; or a dict of them), with each field's shape and dtype. Fields must have the same shape and dtype in every sample; supported dtypes are bool, uint8, int32, int64, float32 and float64. `ds.shapes` / `ds.dtypes` describe the fields, `ds.ddstore` is the underlying `PyDDStore`. +- **`DistDataset(source, name, comm=None, ddstore_width=None, device=None, add_device=None, method=None, handshake_dir=None, chunk_size=None)`**: each rank loads its contiguous share of `source` (anything with `len()` and `[i]`) into DDStore; every rank can then read every sample. Samples keep the source's structure (a tensor, numpy array or number; a tuple or list of them; or a dict of them), with each field's shape and dtype. Fields must have the same shape and dtype in every sample; supported dtypes are bool, uint8, int32, int64, float32 and float64. A field (or the whole sample) may also be a numpy structured record (`np.void`, a structured `ndarray`, or `np.recarray`) of any field types: it is stored as raw bytes and comes back as the same kind of object with the same layout (`default_collate` can't batch records, so pass a `collate_fn`). `ds.shapes` / `ds.dtypes` describe the fields, `ds.ddstore` is the underlying `PyDDStore`. - `method` (default `DDSTORE_METHOD` or 0) picks the backend; `ddstore_width` splits `comm` into independent stores (see [Partitioned usage](#partitioned--sub-communicator-usage)). - `device` puts tensor fields of read samples on a GPU and `add_device` keeps each rank's share there ([GPUDirect](#gpudirect-rdma-gpu-resident-buffers)). - `chunk_size` loads each rank's share that many samples at a time, writing each chunk into the store before reading the next: peak memory is about the share plus one chunk, instead of about three times the share (400 MiB share: 461 vs 1202 MiB). Host storage only (not with `add_device`). diff --git a/src/pyddstore/torch.py b/src/pyddstore/torch.py index 47b953d..7b6a3b3 100644 --- a/src/pyddstore/torch.py +++ b/src/pyddstore/torch.py @@ -76,8 +76,43 @@ def _meta_path(handshake_dir, name): # --------------------------------------------------------------------------- +def _record_descr(dtype): + """JSON-safe description of a structured dtype (see _record_dtype).""" + return json.loads(json.dumps(np.lib.format.dtype_to_descr(dtype))) + + +def _record_dtype(descr): + """Structured dtype from _record_descr's output (JSON turns tuples into + lists; numpy needs them back as tuples).""" + + def fix(d): + if isinstance(d, str): + return d + out = [] + for item in d: + name = tuple(item[0]) if isinstance(item[0], list) else item[0] + entry = (name, fix(item[1])) + out.append(entry + (tuple(item[2]),) if len(item) > 2 else entry) + return out + + return np.lib.format.descr_to_dtype(fix(descr)) + + def _field_spec(value, where): """(kind, numpy dtype, shape) of one leaf value.""" + # numpy structured records: stored as raw bytes (uint8), rebuilt on read + if isinstance(value, (np.ndarray, np.void)) and value.dtype.names is not None: + if isinstance(value, np.void): + kind, shape = "record", () + else: + kind = "recarray" if isinstance(value, np.recarray) else "structarray" + shape = value.shape + return { + "kind": kind, + "dtype": "uint8", + "shape": list(shape), + "record": _record_descr(value.dtype), + } if isinstance(value, torch.Tensor): kind, dtype, shape = ( "torch", @@ -130,6 +165,8 @@ def _schema_of(sample, where): def _to_numpy_row(value): if isinstance(value, torch.Tensor): return value.detach().cpu().numpy().reshape(-1) + if isinstance(value, (np.ndarray, np.void)) and value.dtype.names is not None: + return np.frombuffer(np.array(value, dtype=value.dtype).tobytes(), np.uint8) return np.asarray(value).reshape(-1) @@ -146,8 +183,14 @@ def _setup_fields(self, schema, name, device): self.name = name self.device = device self._var = [f"{name}/{k}" for k in schema["keys"]] + self._record = [ + _record_dtype(f["record"]) if "record" in f else None + for f in schema["fields"] + ] self._size = [ - int(np.prod(f["shape"], dtype=np.int64)) for f in schema["fields"] + int(np.prod(f["shape"], dtype=np.int64)) + * (rec.itemsize if rec is not None else 1) + for f, rec in zip(schema["fields"], self._record) ] self._batch_get = os.environ.get("DDSTORE_BATCH_GET", "1") != "0" @@ -158,7 +201,13 @@ def shapes(self): @property def dtypes(self): - return self._rebuild([f["dtype"] for f in self.schema["fields"]]) + # plain fields: dtype name; record fields: the structured numpy dtype + return self._rebuild( + [ + rec if rec is not None else f["dtype"] + for f, rec in zip(self.schema["fields"], self._record) + ] + ) def _rebuild(self, values): s = self.schema["structure"] @@ -185,6 +234,11 @@ def _value(self, row, j): return t.reshape(shape) if f["kind"] == "numpy": return row.reshape(shape) + if f["kind"] == "record": + return row.view(self._record[j])[0] + if f["kind"] in ("structarray", "recarray"): + arr = row.view(self._record[j]).reshape(shape) + return arr.view(np.recarray) if f["kind"] == "recarray" else arr if f["kind"] == "npscalar": return row[0] return row[0].item() diff --git a/test/test_torch.py b/test/test_torch.py index 0d1cd34..9065b4b 100644 --- a/test/test_torch.py +++ b/test/test_torch.py @@ -70,6 +70,59 @@ def __getitem__(self, i): return np.full((5,), i, dtype=np.int32) +REC = np.dtype( + [ + ("x_modules", np.float32, (4, 3)), + ("mask", np.bool_, (4,)), + ("params", np.int64, (2,)), + ] +) +# padded (align=True) and nested layout +REC_NESTED = np.dtype( + [ + ("a", np.uint8), + ("b", np.float64), + ("sub", [("c", np.int32, (2,)), ("d", np.bool_)]), + ], + align=True, +) + + +def _record(i, dtype=REC): + a = np.zeros((), dtype=dtype) + if dtype is REC: + a["x_modules"], a["mask"], a["params"] = i, i % 2 == 0, (i, -i) + else: + a["a"], a["b"], a["sub"]["c"], a["sub"]["d"] = ( + i % 256, + i / 7, + (i, 2 * i), + i % 3 == 0, + ) + return a[()] # np.void + + +class RecordSource(Dataset): + """Items are numpy structured records, in the forms projects use.""" + + def __init__(self, form): + self.form = form + + def __len__(self): + return N + + def __getitem__(self, i): + if self.form == "void": + return _record(i) + if self.form == "array": # 1-element structured ndarray + return np.array([_record(i)], dtype=REC) + if self.form == "recarray": # np.recarray of shape (2,) + return np.array([_record(i), _record(i + 1)], dtype=REC).view(np.recarray) + if self.form == "nested": + return _record(i, REC_NESTED) + return {"rec": _record(i), "t": torch.full((3,), float(i))} # mixed dict + + def same(a, b): """Equal structure, types and values.""" if type(a) is not type(b): @@ -232,7 +285,7 @@ def test_gpu_device_and_add_device(comm, monkeypatch): def test_method2_reader(comm, monkeypatch, tmp_path): monkeypatch.setenv("DDSTORE_FABRIC", FABRIC) hs = comm.bcast(str(tmp_path / "hs") if comm.Get_rank() == 0 else None, root=0) - src = DictSource() + src = RecordSource("dict") # records + tensors: layout round-trips via meta.json core = DistDataset(src, "rd", comm, method=2, handshake_dir=hs) comm.Barrier() ok = True @@ -244,3 +297,36 @@ def test_method2_reader(comm, monkeypatch, tmp_path): reader.ddstore.free() finish(comm, core) assert all_ok(comm, ok) + + +@pytest.mark.parametrize("chunk_size", [None, 4]) +@pytest.mark.parametrize("method", METHODS) +@pytest.mark.parametrize("form", ["void", "array", "recarray", "nested", "dict"]) +def test_record_items(comm, monkeypatch, method, form, chunk_size): + """numpy structured records as items (or a field) come back as the same + kind of object with the same layout and values.""" + src = RecordSource(form) + ds = make(comm, monkeypatch, src, method, chunk_size=chunk_size) + idx = list(range(N))[::-2] + ok = all(same(ds[i], src[i]) for i in idx[:4]) + ok &= all(same(b, src[i]) for b, i in zip(ds.__getitems__(idx), idx)) + # records don't collate with default_collate: pass collate_fn through + batches = list( + ThreadDataLoader(ds, batch_size=5, num_workers=1, collate_fn=lambda b: b) + ) + ok &= all( + same(b, src[i]) for i, b in zip(range(N), [x for bt in batches for x in bt]) + ) + finish(comm, ds) + assert all_ok(comm, ok) + + +def test_record_dtypes_property(comm, monkeypatch): + ds = make(comm, monkeypatch, RecordSource("dict"), 0) + ok = ( + ds.dtypes["rec"] == REC + and ds.dtypes["t"] == "float32" + and ds.shapes["rec"] == () + ) + finish(comm, ds) + assert all_ok(comm, ok) From dfff88a3e7f24f66bab6d9490c453c4d0b07204d Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Mon, 5 Oct 2026 09:31:32 -0400 Subject: [PATCH 47/56] README: 2.0 overview, PyTorch extra, library env vars, test_torch - Intro: short feature list (batched reads, GPUDirect, pyddstore.torch, thread safety / profiler / split mode). - Prerequisites/Installation: optional PyTorch, `pip install ".[torch]"`, in-place builds need PYTHONPATH=$PWD/src, package layout note and rebuild advice for 1.x checkouts. - Quick Start: pointer to pyddstore.torch.DistDataset. - Environment variables: new "Read by pyddstore.torch" table (DDSTORE_METHOD, DDSTORE_BATCH_GET, handshake dir/timeout, DDSTORE_N_CORE, DDSTORE_AFFINITY_*); the examples table keeps only example-only vars. - Testing: command and table row for test_torch.py (with test_get_batch). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- README.md | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 55d21b5..111c828 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,11 @@ Efficient distributed data loading for distributed data-parallel (DDP) training. Each MPI rank holds a shard of the full dataset in memory. DDStore exposes a global index space so any rank can read any sample via one-sided remote memory access — either MPI RMA (default) or libfabric RDMA — without coordinator synchronization. +- **Batched reads**: [`get_batch()`](#get_batchname-arr-indices) fetches a whole training batch in one call (one-sided RDMA reads in flight together, or an MPI collective for `method=0`). +- **GPUDirect RDMA**: data can live in, and be read straight into, GPU memory ([details](#gpudirect-rdma-gpu-resident-buffers)). +- **PyTorch integration**: [`pyddstore.torch`](#pytorch-dataset-integration) turns any map-style dataset into a distributed one (`DistDataset`) and provides a thread-based `ThreadDataLoader` that is safe with MPI and GPU buffers. +- **Thread-safe** reads, a [profiler](#performance) for where read time goes, and a split mode (`method=2`) where a separate job reads data published by another. + DDStore architecture ## Prerequisites @@ -16,6 +21,7 @@ Each MPI rank holds a shard of the full dataset in memory. DDStore exposes a glo | libfabric | Required for the RDMA backends (`method=1` and `method=2`) | | Python ≥ 3.6 | | | NumPy, mpi4py, Cython | Python build dependencies | +| PyTorch (optional) | For `pyddstore.torch` and GPU buffers (CUDA or ROCm build) | ## Installation @@ -23,11 +29,12 @@ Each MPI rank holds a shard of the full dataset in memory. DDStore exposes a glo # Install Python build dependencies pip install numpy mpi4py Cython -# Build in-place (use with PYTHONPATH=$PWD:$PYTHONPATH) +# Build in-place (use with PYTHONPATH=$PWD/src:$PYTHONPATH) CC=mpicc CXX=mpicxx python setup.py build_ext --inplace # Or install into the active virtual environment CC=mpicc CXX=mpicxx pip install . +CC=mpicc CXX=mpicxx pip install ".[torch]" # also pulls PyTorch, for pyddstore.torch # Or install in editable/development mode CC=mpicc CXX=mpicxx pip install -e . @@ -48,6 +55,8 @@ If that fails with `ModuleNotFoundError: No module named 'distutils.msvccompiler SETUPTOOLS_USE_DISTUTILS=stdlib CC=cc CXX=CC pip install --no-build-isolation --no-deps -e . ``` +The package is `pyddstore` (compiled core `pyddstore._core`, plus `pyddstore.torch`). After updating from a 1.x checkout, rebuild; an old `src/pyddstore.cpython-*.so` left behind is unused and can be deleted. + ## Quick Start ```python @@ -79,6 +88,8 @@ Run with: mpirun -n 4 python my_script.py ``` +With PyTorch, [`pyddstore.torch.DistDataset`](#pytorch-dataset-integration) does the sharding, `add()` and batched reads for you. + ## API Reference ### `PyDDStore(comm_or_none=None, method=0, handshake_dir="", n_core=0, nic_map=None)` @@ -210,16 +221,20 @@ Release every variable's MPI window (`method=0`) or libfabric endpoints and memo | `DDSTORE_PROFILE` | off | `1` turns on `get()`/`get_batch()` timing counters, read with `get_profile(name)`. See [Performance](#performance). | | `DDSTORE_ALLTOALL_MAX_BYTES` | `2097152` (2 MiB) | `method=0` `get_batch()`: bytes each rank receives per exchange round. Must be equal on all ranks. | -The backend itself is not an environment variable in the library: pass `method=` to `PyDDStore` (`DDSTORE_METHOD` below is how the examples choose it). +**Read by `pyddstore.torch`** (defaults for arguments not given): + +| Variable | Default | Effect | +|---|---|---| +| `DDSTORE_METHOD` | `0` | `DistDataset`'s backend when `method=` isn't passed: `0` MPI RMA, `1` libfabric, `2` file-based handshake. (`PyDDStore` itself takes `method=` only.) | +| `DDSTORE_BATCH_GET` | `1` | `DistDataset.__getitems__` reads a whole batch with one `get_batch()` per field; `0` reads one sample at a time. | +| `DDSTORE_HANDSHAKE_DIR`, `DDSTORE_HANDSHAKE_TIMEOUT_S` | `./ddstore_hs`, `300` | `method=2` directory, and how long `DistDatasetReader` waits for the core group to publish. | +| `DDSTORE_N_CORE` | unset | `DistDatasetReader`'s number of core ranks when `n_core=` isn't passed (the examples default it to 4). | +| `DDSTORE_AFFINITY_WIDTH` / `DDSTORE_AFFINITY_OFFSET` | `0` / `0` | `ThreadDataLoader`: pin worker thread *i* to CPUs `[offset + i·width, offset + (i+1)·width)` of the process's affinity; width `0` = no pinning. | **Read by the examples** (`examples/vae/`, `examples/scripts/`, job scripts): | Variable | Default | Effect | |---|---|---| -| `DDSTORE_METHOD` | `0` (`bench_get.py`: `1`) | Backend passed as `method=`: `0` MPI RMA, `1` libfabric, `2` file-based handshake. `--num-workers > 0` in `vae-ddp.py` needs `1` or `2`. | -| `DDSTORE_BATCH_GET` | `1` | `DistDataset.__getitems__` reads a whole batch with one `get_batch()`; `0` falls back to one `get()` per sample. | -| `DDSTORE_N_CORE` | `4` | `vae_extra_train.py`, `test_method2_*.py`: number of core ranks that published the data (`--n-core` overrides). | -| `DDSTORE_AFFINITY_WIDTH` / `DDSTORE_AFFINITY_OFFSET` | `0` / `0` | `ThreadDataLoader`: pin worker thread *i* to CPUs `[offset + i·width, offset + (i+1)·width)` of the process's affinity; width `0` = no pinning. | | `DDSTORE_BACKEND` | auto | `torch.distributed` backend for the examples' DDP setup (`nccl`, `gloo`, `xccl`). | | `VAE_PROFILE` | off | `1`: `vae-ddp.py` prints per-epoch fetch vs compute time. | | `MASTER_PORT` | `2345` | DDP rendezvous port; the core/extra job script gives each step its own. | @@ -440,6 +455,12 @@ mpirun -n 1 python -m pytest test/test_single.py -v mpirun -n 4 python -m pytest test/test_multirank.py -v ``` +**Batched reads and the PyTorch layer** — method 0 everywhere; method 1, method 2 and GPU cases run inside a Slurm step with a CXI device (provider from `DDSTORE_FABRIC`, default `cxi`): + +```bash +mpirun -n 4 python -m pytest test/test_get_batch.py test/test_torch.py -v +``` + **GPUDirect RDMA** — requires a live `cxi` fabric and a CUDA/HIP GPU per rank (skipped automatically otherwise); see [GPUDirect RDMA](#gpudirect-rdma-gpu-resident-buffers): ```bash @@ -452,6 +473,7 @@ DDSTORE_FABRIC=cxi mpirun -n 2 python -m pytest test/test_gpu_rdma.py -v | `test/test_multirank.py` | 2 (4 recommended) | Remote reads, shard boundaries, multiple variables, `ddstore_width` grouping | | `test/test_gpu_rdma.py` | 2 | GPU-resident `add()`/`get()` in both directions, both libfabric methods, negative/error cases | | `test/test_get_batch.py` | 2 (4 recommended) | `get_batch()`: shuffled indices across ranks with repeats, single row, dtypes, error recovery, GPU destination, concurrent threads; method 0, plus method 1 over `cxi` inside a Slurm step | +| `test/test_torch.py` | 2 (4 recommended) | `pyddstore.torch`: tuple/dict/single samples of every field kind, numpy records, loaders vs the plain source, chunked loading, error handling, `ddstore_width`, GPU placement, `DistDatasetReader` | ### Integration scripts From 0e6e6e3d22e2492b0c2685b2a200022b24827263 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Mon, 5 Oct 2026 09:48:10 -0400 Subject: [PATCH 48/56] DistDatasetReader: clear error when n_core is missing Raise ValueError ("pass n_core= or set DDSTORE_N_CORE") instead of a bare KeyError when neither is given. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RGZFgjHgEW6QidNBZxaE5z --- src/pyddstore/torch.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pyddstore/torch.py b/src/pyddstore/torch.py index 7b6a3b3..4ac3cd9 100644 --- a/src/pyddstore/torch.py +++ b/src/pyddstore/torch.py @@ -498,6 +498,11 @@ def __init__(self, name, handshake_dir=None, n_core=None, device=None): hs = _handshake_dir(handshake_dir) if n_core is None: + if "DDSTORE_N_CORE" not in os.environ: + raise ValueError( + "DistDatasetReader needs the number of core ranks: pass n_core= " + "or set DDSTORE_N_CORE" + ) n_core = int(os.environ["DDSTORE_N_CORE"]) timeout = float(os.environ.get("DDSTORE_HANDSHAKE_TIMEOUT_S", "300")) path = _meta_path(hs, name) From 96ad8c20f42f44d50143b52972da1b948a79274b Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Mon, 5 Oct 2026 06:54:43 -0700 Subject: [PATCH 49/56] test_gpu_rdma: barrier before free() in matrix and sync_before_get With method 1, epoch_end() and free() don't synchronize, so a rank that finished its reads could close its endpoint while its peer was still reading from it. The peer's get() then failed with PTLTE_NOT_FOUND, skipped all_passed(), and the other rank hung in allreduce. Seen on Perlmutter in about 1 of 5 runs; 15/15 pass with the barriers. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EasT2JuGMcWVMaVcZXAYxi --- test/test_gpu_rdma.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/test_gpu_rdma.py b/test/test_gpu_rdma.py index 8fd24d3..d955429 100644 --- a/test/test_gpu_rdma.py +++ b/test/test_gpu_rdma.py @@ -178,6 +178,9 @@ def test_get_into_gpu_tensor_cxi_matrix(comm, monkeypatch): gathered = comm.gather(results, root=0) if rank == 0: print(f"[rank 0] ALL RESULTS: {gathered}", flush=True) + # gather() doesn't hold back non-root ranks; wait for every rank's reads + # before tearing down (see test_get_into_gpu_tensor_cxi_compute_kernel_read). + comm.Barrier() store.free() # Fail loudly with the full matrix visible in the log even if only one # combination is wrong -- this test is diagnostic, not a pass/fail gate. @@ -220,6 +223,8 @@ def test_get_into_gpu_tensor_cxi_sync_before_get(comm, monkeypatch): print(f"[rank {rank}] sync-before-get: ok={ok} got={snapshot.tolist()}", flush=True) store.epoch_end() + # Wait for every rank's reads before tearing down (PTLTE_NOT_FOUND otherwise). + comm.Barrier() store.free() assert all_passed(comm, ok) From 322e2f16cd068cbbc8cdb83336b5e0ebd7201918 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Mon, 5 Oct 2026 07:41:41 -0700 Subject: [PATCH 50/56] Large rows and reusable receive buffers for method 1/2 reads - get(): compute the byte count in size_t. disp * itemsize was an int product, so rows of 2 GiB or more got a wrapped length and fi_read failed with EMSGSIZE. - Split rows longer than DDSTORE_MAX_READ_BYTES (default 1 GiB, lowered to the endpoint's max_msg_size when reported) into several fi_reads. On Perlmutter's cxi one 5 GB read fails with EMSGSIZE (2.5 GB works) and FI_OPT_MAX_MSG_SIZE reports nothing. - register_recv(name, arr) / unregister_recv(name, arr): register a host or GPU destination buffer once; reads into it or any slice skip memory registration. Several per variable, never evicted; the store keeps a reference until unregister or free(). No-op for method 0. - Tests: registered pools from two threads (mr_miss unchanged under DDSTORE_PROFILE=1), registered GPU buffer, wide rows (split with DDSTORE_MAX_READ_BYTES=4096). Verified on Perlmutter: test_get_batch (8 and 4 ranks, also with 4 KiB pieces), test_torch, test_gpu_rdma, test_multirank pass; 5 GB rows read correctly via get() and get_batch() at 17-20 GB/s. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EasT2JuGMcWVMaVcZXAYxi --- README.md | 19 +++- include/common.h | 27 +++++ include/ddstore.hpp | 33 +++++- src/common.cxx | 245 +++++++++++++++++++++++++++------------- src/ddstore.cxx | 1 + src/pyddstore/_core.pyx | 43 +++++++ test/test_get_batch.py | 122 ++++++++++++++++++++ 7 files changed, 409 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 111c828..be54302 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ Read `arr.shape[0]` consecutive rows starting at global index `start` into `arr` Read rows `indices` (global row ids; any order, any ranks, repeats allowed) into `arr`: row `i` of `arr` receives row `indices[i]`, so `arr.shape[0]` must equal `len(indices)` (else `ValueError`). Same buffer rules as `get()` (NumPy array or CUDA/HIP tensor). Every index is checked before anything is read; an out-of-range one raises `IndexError` and leaves the store usable. -For `method=1`/`2` the whole batch is one call: one lock acquisition, one memory registration, and (GPU destination) one device sync, with all of the batch's `fi_read`s posted before any is waited for, so the reads overlap on the network. It is still one `fi_read` per row. +For `method=1`/`2` the whole batch is one call: one lock acquisition, one memory registration, and (GPU destination) one device sync, with all of the batch's `fi_read`s posted before any is waited for, so the reads overlap on the network. It is one `fi_read` per row, or several for a row longer than `DDSTORE_MAX_READ_BYTES` (default 1 GiB). For `method=0`, `get_batch()` is **collective**, after the collective module of [MDLoader](https://ieeexplore.ieee.org/abstract/document/10820758) (see [Citation](#citation)): every rank all-gathers all ranks' indices (`MPI_Allgatherv`), packs the rows it owns for each requester, and one `MPI_Alltoallv` delivers them, on a private duplicate of the store's communicator, in rounds of at most `DDSTORE_ALLTOALL_MAX_BYTES` (default 2 MiB) received per rank so large rows don't turn into one huge exchange. So every rank must call it for the variable the same number of times, in the same order, from one thread at a time; the number of indices may differ per rank (including 0). Indices are checked on the gathered list, so a bad index raises on every rank together. `DistributedSampler` gives every rank the same number of batches, and `vae-ddp.py` allows no worker threads with `method=0`, so the data loaders meet this automatically. @@ -181,6 +181,19 @@ store.get_batch("features", out, idx) --- +### `register_recv(name, arr)` / `unregister_recv(name, arr)` + +Register `arr` (a C-contiguous NumPy array or CUDA/HIP tensor) once as a destination for `get()`/`get_batch()` of `name`. Reads into `arr` or any slice of it then skip memory registration, which otherwise happens whenever the destination isn't the buffer registered by the previous read. For large rows, registration can cost more than the transfer. Use it for buffers you reuse, such as a pool per loader thread: several can be registered per variable and none is evicted. The store holds a reference to `arr` until `unregister_recv()` or `free()`. No-op for `method=0`. + +```python +pool = np.empty((batch_size, ncols), dtype=np.float32) +store.register_recv("features", pool) +for idx in batches: + store.get_batch("features", pool[: len(idx)], idx) # no registration +``` + +--- + ### `join(name)` `method=2` extra member only. Discovers a variable published by the core group by polling the handshake directory until the combined record file (`{name}.bin`) written by core rank 0 reaches its expected size (up to `DDSTORE_HANDSHAKE_TIMEOUT_S` seconds), then registers it for `get()`. @@ -219,6 +232,7 @@ Release every variable's MPI window (`method=0`) or libfabric endpoints and memo | `DDSTORE_HANDSHAKE_DIR` | `./ddstore_hs` | `method=2` handshake directory when none is given (C++ API; `PyDDStore` requires `handshake_dir`, and the examples fill it from this variable). Must be on a shared filesystem. | | `DDSTORE_HANDSHAKE_TIMEOUT_S` | `300` | Seconds a `method=2` extra member's `join()` polls for the core group's record file. | | `DDSTORE_PROFILE` | off | `1` turns on `get()`/`get_batch()` timing counters, read with `get_profile(name)`. See [Performance](#performance). | +| `DDSTORE_MAX_READ_BYTES` | `1073741824` (1 GiB) | `method=1`/`2`: largest single `fi_read`; longer rows are read in pieces (on Perlmutter's `cxi` one 5 GB read fails with `EMSGSIZE`, 2.5 GB works, and the provider doesn't report the limit). Lowered to the endpoint's `max_msg_size` when the provider reports one. | | `DDSTORE_ALLTOALL_MAX_BYTES` | `2097152` (2 MiB) | `method=0` `get_batch()`: bytes each rank receives per exchange round. Must be equal on all ranks. | **Read by `pyddstore.torch`** (defaults for arguments not given): @@ -400,6 +414,7 @@ This keeps sample fetches inside a node at the cost of replicating the data per ## Performance - Use **batched reads** (the default with `DistDataset`, or `get_batch()` directly). They cut per-sample cost by 10–27× for small rows and make the GPU path insensitive to worker threads; in the VAE every configuration got 1.2–3.9× faster per epoch. +- **Reuse destination buffers** and [`register_recv()`](#register_recvname-arr--unregister_recvname-arr) them. Reading into a fresh buffer every time re-registers memory on every read; `get_profile(name)["mr_miss"]` counts those registrations. - `method=1` (one-sided `fi_read`) is the fastest backend; `method=0` with batching (collective) comes close for small rows. - `DDSTORE_PROFILE=1` + `get_profile(name)` shows where `get()`/`get_batch()` time goes: lock wait, memory registration, posting and completing `fi_read`, GPU sync. `vae-ddp.py` prints an all-rank summary when it is set. [examples/scripts/bench_get.py](examples/scripts/bench_get.py) measures per-row latency and throughput vs row size, destination, batch size and threads. @@ -472,7 +487,7 @@ DDSTORE_FABRIC=cxi mpirun -n 2 python -m pytest test/test_gpu_rdma.py -v | `test/test_single.py` | 1 | All dtypes, `add`/`get`, `init`/`update`/`get`, error handling, double `free()` | | `test/test_multirank.py` | 2 (4 recommended) | Remote reads, shard boundaries, multiple variables, `ddstore_width` grouping | | `test/test_gpu_rdma.py` | 2 | GPU-resident `add()`/`get()` in both directions, both libfabric methods, negative/error cases | -| `test/test_get_batch.py` | 2 (4 recommended) | `get_batch()`: shuffled indices across ranks with repeats, single row, dtypes, error recovery, GPU destination, concurrent threads; method 0, plus method 1 over `cxi` inside a Slurm step | +| `test/test_get_batch.py` | 2 (4 recommended) | `get_batch()`: shuffled indices across ranks with repeats, single row, dtypes, error recovery, GPU destination, concurrent threads, registered destination buffers, wide rows (with `DDSTORE_MAX_READ_BYTES=4096` they are read in pieces); method 0, plus method 1 over `cxi` inside a Slurm step | | `test/test_torch.py` | 2 (4 recommended) | `pyddstore.torch`: tuple/dict/single samples of every field kind, numpy records, loaders vs the plain source, chunked loading, error handling, `ddstore_width`, GPU placement, `DistDatasetReader` | ### Integration scripts diff --git a/include/common.h b/include/common.h index 8ab18e8..742f207 100644 --- a/include/common.h +++ b/include/common.h @@ -36,6 +36,15 @@ extern "C" { #endif + /* One caller-registered recv buffer (see fabric_state::pinned). */ + struct recv_region + { + struct fid_mr *mr; + char *base; + size_t len; + int hmem_iface; + }; + struct fabric_state { struct fi_context *ctx; @@ -81,6 +90,14 @@ extern "C" * Initialised to NULL/0 so the first call always registers. */ char *recv_mr_base; size_t recv_mr_reg_len; + /* Caller-registered recv regions (register_recv_region()), checked + * before the one-slot cache above and never evicted: the caller owns + * these buffers and keeps them alive until unregister / free(). */ + struct recv_region *pinned; + int n_pinned; + /* Largest single fi_read() the endpoint accepts (FI_OPT_MAX_MSG_SIZE + * or ep_attr->max_msg_size); 0 if unknown. Longer rows are split. */ + size_t max_msg_size; uint64_t key; uint64_t *remote_key; uint64_t *remote_address; @@ -196,6 +213,16 @@ extern "C" * before any is waited for. 0 on success. See common.cxx. */ int read_batch_from_remote(struct fabric_state *fabric_state, long n, const int *src, const uint64_t *offset, size_t row_len); + /* Register [base, base + len) once as a recv buffer (hmem_iface as for + * recv_hmem_iface); reads into it then skip registration. Registering a + * region already registered is a no-op. 0 on success. Caller holds + * recv_lock and keeps the buffer alive until unregister / free. */ + int register_recv_region(struct fabric_state *fs, char *base, size_t len, int hmem_iface); + /* Undo register_recv_region() for the region starting at base. 0 on + * success, 1 if no such region. Caller holds recv_lock. */ + int unregister_recv_region(struct fabric_state *fs, char *base); + /* Close every registered recv region (free()). */ + void close_recv_regions(struct fabric_state *fs); /* --- Method 2: file-based handshake ---------------------------------- */ diff --git a/include/ddstore.hpp b/include/ddstore.hpp index 49e6b0a..7584661 100644 --- a/include/ddstore.hpp +++ b/include/ddstore.hpp @@ -518,7 +518,7 @@ class DDStore "GPU destination buffer requires DDSTORE_FABRIC=cxi " "(current fabric does not support FI_HMEM)"); varinfo.fabric_state->recv_data = (char *)buffer; - varinfo.fabric_state->recv_data_len = varinfo.disp * varinfo.itemsize * count; + varinfo.fabric_state->recv_data_len = (size_t)varinfo.disp * varinfo.itemsize * count; varinfo.fabric_state->recv_hmem_iface = hmem_iface; int rc = read_from_remote(varinfo.fabric_state, target, (start - offset) * varinfo.disp * varinfo.itemsize); if (rc != 0) @@ -528,6 +528,37 @@ class DDStore } } + /* Register `len` bytes at `buffer` once as a destination for get() / + * get_batch() of `name` (hmem_iface as in get()), so reads into it or + * any part of it skip memory registration. Several buffers can be + * registered per variable (e.g. one per loader thread); none is ever + * evicted. The caller keeps the buffer alive until unregister_recv() or + * free(). No-op for method 0. */ + void register_recv(std::string name, void *buffer, size_t len, int hmem_iface = 0) + { + const VarInfo_t &varinfo = this->varlist.at(name); + if (this->method == 0) + return; + if (hmem_iface != 0 && !is_hmem_capable(varinfo.fabric_state)) + throw std::runtime_error( + "GPU destination buffer requires DDSTORE_FABRIC=cxi " + "(current fabric does not support FI_HMEM)"); + fabric_state_lock_guard lock(varinfo.fabric_state); + if (register_recv_region(varinfo.fabric_state, (char *)buffer, len, hmem_iface) != 0) + throw std::runtime_error("register_recv failed for " + name); + } + + /* Undo register_recv() for the buffer starting at `buffer`. */ + void unregister_recv(std::string name, void *buffer) + { + const VarInfo_t &varinfo = this->varlist.at(name); + if (this->method == 0) + return; + fabric_state_lock_guard lock(varinfo.fabric_state); + if (unregister_recv_region(varinfo.fabric_state, (char *)buffer) != 0) + throw std::invalid_argument("buffer is not registered for " + name); + } + /* Batched get: row i of `buffer` (n contiguous rows) receives global * row idx[i]; rows may come from any ranks, in any order, with repeats. * Every index is validated before anything is read. hmem_iface as in get(). diff --git a/src/common.cxx b/src/common.cxx index a8d49cb..ed48f15 100644 --- a/src/common.cxx +++ b/src/common.cxx @@ -186,6 +186,8 @@ static void init_fabric_hsn(struct fabric_state *fabric) fi_strerror(result)); return; } + if (info->ep_attr && info->ep_attr->max_msg_size > 0) + fabric->max_msg_size = info->ep_attr->max_msg_size; av_attr.type = FI_AV_MAP; av_attr.count = DP_AV_DEF_SIZE; @@ -440,6 +442,7 @@ static void init_fabric_cxi(struct fabric_state *fabric) && max_msg_size > 0) { info->ep_attr->max_msg_size = max_msg_size; + fabric->max_msg_size = max_msg_size; fprintf(stderr, "endpoint max_msg_size=%zu\n", max_msg_size); } } @@ -659,71 +662,40 @@ int handshake(struct fabric_state *fabric_state, MPI_Comm comm) return 0; } -/* Make sure recv_data..recv_data+recv_data_len is covered by a registered - * recv MR. Returns 0, or 1 after printing the libfabric error. - * - * The recv MR is cached by registered region rather than exact pointer: - * register recv_data..recv_data+recv_data_len on a miss, and reuse the - * MR while later buffers lie inside [recv_mr_base, recv_mr_base + - * recv_mr_reg_len). Hits when the caller passes the same buffer again -- - * e.g. PyTorch's caching allocator returning the same block for a - * same-shape torch.empty() -- so each get() needn't re-register. */ -static int ensure_recv_mr(struct fabric_state *fabric_state) +/* Register [base, base + len) for receiving (FI_READ) into *out; hmem_iface + * as for recv_hmem_iface. Returns 0, or 1 after printing the libfabric error + * (*out is then NULL). */ +static int register_recv_mr(struct fabric_state *fabric_state, char *base, size_t len, + int hmem_iface, struct fid_mr **out) { - bool recv_is_hmem = fabric_state->recv_hmem_iface != FI_HMEM_SYSTEM; - char *cur_base = fabric_state->recv_data; - size_t cur_len = fabric_state->recv_data_len; - bool in_cached_region = - (fabric_state->recv_mr != NULL) && - (cur_base >= fabric_state->recv_mr_base) && - (cur_base + cur_len <= fabric_state->recv_mr_base + fabric_state->recv_mr_reg_len); - - if (in_cached_region) - return 0; - - if (ddstore_profile_enabled()) - fabric_state->prof_mr_miss++; - /* Close the stale registration before creating a new one. */ - if (fabric_state->recv_mr) - { - fi_close(&fabric_state->recv_mr->fid); - fabric_state->recv_mr = NULL; - } - int mr_rc; - if (recv_is_hmem) + *out = NULL; + if (hmem_iface != FI_HMEM_SYSTEM) { /* GPU destination buffer (ROCr on AMD, CUDA on NVIDIA -- whichever * iface the caller set). No host-staged fallback exists: either * fi_mr_regattr succeeds and fi_read() DMAs straight into device * memory, or it fails loudly here (checked below). */ - struct iovec iov = {cur_base, cur_len}; + struct iovec iov = {base, len}; struct fi_mr_attr attr; memset(&attr, 0, sizeof(attr)); attr.mr_iov = &iov; attr.iov_count = 1; attr.access = FI_READ; - attr.iface = (enum fi_hmem_iface)fabric_state->recv_hmem_iface; + attr.iface = (enum fi_hmem_iface)hmem_iface; attr.device.reserved = 0; /* ROCr/CUDA both resolve the device from the pointer */ - mr_rc = fi_mr_regattr(fabric_state->domain, &attr, 0, &fabric_state->recv_mr); + mr_rc = fi_mr_regattr(fabric_state->domain, &attr, 0, out); } else { - mr_rc = fi_mr_reg( - fabric_state->domain, - cur_base, - cur_len, - FI_READ, - 0, 0, 0, - &fabric_state->recv_mr, - NULL); + mr_rc = fi_mr_reg(fabric_state->domain, base, len, FI_READ, 0, 0, 0, out, NULL); } if (mr_rc != FI_SUCCESS) { fprintf(stderr, "%s failed: %s\n", - recv_is_hmem ? "fi_mr_regattr" : "fi_mr_reg", + hmem_iface != FI_HMEM_SYSTEM ? "fi_mr_regattr" : "fi_mr_reg", fi_strerror(mr_rc)); - fabric_state->recv_mr = NULL; + *out = NULL; return 1; } @@ -731,24 +703,133 @@ static int ensure_recv_mr(struct fabric_state *fabric_state) * hsn/verbs/gni/psm2 (is_mr_endpoint() is false for those). */ if (is_mr_endpoint(fabric_state)) { - int rc_mr = fi_mr_bind(fabric_state->recv_mr, &fabric_state->signal->fid, 0); + int rc_mr = fi_mr_bind(*out, &fabric_state->signal->fid, 0); if (rc_mr == FI_SUCCESS) - rc_mr = fi_mr_enable(fabric_state->recv_mr); + rc_mr = fi_mr_enable(*out); if (rc_mr != FI_SUCCESS) { fprintf(stderr, "fi_mr_bind/fi_mr_enable (recv) failed: %s\n", fi_strerror(rc_mr)); - fi_close(&fabric_state->recv_mr->fid); - fabric_state->recv_mr = NULL; + fi_close(&(*out)->fid); + *out = NULL; return 1; } } + return 0; +} + +/* Find the recv MR covering recv_data..recv_data+recv_data_len, into *use. + * Returns 0, or 1 after printing the libfabric error. + * + * Caller-registered regions (register_recv_region()) are checked first; + * they are never evicted. Otherwise the one-slot cache: register on a miss + * and reuse the MR while later buffers lie inside [recv_mr_base, + * recv_mr_base + recv_mr_reg_len). Hits when the caller passes the same + * buffer again -- e.g. PyTorch's caching allocator returning the same block + * for a same-shape torch.empty() -- so each get() needn't re-register. */ +static int ensure_recv_mr(struct fabric_state *fabric_state, struct fid_mr **use) +{ + char *cur_base = fabric_state->recv_data; + size_t cur_len = fabric_state->recv_data_len; + + for (int i = 0; i < fabric_state->n_pinned; i++) + { + const struct recv_region *r = &fabric_state->pinned[i]; + if (r->hmem_iface == fabric_state->recv_hmem_iface && + cur_base >= r->base && cur_base + cur_len <= r->base + r->len) + { + *use = r->mr; + return 0; + } + } + + bool in_cached_region = + (fabric_state->recv_mr != NULL) && + (cur_base >= fabric_state->recv_mr_base) && + (cur_base + cur_len <= fabric_state->recv_mr_base + fabric_state->recv_mr_reg_len); + if (in_cached_region) + { + *use = fabric_state->recv_mr; + return 0; + } + + if (ddstore_profile_enabled()) + fabric_state->prof_mr_miss++; + /* Close the stale registration before creating a new one. */ + if (fabric_state->recv_mr) + { + fi_close(&fabric_state->recv_mr->fid); + fabric_state->recv_mr = NULL; + } + if (register_recv_mr(fabric_state, cur_base, cur_len, + fabric_state->recv_hmem_iface, &fabric_state->recv_mr) != 0) + return 1; /* Record the registered region for future range checks. */ fabric_state->recv_mr_base = cur_base; fabric_state->recv_mr_reg_len = cur_len; + *use = fabric_state->recv_mr; + return 0; +} + +int register_recv_region(struct fabric_state *fs, char *base, size_t len, int hmem_iface) +{ + for (int i = 0; i < fs->n_pinned; i++) + if (fs->pinned[i].base == base && fs->pinned[i].len == len && + fs->pinned[i].hmem_iface == hmem_iface) + return 0; + struct recv_region *grown = (struct recv_region *)realloc( + fs->pinned, (fs->n_pinned + 1) * sizeof(struct recv_region)); + if (!grown) + return 1; + fs->pinned = grown; + struct fid_mr *mr; + if (register_recv_mr(fs, base, len, hmem_iface, &mr) != 0) + return 1; + fs->pinned[fs->n_pinned++] = (struct recv_region){mr, base, len, hmem_iface}; return 0; } +int unregister_recv_region(struct fabric_state *fs, char *base) +{ + for (int i = 0; i < fs->n_pinned; i++) + { + if (fs->pinned[i].base != base) + continue; + fi_close(&fs->pinned[i].mr->fid); + fs->pinned[i] = fs->pinned[--fs->n_pinned]; + return 0; + } + return 1; +} + +void close_recv_regions(struct fabric_state *fs) +{ + for (int i = 0; i < fs->n_pinned; i++) + fi_close(&fs->pinned[i].mr->fid); + free(fs->pinned); + fs->pinned = NULL; + fs->n_pinned = 0; +} + +/* Largest piece one fi_read() may carry: DDSTORE_MAX_READ_BYTES (default + * 1 GiB), lowered to the endpoint's max_msg_size when that is known. On + * Perlmutter's cxi one 5 GB read fails with EMSGSIZE (2.5 GB works) and + * FI_OPT_MAX_MSG_SIZE reports nothing, hence a default well below that. */ +static size_t max_read_bytes(const struct fabric_state *fabric_state) +{ + static size_t env_limit = 0; + if (env_limit == 0) + { + const char *v = getenv("DDSTORE_MAX_READ_BYTES"); + long long n = v ? atoll(v) : 0; + env_limit = n > 0 ? (size_t)n : ((size_t)1 << 30); + } + size_t limit = env_limit; + if (fabric_state->max_msg_size > 0 && fabric_state->max_msg_size < limit) + limit = fabric_state->max_msg_size; + return limit; +} + /* Reap at most one completion from the CQ. Returns 1 for a successful * completion, -1 for an error completion (printed and consumed, so it * still counts as one finished read), 0 if none is ready yet, and -2 if @@ -796,7 +877,8 @@ static int reap_one(struct fabric_state *fabric_state) * * All n fi_read()s are posted back to back and only then waited for, so * the round trips overlap on the network; one recv MR covers the whole - * buffer. Returns 0, or non-zero if any read failed. + * buffer. A row longer than max_read_bytes() is read in several pieces. + * Returns 0, or non-zero if any read failed. * * Every read that was posted is waited for before returning, even after * an error: a completion left in the CQ would be taken as the next call's. @@ -817,7 +899,8 @@ int read_batch_from_remote(struct fabric_state *fabric_state, long n, const bool prof = ddstore_profile_enabled(); uint64_t t_mr = prof ? ddstore_now_ns() : 0; - if (ensure_recv_mr(fabric_state) != 0) + struct fid_mr *recv_mr = NULL; + if (ensure_recv_mr(fabric_state, &recv_mr) != 0) return 1; void *memory_descriptor = NULL; @@ -828,7 +911,7 @@ int read_batch_from_remote(struct fabric_state *fabric_state, long n, * descriptor stayed NULL for HMEM too and fi_read() silently no-op'd * instead of DMAing into the GPU buffer. */ if (is_local_mr_req(fabric_state) || fabric_state->recv_hmem_iface != FI_HMEM_SYSTEM) - memory_descriptor = fi_mr_desc(fabric_state->recv_mr); + memory_descriptor = fi_mr_desc(recv_mr); uint64_t t_read = 0; if (prof) @@ -837,40 +920,46 @@ int read_batch_from_remote(struct fabric_state *fabric_state, long n, fabric_state->prof_mr_ns += t_read - t_mr; } + /* Rows longer than max_read_bytes() go as several fi_read()s. */ + const size_t chunk = max_read_bytes(fabric_state); long posted = 0, done = 0; int failed = 0; for (long i = 0; i < n && !failed; i++) { - for (;;) + for (size_t off = 0; off < row_len && !failed; off += chunk) { - ssize_t rc = fi_read( - fabric_state->signal, - fabric_state->recv_data + (size_t)i * row_len, - row_len, - memory_descriptor, - fabric_state->comm_partner[src[i]], - fabric_state->remote_address[src[i]] + offset[i], - fabric_state->remote_key[src[i]], - NULL); - if (rc == 0) - { - posted++; - break; - } - if (rc != -FI_EAGAIN) + size_t len = row_len - off < chunk ? row_len - off : chunk; + for (;;) { - fprintf(stderr, "fi_read failed: %zd (%s)\n", rc, fi_strerror((int)-rc)); - failed = 1; - break; + ssize_t rc = fi_read( + fabric_state->signal, + fabric_state->recv_data + (size_t)i * row_len + off, + len, + memory_descriptor, + fabric_state->comm_partner[src[i]], + fabric_state->remote_address[src[i]] + offset[i] + off, + fabric_state->remote_key[src[i]], + NULL); + if (rc == 0) + { + posted++; + break; + } + if (rc != -FI_EAGAIN) + { + fprintf(stderr, "fi_read failed: %zd (%s)\n", rc, fi_strerror((int)-rc)); + failed = 1; + break; + } + /* Transmit queue full: make progress by reaping a completion. */ + int r = reap_one(fabric_state); + if (r == -2) + return 1; /* CQ broken: cannot account for in-flight reads */ + if (r != 0) + done++; + if (r < 0) + failed = 1; } - /* Transmit queue full: make progress by reaping a completion. */ - int r = reap_one(fabric_state); - if (r == -2) - return 1; /* CQ broken: cannot account for in-flight reads */ - if (r != 0) - done++; - if (r < 0) - failed = 1; } } diff --git a/src/ddstore.cxx b/src/ddstore.cxx index 3aa9e6b..3ac33c6 100644 --- a/src/ddstore.cxx +++ b/src/ddstore.cxx @@ -204,6 +204,7 @@ void DDStore::free() else if (var.fabric_state) { struct fabric_state *fs = var.fabric_state; + close_recv_regions(fs); if (fs->recv_mr) fi_close(&fs->recv_mr->fid); if (fs->mr) fi_close(&fs->mr->fid); if (fs->signal) fi_close(&fs->signal->fid); diff --git a/src/pyddstore/_core.pyx b/src/pyddstore/_core.pyx index 6e1614d..1ce2d56 100644 --- a/src/pyddstore/_core.pyx +++ b/src/pyddstore/_core.pyx @@ -110,6 +110,8 @@ cdef extern from "ddstore.hpp": void add[T](string name, T* buffer, long nrows, int disp, int hmem_iface) except + void get[T](string name, long start, long count, T* buffer, int hmem_iface) except + nogil void get_batch[T](string name, const long *idx, long n, T* buffer, int hmem_iface) except + nogil + void register_recv(string name, void *buffer, size_t len, int hmem_iface) except + + void unregister_recv(string name, void *buffer) except + void epoch_begin() void epoch_end() void free() @@ -134,6 +136,7 @@ cdef class PyDDStore: # lifetime-contract comment) -- this dict keeps the Python reference # alive for as long as the variable stays registered. cdef dict _gpu_owned_buffers + cdef dict _recv_buffers # DDSTORE_PROFILE=1: Python-side get() timing (see get_profile()). cdef bint _prof cdef double _prof_get_s @@ -160,6 +163,7 @@ cdef class PyDDStore: cdef MPI.Comm mpi_comm self.method = method self._gpu_owned_buffers = {} + self._recv_buffers = {} self._prof = os.environ.get("DDSTORE_PROFILE", "0") not in ("", "0") self._prof_get_s = 0.0 self._prof_sync_s = 0.0 @@ -199,6 +203,7 @@ cdef class PyDDStore: del self.c_ddstore self.c_ddstore = NULL self._gpu_owned_buffers.clear() + self._recv_buffers.clear() def add(self, str name, arr): cdef size_t ptr @@ -356,6 +361,43 @@ cdef class PyDDStore: self._prof_get_s += time.perf_counter() - t_get self._prof_gets += 1 + def register_recv(self, str name, arr): + """Register `arr` (contiguous host numpy array or GPU tensor) once as + a destination for get()/get_batch() of `name`: reads into it, or into + any slice of it, then skip memory registration. Use for buffers that + are reused across reads (e.g. a per-thread pool); several can be + registered per variable and none is evicted. The store keeps `arr` + alive until unregister_recv() or free(). No-op for method 0.""" + cdef size_t ptr + cdef size_t nbytes + cdef int iface + cdef bint is_gpu = _is_cuda_tensor(arr) + if is_gpu: + _check_gpu_fabric_preconditions(self.method, "GPU destination buffer") + assert arr.is_contiguous() + ptr = arr.data_ptr() + nbytes = arr.numel() * arr.element_size() + iface = _hmem_iface_for(arr) + else: + assert arr.flags.c_contiguous + ptr = arr.ctypes.data + nbytes = arr.nbytes + iface = 0 + if nbytes == 0: + return + self.c_ddstore.register_recv(s2b(name), ptr, nbytes, iface) + self._recv_buffers[(name, ptr)] = arr + + def unregister_recv(self, str name, arr): + """Undo register_recv(name, arr).""" + cdef size_t ptr = arr.data_ptr() if _is_cuda_tensor(arr) else arr.ctypes.data + if (name, ptr) not in self._recv_buffers: + if self.method == 0: + return + raise ValueError("buffer is not registered for %r" % name) + self.c_ddstore.unregister_recv(s2b(name), ptr) + del self._recv_buffers[(name, ptr)] + def get_profile(self, str name): """DDSTORE_PROFILE=1 timing for `name` (methods 1/2), in seconds. @@ -385,6 +427,7 @@ cdef class PyDDStore: def free(self): self.c_ddstore.free() self._gpu_owned_buffers.clear() + self._recv_buffers.clear() def init(self, str name, long nrows, int disp, int itemsize=1): self.c_ddstore.init(s2b(name), nrows, disp, itemsize) diff --git a/test/test_get_batch.py b/test/test_get_batch.py index 8ef8db0..8c59bc5 100644 --- a/test/test_get_batch.py +++ b/test/test_get_batch.py @@ -205,3 +205,125 @@ def worker(seed): finish(store) assert not errors, f"worker thread(s) raised: {errors}" assert all_passed(comm, not bad) + + +def _mr_miss(store): + """recv registrations so far (None unless DDSTORE_PROFILE was set at start).""" + if os.environ.get("DDSTORE_PROFILE", "0") in ("", "0"): + return None + return store.get_profile("x")["mr_miss"] + + +@pytest.mark.parametrize("method", METHODS) +def test_register_recv_pool(comm, monkeypatch, method): + """Reads into registered buffers, from several threads, each with its own + buffer: every row correct and (method 1) no registration per read.""" + size = comm.Get_size() + store = make_store(comm, method, monkeypatch) + nthreads, batch = 2, 16 + pools = [np.zeros((4 * batch, NCOLS), dtype=np.float32) for _ in range(nthreads)] + for pool in pools: + store.register_recv("x", pool) + store.register_recv("x", pools[0]) # registering twice is a no-op + miss0 = _mr_miss(store) + errors, bad = [], [] + + def worker(t): + rng = np.random.default_rng(1000 * comm.Get_rank() + t) + pool = pools[t] + try: + for it in range(40): + k = it % 4 + out = pool[k * batch : (k + 1) * batch] # a slice of the pool + idx = rng.integers(0, NROWS * size, size=batch) + store.get_batch("x", out, idx) + if not np.array_equal(out, expected_rows(idx)): + bad.append((t, "batch")) + one = pool[k * batch : k * batch + 1] + store.get("x", one, int(idx[0])) + if not np.array_equal(one, expected_rows(idx[:1])): + bad.append((t, "get")) + except Exception as exc: # noqa: BLE001 - surface any thread exception + errors.append(exc) + + if method == 0: # get_batch is collective there: one thread at a time + for t in range(nthreads): + worker(t) + else: + threads = [threading.Thread(target=worker, args=(t,)) for t in range(nthreads)] + for t in threads: + t.start() + for t in threads: + t.join() + ok = not errors and not bad + if method != 0 and miss0 is not None: + ok &= _mr_miss(store) == miss0 + for pool in pools: + store.unregister_recv("x", pool) + if method != 0: + with pytest.raises(ValueError): + store.unregister_recv("x", pools[0]) + # unregistered buffers still work (through the one-slot cache) + idx = np.arange(batch) % (NROWS * size) + store.get_batch("x", pools[1][:batch], idx) + ok &= np.array_equal(pools[1][:batch], expected_rows(idx)) + comm.Barrier() + finish(store) + assert not errors, f"worker thread(s) raised: {errors}" + assert all_passed(comm, ok), bad + + +@pytest.mark.skipif( + not (HAVE_CXI and HAVE_GPU and FABRIC == "cxi"), + reason="requires the cxi provider and a GPU", +) +def test_register_recv_gpu(comm, monkeypatch): + size = comm.Get_size() + store = make_store(comm, 1, monkeypatch) + pool = torch.empty((64, NCOLS), dtype=torch.float32, device="cuda") + store.register_recv("x", pool) + miss0 = _mr_miss(store) + rng = np.random.default_rng(200 + comm.Get_rank()) + ok = True + for it in range(20): + k = it % 4 + out = pool[k * 16 : (k + 1) * 16] + idx = rng.integers(0, NROWS * size, size=16) + store.get_batch("x", out, idx) + diff = (out - torch.from_numpy(expected_rows(idx)).cuda()).abs().sum().item() + ok &= diff == 0.0 + if miss0 is not None: + ok &= _mr_miss(store) == miss0 + store.unregister_recv("x", pool) + comm.Barrier() + finish(store) + assert all_passed(comm, ok) + + +WIDE = 3001 # float32 columns: 12004-byte rows + + +@pytest.mark.parametrize("method", METHODS) +def test_wide_rows(comm, monkeypatch, method): + """Rows of 12004 bytes. Run with DDSTORE_MAX_READ_BYTES=4096 (method 1) + to check that rows longer than one read are split, remainder included.""" + size = comm.Get_size() + if method != 0: + monkeypatch.setenv("DDSTORE_FABRIC", FABRIC) + rank = comm.Get_rank() + store = dds.PyDDStore(comm, method=method) + first = rank * 4 + rows = np.arange(first, first + 4)[:, None] * 10000.0 + np.arange(WIDE) + store.add("w", rows.astype(np.float32)) + store.epoch_begin() + idx = np.array([(rank + 1) % size * 4 + 3, 0, size * 4 - 1]) + out = np.zeros((len(idx), WIDE), dtype=np.float32) + store.get_batch("w", out, idx) + want = (idx[:, None] * 10000.0 + np.arange(WIDE)).astype(np.float32) + ok = np.array_equal(out, want) + one = np.zeros((1, WIDE), dtype=np.float32) + store.get("w", one, int(idx[0])) + ok &= np.array_equal(one, want[:1]) + comm.Barrier() + finish(store) + assert all_passed(comm, ok) From f82325785ef5f3b90ce744e54a0263b0f90a0470 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Mon, 5 Oct 2026 07:47:37 -0700 Subject: [PATCH 51/56] ThreadDataLoader: separate iterator; setup.py: regenerate on NumPy change - ThreadDataLoader.__iter__ returns a new _ThreadLoaderIter holding the epoch state, whose __iter__ returns itself, as torch's DataLoader does. list(it) / islice(it, ...) after next(it) continue the epoch instead of restarting it, iterators over one loader are independent (shared thread pool), and an iterator dropped early cancels its queued batches. next(loader) on the loader itself no longer works (nor does it on DataLoader). - setup.py records the NumPy major version that generated _core.cpp (src/pyddstore/_core.numpy-version, ignored) and regenerates the file when it changes: a NumPy 2 generated file does not compile against NumPy 1.x headers. Also warns about src/pyddstore.cpp / .so left over from the old layout. Verified on Perlmutter: test_torch (8 and 4 ranks, new iterator test included), test_get_batch, and vae-ddp with 2 loader threads (batched and per-sample, same loss). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EasT2JuGMcWVMaVcZXAYxi --- .gitignore | 1 + README.md | 4 +++- setup.py | 25 +++++++++++++++++++- src/pyddstore/torch.py | 52 +++++++++++++++++++++++++++++------------- test/test_torch.py | 33 +++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index f0f2345..8a70968 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ build/ src/pyddstore.cpp src/pyddstore/_core.cpp +src/pyddstore/_core.numpy-version # built module PyDDStore.egg-info/ pyddstore.cpython-*.so diff --git a/README.md b/README.md index be54302..053ba7a 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,9 @@ If that fails with `ModuleNotFoundError: No module named 'distutils.msvccompiler SETUPTOOLS_USE_DISTUTILS=stdlib CC=cc CXX=CC pip install --no-build-isolation --no-deps -e . ``` -The package is `pyddstore` (compiled core `pyddstore._core`, plus `pyddstore.torch`). After updating from a 1.x checkout, rebuild; an old `src/pyddstore.cpython-*.so` left behind is unused and can be deleted. +The package is `pyddstore` (compiled core `pyddstore._core`, plus `pyddstore.torch`). After updating from a 1.x checkout, rebuild; an old `src/pyddstore.cpython-*.so` or `src/pyddstore.cpp` left behind is unused and can be deleted (the build warns about them). + +Editable and in-place builds keep the generated `src/pyddstore/_core.cpp` in the checkout, shared by every environment that builds from it. `setup.py` regenerates it whenever the NumPy major version differs from the previous build's, because a file generated against NumPy 2 doesn't compile against NumPy 1.x headers. ## Quick Start diff --git a/setup.py b/setup.py index be9d7d7..f4f1351 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,29 @@ extending, ] +# The generated _core.cpp depends on the NumPy headers it was generated +# against: one generated with NumPy 2 does not compile against NumPy 1.x +# headers (PyDataType_ELSIZE). An editable or in-place build keeps it in +# src/, shared by every environment that builds from this checkout, so +# regenerate it whenever the NumPy major version differs from last time. +numpy_major = np.__version__.split(".")[0] +stamp = join("src", "pyddstore", "_core.numpy-version") +try: + with open(stamp) as f: + regenerate = f.read().strip() != numpy_major +except OSError: + regenerate = True +with open(stamp, "w") as f: + f.write(numpy_major + "\n") + +# Left over from the layout before pyddstore became a package. +for old in ("src/pyddstore.cpp",) + tuple( + join("src", f) for f in os.listdir("src") + if f.startswith("pyddstore.") and f.endswith(".so") +): + if os.path.exists(old): + print(f"warning: stale build output {old} from the old layout; remove it") + setup( name="PyDDStore", version="2.0", @@ -52,5 +75,5 @@ package_dir={"": "src"}, packages=["pyddstore"], py_modules=["cpu_nic_map"], - ext_modules=cythonize(extensions), + ext_modules=cythonize(extensions, force=regenerate), ) diff --git a/src/pyddstore/torch.py b/src/pyddstore/torch.py index 4ac3cd9..3b5279a 100644 --- a/src/pyddstore/torch.py +++ b/src/pyddstore/torch.py @@ -545,7 +545,6 @@ class ThreadDataLoader(DataLoader): def __init__(self, dataset, **kwargs): super().__init__(dataset, **kwargs) - self.fs = queue.Queue() # Persistent across epochs -- recreating the pool in every __iter__() # would leak OS threads since the old pool is never shut down. self._counter = mp.Value("i", 0) @@ -609,16 +608,30 @@ def fetch( return (ibatch, batch) def __iter__(self): - # Drop what an earlier epoch left behind (it may have stopped early) - self.clean() + """A new iterator over one epoch. Each has its own sampler position + and queue (the thread pool is shared), so several iterators over one + loader don't interfere, as with ``DataLoader``.""" + return _ThreadLoaderIter(self) - self._num_yielded = 0 - self._sampler_iter = iter(self._index_sampler) + def __del__(self): + executor = getattr(self, "executor", None) + if executor is not None: + executor.shutdown(wait=False) + + +class _ThreadLoaderIter: + """One epoch of a ThreadDataLoader; ``__iter__`` returns itself.""" + + def __init__(self, loader): + self.loader = loader + self._sampler_iter = iter(loader._index_sampler) # torch's DataLoader iterator draws a base seed from the global RNG # here, every epoch; draw it too, so the training loop's later random # draws are the same as with DataLoader - torch.empty((), dtype=torch.int64).random_(generator=self.generator) + torch.empty((), dtype=torch.int64).random_(generator=loader.generator) + self.fs = queue.Queue() self.fs_iter = iter(self.fs.get, None) + self._num_yielded = 0 self._next_batch_i = 0 self._inflight = 0 self._sampler_exhausted = False @@ -628,12 +641,18 @@ def __iter__(self): # regardless of dataset size. Mirrors torch's own prefetch_factor # (default 2 per worker). self._max_inflight = max( - 1, (self.num_workers or 1) * (self.prefetch_factor or 2) + 1, (loader.num_workers or 1) * (loader.prefetch_factor or 2) ) self._refill() + + def __iter__(self): return self + def __len__(self): + return len(self.loader) + def _refill(self): + loader = self.loader while self._inflight < self._max_inflight: try: index = next(self._sampler_iter) @@ -642,14 +661,14 @@ def _refill(self): self._sampler_exhausted = True self.fs.put(None) return - future = self.executor.submit( - self.fetch, - self.dataset, + future = loader.executor.submit( + loader.fetch, + loader.dataset, self._next_batch_i, index, - collate_fn=self.collate_fn, - pin_memory=self.pin_memory, - auto_collation=self._auto_collation, + collate_fn=loader.collate_fn, + pin_memory=loader.pin_memory, + auto_collation=loader._auto_collation, ) self.fs.put(future) self._next_batch_i += 1 @@ -670,7 +689,8 @@ def __next__(self): self._num_yielded += 1 return data - def clean(self): + def close(self): + """Cancel the batches still queued (an epoch stopped early).""" # Without blocking: the end marker (None) is queued only once the # sampler is exhausted, so an epoch that stopped early has none, and # waiting for it (iter(self.fs.get, None)) would block forever. Only @@ -681,5 +701,5 @@ def clean(self): future.cancel() def __del__(self): - self.clean() - self.executor.shutdown(wait=False) + if hasattr(self, "fs"): + self.close() diff --git a/test/test_torch.py b/test/test_torch.py index 9065b4b..5e8287d 100644 --- a/test/test_torch.py +++ b/test/test_torch.py @@ -202,6 +202,39 @@ def test_loaders_match_plain_source(comm, monkeypatch, method, loader): assert all_ok(comm, ok) +def test_thread_loader_iterators(comm): + """iter(loader) is a separate iterator, as with DataLoader: iterating it + again continues the epoch (list(it), islice), two iterators over one + loader are independent, and an epoch stopped early doesn't leak into the + next one.""" + import itertools + + src = TupleSource() + ref = list(DataLoader(src, batch_size=4)) + loader = ThreadDataLoader(src, batch_size=4, num_workers=2) + + def eq(got, want): + return len(got) == len(want) and all(same(g, r) for g, r in zip(got, want)) + + it = iter(loader) + ok = iter(it) is it and len(it) == len(ref) + first = next(it) + two = list(itertools.islice(it, 2)) + rest = list(it) # continues, does not restart + ok &= eq([first] + two + rest, ref) + a, b = iter(loader), iter(loader) + got_a, got_b = [], [] + for x, y in zip(a, b): # interleaved + got_a.append(x) + got_b.append(y) + ok &= eq(got_a, ref) and eq(got_b, ref) + for i, _ in enumerate(loader): # stop early + if i == 1: + break + ok &= eq(list(loader), ref) + assert all_ok(comm, ok) + + def test_per_sample_fallback(comm, monkeypatch): monkeypatch.setenv("DDSTORE_BATCH_GET", "0") src = TupleSource() From 5439e07089854bf19e5c8899d7f08ae9b1f08222 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Mon, 5 Oct 2026 07:56:27 -0700 Subject: [PATCH 52/56] ThreadDataLoader: submit the next batch as soon as one is taken __next__ refilled before taking its batch, so the slot that batch freed was only reused at the next __next__: during each training step one fewer than num_workers * prefetch_factor batches were being fetched (with 2 threads and prefetch_factor=1, one thread idle every step; in Walrus this alternated 0.3 s and 2.0-2.4 s data waits). Take the batch, then refill, as torch's DataLoader does; one more batch is held during the step. Test: test_thread_loader_keeps_prefetch_full (3 batches fetched while one is held with 2 workers, prefetch_factor=1; the old order gave 2). README: ThreadDataLoader iterator and prefetch depth, registration wording for get_batch() and free(), tests table. Verified on Perlmutter: test_torch (8 and 4 ranks), test_get_batch, and vae-ddp with 2 loader threads (batched and per-sample, same loss). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EasT2JuGMcWVMaVcZXAYxi --- README.md | 8 ++++---- src/pyddstore/torch.py | 14 ++++++-------- test/test_torch.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 053ba7a..6a9db4c 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ Read `arr.shape[0]` consecutive rows starting at global index `start` into `arr` Read rows `indices` (global row ids; any order, any ranks, repeats allowed) into `arr`: row `i` of `arr` receives row `indices[i]`, so `arr.shape[0]` must equal `len(indices)` (else `ValueError`). Same buffer rules as `get()` (NumPy array or CUDA/HIP tensor). Every index is checked before anything is read; an out-of-range one raises `IndexError` and leaves the store usable. -For `method=1`/`2` the whole batch is one call: one lock acquisition, one memory registration, and (GPU destination) one device sync, with all of the batch's `fi_read`s posted before any is waited for, so the reads overlap on the network. It is one `fi_read` per row, or several for a row longer than `DDSTORE_MAX_READ_BYTES` (default 1 GiB). +For `method=1`/`2` the whole batch is one call: one lock acquisition, at most one memory registration (none into a [registered](#register_recvname-arr--unregister_recvname-arr) buffer), and (GPU destination) one device sync, with all of the batch's `fi_read`s posted before any is waited for, so the reads overlap on the network. It is one `fi_read` per row, or several for a row longer than `DDSTORE_MAX_READ_BYTES` (default 1 GiB). For `method=0`, `get_batch()` is **collective**, after the collective module of [MDLoader](https://ieeexplore.ieee.org/abstract/document/10820758) (see [Citation](#citation)): every rank all-gathers all ranks' indices (`MPI_Allgatherv`), packs the rows it owns for each requester, and one `MPI_Alltoallv` delivers them, on a private duplicate of the store's communicator, in rounds of at most `DDSTORE_ALLTOALL_MAX_BYTES` (default 2 MiB) received per rank so large rows don't turn into one huge exchange. So every rank must call it for the variable the same number of times, in the same order, from one thread at a time; the number of indices may differ per rank (including 0). Indices are checked on the gathered list, so a bad index raises on every rank together. `DistributedSampler` gives every rank the same number of batches, and `vae-ddp.py` allows no worker threads with `method=0`, so the data loaders meet this automatically. @@ -220,7 +220,7 @@ Open and close an MPI RMA access epoch (calls `MPI_Win_fence`). **Collective**. ### `free()` -Release every variable's MPI window (`method=0`) or libfabric endpoints and memory registrations (`method=1`/`2`), then the host buffer DDStore allocated for it in `add()`/`init()` (a GPU tensor passed to `add()` is the caller's and is not freed). Safe to call more than once. After `MPI_Finalize` the MPI window and buffer can no longer be released and are skipped. +Release every variable's MPI window (`method=0`) or libfabric endpoints and memory registrations, including [`register_recv()`](#register_recvname-arr--unregister_recvname-arr) buffers (`method=1`/`2`), then the host buffer DDStore allocated for it in `add()`/`init()` (a GPU tensor passed to `add()` is the caller's and is not freed). Safe to call more than once. After `MPI_Finalize` the MPI window and buffer can no longer be released and are skipped. ## Environment variables @@ -368,7 +368,7 @@ for x, y in loader: - `chunk_size` loads each rank's share that many samples at a time, writing each chunk into the store before reading the next: peak memory is about the share plus one chunk, instead of about three times the share (400 MiB share: 461 vs 1202 MiB). Host storage only (not with `add_device`). - **Batched by default**: `__getitems__` reads a whole batch with one [`get_batch()`](#get_batchname-arr-indices) per field, which `DataLoader` and `ThreadDataLoader` call automatically; `DDSTORE_BATCH_GET=0` reads one sample at a time. With `method=0` batched reads are collective, so every rank must iterate the same number of batches from one thread (`DistributedSampler` does). - **`DistDatasetReader(name, handshake_dir=None, n_core=None, device=None)`**: the same dataset read by a separate `method=2` extra job; it learns the fields from a `{name}.meta.json` file the core group writes next to the handshake records. -- **`ThreadDataLoader(dataset, **DataLoader args)`**: a `DataLoader` whose workers are threads, not forked processes, so it is safe with MPI and GPU buffers. Each batch is fetched, collated and optionally pinned in a worker thread; random draws match `DataLoader`'s. One or two workers are enough: reads on one variable are serialized by its lock, and one batched read already keeps the network busy. `DDSTORE_AFFINITY_WIDTH` / `DDSTORE_AFFINITY_OFFSET` pin worker threads to CPUs. +- **`ThreadDataLoader(dataset, **DataLoader args)`**: a `DataLoader` whose workers are threads, not forked processes, so it is safe with MPI and GPU buffers. Each batch is fetched, collated and optionally pinned in a worker thread; random draws match `DataLoader`'s. As with `DataLoader`, `iter(loader)` returns a separate iterator for one epoch (`list(it)` or `islice(it, …)` after `next(it)` continue the epoch; iterators over one loader are independent), and while the training step holds a batch, `num_workers * prefetch_factor` more are being fetched, so that many plus one are in memory. One or two workers are enough: reads on one variable are serialized by its lock, and one batched read already keeps the network busy. `DDSTORE_AFFINITY_WIDTH` / `DDSTORE_AFFINITY_OFFSET` pin worker threads to CPUs. [examples/vae/vae-ddp.py](examples/vae/vae-ddp.py) trains a VAE with DDP on top of it: @@ -490,7 +490,7 @@ DDSTORE_FABRIC=cxi mpirun -n 2 python -m pytest test/test_gpu_rdma.py -v | `test/test_multirank.py` | 2 (4 recommended) | Remote reads, shard boundaries, multiple variables, `ddstore_width` grouping | | `test/test_gpu_rdma.py` | 2 | GPU-resident `add()`/`get()` in both directions, both libfabric methods, negative/error cases | | `test/test_get_batch.py` | 2 (4 recommended) | `get_batch()`: shuffled indices across ranks with repeats, single row, dtypes, error recovery, GPU destination, concurrent threads, registered destination buffers, wide rows (with `DDSTORE_MAX_READ_BYTES=4096` they are read in pieces); method 0, plus method 1 over `cxi` inside a Slurm step | -| `test/test_torch.py` | 2 (4 recommended) | `pyddstore.torch`: tuple/dict/single samples of every field kind, numpy records, loaders vs the plain source, chunked loading, error handling, `ddstore_width`, GPU placement, `DistDatasetReader` | +| `test/test_torch.py` | 2 (4 recommended) | `pyddstore.torch`: tuple/dict/single samples of every field kind, numpy records, loaders vs the plain source, `ThreadDataLoader` iterators and prefetch depth, chunked loading, error handling, `ddstore_width`, GPU placement, `DistDatasetReader` | ### Integration scripts diff --git a/src/pyddstore/torch.py b/src/pyddstore/torch.py index 3b5279a..e6bcd9d 100644 --- a/src/pyddstore/torch.py +++ b/src/pyddstore/torch.py @@ -675,18 +675,16 @@ def _refill(self): self._inflight += 1 def __next__(self): - # Refill *before* popping this call's batch, not after: refilling - # here only uses capacity freed by the *previous* call's batch, - # which -- by ordinary for-loop semantics -- the caller's loop body - # has already fully consumed by the time it asks for the next item - # (i.e. calls __next__ again). Bounds how far the executor can race - # ahead of consumption (memory, not correctness -- each batch is read - # into its own freshly allocated buffers). - self._refill() + # Submit the replacement as soon as this batch is taken, as torch's + # DataLoader does, so num_workers * prefetch_factor batches are being + # fetched while the caller's training step runs (plus the one it + # holds). Refilling at the start of the *next* call instead would leave + # one slot idle during every step. future = next(self.fs_iter) ibatch, data = future.result() self._inflight -= 1 self._num_yielded += 1 + self._refill() return data def close(self): diff --git a/test/test_torch.py b/test/test_torch.py index 5e8287d..0e5b88d 100644 --- a/test/test_torch.py +++ b/test/test_torch.py @@ -235,6 +235,38 @@ def eq(got, want): assert all_ok(comm, ok) +def test_thread_loader_keeps_prefetch_full(comm): + """While the caller holds a batch, num_workers * prefetch_factor more are + being fetched (as with DataLoader), not one fewer.""" + import threading + import time + + class Counting(Dataset): + def __init__(self): + self.lock, self.batches = threading.Lock(), 0 + + def __len__(self): + return 64 + + def __getitems__(self, idx): + with self.lock: + self.batches += 1 + return [torch.tensor(i) for i in idx] + + src = Counting() + loader = ThreadDataLoader(src, batch_size=4, num_workers=2, prefetch_factor=1) + it = iter(loader) + next(it) # held by the "training step" + want = 1 + 2 * 1 + deadline = time.time() + 10 + while src.batches < want and time.time() < deadline: + time.sleep(0.01) + time.sleep(0.2) # nothing beyond the bound should start + ok = src.batches == want + it.close() + assert all_ok(comm, ok), f"{src.batches} batches fetched, want {want}" + + def test_per_sample_fallback(comm, monkeypatch): monkeypatch.setenv("DDSTORE_BATCH_GET", "0") src = TupleSource() From 688124e31262e81e6bf985641fc87e4723c185de Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Mon, 5 Oct 2026 08:55:13 -0700 Subject: [PATCH 53/56] pyddstore.torch: read_rows, alloc/out=, WindowedDataset, row_of For samples made of several stored rows (time windows, clips) and for reading into reused, registered buffers; on DistDataset and DistDatasetReader. - ds.read_rows(rows, fields=None, out=None): any list of stored rows of the selected fields, one get_batch() per field, as a dict of (len(rows), *shape) values keyed like the sample. Collective for method 0, like get_batch(). - ds.alloc(n, fields=None) / ds.release(bufs): one buffer per field, register_recv()'d once; read_rows(out=) and __getitems__(idx, out=) read into them (views, no registration per read). - WindowedDataset(ds, window, stride=1, dilation=1, starts=None, fields=None): sample i = rows s, s+dilation, ... (s = i*stride or starts[i]); a batch of windows is one read_rows(). - row_of(concat, source, index): the row of a source's sample in a store built over a ConcatDataset (several files in one store). Tests: test_read_rows_and_out, test_windowed_dataset, test_row_of_concat (methods 0/1, DataLoader and ThreadDataLoader). README: PyTorch section and tests table. Verified on Perlmutter: test_torch (8 ranks with DDSTORE_PROFILE=1, and 4 ranks on 1 node) and test_get_batch pass. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EasT2JuGMcWVMaVcZXAYxi --- README.md | 19 ++++- src/pyddstore/torch.py | 189 +++++++++++++++++++++++++++++++++++++++-- test/test_torch.py | 124 +++++++++++++++++++++++++++ 3 files changed, 324 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6a9db4c..00b9601 100644 --- a/README.md +++ b/README.md @@ -367,6 +367,23 @@ for x, y in loader: - `device` puts tensor fields of read samples on a GPU and `add_device` keeps each rank's share there ([GPUDirect](#gpudirect-rdma-gpu-resident-buffers)). - `chunk_size` loads each rank's share that many samples at a time, writing each chunk into the store before reading the next: peak memory is about the share plus one chunk, instead of about three times the share (400 MiB share: 461 vs 1202 MiB). Host storage only (not with `add_device`). - **Batched by default**: `__getitems__` reads a whole batch with one [`get_batch()`](#get_batchname-arr-indices) per field, which `DataLoader` and `ThreadDataLoader` call automatically; `DDSTORE_BATCH_GET=0` reads one sample at a time. With `method=0` batched reads are collective, so every rank must iterate the same number of batches from one thread (`DistributedSampler` does). +- **Reading rows and reusing buffers** (`DistDataset` and `DistDatasetReader`): + - `ds.read_rows(rows, fields=None, out=None)` reads stored rows `rows` (any order, repeats allowed) of the selected fields (default all), one `get_batch()` per field, and returns a dict of values shaped `(len(rows), *field_shape)`. Keys are the sample's: dict keys, tuple/list positions, or `0` for a single value. With `method=0` it is collective, like `get_batch()`. + - `ds.alloc(n, fields=None)` returns buffers for `n` rows, one per field and keyed the same way, each [registered](#register_recvname-arr--unregister_recvname-arr) once. Pass them as `out=` to `read_rows()` or `ds.__getitems__(idx, out=)`: reads then skip memory registration, and the results are views into the buffers, so you decide when a buffer can be reused. `ds.release(bufs)` unregisters them; `free()` does too. +- **`WindowedDataset(ds, window, stride=1, dilation=1, starts=None, fields=None)`**: samples made of several stored rows of `ds` (time windows, clips, sequences), each stored row held once. Sample `i` is rows `s, s + dilation, …, s + (window - 1)·dilation` with `s = i·stride`, or `s = starts[i]` when `starts` is given; use `starts` to keep only windows that don't cross a trajectory or file boundary. Fields come back stacked, `(window, *field_shape)`, in the structure of `ds`'s samples (a dict when `fields` is given). A batch of windows is one `read_rows()`. +- **`row_of(concat, source, index)`**: with several sources in one store (a `DistDataset` over a `torch.utils.data.ConcatDataset`), the row of sample `index` of source `source`, e.g. to map (file, trajectory, step) to `starts`: + +```python +from torch.utils.data import ConcatDataset +from pyddstore.torch import DistDataset, WindowedDataset, row_of + +files = ConcatDataset([StepsOf(f) for f in paths]) # one sample per time step +frames = DistDataset(files, "frames", comm, method=1) +starts = [row_of(files, k, t) for k, f in enumerate(paths) # windows inside each file + for t in range(0, len(files.datasets[k]) - 2 * dt)] +pairs = WindowedDataset(frames, window=3, dilation=dt, starts=starts) # (t, t+dt, t+2dt) +``` + - **`DistDatasetReader(name, handshake_dir=None, n_core=None, device=None)`**: the same dataset read by a separate `method=2` extra job; it learns the fields from a `{name}.meta.json` file the core group writes next to the handshake records. - **`ThreadDataLoader(dataset, **DataLoader args)`**: a `DataLoader` whose workers are threads, not forked processes, so it is safe with MPI and GPU buffers. Each batch is fetched, collated and optionally pinned in a worker thread; random draws match `DataLoader`'s. As with `DataLoader`, `iter(loader)` returns a separate iterator for one epoch (`list(it)` or `islice(it, …)` after `next(it)` continue the epoch; iterators over one loader are independent), and while the training step holds a batch, `num_workers * prefetch_factor` more are being fetched, so that many plus one are in memory. One or two workers are enough: reads on one variable are serialized by its lock, and one batched read already keeps the network busy. `DDSTORE_AFFINITY_WIDTH` / `DDSTORE_AFFINITY_OFFSET` pin worker threads to CPUs. @@ -490,7 +507,7 @@ DDSTORE_FABRIC=cxi mpirun -n 2 python -m pytest test/test_gpu_rdma.py -v | `test/test_multirank.py` | 2 (4 recommended) | Remote reads, shard boundaries, multiple variables, `ddstore_width` grouping | | `test/test_gpu_rdma.py` | 2 | GPU-resident `add()`/`get()` in both directions, both libfabric methods, negative/error cases | | `test/test_get_batch.py` | 2 (4 recommended) | `get_batch()`: shuffled indices across ranks with repeats, single row, dtypes, error recovery, GPU destination, concurrent threads, registered destination buffers, wide rows (with `DDSTORE_MAX_READ_BYTES=4096` they are read in pieces); method 0, plus method 1 over `cxi` inside a Slurm step | -| `test/test_torch.py` | 2 (4 recommended) | `pyddstore.torch`: tuple/dict/single samples of every field kind, numpy records, loaders vs the plain source, `ThreadDataLoader` iterators and prefetch depth, chunked loading, error handling, `ddstore_width`, GPU placement, `DistDatasetReader` | +| `test/test_torch.py` | 2 (4 recommended) | `pyddstore.torch`: tuple/dict/single samples of every field kind, numpy records, loaders vs the plain source, `ThreadDataLoader` iterators and prefetch depth, `read_rows`/`alloc` buffers, `WindowedDataset`, `row_of`, chunked loading, error handling, `ddstore_width`, GPU placement, `DistDatasetReader` | ### Integration scripts diff --git a/src/pyddstore/torch.py b/src/pyddstore/torch.py index e6bcd9d..760cad6 100644 --- a/src/pyddstore/torch.py +++ b/src/pyddstore/torch.py @@ -12,6 +12,9 @@ handshake directory. - ``ThreadDataLoader``: a ``DataLoader`` whose workers are threads instead of forked processes (safe with MPI and GPU-resident buffers). +- ``WindowedDataset``: samples made of several stored rows (time windows, + clips), read with ``read_rows()``; ``row_of()`` maps a sample of one source + in a ``ConcatDataset`` to its row. Fields must have the same shape and dtype in every sample (fixed-shape). Supported dtypes: bool, uint8, int32, int64, float32, float64. @@ -35,7 +38,13 @@ logger = logging.getLogger(__name__) -__all__ = ["DistDataset", "DistDatasetReader", "ThreadDataLoader"] +__all__ = [ + "DistDataset", + "DistDatasetReader", + "ThreadDataLoader", + "WindowedDataset", + "row_of", +] _SUPPORTED = { np.dtype(np.bool_), @@ -243,6 +252,89 @@ def _value(self, row, j): return row[0] return row[0].item() + def _batch_value(self, buf, j): + """All rows of field j in buf as one value of shape (n, *shape) (a + view, no copy). Scalar fields give a 1-D array.""" + f = self.schema["fields"][j] + n = buf.shape[0] + shape = (n,) + tuple(f["shape"]) + if f["kind"] == "torch": + t = buf if isinstance(buf, torch.Tensor) else torch.from_numpy(buf) + return t.reshape(shape) + if f["kind"] == "record": + return buf.view(self._record[j]).reshape(n) + if f["kind"] in ("structarray", "recarray"): + arr = buf.view(self._record[j]).reshape(shape) + return arr.view(np.recarray) if f["kind"] == "recarray" else arr + return buf.reshape(shape) + + def _key(self, j): + """Field j's key as in a sample: dict key, tuple/list position, or 0.""" + k = self.schema["keys"][j] + return k if self.schema["structure"] == "dict" else int(k) + + def _field_ids(self, fields): + if fields is None: + return list(range(len(self._var))) + index = {self._key(j): j for j in range(len(self._var))} + try: + return [index[k] for k in fields] + except KeyError as exc: + raise KeyError( + f"{self.name}: no field {exc.args[0]!r} (fields: {list(index)})" + ) from None + + def alloc(self, n, fields=None): + """Buffers for reads of `n` rows: a dict keyed like the sample (dict + key, or tuple/list position, or 0 for a single value) of one buffer + per field, each registered once with ``register_recv()`` so reads + into it skip memory registration. Pass to ``read_rows(out=)`` or + ``__getitems__(out=)``; reads may use the first rows only. Samples + read into these buffers are views into them: the caller decides when + a buffer can be reused. ``release()`` unregisters them.""" + bufs = {} + for j in self._field_ids(fields): + buf = self._alloc(n, j) + self.ddstore.register_recv(self._var[j], buf) + bufs[self._key(j)] = buf + return bufs + + def release(self, bufs): + """Unregister buffers from ``alloc()`` (``free()`` does it too).""" + for j in self._field_ids(list(bufs)): + self.ddstore.unregister_recv(self._var[j], bufs[self._key(j)]) + + def _read_field(self, j, idx, out): + n = len(idx) + if out is None: + buf = self._alloc(n, j) + else: + buf = out[self._key(j)] + if buf.shape[0] < n or tuple(buf.shape[1:]) != (self._size[j],): + raise ValueError( + f"{self.name}: out[{self._key(j)!r}] has shape {tuple(buf.shape)}, " + f"need at least ({n}, {self._size[j]}) (use alloc())" + ) + buf = buf[:n] + self.ddstore.get_batch(self._var[j], buf, idx) + return buf + + def read_rows(self, rows, fields=None, out=None): + """Read stored rows `rows` (global sample indices; any order, repeats + allowed) of the selected fields (default: all) with one + ``get_batch()`` per field. Returns a dict keyed like ``alloc()`` of + values shaped ``(len(rows), *field_shape)``; scalar fields give 1-D + arrays. `out`: buffers from ``alloc()`` (at least ``len(rows)`` + rows); the values are then views into them. + + With ``method=0`` this is collective, like ``get_batch()``: every + rank calls it the same number of times, in the same order.""" + idx = np.asarray(rows, dtype=np.int64).reshape(-1) + return { + self._key(j): self._batch_value(self._read_field(j, idx, out), j) + for j in self._field_ids(fields) + } + def __len__(self): return self.total_ns @@ -260,16 +352,17 @@ def get(self, idx): def __getitem__(self, idx): return self.get(idx) - def __getitems__(self, indices): + def __getitems__(self, indices, out=None): """A whole batch: one get_batch() per field (DDSTORE_BATCH_GET=0: - one get() per sample). Called by DataLoader and ThreadDataLoader.""" - if not self._batch_get: + one get() per sample). Called by DataLoader and ThreadDataLoader. + `out`: buffers from ``alloc()``; the samples are then views into + them.""" + if not self._batch_get and out is None: return [self.get(i) for i in indices] idx = np.asarray(indices, dtype=np.int64) columns = [] - for j, var in enumerate(self._var): - buf = self._alloc(len(idx), j) - self.ddstore.get_batch(var, buf, idx) + for j in range(len(self._var)): + buf = self._read_field(j, idx, out) columns.append([self._value(buf[i], j) for i in range(len(idx))]) return [self._rebuild([col[i] for col in columns]) for i in range(len(idx))] @@ -522,6 +615,88 @@ def __init__(self, name, handshake_dir=None, n_core=None, device=None): self.ddstore.join(var) +class WindowedDataset(Dataset): + """Samples made of several stored rows of `ds` (a ``DistDataset`` or + ``DistDatasetReader``): time windows, clips, sequences. Each stored row + is held once, however many windows use it. + + Sample ``i`` is rows ``s, s + dilation, ..., s + (window - 1) * dilation`` + with ``s = starts[i]`` if `starts` is given, else ``s = i * stride``. + Use `starts` to keep only windows that don't cross a boundary between + trajectories or files. Each field comes back stacked, shaped + ``(window, *field_shape)``, in the structure of `ds`'s samples, or as a + dict of the selected `fields`. + + ``__getitems__`` reads a whole batch of windows with one + ``read_rows()`` (one ``get_batch()`` per field), so with ``method=0`` the + same collective rule applies as for ``ds``. + """ + + def __init__(self, ds, window, stride=1, dilation=1, starts=None, fields=None): + if window < 1 or stride < 1 or dilation < 1: + raise ValueError("window, stride and dilation must be >= 1") + self.ds = ds + self.window = int(window) + self.dilation = int(dilation) + self.fields = None if fields is None else list(fields) + self._offsets = np.arange(self.window, dtype=np.int64) * self.dilation + span = int(self._offsets[-1]) + 1 + if starts is not None: + self.starts = np.asarray(starts, dtype=np.int64).reshape(-1) + bad = (self.starts < 0) | (self.starts + span > len(ds)) + if bad.any(): + raise IndexError( + f"start {int(self.starts[bad][0])} + window span {span} is out " + f"of range for {len(ds)} rows" + ) + else: + n = (len(ds) - span) // int(stride) + 1 if len(ds) >= span else 0 + self.starts = np.arange(n, dtype=np.int64) * int(stride) + + def __len__(self): + return len(self.starts) + + def _rows(self, indices): + return (self.starts[np.asarray(indices, dtype=np.int64)][:, None] + + self._offsets).reshape(-1) + + def _sample(self, values): + if self.fields is not None: + return values + ds = self.ds + return ds._rebuild([values[ds._key(j)] for j in range(len(ds._var))]) + + def __getitem__(self, i): + if not -len(self) <= i < len(self): + raise IndexError(f"window {i} out of range ({len(self)} windows)") + return self._sample(self.ds.read_rows(self._rows([i]), self.fields)) + + def __getitems__(self, indices): + cols = self.ds.read_rows(self._rows(indices), self.fields) + w = self.window + return [ + self._sample({k: v[b * w : (b + 1) * w] for k, v in cols.items()}) + for b in range(len(indices)) + ] + + +def row_of(concat, source, index): + """The row of sample `index` of source `source` in + ``torch.utils.data.ConcatDataset`` `concat`, i.e. its index in a + ``DistDataset`` built over `concat`. Use it to map (file, trajectory, + step) to a row when several sources share one store.""" + sizes = concat.cumulative_sizes + if not 0 <= source < len(sizes): + raise IndexError(f"source {source} out of range ({len(sizes)} sources)") + first = sizes[source - 1] if source > 0 else 0 + if not 0 <= index < sizes[source] - first: + raise IndexError( + f"index {index} out of range for source {source} " + f"({sizes[source] - first} samples)" + ) + return first + index + + # --------------------------------------------------------------------------- # loader # --------------------------------------------------------------------------- diff --git a/test/test_torch.py b/test/test_torch.py index 0e5b88d..957b331 100644 --- a/test/test_torch.py +++ b/test/test_torch.py @@ -20,6 +20,8 @@ DistDataset, DistDatasetReader, ThreadDataLoader, + WindowedDataset, + row_of, ) # noqa: E402 HAVE_CXI = bool(glob.glob("/dev/cxi*")) and "SLURM_STEP_ID" in os.environ @@ -267,6 +269,128 @@ def __getitems__(self, idx): assert all_ok(comm, ok), f"{src.batches} batches fetched, want {want}" +def stacked(src, rows, key): + """Field `key` of src[r] for r in rows, stacked as read_rows returns it.""" + vals = [src[int(r)] if key == 0 and not isinstance(src[0], (tuple, list, dict)) + else src[int(r)][key] for r in rows] + if isinstance(vals[0], torch.Tensor): + return torch.stack(vals) + return np.stack([np.asarray(v) for v in vals]) + + +def same_values(a, b): + a = a.cpu().numpy() if isinstance(a, torch.Tensor) else np.asarray(a) + b = b.cpu().numpy() if isinstance(b, torch.Tensor) else np.asarray(b) + return a.dtype == b.dtype and a.shape == b.shape and np.array_equal(a, b) + + +def shares(value, buf): + if isinstance(buf, torch.Tensor): + return value.data_ptr() == buf.data_ptr() + return np.shares_memory(np.asarray(value), buf) + + +@pytest.mark.parametrize("method", METHODS) +@pytest.mark.parametrize("source_cls", [TupleSource, DictSource, SingleSource]) +def test_read_rows_and_out(comm, monkeypatch, method, source_cls): + """read_rows (all fields / a subset), alloc() buffers reused across + reads, __getitems__(out=). Same number of calls on every rank (method 0 + is collective).""" + src = source_cls() + ds = make(comm, monkeypatch, src, method) + keys = [ds._key(j) for j in range(len(ds._var))] + rng = np.random.default_rng(comm.Get_rank()) + rows = rng.integers(0, N, size=10) # any order, repeats + got = ds.read_rows(rows) + ok = list(got) == keys + ok &= all(same_values(got[k], stacked(src, rows, k)) for k in keys) + sub = ds.read_rows(rows[:3], fields=keys[-1:]) + ok &= list(sub) == keys[-1:] and same_values(sub[keys[-1]], stacked(src, rows[:3], keys[-1])) + with pytest.raises(KeyError): + ds.read_rows(rows, fields=["nope"]) + + bufs = ds.alloc(16) + miss0 = None + if os.environ.get("DDSTORE_PROFILE", "0") not in ("", "0") and method != 0: + miss0 = [ds.ddstore.get_profile(v)["mr_miss"] for v in ds._var] + for it in range(3): # the same buffers, different rows each time + r = rng.integers(0, N, size=12) + got = ds.read_rows(r, out=bufs) + ok &= all(same_values(got[k], stacked(src, r, k)) for k in keys) + ok &= all(shares(got[k], bufs[k]) for k in keys) + batch = ds.__getitems__(r[:5], out=bufs) + ok &= all(same(b, src[int(i)]) for b, i in zip(batch, r[:5])) + if miss0 is not None: + ok &= [ds.ddstore.get_profile(v)["mr_miss"] for v in ds._var] == miss0 + with pytest.raises(ValueError): + ds.read_rows(np.arange(17) % N, out=bufs) # more rows than the buffers + ds.release(bufs) + finish(comm, ds) + assert all_ok(comm, ok) + + +@pytest.mark.parametrize("method", METHODS) +@pytest.mark.parametrize("loader", ["DataLoader", "ThreadDataLoader"]) +def test_windowed_dataset(comm, monkeypatch, method, loader): + """Windows with stride and dilation, explicit starts, a field subset, and + whole batches through a loader (one read_rows per batch).""" + src = TupleSource() + ds = make(comm, monkeypatch, src, method) + nf = len(ds._var) + + def window(rows): + return tuple(stacked(src, rows, j) for j in range(nf)) + + wd = WindowedDataset(ds, window=3, stride=2, dilation=2) # rows s, s+2, s+4 + ok = len(wd) == (N - 5) // 2 + 1 + ok &= same(wd[4], window([8, 10, 12])) and same(wd[-1], window([32, 34, 36])) + cls = DataLoader if loader == "DataLoader" else ThreadDataLoader + kw = {} if cls is DataLoader else {"num_workers": 1 if method == 0 else 2} + got = list(cls(wd, batch_size=4, **kw)) + ref = [ + torch.utils.data.default_collate([window([s, s + 2, s + 4]) for s in range(b, min(b + 8, len(wd) * 2), 2)]) + for b in range(0, len(wd) * 2, 8) + ] + ok &= len(got) == len(ref) and all(same(g, r) for g, r in zip(got, ref)) + + starts = [0, 10, 30] # e.g. one window per trajectory + ws = WindowedDataset(ds, window=2, starts=starts, fields=[0, 2]) + ok &= len(ws) == 3 + w = ws[1] + ok &= list(w) == [0, 2] and same_values(w[2], stacked(src, [10, 11], 2)) + with pytest.raises(IndexError): + WindowedDataset(ds, window=2, starts=[N - 1]) + finish(comm, ds) + assert all_ok(comm, ok) + + +class Offset(Dataset): + def __init__(self, n, base): + self.n, self.base = n, base + + def __len__(self): + return self.n + + def __getitem__(self, i): + return np.full((3,), self.base + i, dtype=np.int64) + + +def test_row_of_concat(comm, monkeypatch): + """Several sources in one store: row_of maps (source, index) to the row.""" + parts = [Offset(11, 0), Offset(7, 1000), Offset(19, 2000)] + concat = torch.utils.data.ConcatDataset(parts) + ds = make(comm, monkeypatch, concat, 0) + ok = row_of(concat, 0, 3) == 3 and row_of(concat, 2, 0) == 18 + rows = [row_of(concat, s, i) for s, i in [(1, 6), (2, 18), (0, 0)]] + got = ds.read_rows(rows)[0] + ok &= same_values(got, np.stack([parts[1][6], parts[2][18], parts[0][0]])) + for bad in [(3, 0), (1, 7), (0, -1)]: + with pytest.raises(IndexError): + row_of(concat, *bad) + finish(comm, ds) + assert all_ok(comm, ok) + + def test_per_sample_fallback(comm, monkeypatch): monkeypatch.setenv("DDSTORE_BATCH_GET", "0") src = TupleSource() From 50d31d3b5096f9aeaae457add259419d5e8d7000 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Mon, 5 Oct 2026 09:00:49 -0700 Subject: [PATCH 54/56] ThreadDataLoader: reuse_buffers, a loader-owned pool of registered buffers reuse_buffers=True reads every batch into one of num_workers buffer sets from dataset.alloc(batch_size), registered once, instead of fresh buffers registered on every read. A worker reads and collates its batch, then returns the set, so the collate must copy: requires batch_size and the default collate_fn, or collate_copies=True for a custom collate that copies; other setups (batch_size=None, a non-copying collate, a dataset without alloc()) raise. The pool belongs to the loader: close() (or deletion) waits for running fetches and unregisters it, so new loaders (e.g. one per validation) don't accumulate pinned memory. GPU buffers are safe because get_batch() synchronizes the device before reading. Tests: test_thread_loader_reuse_buffers (methods 0/1: two epochs equal the plain source, no registration per read under DDSTORE_PROFILE=1, close() unregisters, refusals, copying custom collate) and test_gpu_reuse_buffers. README: ThreadDataLoader entry, tests table. Verified on Perlmutter: test_torch (8 ranks with DDSTORE_PROFILE=1, 4 ranks on 1 node) and test_get_batch pass. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EasT2JuGMcWVMaVcZXAYxi --- README.md | 5 +-- src/pyddstore/torch.py | 78 +++++++++++++++++++++++++++++++++++++++--- test/test_torch.py | 67 ++++++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 00b9601..d7064bf 100644 --- a/README.md +++ b/README.md @@ -385,7 +385,8 @@ pairs = WindowedDataset(frames, window=3, dilation=dt, starts=starts) # (t, t+ ``` - **`DistDatasetReader(name, handshake_dir=None, n_core=None, device=None)`**: the same dataset read by a separate `method=2` extra job; it learns the fields from a `{name}.meta.json` file the core group writes next to the handshake records. -- **`ThreadDataLoader(dataset, **DataLoader args)`**: a `DataLoader` whose workers are threads, not forked processes, so it is safe with MPI and GPU buffers. Each batch is fetched, collated and optionally pinned in a worker thread; random draws match `DataLoader`'s. As with `DataLoader`, `iter(loader)` returns a separate iterator for one epoch (`list(it)` or `islice(it, …)` after `next(it)` continue the epoch; iterators over one loader are independent), and while the training step holds a batch, `num_workers * prefetch_factor` more are being fetched, so that many plus one are in memory. One or two workers are enough: reads on one variable are serialized by its lock, and one batched read already keeps the network busy. `DDSTORE_AFFINITY_WIDTH` / `DDSTORE_AFFINITY_OFFSET` pin worker threads to CPUs. +- **`ThreadDataLoader(dataset, **DataLoader args)`**: a `DataLoader` whose workers are threads, not forked processes, so it is safe with MPI and GPU buffers. Each batch is fetched, collated and optionally pinned in a worker thread; random draws match `DataLoader`'s. As with `DataLoader`, `iter(loader)` returns a separate iterator for one epoch (`list(it)` or `islice(it, …)` after `next(it)` continue the epoch; iterators over one loader are independent), and while the training step holds a batch, `num_workers * prefetch_factor` more are being fetched, so that many plus one are in memory. One or two workers are enough: reads on one variable are serialized by its lock, and one batched read already keeps the network busy. + - `reuse_buffers=True` reads every batch into one of a fixed pool of `num_workers` buffer sets from `dataset.alloc(batch_size)`, registered once, instead of fresh buffers registered on every read (for large rows, registration can cost more than the transfer). A worker reads and collates its batch, then returns the set, so the collate must copy: it needs `batch_size` and the default `collate_fn`, or `collate_copies=True` to declare that your `collate_fn` copies; other setups raise. `loader.close()` (or deleting the loader) waits for running fetches and unregisters the pool. `DDSTORE_AFFINITY_WIDTH` / `DDSTORE_AFFINITY_OFFSET` pin worker threads to CPUs. [examples/vae/vae-ddp.py](examples/vae/vae-ddp.py) trains a VAE with DDP on top of it: @@ -507,7 +508,7 @@ DDSTORE_FABRIC=cxi mpirun -n 2 python -m pytest test/test_gpu_rdma.py -v | `test/test_multirank.py` | 2 (4 recommended) | Remote reads, shard boundaries, multiple variables, `ddstore_width` grouping | | `test/test_gpu_rdma.py` | 2 | GPU-resident `add()`/`get()` in both directions, both libfabric methods, negative/error cases | | `test/test_get_batch.py` | 2 (4 recommended) | `get_batch()`: shuffled indices across ranks with repeats, single row, dtypes, error recovery, GPU destination, concurrent threads, registered destination buffers, wide rows (with `DDSTORE_MAX_READ_BYTES=4096` they are read in pieces); method 0, plus method 1 over `cxi` inside a Slurm step | -| `test/test_torch.py` | 2 (4 recommended) | `pyddstore.torch`: tuple/dict/single samples of every field kind, numpy records, loaders vs the plain source, `ThreadDataLoader` iterators and prefetch depth, `read_rows`/`alloc` buffers, `WindowedDataset`, `row_of`, chunked loading, error handling, `ddstore_width`, GPU placement, `DistDatasetReader` | +| `test/test_torch.py` | 2 (4 recommended) | `pyddstore.torch`: tuple/dict/single samples of every field kind, numpy records, loaders vs the plain source, `ThreadDataLoader` iterators and prefetch depth, `read_rows`/`alloc` buffers, `reuse_buffers` (also with GPU buffers), `WindowedDataset`, `row_of`, chunked loading, error handling, `ddstore_width`, GPU placement, `DistDatasetReader` | ### Integration scripts diff --git a/src/pyddstore/torch.py b/src/pyddstore/torch.py index 760cad6..b1d2997 100644 --- a/src/pyddstore/torch.py +++ b/src/pyddstore/torch.py @@ -715,11 +715,49 @@ class ThreadDataLoader(DataLoader): match ``DataLoader``'s. ``DDSTORE_AFFINITY_WIDTH`` / ``DDSTORE_AFFINITY_OFFSET`` pin worker thread *i* to CPUs ``[offset + i*width, offset + (i+1)*width)`` of the process's affinity. + + ``reuse_buffers=True`` (dataset with ``alloc()``, e.g. ``DistDataset``): + read every batch into one of a fixed pool of ``num_workers`` buffer sets + from ``dataset.alloc(batch_size)``, registered once, instead of fresh + buffers that are registered on every read. A worker takes a set, reads + and collates the batch, and returns the set, so the collate must copy: + needs ``batch_size`` and the default ``collate_fn``, or + ``collate_copies=True`` to declare that a custom ``collate_fn`` copies. + ``close()`` (or deleting the loader) waits for running fetches and + unregisters the pool. """ - def __init__(self, dataset, **kwargs): + def __init__(self, dataset, reuse_buffers=False, collate_copies=False, **kwargs): super().__init__(dataset, **kwargs) + # Fixed pool of registered read buffers, one set per worker thread, + # owned by this loader (see reuse_buffers in the class docstring). + self._pool, self._pool_sets = None, [] + if reuse_buffers: + if not hasattr(dataset, "alloc"): + raise TypeError( + "reuse_buffers needs a dataset with alloc() (DistDataset, " + "DistDatasetReader)" + ) + if self.batch_size is None: + raise ValueError( + "reuse_buffers needs batch_size: without auto-collation the " + "batch is not copied out of the reused buffer" + ) + if ( + self.collate_fn is not torch.utils.data.default_collate + and not collate_copies + ): + raise ValueError( + "reuse_buffers with a custom collate_fn: pass collate_copies=True " + "if it copies the samples (the buffer is reused right after it)" + ) + self._pool = queue.Queue() + for _ in range(self.num_workers or 1): + bufs = dataset.alloc(self.batch_size) + self._pool_sets.append(bufs) + self._pool.put(bufs) + # Persistent across epochs -- recreating the pool in every __iter__() # would leak OS threads since the old pool is never shut down. self._counter = mp.Value("i", 0) @@ -762,8 +800,27 @@ def worker_init(counter): @staticmethod def fetch( - dataset, ibatch, index, collate_fn=None, pin_memory=False, auto_collation=True + dataset, + ibatch, + index, + collate_fn=None, + pin_memory=False, + auto_collation=True, + pool=None, ): + if pool is not None: + # Read into one of the loader's registered buffer sets; collate + # copies the batch out, then the set goes back to the pool. For a + # GPU buffer, get_batch() synchronizes the device before reading, + # so the collate's copy has finished before the set is refilled. + bufs = pool.get() + try: + batch = collate_fn(dataset.__getitems__(index, out=bufs)) + finally: + pool.put(bufs) + if pin_memory: + batch = torch.utils.data._utils.pin_memory.pin_memory(batch) + return (ibatch, batch) # Collate here, in the worker, before pinning: pinning per-sample # tensors and collating afterwards would just torch.stack them into # a new, unpinned tensor. Use the dataset's whole-batch fetch when it @@ -788,10 +845,22 @@ def __iter__(self): loader don't interfere, as with ``DataLoader``.""" return _ThreadLoaderIter(self) - def __del__(self): + def close(self): + """Stop the worker threads; with ``reuse_buffers``, wait for running + fetches first, then unregister the buffer pool. Called on deletion.""" executor = getattr(self, "executor", None) + sets = getattr(self, "_pool_sets", []) if executor is not None: - executor.shutdown(wait=False) + executor.shutdown(wait=bool(sets), cancel_futures=True) + for bufs in sets: + try: + self.dataset.release(bufs) + except (ValueError, KeyError): + pass # the store was freed first: nothing left to unregister + self._pool_sets = [] + + def __del__(self): + self.close() class _ThreadLoaderIter: @@ -844,6 +913,7 @@ def _refill(self): collate_fn=loader.collate_fn, pin_memory=loader.pin_memory, auto_collation=loader._auto_collation, + pool=loader._pool, ) self.fs.put(future) self._next_batch_i += 1 diff --git a/test/test_torch.py b/test/test_torch.py index 957b331..350850c 100644 --- a/test/test_torch.py +++ b/test/test_torch.py @@ -391,6 +391,52 @@ def test_row_of_concat(comm, monkeypatch): assert all_ok(comm, ok) +@pytest.mark.parametrize("method", METHODS) +def test_thread_loader_reuse_buffers(comm, monkeypatch, method): + """reuse_buffers: two epochs equal the plain source, no registration per + read (method 1, DDSTORE_PROFILE=1), setups that can't copy are refused, + and close() unregisters the pool.""" + src = TupleSource() + ds = make(comm, monkeypatch, src, method) + nw = 1 if method == 0 else 2 + ref = list(DataLoader(src, batch_size=8)) # last batch is short (37 = 4*8 + 5) + loader = ThreadDataLoader(ds, batch_size=8, num_workers=nw, reuse_buffers=True) + ok = len(loader._pool_sets) == nw + prof = os.environ.get("DDSTORE_PROFILE", "0") not in ("", "0") and method != 0 + miss0 = [ds.ddstore.get_profile(v)["mr_miss"] for v in ds._var] if prof else None + for _ in range(2): + got = list(loader) + ok &= len(got) == len(ref) and all(same(g, r) for g, r in zip(got, ref)) + if prof: + ok &= [ds.ddstore.get_profile(v)["mr_miss"] for v in ds._var] == miss0 + sets = loader._pool_sets + loader.close() + if method != 0: # unregistered: releasing again raises + for bufs in sets: + with pytest.raises(ValueError): + ds.release(bufs) + + with pytest.raises(ValueError): # no auto-collation: nothing copies + ThreadDataLoader(ds, batch_size=None, reuse_buffers=True) + with pytest.raises(ValueError): # custom collate not declared as copying + ThreadDataLoader(ds, batch_size=8, collate_fn=lambda b: b, reuse_buffers=True) + with pytest.raises(TypeError): # dataset without alloc() + ThreadDataLoader(src, batch_size=8, reuse_buffers=True) + copying = ThreadDataLoader( + ds, batch_size=8, num_workers=nw, reuse_buffers=True, collate_copies=True, + collate_fn=lambda b: [tuple(x.clone() if isinstance(x, torch.Tensor) else + np.array(x, copy=True) for x in s) for s in b], + ) + got = [s for b in copying for s in b] + ok &= len(got) == N and all( + same_values(g[0], src[i][0]) and same_values(g[2], src[i][2]) + for i, g in enumerate(got) + ) + copying.close() + finish(comm, ds) + assert all_ok(comm, ok) + + def test_per_sample_fallback(comm, monkeypatch): monkeypatch.setenv("DDSTORE_BATCH_GET", "0") src = TupleSource() @@ -470,6 +516,27 @@ def test_gpu_device_and_add_device(comm, monkeypatch): assert all_ok(comm, ok) +@pytest.mark.skipif( + not (HAVE_CXI and HAVE_GPU and FABRIC == "cxi"), + reason="requires the cxi provider and a GPU", +) +def test_gpu_reuse_buffers(comm, monkeypatch): + """reuse_buffers with GPU read buffers: the collate's GPU copy must finish + before a buffer is refilled (several epochs, 2 threads, small pool).""" + src = TupleSource() + ds = make(comm, monkeypatch, src, 1, device="cuda") + ref = list(DataLoader(src, batch_size=4)) + loader = ThreadDataLoader(ds, batch_size=4, num_workers=2, reuse_buffers=True) + ok = True + for _ in range(3): + got = list(loader) + ok &= got[0][0].is_cuda and len(got) == len(ref) + ok &= all(same(g, r) for g, r in zip(got, ref)) + loader.close() + finish(comm, ds) + assert all_ok(comm, ok) + + @pytest.mark.skipif(not HAVE_CXI, reason="no CXI device") def test_method2_reader(comm, monkeypatch, tmp_path): monkeypatch.setenv("DDSTORE_FABRIC", FABRIC) From 3699e570f4321358a143ed1588b4fecc3438fdff Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Mon, 5 Oct 2026 09:04:49 -0700 Subject: [PATCH 55/56] DistDataset: encode/decode hooks and fields= selection For samples that can't be stored as they are (strings, labels, metadata objects, data shared by a group of samples): - encode(sample) runs on every source sample before it is stored and returns what to store. - decode(stored, index) runs on every sample read (ds[i], __getitems__, so in loaders too) and rebuilds the full sample, e.g. decoding ids or adding per-group data looked up by a stored id or the index. Runs on the reading rank. DistDatasetReader takes decode too. - fields=[...] keeps only those keys / tuple positions, in order; shorthand for an encode, not combinable with one. - read_rows() and WindowedDataset stay row-level (no decode). Tests: test_encode_decode (methods 0/1, string labels and a per-group object through ds[i], __getitems__ and ThreadDataLoader; read_rows returns stored ids), test_fields_selection, test_method2_reader_decode. README: DistDataset/DistDatasetReader entries, tests table. Verified on Perlmutter: test_torch (8 ranks with DDSTORE_PROFILE=1, 4 ranks on 1 node) and test_get_batch pass. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EasT2JuGMcWVMaVcZXAYxi --- README.md | 15 ++++-- src/pyddstore/torch.py | 71 ++++++++++++++++++++++++++-- test/test_torch.py | 104 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d7064bf..dabfce9 100644 --- a/README.md +++ b/README.md @@ -362,10 +362,19 @@ for x, y in loader: ... ``` -- **`DistDataset(source, name, comm=None, ddstore_width=None, device=None, add_device=None, method=None, handshake_dir=None, chunk_size=None)`**: each rank loads its contiguous share of `source` (anything with `len()` and `[i]`) into DDStore; every rank can then read every sample. Samples keep the source's structure (a tensor, numpy array or number; a tuple or list of them; or a dict of them), with each field's shape and dtype. Fields must have the same shape and dtype in every sample; supported dtypes are bool, uint8, int32, int64, float32 and float64. A field (or the whole sample) may also be a numpy structured record (`np.void`, a structured `ndarray`, or `np.recarray`) of any field types: it is stored as raw bytes and comes back as the same kind of object with the same layout (`default_collate` can't batch records, so pass a `collate_fn`). `ds.shapes` / `ds.dtypes` describe the fields, `ds.ddstore` is the underlying `PyDDStore`. +- **`DistDataset(source, name, comm=None, ddstore_width=None, device=None, add_device=None, method=None, handshake_dir=None, chunk_size=None, encode=None, decode=None, fields=None)`**: each rank loads its contiguous share of `source` (anything with `len()` and `[i]`) into DDStore; every rank can then read every sample. Samples keep the source's structure (a tensor, numpy array or number; a tuple or list of them; or a dict of them), with each field's shape and dtype. Fields must have the same shape and dtype in every sample; supported dtypes are bool, uint8, int32, int64, float32 and float64. A field (or the whole sample) may also be a numpy structured record (`np.void`, a structured `ndarray`, or `np.recarray`) of any field types: it is stored as raw bytes and comes back as the same kind of object with the same layout (`default_collate` can't batch records, so pass a `collate_fn`). `ds.shapes` / `ds.dtypes` describe the fields, `ds.ddstore` is the underlying `PyDDStore`. - `method` (default `DDSTORE_METHOD` or 0) picks the backend; `ddstore_width` splits `comm` into independent stores (see [Partitioned usage](#partitioned--sub-communicator-usage)). - `device` puts tensor fields of read samples on a GPU and `add_device` keeps each rank's share there ([GPUDirect](#gpudirect-rdma-gpu-resident-buffers)). - `chunk_size` loads each rank's share that many samples at a time, writing each chunk into the store before reading the next: peak memory is about the share plus one chunk, instead of about three times the share (400 MiB share: 461 vs 1202 MiB). Host storage only (not with `add_device`). + - `encode` / `decode` / `fields` handle samples that can't be stored as they are (strings, labels, metadata objects, data shared by a group of samples). `encode(sample)` runs on every source sample before it is stored and returns what to store; `decode(stored, index)` runs on every sample read (`ds[i]`, `__getitems__`, so also in loaders) and rebuilds the full sample, e.g. adding constants or looking up tables by index or by a stored id. `decode` runs on the reading rank, so anything it looks up must exist on every rank. `fields=[...]` keeps only those keys (dict samples) or positions (tuple/list samples); it can't be combined with `encode`. `read_rows()` and `WindowedDataset` return stored rows without `decode`. `DistDatasetReader` takes `decode` too. + +```python +LABELS = ["cat", "dog", "owl"] +ds = DistDataset(src, "pets", comm, + encode=lambda s: {"x": s["x"], "label": LABELS.index(s["label"]), "group": s["group"]}, + decode=lambda d, i: {**d, "label": LABELS[d["label"]], "meta": GROUP_INFO[d["group"]]}) +``` + - **Batched by default**: `__getitems__` reads a whole batch with one [`get_batch()`](#get_batchname-arr-indices) per field, which `DataLoader` and `ThreadDataLoader` call automatically; `DDSTORE_BATCH_GET=0` reads one sample at a time. With `method=0` batched reads are collective, so every rank must iterate the same number of batches from one thread (`DistributedSampler` does). - **Reading rows and reusing buffers** (`DistDataset` and `DistDatasetReader`): - `ds.read_rows(rows, fields=None, out=None)` reads stored rows `rows` (any order, repeats allowed) of the selected fields (default all), one `get_batch()` per field, and returns a dict of values shaped `(len(rows), *field_shape)`. Keys are the sample's: dict keys, tuple/list positions, or `0` for a single value. With `method=0` it is collective, like `get_batch()`. @@ -384,7 +393,7 @@ starts = [row_of(files, k, t) for k, f in enumerate(paths) # windows insid pairs = WindowedDataset(frames, window=3, dilation=dt, starts=starts) # (t, t+dt, t+2dt) ``` -- **`DistDatasetReader(name, handshake_dir=None, n_core=None, device=None)`**: the same dataset read by a separate `method=2` extra job; it learns the fields from a `{name}.meta.json` file the core group writes next to the handshake records. +- **`DistDatasetReader(name, handshake_dir=None, n_core=None, device=None, decode=None)`**: the same dataset read by a separate `method=2` extra job; it learns the fields from a `{name}.meta.json` file the core group writes next to the handshake records. - **`ThreadDataLoader(dataset, **DataLoader args)`**: a `DataLoader` whose workers are threads, not forked processes, so it is safe with MPI and GPU buffers. Each batch is fetched, collated and optionally pinned in a worker thread; random draws match `DataLoader`'s. As with `DataLoader`, `iter(loader)` returns a separate iterator for one epoch (`list(it)` or `islice(it, …)` after `next(it)` continue the epoch; iterators over one loader are independent), and while the training step holds a batch, `num_workers * prefetch_factor` more are being fetched, so that many plus one are in memory. One or two workers are enough: reads on one variable are serialized by its lock, and one batched read already keeps the network busy. - `reuse_buffers=True` reads every batch into one of a fixed pool of `num_workers` buffer sets from `dataset.alloc(batch_size)`, registered once, instead of fresh buffers registered on every read (for large rows, registration can cost more than the transfer). A worker reads and collates its batch, then returns the set, so the collate must copy: it needs `batch_size` and the default `collate_fn`, or `collate_copies=True` to declare that your `collate_fn` copies; other setups raise. `loader.close()` (or deleting the loader) waits for running fetches and unregisters the pool. `DDSTORE_AFFINITY_WIDTH` / `DDSTORE_AFFINITY_OFFSET` pin worker threads to CPUs. @@ -508,7 +517,7 @@ DDSTORE_FABRIC=cxi mpirun -n 2 python -m pytest test/test_gpu_rdma.py -v | `test/test_multirank.py` | 2 (4 recommended) | Remote reads, shard boundaries, multiple variables, `ddstore_width` grouping | | `test/test_gpu_rdma.py` | 2 | GPU-resident `add()`/`get()` in both directions, both libfabric methods, negative/error cases | | `test/test_get_batch.py` | 2 (4 recommended) | `get_batch()`: shuffled indices across ranks with repeats, single row, dtypes, error recovery, GPU destination, concurrent threads, registered destination buffers, wide rows (with `DDSTORE_MAX_READ_BYTES=4096` they are read in pieces); method 0, plus method 1 over `cxi` inside a Slurm step | -| `test/test_torch.py` | 2 (4 recommended) | `pyddstore.torch`: tuple/dict/single samples of every field kind, numpy records, loaders vs the plain source, `ThreadDataLoader` iterators and prefetch depth, `read_rows`/`alloc` buffers, `reuse_buffers` (also with GPU buffers), `WindowedDataset`, `row_of`, chunked loading, error handling, `ddstore_width`, GPU placement, `DistDatasetReader` | +| `test/test_torch.py` | 2 (4 recommended) | `pyddstore.torch`: tuple/dict/single samples of every field kind, numpy records, loaders vs the plain source, `ThreadDataLoader` iterators and prefetch depth, `read_rows`/`alloc` buffers, `reuse_buffers` (also with GPU buffers), `WindowedDataset`, `row_of`, `encode`/`decode`/`fields`, chunked loading, error handling, `ddstore_width`, GPU placement, `DistDatasetReader` | ### Integration scripts diff --git a/src/pyddstore/torch.py b/src/pyddstore/torch.py index b1d2997..33f9e85 100644 --- a/src/pyddstore/torch.py +++ b/src/pyddstore/torch.py @@ -184,6 +184,36 @@ def _to_numpy_row(value): # --------------------------------------------------------------------------- +def _selector(fields): + """encode() keeping `fields` of a dict sample, or positions of a + tuple/list sample, in that order.""" + + def select(sample): + try: + if isinstance(sample, dict): + return {k: sample[k] for k in fields} + if isinstance(sample, (tuple, list)): + return type(sample)(sample[k] for k in fields) + except (KeyError, IndexError, TypeError) as exc: + raise ValueError(f"fields={fields}: not in the sample ({exc!r})") from None + raise ValueError("fields= needs dict, tuple or list samples") + + return select + + +class _Encoded(Dataset): + """`source` with `encode` applied to every sample.""" + + def __init__(self, source, encode): + self.source, self.encode = source, encode + + def __len__(self): + return len(self.source) + + def __getitem__(self, i): + return self.encode(self.source[i]) + + class _StoreDataset(Dataset): """Reads samples described by `self.schema` from `self.ddstore`.""" @@ -202,6 +232,8 @@ def _setup_fields(self, schema, name, device): for f, rec in zip(schema["fields"], self._record) ] self._batch_get = os.environ.get("DDSTORE_BATCH_GET", "1") != "0" + if not hasattr(self, "_decode"): + self._decode = None # -- shapes as the user sees them (same structure as a sample) -------- @property @@ -327,6 +359,8 @@ def read_rows(self, rows, fields=None, out=None): arrays. `out`: buffers from ``alloc()`` (at least ``len(rows)`` rows); the values are then views into them. + Rows come back as stored: ``decode`` is not applied. + With ``method=0`` this is collective, like ``get_batch()``: every rank calls it the same number of times, in the same order.""" idx = np.asarray(rows, dtype=np.int64).reshape(-1) @@ -347,7 +381,8 @@ def get(self, idx): buf = self._alloc(1, j) self.ddstore.get(var, buf, int(idx)) values.append(self._value(buf[0], j)) - return self._rebuild(values) + sample = self._rebuild(values) + return sample if self._decode is None else self._decode(sample, int(idx)) def __getitem__(self, idx): return self.get(idx) @@ -364,7 +399,10 @@ def __getitems__(self, indices, out=None): for j in range(len(self._var)): buf = self._read_field(j, idx, out) columns.append([self._value(buf[i], j) for i in range(len(idx))]) - return [self._rebuild([col[i] for col in columns]) for i in range(len(idx))] + samples = [self._rebuild([col[i] for col in columns]) for i in range(len(idx))] + if self._decode is not None: + samples = [self._decode(sm, int(i)) for sm, i in zip(samples, idx)] + return samples def _rows(values, dtype): @@ -397,6 +435,19 @@ class DistDataset(_StoreDataset): only one chunk is held in memory besides the store (default: read the whole share, then add it). Host storage only (no ``add_device`` for tensor fields). + encode: ``encode(sample) -> sample`` applied to every source sample + before it is stored: pick and convert what to store (e.g. drop + metadata objects, turn a label string into an id). Its result + must meet the rules above. + decode: ``decode(stored, index) -> sample`` applied to every sample + read (``ds[i]``, ``__getitems__``), with its index: add back + what wasn't stored (constants, tables looked up by index or by + a stored id). Runs on the reading rank, in the loader thread; + anything it looks up must be on every rank. Not applied by + ``read_rows()`` or ``WindowedDataset`` (row-level reads). + fields: store only these keys (dict samples) or positions + (tuple/list samples), in this order: shorthand for an + ``encode`` that selects them. Not together with ``encode``. ``ds[i]`` returns a sample with the source's structure: tensors stay tensors (on ``device`` if given), numpy arrays stay arrays, numbers stay @@ -417,10 +468,21 @@ def __init__( method=None, handshake_dir=None, chunk_size=None, + encode=None, + decode=None, + fields=None, ): super().__init__() from mpi4py import MPI + if fields is not None: + if encode is not None: + raise ValueError("pass either fields or encode, not both") + encode = _selector(list(fields)) + if encode is not None: + source = _Encoded(source, encode) + self._decode = decode + from ._core import PyDDStore self.comm = comm if comm is not None else MPI.COMM_WORLD @@ -580,13 +642,16 @@ class DistDatasetReader(_StoreDataset): ``./ddstore_hs``). n_core: number of core ranks (default ``DDSTORE_N_CORE``). device: put tensor fields of read samples on this device. + decode: as for ``DistDataset`` (the core group's ``encode`` already + ran before storing). Waits up to ``DDSTORE_HANDSHAKE_TIMEOUT_S`` (default 300 s) for the core group to publish. """ - def __init__(self, name, handshake_dir=None, n_core=None, device=None): + def __init__(self, name, handshake_dir=None, n_core=None, device=None, decode=None): super().__init__() + self._decode = decode from ._core import PyDDStore hs = _handshake_dir(handshake_dir) diff --git a/test/test_torch.py b/test/test_torch.py index 350850c..97e844c 100644 --- a/test/test_torch.py +++ b/test/test_torch.py @@ -437,6 +437,90 @@ def test_thread_loader_reuse_buffers(comm, monkeypatch, method): assert all_ok(comm, ok) +LABELS = ["cat", "dog", "owl"] +GROUP_INFO = {g: {"name": f"group-{g}", "scale": 1.5 * g} for g in range(4)} + + +class Labeled(Dataset): + """Samples with a string label and an object that can't be stored.""" + + def __len__(self): + return N + + def __getitem__(self, i): + return { + "x": torch.full((3,), float(i)), + "label": LABELS[i % 3], + "group": i // 10, + "meta": GROUP_INFO[i // 10], # per-group object + } + + +def labeled_encode(s): + return {"x": s["x"], "label": LABELS.index(s["label"]), "group": s["group"]} + + +def labeled_decode(d, i): + return { + "x": d["x"], + "label": LABELS[d["label"]], + "group": d["group"], + "meta": GROUP_INFO[d["group"]], + "index": i, + } + + +def labeled_ok(sample, i): + want = Labeled()[i] + return ( + torch.equal(sample["x"], want["x"]) + and sample["label"] == want["label"] + and sample["group"] == want["group"] + and sample["meta"] is GROUP_INFO[want["group"]] + and sample["index"] == i + ) + + +@pytest.mark.parametrize("method", METHODS) +def test_encode_decode(comm, monkeypatch, method): + """encode stores ids instead of strings/objects, decode rebuilds the + sample with its index; ds[i], __getitems__ and a loader all decode; + read_rows stays row-level.""" + with pytest.raises(TypeError): # strings can't be stored as they are + make(comm, monkeypatch, Labeled(), method) + ds = make(comm, monkeypatch, Labeled(), method, encode=labeled_encode, + decode=labeled_decode) + ok = labeled_ok(ds[7], 7) and labeled_ok(ds[N - 1], N - 1) + idx = list(range(N))[::-2] + ok &= all(labeled_ok(sm, i) for sm, i in zip(ds.__getitems__(idx), idx)) + kw = {"num_workers": 1 if method == 0 else 2} + seen = 0 + for batch in ThreadDataLoader(ds, batch_size=8, collate_fn=lambda b: b, **kw): + ok &= all(labeled_ok(sm, sm["index"]) for sm in batch) + seen += len(batch) + ok &= seen == N + raw = ds.read_rows([4, 5]) # stored form: label ids, no meta/index + ok &= sorted(raw) == ["group", "label", "x"] and raw["label"].tolist() == [1, 2] + finish(comm, ds) + assert all_ok(comm, ok) + + +def test_fields_selection(comm, monkeypatch): + """fields= keeps the given keys / positions, in that order.""" + ds = make(comm, monkeypatch, DictSource(), 0, fields=["y"]) + ok = same(ds[5], {"y": DictSource()[5]["y"]}) + finish(comm, ds) + src = TupleSource() + dt = make(comm, monkeypatch, src, 0, fields=[2, 0]) + ok &= same(dt[9], (src[9][2], src[9][0])) + finish(comm, dt) + with pytest.raises(ValueError): + make(comm, monkeypatch, src, 0, fields=[0], encode=lambda s: s) + with pytest.raises(ValueError): # missing key, raised on every rank + make(comm, monkeypatch, DictSource(), 0, fields=["nope"]) + assert all_ok(comm, ok) + + def test_per_sample_fallback(comm, monkeypatch): monkeypatch.setenv("DDSTORE_BATCH_GET", "0") src = TupleSource() @@ -555,6 +639,26 @@ def test_method2_reader(comm, monkeypatch, tmp_path): assert all_ok(comm, ok) +@pytest.mark.skipif(not HAVE_CXI, reason="no CXI device") +def test_method2_reader_decode(comm, monkeypatch, tmp_path): + """The core group encodes, a DistDatasetReader decodes.""" + monkeypatch.setenv("DDSTORE_FABRIC", FABRIC) + hs = comm.bcast(str(tmp_path / "hs") if comm.Get_rank() == 0 else None, root=0) + core = DistDataset(Labeled(), "lab", comm, method=2, handshake_dir=hs, + encode=labeled_encode) + comm.Barrier() + ok = True + if comm.Get_rank() == 0: + reader = DistDatasetReader("lab", handshake_dir=hs, n_core=comm.Get_size(), + decode=labeled_decode) + idx = list(range(N)) + ok = all(labeled_ok(sm, i) for sm, i in zip(reader.__getitems__(idx), idx)) + ok &= labeled_ok(reader[3], 3) + reader.ddstore.free() + finish(comm, core) + assert all_ok(comm, ok) + + @pytest.mark.parametrize("chunk_size", [None, 4]) @pytest.mark.parametrize("method", METHODS) @pytest.mark.parametrize("form", ["void", "array", "recarray", "nested", "dict"]) From 954ef5952a5b6ed3fbc073b0589b3387d3022ad8 Mon Sep 17 00:00:00 2001 From: Jong Choi Date: Mon, 5 Oct 2026 09:34:51 -0700 Subject: [PATCH 56/56] Documentation site (Sphinx + MyST) for GitHub Pages; shorter README - docs/: the README's content as pages (installation, quick start, backends, GPUDirect, PyTorch integration, HPC systems, performance, concurrency, testing, citation) plus references for PyDDStore (hand-written, now with get_profile()), pyddstore.torch (autodoc) and environment variables. The HPC page adds a table of which Slurm --network options each layout needs on Perlmutter and Frontier. conf.py mocks mpi4py, torch and the compiled core, so the docs build needs no compiler, MPI or libfabric. - .github/workflows/docs.yml: build with -W on every push and pull request; deploy to GitHub Pages from main (needs Settings > Pages > Source: GitHub Actions). - README: 580 -> 117 lines (overview, install, quick start, links to the docs pages, citation). - Consistency: Python >= 3.9 (README, pyproject; the code needs importlib.metadata and Executor.shutdown(cancel_futures=)); comments naming src/pyddstore.pyx now name src/pyddstore/_core.pyx; README/docs show ThreadDataLoader's reuse_buffers and collate_copies. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EasT2JuGMcWVMaVcZXAYxi --- .github/workflows/docs.yml | 42 +++ .gitignore | 2 + CMakeLists.txt | 8 +- README.md | 517 ++----------------------------------- docs/api-pyddstore.md | 135 ++++++++++ docs/api-torch.md | 22 ++ docs/backends.md | 76 ++++++ docs/citation.md | 28 ++ docs/concurrency.md | 5 + docs/conf.py | 39 +++ docs/environment.md | 39 +++ docs/gpudirect.md | 19 ++ docs/hpc.md | 60 +++++ docs/index.md | 52 ++++ docs/installation.md | 47 ++++ docs/performance.md | 8 + docs/pytorch.md | 62 +++++ docs/quickstart.md | 32 +++ docs/requirements.txt | 4 + docs/results.md | 2 +- docs/testing.md | 72 ++++++ include/ddstore.hpp | 4 +- pyproject.toml | 2 +- src/cpu_nic_map.py | 4 +- test/test_gpu_rdma.py | 4 +- 25 files changed, 783 insertions(+), 502 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/api-pyddstore.md create mode 100644 docs/api-torch.md create mode 100644 docs/backends.md create mode 100644 docs/citation.md create mode 100644 docs/concurrency.md create mode 100644 docs/conf.py create mode 100644 docs/environment.md create mode 100644 docs/gpudirect.md create mode 100644 docs/hpc.md create mode 100644 docs/index.md create mode 100644 docs/installation.md create mode 100644 docs/performance.md create mode 100644 docs/pytorch.md create mode 100644 docs/quickstart.md create mode 100644 docs/requirements.txt create mode 100644 docs/testing.md diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..12f1491 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,42 @@ +name: docs + +# Build the Sphinx docs on every push and pull request; publish them to +# GitHub Pages from main. Needs Settings > Pages > Source: "GitHub Actions". +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install -r docs/requirements.txt + - run: sphinx-build -W --keep-going -b html docs docs/_build/html + - uses: actions/upload-pages-artifact@v3 + with: + path: docs/_build/html + + deploy: + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 8a70968..b85a2c8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ pyddstore.cpython-*.so _core.cpython-*.so __pycache__/ *.pyc +# Sphinx output +docs/_build/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 00f5caf..72e5a29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,14 +28,14 @@ target_include_directories(demo PUBLIC ddstore) # option(BUILD_PYTHON_BINDINGS "Build Python bindings using cython" ON) # find_package(Python3 REQUIRED COMPONENTS Interpreter Development NumPy) # add_custom_command( -# OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/src/pyddstore.cpp -# COMMAND ${Python3_EXECUTABLE} -m cython -3 --cplus ${CMAKE_CURRENT_SOURCE_DIR}/src/pyddstore.pyx +# OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/src/pyddstore/_core.cpp +# COMMAND ${Python3_EXECUTABLE} -m cython -3 --cplus ${CMAKE_CURRENT_SOURCE_DIR}/src/pyddstore/_core.pyx # WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src # DEPENDS -# ${CMAKE_CURRENT_SOURCE_DIR}/src/pyddstore.pyx +# ${CMAKE_CURRENT_SOURCE_DIR}/src/pyddstore/_core.pyx # ) # add_library(pyddstore SHARED -# ${CMAKE_CURRENT_SOURCE_DIR}/src/pyddstore.cpp +# ${CMAKE_CURRENT_SOURCE_DIR}/src/pyddstore/_core.cpp # ) # set_target_properties(pyddstore PROPERTIES PREFIX "") # set_target_properties(pyddstore PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/README.md b/README.md index dabfce9..e1a9922 100644 --- a/README.md +++ b/README.md @@ -6,60 +6,36 @@ Efficient distributed data loading for distributed data-parallel (DDP) training. Each MPI rank holds a shard of the full dataset in memory. DDStore exposes a global index space so any rank can read any sample via one-sided remote memory access — either MPI RMA (default) or libfabric RDMA — without coordinator synchronization. -- **Batched reads**: [`get_batch()`](#get_batchname-arr-indices) fetches a whole training batch in one call (one-sided RDMA reads in flight together, or an MPI collective for `method=0`). -- **GPUDirect RDMA**: data can live in, and be read straight into, GPU memory ([details](#gpudirect-rdma-gpu-resident-buffers)). -- **PyTorch integration**: [`pyddstore.torch`](#pytorch-dataset-integration) turns any map-style dataset into a distributed one (`DistDataset`) and provides a thread-based `ThreadDataLoader` that is safe with MPI and GPU buffers. -- **Thread-safe** reads, a [profiler](#performance) for where read time goes, and a split mode (`method=2`) where a separate job reads data published by another. +- **Batched reads**: `get_batch()` fetches a whole training batch in one call (one-sided RDMA reads in flight together, or an MPI collective for `method=0`). +- **GPUDirect RDMA**: data can live in, and be read straight into, GPU memory. +- **PyTorch integration**: `pyddstore.torch` turns any map-style dataset into a distributed one (`DistDataset`), reads samples made of several stored rows (`WindowedDataset`), and provides a thread-based `ThreadDataLoader` that is safe with MPI and GPU buffers. +- **Thread-safe** reads, a profiler for where read time goes, and a split mode (`method=2`) where a separate job reads data published by another. DDStore architecture +**Documentation: ** (source in [docs/](docs/index.md)). + ## Prerequisites | Dependency | Notes | |---|---| | MPI (OpenMPI / MPICH) | `mpicc` and `mpicxx` must be on `PATH` | | libfabric | Required for the RDMA backends (`method=1` and `method=2`) | -| Python ≥ 3.6 | | +| Python ≥ 3.9 | | | NumPy, mpi4py, Cython | Python build dependencies | | PyTorch (optional) | For `pyddstore.torch` and GPU buffers (CUDA or ROCm build) | ## Installation ```bash -# Install Python build dependencies pip install numpy mpi4py Cython - -# Build in-place (use with PYTHONPATH=$PWD/src:$PYTHONPATH) -CC=mpicc CXX=mpicxx python setup.py build_ext --inplace - -# Or install into the active virtual environment -CC=mpicc CXX=mpicxx pip install . -CC=mpicc CXX=mpicxx pip install ".[torch]" # also pulls PyTorch, for pyddstore.torch - -# Or install in editable/development mode -CC=mpicc CXX=mpicxx pip install -e . - -# Or install directly from GitHub -CC=mpicc CXX=mpicxx pip install git+https://github.com/ORNL/DDStore.git -``` - -To build against the packages already in the current environment (e.g. an `mpi4py` built against Cray MPICH) instead of letting pip fetch fresh build dependencies into an isolated build environment, disable build isolation: - -```bash -CC=cc CXX=CC pip install --no-build-isolation --no-deps -e . -``` - -If that fails with `ModuleNotFoundError: No module named 'distutils.msvccompiler'` (newer setuptools combined with an older system NumPy, e.g. `cray-python/3.11.7` on Frontier), point setuptools at the standard-library `distutils` for the build: - -```bash -SETUPTOOLS_USE_DISTUTILS=stdlib CC=cc CXX=CC pip install --no-build-isolation --no-deps -e . +CC=mpicc CXX=mpicxx pip install . # or ".[torch]" to also pull PyTorch +CC=mpicc CXX=mpicxx pip install -e . # editable, for development ``` -The package is `pyddstore` (compiled core `pyddstore._core`, plus `pyddstore.torch`). After updating from a 1.x checkout, rebuild; an old `src/pyddstore.cpython-*.so` or `src/pyddstore.cpp` left behind is unused and can be deleted (the build warns about them). - -Editable and in-place builds keep the generated `src/pyddstore/_core.cpp` in the checkout, shared by every environment that builds from it. `setup.py` regenerates it whenever the NumPy major version differs from the previous build's, because a file generated against NumPy 2 doesn't compile against NumPy 1.x headers. +On Cray systems, building against the environment's own `mpi4py` (`--no-build-isolation`), and other build details: [Installation](docs/installation.md). -## Quick Start +## Quick start ```python import numpy as np @@ -67,291 +43,22 @@ from mpi4py import MPI import pyddstore as dds comm = MPI.COMM_WORLD -rank = comm.Get_rank() - -# Each rank contributes its own shard -store = dds.PyDDStore(comm) # MPI RMA backend (default) -# store = dds.PyDDStore(comm, method=1) # libfabric RDMA backend +store = dds.PyDDStore(comm) # MPI RMA; method=1 for libfabric RDMA data = np.random.rand(1024, 64).astype(np.float32) -store.add("features", data) # collective — all ranks must call +store.add("features", data) # collective: each rank adds its shard -# Read any global sample index out = np.zeros((1, 64), dtype=np.float32) store.epoch_begin() -store.get("features", out, start=2048) # global index across all shards +store.get("features", out, start=2048) # any global row, from any rank store.epoch_end() - store.free() ``` -Run with: -```bash -mpirun -n 4 python my_script.py -``` - -With PyTorch, [`pyddstore.torch.DistDataset`](#pytorch-dataset-integration) does the sharding, `add()` and batched reads for you. - -## API Reference - -### `PyDDStore(comm_or_none=None, method=0, handshake_dir="", n_core=0, nic_map=None)` - -| Parameter | Type | Description | -|---|---|---| -| `comm_or_none` | `mpi4py.MPI.Comm` or `None` | MPI communicator covering all ranks. `None` only for a `method=2` extra member | -| `method` | `int` | `0` = MPI RMA (default), `1` = libfabric RDMA, `2` = file-based handshake (see [below](#file-based-handshake-method2)) | -| `handshake_dir` | `str` | Required for `method=2`: shared-filesystem directory used to exchange RDMA addresses | -| `n_core` | `int` | Required for a `method=2` extra member: number of core ranks that published data | -| `nic_map` | `str` or `None` | Optional, `method=1`/`2` only: a precomputed CPU→NIC map string (see [`DDSTORE_NIC_MAP`](#libfabric-rdma-method1) below) to use instead of the environment variable. Ignored if `FABRIC_IFACE` is already set | - -Four call shapes: - -```python -PyDDStore(comm) # method 0, MPI RMA -PyDDStore(comm, method=1) # method 1, libfabric RDMA -PyDDStore(comm, method=2, handshake_dir="/path") # method 2, core member (n_core == comm size) -PyDDStore(None, method=2, handshake_dir="/path", n_core=N) # method 2, extra member (no comm) -``` - -Note: grouping ranks into independent stores (the "sub-communicator" pattern below) is done by splitting `comm` yourself before constructing `PyDDStore` — there is no `ddstore_width` constructor parameter. `pyddstore.torch.DistDataset` does this for you (`ddstore_width`) and shows the pattern (`comm.Split()` then `PyDDStore(sub_comm)`). - ---- - -### `init(name, nrows, disp, itemsize=1)` - -Pre-allocate a named variable without providing data yet. Use `update()` to fill it in afterwards. **Collective**. - -| Parameter | Type | Description | -|---|---|---| -| `name` | `str` | Variable identifier | -| `nrows` | `int` | Number of rows in this rank's shard | -| `disp` | `int` | Number of elements per row | -| `itemsize` | `int` | Bytes per element (default `1`) | - ---- - -### `add(name, arr)` - -Register a NumPy array as a named variable. Each rank contributes its local shard; the global index space is the concatenation of all shards in rank order. **Collective** — all ranks in `comm` must call with the same `name`. - -| Parameter | Type | Description | -|---|---|---| -| `name` | `str` | Variable identifier | -| `arr` | `np.ndarray` or `torch.Tensor` | C-contiguous 2-D (or 1-D) array/tensor. Supported dtypes: `int32`, `int64`, `uint8`, `float32`, `float64`, `bool_`/`bool`. A CUDA/HIP tensor registers GPU memory directly — see [GPUDirect RDMA](#gpudirect-rdma-gpu-resident-buffers) below | - ---- - -### `update(name, arr, offset)` - -Overwrite a region of the local shard for a variable registered with `init()`. Local operation — does not require epoch or barrier. - -| Parameter | Type | Description | -|---|---|---| -| `name` | `str` | Variable identifier | -| `arr` | `np.ndarray` | Data to write | -| `offset` | `int` | Row offset within the local shard | - ---- - -### `get(name, arr, start=0)` - -Read `arr.shape[0]` consecutive rows starting at global index `start` into `arr`. The range must fall within a single rank's shard. Must be called inside an `epoch_begin` / `epoch_end` pair when using the MPI backend. - -| Parameter | Type | Description | -|---|---|---| -| `name` | `str` | Variable identifier | -| `arr` | `np.ndarray` or `torch.Tensor` | Pre-allocated, C-contiguous output buffer. A CUDA/HIP tensor writes the RDMA transfer directly into GPU memory — see [GPUDirect RDMA](#gpudirect-rdma-gpu-resident-buffers) below | -| `start` | `int` | Global row index | - ---- - -### `get_batch(name, arr, indices)` - -Read rows `indices` (global row ids; any order, any ranks, repeats allowed) into `arr`: row `i` of `arr` receives row `indices[i]`, so `arr.shape[0]` must equal `len(indices)` (else `ValueError`). Same buffer rules as `get()` (NumPy array or CUDA/HIP tensor). Every index is checked before anything is read; an out-of-range one raises `IndexError` and leaves the store usable. - -For `method=1`/`2` the whole batch is one call: one lock acquisition, at most one memory registration (none into a [registered](#register_recvname-arr--unregister_recvname-arr) buffer), and (GPU destination) one device sync, with all of the batch's `fi_read`s posted before any is waited for, so the reads overlap on the network. It is one `fi_read` per row, or several for a row longer than `DDSTORE_MAX_READ_BYTES` (default 1 GiB). - -For `method=0`, `get_batch()` is **collective**, after the collective module of [MDLoader](https://ieeexplore.ieee.org/abstract/document/10820758) (see [Citation](#citation)): every rank all-gathers all ranks' indices (`MPI_Allgatherv`), packs the rows it owns for each requester, and one `MPI_Alltoallv` delivers them, on a private duplicate of the store's communicator, in rounds of at most `DDSTORE_ALLTOALL_MAX_BYTES` (default 2 MiB) received per rank so large rows don't turn into one huge exchange. So every rank must call it for the variable the same number of times, in the same order, from one thread at a time; the number of indices may differ per rank (including 0). Indices are checked on the gathered list, so a bad index raises on every rank together. `DistributedSampler` gives every rank the same number of batches, and `vae-ddp.py` allows no worker threads with `method=0`, so the data loaders meet this automatically. - -```python -idx = np.array([2048, 7, 4096, 7]) -out = np.zeros((len(idx), 64), dtype=np.float32) -store.get_batch("features", out, idx) -``` - -`DistDataset`/`DistDatasetReader` use it by default through `__getitems__`, which PyTorch's `DataLoader` (and `ThreadDataLoader`) calls with a whole batch's indices, so the VAE examples and job scripts read in batches with no extra flag. Set `DDSTORE_BATCH_GET=0` to fall back to one `get()` per sample. - ---- - -### `register_recv(name, arr)` / `unregister_recv(name, arr)` - -Register `arr` (a C-contiguous NumPy array or CUDA/HIP tensor) once as a destination for `get()`/`get_batch()` of `name`. Reads into `arr` or any slice of it then skip memory registration, which otherwise happens whenever the destination isn't the buffer registered by the previous read. For large rows, registration can cost more than the transfer. Use it for buffers you reuse, such as a pool per loader thread: several can be registered per variable and none is evicted. The store holds a reference to `arr` until `unregister_recv()` or `free()`. No-op for `method=0`. +With PyTorch: ```python -pool = np.empty((batch_size, ncols), dtype=np.float32) -store.register_recv("features", pool) -for idx in batches: - store.get_batch("features", pool[: len(idx)], idx) # no registration -``` - ---- - -### `join(name)` - -`method=2` extra member only. Discovers a variable published by the core group by polling the handshake directory until the combined record file (`{name}.bin`) written by core rank 0 reaches its expected size (up to `DDSTORE_HANDSHAKE_TIMEOUT_S` seconds), then registers it for `get()`. - -| Parameter | Type | Description | -|---|---|---| -| `name` | `str` | Variable identifier, matching the `name` used in the core group's `add()` | - ---- - -### `info(name)` - -Returns `(total_rows, disp, itemsize)` for a variable that has been `add()`-ed or `join()`-ed. Useful on the extra side to size output buffers without hardcoding shapes. - ---- - -### `epoch_begin()` / `epoch_end()` - -Open and close an MPI RMA access epoch (calls `MPI_Win_fence`). **Collective**. Required around `get()` calls when using `method=0`. No-op for `method=1`/`2`. - ---- - -### `free()` - -Release every variable's MPI window (`method=0`) or libfabric endpoints and memory registrations, including [`register_recv()`](#register_recvname-arr--unregister_recvname-arr) buffers (`method=1`/`2`), then the host buffer DDStore allocated for it in `add()`/`init()` (a GPU tensor passed to `add()` is the caller's and is not freed). Safe to call more than once. After `MPI_Finalize` the MPI window and buffer can no longer be released and are skipped. - -## Environment variables - -**Read by DDStore itself** (the C++ library, `pyddstore`, `cpu_nic_map`): - -| Variable | Default | Effect | -|---|---|---| -| `DDSTORE_FABRIC` | `hsn` | libfabric provider for `method=1`/`2`: `hsn` (`tcp;ofi_rxm`) or `cxi` (native Slingshot; required for [GPUDirect RDMA](#gpudirect-rdma-gpu-resident-buffers)). See [libfabric RDMA](#libfabric-rdma-method1). | -| `FABRIC_IFACE` | auto | Network interface (libfabric domain, e.g. `cxi0`, `hsn0`) for `method=1`/`2`. Set it to force one; otherwise picked from the rank's CPU affinity. | -| `DDSTORE_NIC_MAP` | unset | Precomputed CPU→NIC map used for that automatic pick instead of a live hwloc query (`python3 -m cpu_nic_map --env`). The constructor's `nic_map=` argument takes priority. | -| `DDSTORE_HANDSHAKE_DIR` | `./ddstore_hs` | `method=2` handshake directory when none is given (C++ API; `PyDDStore` requires `handshake_dir`, and the examples fill it from this variable). Must be on a shared filesystem. | -| `DDSTORE_HANDSHAKE_TIMEOUT_S` | `300` | Seconds a `method=2` extra member's `join()` polls for the core group's record file. | -| `DDSTORE_PROFILE` | off | `1` turns on `get()`/`get_batch()` timing counters, read with `get_profile(name)`. See [Performance](#performance). | -| `DDSTORE_MAX_READ_BYTES` | `1073741824` (1 GiB) | `method=1`/`2`: largest single `fi_read`; longer rows are read in pieces (on Perlmutter's `cxi` one 5 GB read fails with `EMSGSIZE`, 2.5 GB works, and the provider doesn't report the limit). Lowered to the endpoint's `max_msg_size` when the provider reports one. | -| `DDSTORE_ALLTOALL_MAX_BYTES` | `2097152` (2 MiB) | `method=0` `get_batch()`: bytes each rank receives per exchange round. Must be equal on all ranks. | - -**Read by `pyddstore.torch`** (defaults for arguments not given): - -| Variable | Default | Effect | -|---|---|---| -| `DDSTORE_METHOD` | `0` | `DistDataset`'s backend when `method=` isn't passed: `0` MPI RMA, `1` libfabric, `2` file-based handshake. (`PyDDStore` itself takes `method=` only.) | -| `DDSTORE_BATCH_GET` | `1` | `DistDataset.__getitems__` reads a whole batch with one `get_batch()` per field; `0` reads one sample at a time. | -| `DDSTORE_HANDSHAKE_DIR`, `DDSTORE_HANDSHAKE_TIMEOUT_S` | `./ddstore_hs`, `300` | `method=2` directory, and how long `DistDatasetReader` waits for the core group to publish. | -| `DDSTORE_N_CORE` | unset | `DistDatasetReader`'s number of core ranks when `n_core=` isn't passed (the examples default it to 4). | -| `DDSTORE_AFFINITY_WIDTH` / `DDSTORE_AFFINITY_OFFSET` | `0` / `0` | `ThreadDataLoader`: pin worker thread *i* to CPUs `[offset + i·width, offset + (i+1)·width)` of the process's affinity; width `0` = no pinning. | - -**Read by the examples** (`examples/vae/`, `examples/scripts/`, job scripts): - -| Variable | Default | Effect | -|---|---|---| -| `DDSTORE_BACKEND` | auto | `torch.distributed` backend for the examples' DDP setup (`nccl`, `gloo`, `xccl`). | -| `VAE_PROFILE` | off | `1`: `vae-ddp.py` prints per-epoch fetch vs compute time. | -| `MASTER_PORT` | `2345` | DDP rendezvous port; the core/extra job script gives each step its own. | - -**System settings that matter on Frontier:** - -| Variable | Effect | -|---|---| -| `SLINGSHOT_VNIS` | Set by Slurm per step. With `--network=job_vni`, keep only the last (job-wide) entry before starting Python so separate `srun` steps can reach each other — see [Multiple `srun` steps](#multiple-srun-steps-in-one-job-method2-cxi). | -| `GPU_MAX_HW_QUEUES` | ROCm hardware queues per GPU per process (default 4); raise it if data-loading threads use their own streams — see [HIP streams](docs/results.md#hip-streams-and-hardware-queues-frontier-rocm-72). | - -## Backends - -### MPI RMA (`method=0`, default) - -Uses `MPI_Win_create` and `MPI_Get` for one-sided remote reads. Works on any MPI-capable cluster without additional hardware. `epoch_begin`/`epoch_end` are required to delimit access epochs. - -### libfabric RDMA (`method=1`) - -Uses `fi_read` for true RDMA transfers over high-speed interconnects (Infiniband/verbs, Cray GNI, Intel PSM2, Cray Slingshot). Lower latency than MPI RMA on supported hardware. `epoch_begin`/`epoch_end` are no-ops with this backend. - -**`DDSTORE_FABRIC`** selects which libfabric provider to open, for `method=1`/`2`: - -- `hsn` (default, unset) — opens the `tcp;ofi_rxm` domain over Cray Slingshot (Frontier). -- `cxi` — opens the native `cxi` domain over Cray Slingshot (Frontier and Perlmutter; Perlmutter is CXI-only). Required for [GPUDirect RDMA](#gpudirect-rdma-gpu-resident-buffers), and the default in the Frontier job scripts. - -The two are independent code paths (not runtime auto-detection), so set this explicitly per system rather than relying on a guess: - -```bash -export DDSTORE_FABRIC=hsn # tcp;ofi_rxm (default) -export DDSTORE_FABRIC=cxi # native CXI: Perlmutter, or Frontier with GPUDirect -``` - -`PyDDStore` picks the network interface (`FABRIC_IFACE`) automatically for `method=1`/`2`, based on each rank's real CPU affinity (`os.sched_getaffinity`) — no changes needed in your code: - -- **`DDSTORE_NIC_MAP`** — a precomputed CPU→NIC map, used directly if set (no NIC discovery at construction time). Generate it once from a context with reliable NIC visibility, e.g. an `sbatch` batch step's own shell (not a nested `srun` task — NIC/PCI discovery has been observed to fail there), and export it before launching ranks so every one inherits it: - ```bash - export DDSTORE_NIC_MAP=$(python3 -m cpu_nic_map --env) - srun ... python train.py - ``` -- If `DDSTORE_NIC_MAP` isn't set, each rank falls back to a live `hwloc-calc`/`lstopo` query against its own CPU affinity (`cpu_nic_map.allocated_nics()`, also runnable standalone as `python3 cpu_nic_map.py --allocated`) to find the nearest NIC. -- Set `FABRIC_IFACE` explicitly to override both and force a specific interface, e.g. when the automatic selection picks the wrong one: - ```bash - export FABRIC_IFACE=hsn0 # e.g. Cray Slingshot - ``` -- Or skip the environment entirely and pass a map straight to the constructor: `PyDDStore(comm, method=1, nic_map="hsn0=0-15,64-79;hsn1=...")`. - -### File-based handshake (`method=2`) - -Splits the dataset-holding job from the training job entirely: a **core** group loads and publishes data, and a separate **extra** group reads it over RDMA (`fi_read`, same transport as `method=1`) — the two are independent MPI jobs (e.g. two separate `srun`/`mpirun` launches, possibly on different node allocations) that never share a communicator. They rendezvous only through record files written to a shared-filesystem directory (must be visible to all nodes, e.g. Lustre): - -- **Core member** — has an MPI communicator, publishes with `add()`/`init()`. Core ranks exchange records via `MPI_Allgather`, and rank 0 writes the combined set to a single `{name}.bin` file (fabric address, MR key, base pointer, row count, dtype per rank) into `handshake_dir`. -- **Extra member** — no MPI communicator; constructed with `comm_or_none=None` and an explicit `n_core`. Calls `join(name)` to poll for and read all `n_core` core-rank records, then `get()` works exactly as on the core side, reading directly from core-rank memory over RDMA. - -```python -# core side — one MPI job -store = dds.PyDDStore(comm, method=2, handshake_dir="/lustre/.../ddstore_hs") -store.add("x", data) -... # wait for the extra side to finish (e.g. a sentinel file) -store.free() - -# extra side — a separate MPI job, no comm needed -store = dds.PyDDStore(None, method=2, handshake_dir="/lustre/.../ddstore_hs", n_core=4) -store.join("x") -out = np.zeros((1, ncols), dtype=np.float32) -store.get("x", out, start=global_idx) -store.free() -``` - -Environment variables: `DDSTORE_HANDSHAKE_DIR`, `DDSTORE_HANDSHAKE_TIMEOUT_S`, `DDSTORE_FABRIC` and `DDSTORE_NIC_MAP` — see [Environment variables](#environment-variables). - -See [test/test_method2_core.py](test/test_method2_core.py) / [test/test_method2_extra.py](test/test_method2_extra.py) for a minimal runnable pair, and [examples/vae/vae_core_server.py](examples/vae/vae_core_server.py) / [examples/vae/vae_extra_train.py](examples/vae/vae_extra_train.py) for a full DDP training example using this split. - -`ddstore_width` grouping (below) is not currently supported with `method=2` — every core rank in `comm` is treated as one group. - -## GPUDirect RDMA (GPU-resident buffers) - -`add()`, `get()` and `get_batch()` accept a CUDA/HIP `torch.Tensor` in place of a NumPy array, so RDMA reads from or writes directly into GPU memory, with no `.cpu()`/`.to(device)` copy. Requires `method=1` or `2`, **`DDSTORE_FABRIC=cxi`**, and a CUDA- or ROCm-enabled PyTorch. A GPU tensor with `DDSTORE_FABRIC=hsn` (the default) or `method=0` raises a clear error instead of silently copying through the host. - -```python -import torch -data = torch.rand(1024, 64, dtype=torch.float32, device="cuda") -store.add("features", data) # GPU source, no host copy - -out = torch.empty((1, 64), dtype=torch.float32, device="cuda") -store.get("features", out, start=2048) # GPU destination, no host copy -``` - -- **`add()` with a GPU tensor registers your tensor's own memory; no copy is made.** Keep it alive and unmodified until `free()`. `PyDDStore` holds a reference as a safety net, and adding the same name again with a GPU tensor is rejected. (With NumPy, `add()` copies and the array can be reused right away.) -- **The device is synchronized before each GPU transfer** (`torch.cuda.synchronize()`, once per `get()` / `get_batch()` call): the NIC writes outside PyTorch's stream ordering, and without the sync training hit GPU memory faults. Prefer `get_batch()` on the GPU path so this costs one sync per batch, not per sample. -- `init()`/`update()` stay host-only. -- Whether GPU destinations are faster than host ones depends on the machine: on Frontier they win from ~12.5 KB rows up, on Perlmutter host destinations win at every size ([results](docs/results.md#bench_getpy-µs-per-row)). - -Examples: [test/test_gpu_rdma.py](test/test_gpu_rdma.py), and `--gpu-dest`/`--gpu-source` on [vae-ddp.py](examples/vae/vae-ddp.py), [vae_extra_train.py](examples/vae/vae_extra_train.py) and [vae_core_server.py](examples/vae/vae_core_server.py). - -## PyTorch Dataset Integration - -`pyddstore.torch` (needs PyTorch: `pip install .[torch]` or an existing PyTorch) turns any map-style dataset into a distributed one: - -```python -import torch # import torch before MPI starts +import torch # import torch before MPI starts from mpi4py import MPI from pyddstore.torch import DistDataset, ThreadDataLoader @@ -362,192 +69,22 @@ for x, y in loader: ... ``` -- **`DistDataset(source, name, comm=None, ddstore_width=None, device=None, add_device=None, method=None, handshake_dir=None, chunk_size=None, encode=None, decode=None, fields=None)`**: each rank loads its contiguous share of `source` (anything with `len()` and `[i]`) into DDStore; every rank can then read every sample. Samples keep the source's structure (a tensor, numpy array or number; a tuple or list of them; or a dict of them), with each field's shape and dtype. Fields must have the same shape and dtype in every sample; supported dtypes are bool, uint8, int32, int64, float32 and float64. A field (or the whole sample) may also be a numpy structured record (`np.void`, a structured `ndarray`, or `np.recarray`) of any field types: it is stored as raw bytes and comes back as the same kind of object with the same layout (`default_collate` can't batch records, so pass a `collate_fn`). `ds.shapes` / `ds.dtypes` describe the fields, `ds.ddstore` is the underlying `PyDDStore`. - - `method` (default `DDSTORE_METHOD` or 0) picks the backend; `ddstore_width` splits `comm` into independent stores (see [Partitioned usage](#partitioned--sub-communicator-usage)). - - `device` puts tensor fields of read samples on a GPU and `add_device` keeps each rank's share there ([GPUDirect](#gpudirect-rdma-gpu-resident-buffers)). - - `chunk_size` loads each rank's share that many samples at a time, writing each chunk into the store before reading the next: peak memory is about the share plus one chunk, instead of about three times the share (400 MiB share: 461 vs 1202 MiB). Host storage only (not with `add_device`). - - `encode` / `decode` / `fields` handle samples that can't be stored as they are (strings, labels, metadata objects, data shared by a group of samples). `encode(sample)` runs on every source sample before it is stored and returns what to store; `decode(stored, index)` runs on every sample read (`ds[i]`, `__getitems__`, so also in loaders) and rebuilds the full sample, e.g. adding constants or looking up tables by index or by a stored id. `decode` runs on the reading rank, so anything it looks up must exist on every rank. `fields=[...]` keeps only those keys (dict samples) or positions (tuple/list samples); it can't be combined with `encode`. `read_rows()` and `WindowedDataset` return stored rows without `decode`. `DistDatasetReader` takes `decode` too. - -```python -LABELS = ["cat", "dog", "owl"] -ds = DistDataset(src, "pets", comm, - encode=lambda s: {"x": s["x"], "label": LABELS.index(s["label"]), "group": s["group"]}, - decode=lambda d, i: {**d, "label": LABELS[d["label"]], "meta": GROUP_INFO[d["group"]]}) -``` - - - **Batched by default**: `__getitems__` reads a whole batch with one [`get_batch()`](#get_batchname-arr-indices) per field, which `DataLoader` and `ThreadDataLoader` call automatically; `DDSTORE_BATCH_GET=0` reads one sample at a time. With `method=0` batched reads are collective, so every rank must iterate the same number of batches from one thread (`DistributedSampler` does). -- **Reading rows and reusing buffers** (`DistDataset` and `DistDatasetReader`): - - `ds.read_rows(rows, fields=None, out=None)` reads stored rows `rows` (any order, repeats allowed) of the selected fields (default all), one `get_batch()` per field, and returns a dict of values shaped `(len(rows), *field_shape)`. Keys are the sample's: dict keys, tuple/list positions, or `0` for a single value. With `method=0` it is collective, like `get_batch()`. - - `ds.alloc(n, fields=None)` returns buffers for `n` rows, one per field and keyed the same way, each [registered](#register_recvname-arr--unregister_recvname-arr) once. Pass them as `out=` to `read_rows()` or `ds.__getitems__(idx, out=)`: reads then skip memory registration, and the results are views into the buffers, so you decide when a buffer can be reused. `ds.release(bufs)` unregisters them; `free()` does too. -- **`WindowedDataset(ds, window, stride=1, dilation=1, starts=None, fields=None)`**: samples made of several stored rows of `ds` (time windows, clips, sequences), each stored row held once. Sample `i` is rows `s, s + dilation, …, s + (window - 1)·dilation` with `s = i·stride`, or `s = starts[i]` when `starts` is given; use `starts` to keep only windows that don't cross a trajectory or file boundary. Fields come back stacked, `(window, *field_shape)`, in the structure of `ds`'s samples (a dict when `fields` is given). A batch of windows is one `read_rows()`. -- **`row_of(concat, source, index)`**: with several sources in one store (a `DistDataset` over a `torch.utils.data.ConcatDataset`), the row of sample `index` of source `source`, e.g. to map (file, trajectory, step) to `starts`: +Run with `mpirun -n 4 python my_script.py` (or `srun`). -```python -from torch.utils.data import ConcatDataset -from pyddstore.torch import DistDataset, WindowedDataset, row_of - -files = ConcatDataset([StepsOf(f) for f in paths]) # one sample per time step -frames = DistDataset(files, "frames", comm, method=1) -starts = [row_of(files, k, t) for k, f in enumerate(paths) # windows inside each file - for t in range(0, len(files.datasets[k]) - 2 * dt)] -pairs = WindowedDataset(frames, window=3, dilation=dt, starts=starts) # (t, t+dt, t+2dt) -``` - -- **`DistDatasetReader(name, handshake_dir=None, n_core=None, device=None, decode=None)`**: the same dataset read by a separate `method=2` extra job; it learns the fields from a `{name}.meta.json` file the core group writes next to the handshake records. -- **`ThreadDataLoader(dataset, **DataLoader args)`**: a `DataLoader` whose workers are threads, not forked processes, so it is safe with MPI and GPU buffers. Each batch is fetched, collated and optionally pinned in a worker thread; random draws match `DataLoader`'s. As with `DataLoader`, `iter(loader)` returns a separate iterator for one epoch (`list(it)` or `islice(it, …)` after `next(it)` continue the epoch; iterators over one loader are independent), and while the training step holds a batch, `num_workers * prefetch_factor` more are being fetched, so that many plus one are in memory. One or two workers are enough: reads on one variable are serialized by its lock, and one batched read already keeps the network busy. - - `reuse_buffers=True` reads every batch into one of a fixed pool of `num_workers` buffer sets from `dataset.alloc(batch_size)`, registered once, instead of fresh buffers registered on every read (for large rows, registration can cost more than the transfer). A worker reads and collates its batch, then returns the set, so the collate must copy: it needs `batch_size` and the default `collate_fn`, or `collate_copies=True` to declare that your `collate_fn` copies; other setups raise. `loader.close()` (or deleting the loader) waits for running fetches and unregisters the pool. `DDSTORE_AFFINITY_WIDTH` / `DDSTORE_AFFINITY_OFFSET` pin worker threads to CPUs. - -[examples/vae/vae-ddp.py](examples/vae/vae-ddp.py) trains a VAE with DDP on top of it: - -```bash -DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --num-workers=1 -DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --num-workers=1 --gpu-dest --gpu-source -``` - -- **`--num-workers`** (default 0): `0` uses PyTorch's standard `DataLoader`; `> 0` uses `ThreadDataLoader` (needs `method=1`/`2`). -- **`--gpu-dest` / `--gpu-source`**: `DistDataset`'s `device` / `add_device`. -- **`--replicate R`** repeats the training set R times (longer epochs); **`--image-scale S`** upscales images to (28·S)² so each row is S² larger. Both default to 1, the original example. -- The [method=2 split](#file-based-handshake-method2) variant is [vae_core_server.py](examples/vae/vae_core_server.py) (a `DistDataset` core group) + [vae_extra_train.py](examples/vae/vae_extra_train.py) (a `DistDatasetReader`), with the same options. - -### Slurm job scripts - -[job-vae-single.sh](examples/vae/script/job-vae-single.sh) runs `vae-ddp.py` as one `srun` step; [job-vae-core-extra.sh](examples/vae/script/job-vae-core-extra.sh) runs the core/extra split as two steps. Run either with `--help` for all options. Their `#SBATCH` lines target Frontier (`-A FUS184`, 8 ranks × 7 cores per node); see below for Perlmutter. - -```bash -sbatch examples/vae/script/job-vae-single.sh --method=1 --num-workers=1 -sbatch examples/vae/script/job-vae-single.sh --method=1 --gpudirect --image-scale=2 -sbatch examples/vae/script/job-vae-core-extra.sh # split-node: 1 core node, the rest extra -sbatch examples/vae/script/job-vae-core-extra.sh --gpudirect --core-nnodes=2 -``` - -`job-vae-core-extra.sh` sets up Slingshot networking for its two steps (see [Multiple `srun` steps](#multiple-srun-steps-in-one-job-method2-cxi)). `--layout=colocate` (both steps on the same nodes) works on Perlmutter only. - -Both scripts also run on Perlmutter: they detect the machine (`NERSC_HOST`) and use 4 ranks per node, with the training ranks seeing all 4 GPUs of their node (NCCL needs that there). Override the Frontier `#SBATCH` lines when submitting: - -```bash -sbatch -A -C gpu --gpus-per-node=4 examples/vae/script/job-vae-single.sh --method=1 -``` - -## Partitioned / Sub-communicator Usage - -`PyDDStore` always spans the whole communicator you pass it. To run several independent stores side by side (e.g. one per node), split `comm` first; each group then holds a full replica of the dataset, partitioned across its own members: - -```python -width = 4 # ranks per group, e.g. GPUs per node -sub_comm = comm.Split(rank // width, rank) -store = dds.PyDDStore(sub_comm) # one independent store per group -``` +## Documentation -This keeps sample fetches inside a node at the cost of replicating the data per group. `DistDataset` exposes it as `ddstore_width` (`None` = one store across all ranks). Not supported with `method=2`. - -## Performance - -- Use **batched reads** (the default with `DistDataset`, or `get_batch()` directly). They cut per-sample cost by 10–27× for small rows and make the GPU path insensitive to worker threads; in the VAE every configuration got 1.2–3.9× faster per epoch. -- **Reuse destination buffers** and [`register_recv()`](#register_recvname-arr--unregister_recvname-arr) them. Reading into a fresh buffer every time re-registers memory on every read; `get_profile(name)["mr_miss"]` counts those registrations. -- `method=1` (one-sided `fi_read`) is the fastest backend; `method=0` with batching (collective) comes close for small rows. -- `DDSTORE_PROFILE=1` + `get_profile(name)` shows where `get()`/`get_batch()` time goes: lock wait, memory registration, posting and completing `fi_read`, GPU sync. `vae-ddp.py` prints an all-rank summary when it is set. [examples/scripts/bench_get.py](examples/scripts/bench_get.py) measures per-row latency and throughput vs row size, destination, batch size and threads. - -Measurements, profiles and the experiments behind these choices: [docs/results.md](docs/results.md). - -## Known Limitations - -### Multiple `srun` steps in one job (`method=2`, `cxi`) - -On Slingshot every `srun` step gets its own VNI (network isolation ID), and two endpoints can only talk on the same VNI. For core and extra running as separate steps, [job-vae-core-extra.sh](examples/vae/script/job-vae-core-extra.sh) does both of these: - -1. `#SBATCH --network=single_node_vni,job_vni`: `job_vni` adds a job-wide VNI to every step (`SLINGSHOT_VNIS=,`); on Frontier, `single_node_vni` is also what gives single-node steps a CXI service at all (without it `fi_domain()` fails with `-38`). -2. In each task, before Python starts: `export SLINGSHOT_VNIS=${SLINGSHOT_VNIS##*,}`. libfabric's cxi provider uses only the first VNI listed (the step's own), so without this reads fail with `VNI_NOT_FOUND`. - -Colocating both steps on the same nodes: - -| | `job_vni` + wrapper | `job_vni` + wrapper + `srun --overlap` | no `--network` | -|---|---|---|---| -| Frontier | second step fails to launch: `Error configuring interconnect` | same failure | each step has only its own VNI: extra cannot reach core | -| Perlmutter | second step does not start | **works** | extra cannot reach core | - -So the script defaults to `--layout=split-node`; `--layout=colocate` (with `--overlap`) is for Perlmutter. Perlmutter's single-node steps also work without the `--network` flags. - -### Concurrency - -- `get()` and `get_batch()` are thread-safe. For `method=1`/`2` a per-variable lock in `DDStore::get()` serializes calls on one variable (it guards shared receive state; without it concurrent calls crashed). Both release the GIL during the transfer. -- `method=0` `get_batch()` is **collective**: every rank calls it the same number of times, in the same order, from one thread. `vae-ddp.py` therefore allows no worker threads with `method=0`. -- Only the main thread calls MPI (setup, `epoch_begin`/`epoch_end`, `method=0` reads); mpi4py's default `MPI_THREAD_MULTIPLE` is fine, `FUNNELED` is the minimum. If you call MPI from your own worker threads, keep `MULTIPLE`. - -### Troubleshooting: RDMA fails to connect (`cxi`) - -If `fi_domain()` fails with `-38 (Function not implemented)`, the step has no CXI service: add `#SBATCH --network=single_node_vni` (needed on Frontier for any single-node step, i.e. a `-N 1` job or a one-node step inside a larger job). If ranks in different `srun` steps can't reach each other (`VNI_NOT_FOUND`), see [Multiple `srun` steps](#multiple-srun-steps-in-one-job-method2-cxi) above. - -## Testing - -### Unit tests (pytest) - -Install test dependencies: - -```bash -pip install pytest pytest-mpi -``` - -**Single-rank** — no cluster required, covers all dtypes, `add`/`get`/`init`/`update`, and error cases: - -```bash -mpirun -n 1 python -m pytest test/test_single.py -v -``` - -**Multi-rank** — verifies remote reads across all rank pairs and sub-communicator grouping: - -```bash -mpirun -n 4 python -m pytest test/test_multirank.py -v -``` - -**Batched reads and the PyTorch layer** — method 0 everywhere; method 1, method 2 and GPU cases run inside a Slurm step with a CXI device (provider from `DDSTORE_FABRIC`, default `cxi`): - -```bash -mpirun -n 4 python -m pytest test/test_get_batch.py test/test_torch.py -v -``` - -**GPUDirect RDMA** — requires a live `cxi` fabric and a CUDA/HIP GPU per rank (skipped automatically otherwise); see [GPUDirect RDMA](#gpudirect-rdma-gpu-resident-buffers): - -```bash -DDSTORE_FABRIC=cxi mpirun -n 2 python -m pytest test/test_gpu_rdma.py -v -``` - -| Test file | Min ranks | What is tested | -|---|---|---| -| `test/test_single.py` | 1 | All dtypes, `add`/`get`, `init`/`update`/`get`, error handling, double `free()` | -| `test/test_multirank.py` | 2 (4 recommended) | Remote reads, shard boundaries, multiple variables, `ddstore_width` grouping | -| `test/test_gpu_rdma.py` | 2 | GPU-resident `add()`/`get()` in both directions, both libfabric methods, negative/error cases | -| `test/test_get_batch.py` | 2 (4 recommended) | `get_batch()`: shuffled indices across ranks with repeats, single row, dtypes, error recovery, GPU destination, concurrent threads, registered destination buffers, wide rows (with `DDSTORE_MAX_READ_BYTES=4096` they are read in pieces); method 0, plus method 1 over `cxi` inside a Slurm step | -| `test/test_torch.py` | 2 (4 recommended) | `pyddstore.torch`: tuple/dict/single samples of every field kind, numpy records, loaders vs the plain source, `ThreadDataLoader` iterators and prefetch depth, `read_rows`/`alloc` buffers, `reuse_buffers` (also with GPU buffers), `WindowedDataset`, `row_of`, `encode`/`decode`/`fields`, chunked loading, error handling, `ddstore_width`, GPU placement, `DistDatasetReader` | - -### Integration scripts - -```bash -# Basic functional test (libfabric, method=1) -mpirun -n 4 python examples/scripts/demo.py - -# Integration test with PyTorch DDP (libfabric, method=1) -mpirun -n 4 python examples/scripts/test.py -``` - -Optional arguments for `examples/scripts/demo.py` and `examples/scripts/test.py`: - -| Flag | Default | Description | -|---|---|---| -| `--num` | `1048576` | Rows per rank | -| `--dim` | `64` | Elements per row | -| `--nbatch` | `32` | Number of random reads | -| `--gloo` / `--nccl` | `--gloo` | `test.py` only: `torch.distributed` backend | - -### Method 2 (file-based handshake) +| | | +|---|---| +| Getting started | [Installation](docs/installation.md), [Quick start](docs/quickstart.md) | +| User guide | [Backends](docs/backends.md) (MPI RMA, libfabric, file-based handshake, partitioned stores), [GPUDirect RDMA](docs/gpudirect.md), [PyTorch integration](docs/pytorch.md), [HPC systems](docs/hpc.md) (Slurm, Slingshot, Frontier, Perlmutter), [Performance](docs/performance.md), [Concurrency](docs/concurrency.md) | +| Reference | [`PyDDStore`](docs/api-pyddstore.md), [`pyddstore.torch`](docs/api-torch.md), [Environment variables](docs/environment.md) | +| More | [Testing](docs/testing.md), [Measurements](docs/results.md) | -Two separate launches sharing a handshake directory on a shared filesystem — not a single `mpirun`, since core and extra are independent jobs: +To build the documentation locally: ```bash -# Terminal 1 — core (data-holding) side -mpirun -n 4 python test/test_method2_core.py /path/to/shared/ddstore_hs - -# Terminal 2 — extra (reader) side, after or while the core side is running -python test/test_method2_extra.py /path/to/shared/ddstore_hs 4 +pip install -r docs/requirements.txt +sphinx-build -b html docs docs/_build/html ``` ## Citation diff --git a/docs/api-pyddstore.md b/docs/api-pyddstore.md new file mode 100644 index 0000000..52d2e5b --- /dev/null +++ b/docs/api-pyddstore.md @@ -0,0 +1,135 @@ +# `PyDDStore` reference + +## `PyDDStore(comm_or_none=None, method=0, handshake_dir="", n_core=0, nic_map=None)` + +| Parameter | Type | Description | +|---|---|---| +| `comm_or_none` | `mpi4py.MPI.Comm` or `None` | MPI communicator covering all ranks. `None` only for a `method=2` extra member | +| `method` | `int` | `0` = MPI RMA (default), `1` = libfabric RDMA, `2` = file-based handshake (see [below](backends.md#file-based-handshake-method2)) | +| `handshake_dir` | `str` | Required for `method=2`: shared-filesystem directory used to exchange RDMA addresses | +| `n_core` | `int` | Required for a `method=2` extra member: number of core ranks that published data | +| `nic_map` | `str` or `None` | Optional, `method=1`/`2` only: a precomputed CPU→NIC map string (see [`DDSTORE_NIC_MAP`](backends.md#libfabric-rdma-method1) below) to use instead of the environment variable. Ignored if `FABRIC_IFACE` is already set | + +Four call shapes: + +```python +PyDDStore(comm) # method 0, MPI RMA +PyDDStore(comm, method=1) # method 1, libfabric RDMA +PyDDStore(comm, method=2, handshake_dir="/path") # method 2, core member (n_core == comm size) +PyDDStore(None, method=2, handshake_dir="/path", n_core=N) # method 2, extra member (no comm) +``` + +Note: grouping ranks into independent stores (the "sub-communicator" pattern below) is done by splitting `comm` yourself before constructing `PyDDStore` — there is no `ddstore_width` constructor parameter. `pyddstore.torch.DistDataset` does this for you (`ddstore_width`) and shows the pattern (`comm.Split()` then `PyDDStore(sub_comm)`). + +--- + +## `init(name, nrows, disp, itemsize=1)` + +Pre-allocate a named variable without providing data yet. Use `update()` to fill it in afterwards. **Collective**. + +| Parameter | Type | Description | +|---|---|---| +| `name` | `str` | Variable identifier | +| `nrows` | `int` | Number of rows in this rank's shard | +| `disp` | `int` | Number of elements per row | +| `itemsize` | `int` | Bytes per element (default `1`) | + +--- + +## `add(name, arr)` + +Register a NumPy array as a named variable. Each rank contributes its local shard; the global index space is the concatenation of all shards in rank order. **Collective** — all ranks in `comm` must call with the same `name`. + +| Parameter | Type | Description | +|---|---|---| +| `name` | `str` | Variable identifier | +| `arr` | `np.ndarray` or `torch.Tensor` | C-contiguous 2-D (or 1-D) array/tensor. Supported dtypes: `int32`, `int64`, `uint8`, `float32`, `float64`, `bool_`/`bool`. A CUDA/HIP tensor registers GPU memory directly — see [GPUDirect RDMA](gpudirect.md) below | + +--- + +## `update(name, arr, offset)` + +Overwrite a region of the local shard for a variable registered with `init()`. Local operation — does not require epoch or barrier. + +| Parameter | Type | Description | +|---|---|---| +| `name` | `str` | Variable identifier | +| `arr` | `np.ndarray` | Data to write | +| `offset` | `int` | Row offset within the local shard | + +--- + +## `get(name, arr, start=0)` + +Read `arr.shape[0]` consecutive rows starting at global index `start` into `arr`. The range must fall within a single rank's shard. Must be called inside an `epoch_begin` / `epoch_end` pair when using the MPI backend. + +| Parameter | Type | Description | +|---|---|---| +| `name` | `str` | Variable identifier | +| `arr` | `np.ndarray` or `torch.Tensor` | Pre-allocated, C-contiguous output buffer. A CUDA/HIP tensor writes the RDMA transfer directly into GPU memory — see [GPUDirect RDMA](gpudirect.md) below | +| `start` | `int` | Global row index | + +--- + +## `get_batch(name, arr, indices)` + +Read rows `indices` (global row ids; any order, any ranks, repeats allowed) into `arr`: row `i` of `arr` receives row `indices[i]`, so `arr.shape[0]` must equal `len(indices)` (else `ValueError`). Same buffer rules as `get()` (NumPy array or CUDA/HIP tensor). Every index is checked before anything is read; an out-of-range one raises `IndexError` and leaves the store usable. + +For `method=1`/`2` the whole batch is one call: one lock acquisition, at most one memory registration (none into a [registered](api-pyddstore.md#register_recvname-arr--unregister_recvname-arr) buffer), and (GPU destination) one device sync, with all of the batch's `fi_read`s posted before any is waited for, so the reads overlap on the network. It is one `fi_read` per row, or several for a row longer than `DDSTORE_MAX_READ_BYTES` (default 1 GiB). + +For `method=0`, `get_batch()` is **collective**, after the collective module of [MDLoader](https://ieeexplore.ieee.org/abstract/document/10820758) (see [Citation](citation.md)): every rank all-gathers all ranks' indices (`MPI_Allgatherv`), packs the rows it owns for each requester, and one `MPI_Alltoallv` delivers them, on a private duplicate of the store's communicator, in rounds of at most `DDSTORE_ALLTOALL_MAX_BYTES` (default 2 MiB) received per rank so large rows don't turn into one huge exchange. So every rank must call it for the variable the same number of times, in the same order, from one thread at a time; the number of indices may differ per rank (including 0). Indices are checked on the gathered list, so a bad index raises on every rank together. `DistributedSampler` gives every rank the same number of batches, and `vae-ddp.py` allows no worker threads with `method=0`, so the data loaders meet this automatically. + +```python +idx = np.array([2048, 7, 4096, 7]) +out = np.zeros((len(idx), 64), dtype=np.float32) +store.get_batch("features", out, idx) +``` + +`DistDataset`/`DistDatasetReader` use it by default through `__getitems__`, which PyTorch's `DataLoader` (and `ThreadDataLoader`) calls with a whole batch's indices, so the VAE examples and job scripts read in batches with no extra flag. Set `DDSTORE_BATCH_GET=0` to fall back to one `get()` per sample. + +--- + +## `register_recv(name, arr)` / `unregister_recv(name, arr)` + +Register `arr` (a C-contiguous NumPy array or CUDA/HIP tensor) once as a destination for `get()`/`get_batch()` of `name`. Reads into `arr` or any slice of it then skip memory registration, which otherwise happens whenever the destination isn't the buffer registered by the previous read. For large rows, registration can cost more than the transfer. Use it for buffers you reuse, such as a pool per loader thread: several can be registered per variable and none is evicted. The store holds a reference to `arr` until `unregister_recv()` or `free()`. No-op for `method=0`. + +```python +pool = np.empty((batch_size, ncols), dtype=np.float32) +store.register_recv("features", pool) +for idx in batches: + store.get_batch("features", pool[: len(idx)], idx) # no registration +``` + +--- + +## `get_profile(name)` + +With `DDSTORE_PROFILE=1` set before the process starts: timing of `get()`/`get_batch()` for `name` (`method=1`/`2`), in seconds unless noted. C++ counters for this variable: `calls` (get + get_batch), `rows`, `lock_wait`, `mr` (receive-buffer registration, including cache checks), `mr_miss` (registrations, a count), `read` (posting `fi_read`), `cq` (waiting for completions). Python counters for the whole store: `py_gets`, `py_get` (whole calls), `py_sync` (`torch.cuda.synchronize()` on the GPU path). All zero for `method=0` or without profiling. See [Performance](performance.md). + +--- + +## `join(name)` + +`method=2` extra member only. Discovers a variable published by the core group by polling the handshake directory until the combined record file (`{name}.bin`) written by core rank 0 reaches its expected size (up to `DDSTORE_HANDSHAKE_TIMEOUT_S` seconds), then registers it for `get()`. + +| Parameter | Type | Description | +|---|---|---| +| `name` | `str` | Variable identifier, matching the `name` used in the core group's `add()` | + +--- + +## `info(name)` + +Returns `(total_rows, disp, itemsize)` for a variable that has been `add()`-ed or `join()`-ed. Useful on the extra side to size output buffers without hardcoding shapes. + +--- + +## `epoch_begin()` / `epoch_end()` + +Open and close an MPI RMA access epoch (calls `MPI_Win_fence`). **Collective**. Required around `get()` calls when using `method=0`. No-op for `method=1`/`2`. + +--- + +## `free()` + +Release every variable's MPI window (`method=0`) or libfabric endpoints and memory registrations, including [`register_recv()`](api-pyddstore.md#register_recvname-arr--unregister_recvname-arr) buffers (`method=1`/`2`), then the host buffer DDStore allocated for it in `add()`/`init()` (a GPU tensor passed to `add()` is the caller's and is not freed). Safe to call more than once. After `MPI_Finalize` the MPI window and buffer can no longer be released and are skipped. diff --git a/docs/api-torch.md b/docs/api-torch.md new file mode 100644 index 0000000..dbb9a76 --- /dev/null +++ b/docs/api-torch.md @@ -0,0 +1,22 @@ +# `pyddstore.torch` reference + +Generated from the docstrings. For how the pieces fit together, see +[PyTorch integration](pytorch.md). + +```{eval-rst} +.. automodule:: pyddstore.torch + :no-members: + +.. autoclass:: pyddstore.torch.DistDataset(source, name, comm=None, ddstore_width=None, device=None, add_device=None, method=None, handshake_dir=None, chunk_size=None, encode=None, decode=None, fields=None) + :members: read_rows, alloc, release, shapes, dtypes, __getitems__ + +.. autoclass:: pyddstore.torch.DistDatasetReader(name, handshake_dir=None, n_core=None, device=None, decode=None) + :members: read_rows, alloc, release, shapes, dtypes, __getitems__ + +.. autoclass:: pyddstore.torch.WindowedDataset(ds, window, stride=1, dilation=1, starts=None, fields=None) + +.. autofunction:: pyddstore.torch.row_of + +.. autoclass:: pyddstore.torch.ThreadDataLoader(dataset, reuse_buffers=False, collate_copies=False, **DataLoader_kwargs) + :members: close +``` diff --git a/docs/backends.md b/docs/backends.md new file mode 100644 index 0000000..97d8d27 --- /dev/null +++ b/docs/backends.md @@ -0,0 +1,76 @@ +# Backends + +## MPI RMA (`method=0`, default) + +Uses `MPI_Win_create` and `MPI_Get` for one-sided remote reads. Works on any MPI-capable cluster without additional hardware. `epoch_begin`/`epoch_end` are required to delimit access epochs. + +## libfabric RDMA (`method=1`) + +Uses `fi_read` for true RDMA transfers over high-speed interconnects (Infiniband/verbs, Cray GNI, Intel PSM2, Cray Slingshot). Lower latency than MPI RMA on supported hardware. `epoch_begin`/`epoch_end` are no-ops with this backend. + +**`DDSTORE_FABRIC`** selects which libfabric provider to open, for `method=1`/`2`: + +- `hsn` (default, unset) — opens the `tcp;ofi_rxm` domain over Cray Slingshot (Frontier). +- `cxi` — opens the native `cxi` domain over Cray Slingshot (Frontier and Perlmutter; Perlmutter is CXI-only). Required for [GPUDirect RDMA](gpudirect.md), and the default in the Frontier job scripts. + +The two are independent code paths (not runtime auto-detection), so set this explicitly per system rather than relying on a guess: + +```bash +export DDSTORE_FABRIC=hsn # tcp;ofi_rxm (default) +export DDSTORE_FABRIC=cxi # native CXI: Perlmutter, or Frontier with GPUDirect +``` + +`PyDDStore` picks the network interface (`FABRIC_IFACE`) automatically for `method=1`/`2`, based on each rank's real CPU affinity (`os.sched_getaffinity`) — no changes needed in your code: + +- **`DDSTORE_NIC_MAP`** — a precomputed CPU→NIC map, used directly if set (no NIC discovery at construction time). Generate it once from a context with reliable NIC visibility, e.g. an `sbatch` batch step's own shell (not a nested `srun` task — NIC/PCI discovery has been observed to fail there), and export it before launching ranks so every one inherits it: + ```bash + export DDSTORE_NIC_MAP=$(python3 -m cpu_nic_map --env) + srun ... python train.py + ``` +- If `DDSTORE_NIC_MAP` isn't set, each rank falls back to a live `hwloc-calc`/`lstopo` query against its own CPU affinity (`cpu_nic_map.allocated_nics()`, also runnable standalone as `python3 cpu_nic_map.py --allocated`) to find the nearest NIC. +- Set `FABRIC_IFACE` explicitly to override both and force a specific interface, e.g. when the automatic selection picks the wrong one: + ```bash + export FABRIC_IFACE=hsn0 # e.g. Cray Slingshot + ``` +- Or skip the environment entirely and pass a map straight to the constructor: `PyDDStore(comm, method=1, nic_map="hsn0=0-15,64-79;hsn1=...")`. + +## File-based handshake (`method=2`) + +Splits the dataset-holding job from the training job entirely: a **core** group loads and publishes data, and a separate **extra** group reads it over RDMA (`fi_read`, same transport as `method=1`) — the two are independent MPI jobs (e.g. two separate `srun`/`mpirun` launches, possibly on different node allocations) that never share a communicator. They rendezvous only through record files written to a shared-filesystem directory (must be visible to all nodes, e.g. Lustre): + +- **Core member** — has an MPI communicator, publishes with `add()`/`init()`. Core ranks exchange records via `MPI_Allgather`, and rank 0 writes the combined set to a single `{name}.bin` file (fabric address, MR key, base pointer, row count, dtype per rank) into `handshake_dir`. +- **Extra member** — no MPI communicator; constructed with `comm_or_none=None` and an explicit `n_core`. Calls `join(name)` to poll for and read all `n_core` core-rank records, then `get()` works exactly as on the core side, reading directly from core-rank memory over RDMA. + +```python +# core side — one MPI job +store = dds.PyDDStore(comm, method=2, handshake_dir="/lustre/.../ddstore_hs") +store.add("x", data) +... # wait for the extra side to finish (e.g. a sentinel file) +store.free() + +# extra side — a separate MPI job, no comm needed +store = dds.PyDDStore(None, method=2, handshake_dir="/lustre/.../ddstore_hs", n_core=4) +store.join("x") +out = np.zeros((1, ncols), dtype=np.float32) +store.get("x", out, start=global_idx) +store.free() +``` + +Environment variables: `DDSTORE_HANDSHAKE_DIR`, `DDSTORE_HANDSHAKE_TIMEOUT_S`, `DDSTORE_FABRIC` and `DDSTORE_NIC_MAP` — see [Environment variables](environment.md). + +See [test/test_method2_core.py](https://github.com/ORNL/DDStore/blob/main/test/test_method2_core.py) / [test/test_method2_extra.py](https://github.com/ORNL/DDStore/blob/main/test/test_method2_extra.py) for a minimal runnable pair, and [examples/vae/vae_core_server.py](https://github.com/ORNL/DDStore/blob/main/examples/vae/vae_core_server.py) / [examples/vae/vae_extra_train.py](https://github.com/ORNL/DDStore/blob/main/examples/vae/vae_extra_train.py) for a full DDP training example using this split. + +`ddstore_width` grouping (below) is not currently supported with `method=2` — every core rank in `comm` is treated as one group. + + +## Partitioned / Sub-communicator Usage + +`PyDDStore` always spans the whole communicator you pass it. To run several independent stores side by side (e.g. one per node), split `comm` first; each group then holds a full replica of the dataset, partitioned across its own members: + +```python +width = 4 # ranks per group, e.g. GPUs per node +sub_comm = comm.Split(rank // width, rank) +store = dds.PyDDStore(sub_comm) # one independent store per group +``` + +This keeps sample fetches inside a node at the cost of replicating the data per group. `DistDataset` exposes it as `ddstore_width` (`None` = one store across all ranks). Not supported with `method=2`. diff --git a/docs/citation.md b/docs/citation.md new file mode 100644 index 0000000..7fcf368 --- /dev/null +++ b/docs/citation.md @@ -0,0 +1,28 @@ +# Citation and license + +If you use DDStore in your research, please cite: + +```bibtex +@inproceedings{choi2023ddstore, + title={DDStore: Distributed data store for scalable training of graph neural networks on large atomistic modeling datasets}, + author={Choi, Jong Youl and Lupo Pasini, Massimiliano and Zhang, Pei and Mehta, Kshitij and Liu, Frank and Bae, Jonghyun and Ibrahim, Khaled}, + booktitle={Proceedings of the SC'23 Workshops of the International Conference on High Performance Computing, Network, Storage, and Analysis}, + pages={941--950}, + year={2023} +} +``` + +```bibtex +@inproceedings{bae2024mdloader, + title={MDLoader: A Hybrid Model-Driven Data Loader for Distributed Graph Neural Network Training}, + author={Bae, Jonghyun and Choi, Jong Youl and Lupo Pasini, Massimiliano and Mehta, Kshitij and Zhang, Pei and Ibrahim, Khaled}, + booktitle={SC24-W: Workshops of the International Conference for High Performance Computing, Networking, Storage and Analysis}, + year={2024}, + month={nov}, + doi={10.1109/SCW63240.2024.00145} +} +``` + +## License + +See [LICENSE](https://github.com/ORNL/DDStore/blob/main/LICENSE). diff --git a/docs/concurrency.md b/docs/concurrency.md new file mode 100644 index 0000000..5d3847b --- /dev/null +++ b/docs/concurrency.md @@ -0,0 +1,5 @@ +# Concurrency + +- `get()` and `get_batch()` are thread-safe. For `method=1`/`2` a per-variable lock in `DDStore::get()` serializes calls on one variable (it guards shared receive state; without it concurrent calls crashed). Both release the GIL during the transfer. +- `method=0` `get_batch()` is **collective**: every rank calls it the same number of times, in the same order, from one thread. `vae-ddp.py` therefore allows no worker threads with `method=0`. +- Only the main thread calls MPI (setup, `epoch_begin`/`epoch_end`, `method=0` reads); mpi4py's default `MPI_THREAD_MULTIPLE` is fine, `FUNNELED` is the minimum. If you call MPI from your own worker threads, keep `MULTIPLE`. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..2a9f62f --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,39 @@ +"""Sphinx configuration for the DDStore documentation (MyST Markdown).""" + +import os +import sys + +# Document pyddstore from the source tree; its compiled core, MPI and torch +# are mocked, so the docs build needs neither a compiler, MPI nor libfabric. +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))) + +project = "DDStore" +author = "Oak Ridge National Laboratory" +copyright = "UT-Battelle, LLC" + +extensions = [ + "myst_parser", + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", +] +source_suffix = {".md": "markdown"} +exclude_patterns = ["_build"] + +# GitHub-style anchors on headings, so links like page.md#get_batchname-arr-indices work +myst_heading_anchors = 3 +myst_enable_extensions = ["colon_fence"] + +autodoc_mock_imports = ["mpi4py", "torch", "pyddstore._core"] +autodoc_member_order = "bysource" +autodoc_typehints = "none" +napoleon_google_docstring = True +napoleon_numpy_docstring = False + +html_theme = "furo" +html_title = "DDStore" +html_logo = "../images/DDStore-logo.png" +html_theme_options = { + "source_repository": "https://github.com/ORNL/DDStore/", + "source_branch": "main", + "source_directory": "docs/", +} diff --git a/docs/environment.md b/docs/environment.md new file mode 100644 index 0000000..5ac8e4a --- /dev/null +++ b/docs/environment.md @@ -0,0 +1,39 @@ +# Environment variables + +**Read by DDStore itself** (the C++ library, `pyddstore`, `cpu_nic_map`): + +| Variable | Default | Effect | +|---|---|---| +| `DDSTORE_FABRIC` | `hsn` | libfabric provider for `method=1`/`2`: `hsn` (`tcp;ofi_rxm`) or `cxi` (native Slingshot; required for [GPUDirect RDMA](gpudirect.md)). See [libfabric RDMA](backends.md#libfabric-rdma-method1). | +| `FABRIC_IFACE` | auto | Network interface (libfabric domain, e.g. `cxi0`, `hsn0`) for `method=1`/`2`. Set it to force one; otherwise picked from the rank's CPU affinity. | +| `DDSTORE_NIC_MAP` | unset | Precomputed CPU→NIC map used for that automatic pick instead of a live hwloc query (`python3 -m cpu_nic_map --env`). The constructor's `nic_map=` argument takes priority. | +| `DDSTORE_HANDSHAKE_DIR` | `./ddstore_hs` | `method=2` handshake directory when none is given (C++ API; `PyDDStore` requires `handshake_dir`, and the examples fill it from this variable). Must be on a shared filesystem. | +| `DDSTORE_HANDSHAKE_TIMEOUT_S` | `300` | Seconds a `method=2` extra member's `join()` polls for the core group's record file. | +| `DDSTORE_PROFILE` | off | `1` turns on `get()`/`get_batch()` timing counters, read with `get_profile(name)`. See [Performance](performance.md). | +| `DDSTORE_MAX_READ_BYTES` | `1073741824` (1 GiB) | `method=1`/`2`: largest single `fi_read`; longer rows are read in pieces (on Perlmutter's `cxi` one 5 GB read fails with `EMSGSIZE`, 2.5 GB works, and the provider doesn't report the limit). Lowered to the endpoint's `max_msg_size` when the provider reports one. | +| `DDSTORE_ALLTOALL_MAX_BYTES` | `2097152` (2 MiB) | `method=0` `get_batch()`: bytes each rank receives per exchange round. Must be equal on all ranks. | + +**Read by `pyddstore.torch`** (defaults for arguments not given): + +| Variable | Default | Effect | +|---|---|---| +| `DDSTORE_METHOD` | `0` | `DistDataset`'s backend when `method=` isn't passed: `0` MPI RMA, `1` libfabric, `2` file-based handshake. (`PyDDStore` itself takes `method=` only.) | +| `DDSTORE_BATCH_GET` | `1` | `DistDataset.__getitems__` reads a whole batch with one `get_batch()` per field; `0` reads one sample at a time. | +| `DDSTORE_HANDSHAKE_DIR`, `DDSTORE_HANDSHAKE_TIMEOUT_S` | `./ddstore_hs`, `300` | `method=2` directory, and how long `DistDatasetReader` waits for the core group to publish. | +| `DDSTORE_N_CORE` | unset | `DistDatasetReader`'s number of core ranks when `n_core=` isn't passed (the examples default it to 4). | +| `DDSTORE_AFFINITY_WIDTH` / `DDSTORE_AFFINITY_OFFSET` | `0` / `0` | `ThreadDataLoader`: pin worker thread *i* to CPUs `[offset + i·width, offset + (i+1)·width)` of the process's affinity; width `0` = no pinning. | + +**Read by the examples** (`examples/vae/`, `examples/scripts/`, job scripts): + +| Variable | Default | Effect | +|---|---|---| +| `DDSTORE_BACKEND` | auto | `torch.distributed` backend for the examples' DDP setup (`nccl`, `gloo`, `xccl`). | +| `VAE_PROFILE` | off | `1`: `vae-ddp.py` prints per-epoch fetch vs compute time. | +| `MASTER_PORT` | `2345` | DDP rendezvous port; the core/extra job script gives each step its own. | + +**System settings that matter on Frontier:** + +| Variable | Effect | +|---|---| +| `SLINGSHOT_VNIS` | Set by Slurm per step. With `--network=job_vni`, keep only the last (job-wide) entry before starting Python so separate `srun` steps can reach each other — see [Multiple `srun` steps](hpc.md#multiple-srun-steps-in-one-job-method2-cxi). | +| `GPU_MAX_HW_QUEUES` | ROCm hardware queues per GPU per process (default 4); raise it if data-loading threads use their own streams — see [HIP streams](results.md#hip-streams-and-hardware-queues-frontier-rocm-72). | diff --git a/docs/gpudirect.md b/docs/gpudirect.md new file mode 100644 index 0000000..bd4c650 --- /dev/null +++ b/docs/gpudirect.md @@ -0,0 +1,19 @@ +# GPUDirect RDMA + +`add()`, `get()` and `get_batch()` accept a CUDA/HIP `torch.Tensor` in place of a NumPy array, so RDMA reads from or writes directly into GPU memory, with no `.cpu()`/`.to(device)` copy. Requires `method=1` or `2`, **`DDSTORE_FABRIC=cxi`**, and a CUDA- or ROCm-enabled PyTorch. A GPU tensor with `DDSTORE_FABRIC=hsn` (the default) or `method=0` raises a clear error instead of silently copying through the host. + +```python +import torch +data = torch.rand(1024, 64, dtype=torch.float32, device="cuda") +store.add("features", data) # GPU source, no host copy + +out = torch.empty((1, 64), dtype=torch.float32, device="cuda") +store.get("features", out, start=2048) # GPU destination, no host copy +``` + +- **`add()` with a GPU tensor registers your tensor's own memory; no copy is made.** Keep it alive and unmodified until `free()`. `PyDDStore` holds a reference as a safety net, and adding the same name again with a GPU tensor is rejected. (With NumPy, `add()` copies and the array can be reused right away.) +- **The device is synchronized before each GPU transfer** (`torch.cuda.synchronize()`, once per `get()` / `get_batch()` call): the NIC writes outside PyTorch's stream ordering, and without the sync training hit GPU memory faults. Prefer `get_batch()` on the GPU path so this costs one sync per batch, not per sample. +- `init()`/`update()` stay host-only. +- Whether GPU destinations are faster than host ones depends on the machine: on Frontier they win from ~12.5 KB rows up, on Perlmutter host destinations win at every size ([results](results.md#bench_getpy-µs-per-row)). + +Examples: [test/test_gpu_rdma.py](https://github.com/ORNL/DDStore/blob/main/test/test_gpu_rdma.py), and `--gpu-dest`/`--gpu-source` on [vae-ddp.py](https://github.com/ORNL/DDStore/blob/main/examples/vae/vae-ddp.py), [vae_extra_train.py](https://github.com/ORNL/DDStore/blob/main/examples/vae/vae_extra_train.py) and [vae_core_server.py](https://github.com/ORNL/DDStore/blob/main/examples/vae/vae_core_server.py). diff --git a/docs/hpc.md b/docs/hpc.md new file mode 100644 index 0000000..8826d83 --- /dev/null +++ b/docs/hpc.md @@ -0,0 +1,60 @@ +# HPC systems (Slurm, Slingshot) + +Running DDStore under Slurm on Cray Slingshot systems (Frontier, Perlmutter): +the example job scripts, which `--network` options a layout needs, and what +to do when RDMA can't connect. + +## Do you need `--network` flags? + +Every `srun` step gets its own Slingshot VNI (network isolation ID), and two +endpoints can only reach each other on the same VNI. Whether you need +`#SBATCH --network=single_node_vni,job_vni` depends on how steps talk: + +| Layout | Perlmutter, no flags | Perlmutter, `single_node_vni,job_vni` | Frontier | +|---|---|---|---| +| One `srun` step (any number of nodes): normal training, `method=0`/`1` | works | works | works; `single_node_vni` needed if the step has one node | +| Core and extra as separate one-node steps (`--layout=split-node`) | works: one-node steps get no VNI and share the default one | needs `job_vni` + the wrapper below | needs both flags + the wrapper | +| Core and extra on the same nodes, or steps spanning several nodes | extra can't reach core | needs `job_vni` + the wrapper + `srun --overlap` | second step fails to launch | + +The Perlmutter columns were measured on 2 nodes (job scripts and logs in the +[results](results.md#perlmutter-validation-2-nodes--4-a100-cuda-13-cxi)). +## Slurm job scripts + +[job-vae-single.sh](https://github.com/ORNL/DDStore/blob/main/examples/vae/script/job-vae-single.sh) runs `vae-ddp.py` as one `srun` step; [job-vae-core-extra.sh](https://github.com/ORNL/DDStore/blob/main/examples/vae/script/job-vae-core-extra.sh) runs the core/extra split as two steps. Run either with `--help` for all options. Their `#SBATCH` lines target Frontier (`-A FUS184`, 8 ranks × 7 cores per node); see below for Perlmutter. + +```bash +sbatch examples/vae/script/job-vae-single.sh --method=1 --num-workers=1 +sbatch examples/vae/script/job-vae-single.sh --method=1 --gpudirect --image-scale=2 +sbatch examples/vae/script/job-vae-core-extra.sh # split-node: 1 core node, the rest extra +sbatch examples/vae/script/job-vae-core-extra.sh --gpudirect --core-nnodes=2 +``` + +`job-vae-core-extra.sh` sets up Slingshot networking for its two steps (see [Multiple `srun` steps](hpc.md#multiple-srun-steps-in-one-job-method2-cxi)). `--layout=colocate` (both steps on the same nodes) works on Perlmutter only. + +Both scripts also run on Perlmutter: they detect the machine (`NERSC_HOST`) and use 4 ranks per node, with the training ranks seeing all 4 GPUs of their node (NCCL needs that there). Override the Frontier `#SBATCH` lines when submitting: + +```bash +sbatch -A -C gpu --gpus-per-node=4 examples/vae/script/job-vae-single.sh --method=1 +``` + + +## Multiple `srun` steps in one job (`method=2`, `cxi`) + +On Slingshot every `srun` step gets its own VNI (network isolation ID), and two endpoints can only talk on the same VNI. For core and extra running as separate steps, [job-vae-core-extra.sh](https://github.com/ORNL/DDStore/blob/main/examples/vae/script/job-vae-core-extra.sh) does both of these: + +1. `#SBATCH --network=single_node_vni,job_vni`: `job_vni` adds a job-wide VNI to every step (`SLINGSHOT_VNIS=,`); on Frontier, `single_node_vni` is also what gives single-node steps a CXI service at all (without it `fi_domain()` fails with `-38`). +2. In each task, before Python starts: `export SLINGSHOT_VNIS=${SLINGSHOT_VNIS##*,}`. libfabric's cxi provider uses only the first VNI listed (the step's own), so without this reads fail with `VNI_NOT_FOUND`. + +Colocating both steps on the same nodes: + +| | `job_vni` + wrapper | `job_vni` + wrapper + `srun --overlap` | no `--network` | +|---|---|---|---| +| Frontier | second step fails to launch: `Error configuring interconnect` | same failure | each step has only its own VNI: extra cannot reach core | +| Perlmutter | second step does not start | **works** | extra cannot reach core | + +So the script defaults to `--layout=split-node`; `--layout=colocate` (with `--overlap`) is for Perlmutter. Perlmutter's single-node steps also work without the `--network` flags. + + +## Troubleshooting: RDMA fails to connect (`cxi`) + +If `fi_domain()` fails with `-38 (Function not implemented)`, the step has no CXI service: add `#SBATCH --network=single_node_vni` (needed on Frontier for any single-node step, i.e. a `-N 1` job or a one-node step inside a larger job). If ranks in different `srun` steps can't reach each other (`VNI_NOT_FOUND`), see [Multiple `srun` steps](hpc.md#multiple-srun-steps-in-one-job-method2-cxi) above. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..4265377 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,52 @@ +# DDStore + +DDStore logo + +Efficient distributed data loading for distributed data-parallel (DDP) training. + +Each MPI rank holds a shard of the full dataset in memory. DDStore exposes a global index space so any rank can read any sample via one-sided remote memory access — either MPI RMA (default) or libfabric RDMA — without coordinator synchronization. + +- **Batched reads**: [`get_batch()`](api-pyddstore.md#get_batchname-arr-indices) fetches a whole training batch in one call (one-sided RDMA reads in flight together, or an MPI collective for `method=0`). +- **GPUDirect RDMA**: data can live in, and be read straight into, GPU memory ([details](gpudirect.md)). +- **PyTorch integration**: [`pyddstore.torch`](pytorch.md) turns any map-style dataset into a distributed one (`DistDataset`) and provides a thread-based `ThreadDataLoader` that is safe with MPI and GPU buffers. +- **Thread-safe** reads, a [profiler](performance.md) for where read time goes, and a split mode (`method=2`) where a separate job reads data published by another. + +DDStore architecture + +```{toctree} +:caption: Getting started +:maxdepth: 2 + +installation +quickstart +``` + +```{toctree} +:caption: User guide +:maxdepth: 2 + +backends +gpudirect +pytorch +hpc +performance +concurrency +``` + +```{toctree} +:caption: Reference +:maxdepth: 2 + +api-pyddstore +api-torch +environment +``` + +```{toctree} +:caption: More +:maxdepth: 1 + +testing +results +citation +``` diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..bb088df --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,47 @@ +# Installation + +## Prerequisites + +| Dependency | Notes | +|---|---| +| MPI (OpenMPI / MPICH) | `mpicc` and `mpicxx` must be on `PATH` | +| libfabric | Required for the RDMA backends (`method=1` and `method=2`) | +| Python ≥ 3.9 | | +| NumPy, mpi4py, Cython | Python build dependencies | +| PyTorch (optional) | For `pyddstore.torch` and GPU buffers (CUDA or ROCm build) | + +## Building and installing + +```bash +# Install Python build dependencies +pip install numpy mpi4py Cython + +# Build in-place (use with PYTHONPATH=$PWD/src:$PYTHONPATH) +CC=mpicc CXX=mpicxx python setup.py build_ext --inplace + +# Or install into the active virtual environment +CC=mpicc CXX=mpicxx pip install . +CC=mpicc CXX=mpicxx pip install ".[torch]" # also pulls PyTorch, for pyddstore.torch + +# Or install in editable/development mode +CC=mpicc CXX=mpicxx pip install -e . + +# Or install directly from GitHub +CC=mpicc CXX=mpicxx pip install git+https://github.com/ORNL/DDStore.git +``` + +To build against the packages already in the current environment (e.g. an `mpi4py` built against Cray MPICH) instead of letting pip fetch fresh build dependencies into an isolated build environment, disable build isolation: + +```bash +CC=cc CXX=CC pip install --no-build-isolation --no-deps -e . +``` + +If that fails with `ModuleNotFoundError: No module named 'distutils.msvccompiler'` (newer setuptools combined with an older system NumPy, e.g. `cray-python/3.11.7` on Frontier), point setuptools at the standard-library `distutils` for the build: + +```bash +SETUPTOOLS_USE_DISTUTILS=stdlib CC=cc CXX=CC pip install --no-build-isolation --no-deps -e . +``` + +The package is `pyddstore` (compiled core `pyddstore._core`, plus `pyddstore.torch`). After updating from a 1.x checkout, rebuild; an old `src/pyddstore.cpython-*.so` or `src/pyddstore.cpp` left behind is unused and can be deleted (the build warns about them). + +Editable and in-place builds keep the generated `src/pyddstore/_core.cpp` in the checkout, shared by every environment that builds from it. `setup.py` regenerates it whenever the NumPy major version differs from the previous build's, because a file generated against NumPy 2 doesn't compile against NumPy 1.x headers. diff --git a/docs/performance.md b/docs/performance.md new file mode 100644 index 0000000..1c98047 --- /dev/null +++ b/docs/performance.md @@ -0,0 +1,8 @@ +# Performance + +- Use **batched reads** (the default with `DistDataset`, or `get_batch()` directly). They cut per-sample cost by 10–27× for small rows and make the GPU path insensitive to worker threads; in the VAE every configuration got 1.2–3.9× faster per epoch. +- **Reuse destination buffers** and [`register_recv()`](api-pyddstore.md#register_recvname-arr--unregister_recvname-arr) them. Reading into a fresh buffer every time re-registers memory on every read; `get_profile(name)["mr_miss"]` counts those registrations. +- `method=1` (one-sided `fi_read`) is the fastest backend; `method=0` with batching (collective) comes close for small rows. +- `DDSTORE_PROFILE=1` + `get_profile(name)` shows where `get()`/`get_batch()` time goes: lock wait, memory registration, posting and completing `fi_read`, GPU sync. `vae-ddp.py` prints an all-rank summary when it is set. [examples/scripts/bench_get.py](https://github.com/ORNL/DDStore/blob/main/examples/scripts/bench_get.py) measures per-row latency and throughput vs row size, destination, batch size and threads. + +Measurements, profiles and the experiments behind these choices: [docs/results.md](results.md). diff --git a/docs/pytorch.md b/docs/pytorch.md new file mode 100644 index 0000000..b02dfb3 --- /dev/null +++ b/docs/pytorch.md @@ -0,0 +1,62 @@ +# PyTorch integration + +`pyddstore.torch` (needs PyTorch: `pip install .[torch]` or an existing PyTorch) turns any map-style dataset into a distributed one: + +```python +import torch # import torch before MPI starts +from mpi4py import MPI +from pyddstore.torch import DistDataset, ThreadDataLoader + +trainset = DistDataset(my_dataset, "train", MPI.COMM_WORLD) # each rank loads only its share +sampler = torch.utils.data.distributed.DistributedSampler(trainset) +loader = ThreadDataLoader(trainset, batch_size=128, sampler=sampler, num_workers=1) +for x, y in loader: + ... +``` + +- **`DistDataset(source, name, comm=None, ddstore_width=None, device=None, add_device=None, method=None, handshake_dir=None, chunk_size=None, encode=None, decode=None, fields=None)`**: each rank loads its contiguous share of `source` (anything with `len()` and `[i]`) into DDStore; every rank can then read every sample. Samples keep the source's structure (a tensor, numpy array or number; a tuple or list of them; or a dict of them), with each field's shape and dtype. Fields must have the same shape and dtype in every sample; supported dtypes are bool, uint8, int32, int64, float32 and float64. A field (or the whole sample) may also be a numpy structured record (`np.void`, a structured `ndarray`, or `np.recarray`) of any field types: it is stored as raw bytes and comes back as the same kind of object with the same layout (`default_collate` can't batch records, so pass a `collate_fn`). `ds.shapes` / `ds.dtypes` describe the fields, `ds.ddstore` is the underlying `PyDDStore`. + - `method` (default `DDSTORE_METHOD` or 0) picks the backend; `ddstore_width` splits `comm` into independent stores (see [Partitioned usage](backends.md#partitioned--sub-communicator-usage)). + - `device` puts tensor fields of read samples on a GPU and `add_device` keeps each rank's share there ([GPUDirect](gpudirect.md)). + - `chunk_size` loads each rank's share that many samples at a time, writing each chunk into the store before reading the next: peak memory is about the share plus one chunk, instead of about three times the share (400 MiB share: 461 vs 1202 MiB). Host storage only (not with `add_device`). + - `encode` / `decode` / `fields` handle samples that can't be stored as they are (strings, labels, metadata objects, data shared by a group of samples). `encode(sample)` runs on every source sample before it is stored and returns what to store; `decode(stored, index)` runs on every sample read (`ds[i]`, `__getitems__`, so also in loaders) and rebuilds the full sample, e.g. adding constants or looking up tables by index or by a stored id. `decode` runs on the reading rank, so anything it looks up must exist on every rank. `fields=[...]` keeps only those keys (dict samples) or positions (tuple/list samples); it can't be combined with `encode`. `read_rows()` and `WindowedDataset` return stored rows without `decode`. `DistDatasetReader` takes `decode` too. + +```python +LABELS = ["cat", "dog", "owl"] +ds = DistDataset(src, "pets", comm, + encode=lambda s: {"x": s["x"], "label": LABELS.index(s["label"]), "group": s["group"]}, + decode=lambda d, i: {**d, "label": LABELS[d["label"]], "meta": GROUP_INFO[d["group"]]}) +``` + + - **Batched by default**: `__getitems__` reads a whole batch with one [`get_batch()`](api-pyddstore.md#get_batchname-arr-indices) per field, which `DataLoader` and `ThreadDataLoader` call automatically; `DDSTORE_BATCH_GET=0` reads one sample at a time. With `method=0` batched reads are collective, so every rank must iterate the same number of batches from one thread (`DistributedSampler` does). +- **Reading rows and reusing buffers** (`DistDataset` and `DistDatasetReader`): + - `ds.read_rows(rows, fields=None, out=None)` reads stored rows `rows` (any order, repeats allowed) of the selected fields (default all), one `get_batch()` per field, and returns a dict of values shaped `(len(rows), *field_shape)`. Keys are the sample's: dict keys, tuple/list positions, or `0` for a single value. With `method=0` it is collective, like `get_batch()`. + - `ds.alloc(n, fields=None)` returns buffers for `n` rows, one per field and keyed the same way, each [registered](api-pyddstore.md#register_recvname-arr--unregister_recvname-arr) once. Pass them as `out=` to `read_rows()` or `ds.__getitems__(idx, out=)`: reads then skip memory registration, and the results are views into the buffers, so you decide when a buffer can be reused. `ds.release(bufs)` unregisters them; `free()` does too. +- **`WindowedDataset(ds, window, stride=1, dilation=1, starts=None, fields=None)`**: samples made of several stored rows of `ds` (time windows, clips, sequences), each stored row held once. Sample `i` is rows `s, s + dilation, …, s + (window - 1)·dilation` with `s = i·stride`, or `s = starts[i]` when `starts` is given; use `starts` to keep only windows that don't cross a trajectory or file boundary. Fields come back stacked, `(window, *field_shape)`, in the structure of `ds`'s samples (a dict when `fields` is given). A batch of windows is one `read_rows()`. +- **`row_of(concat, source, index)`**: with several sources in one store (a `DistDataset` over a `torch.utils.data.ConcatDataset`), the row of sample `index` of source `source`, e.g. to map (file, trajectory, step) to `starts`: + +```python +from torch.utils.data import ConcatDataset +from pyddstore.torch import DistDataset, WindowedDataset, row_of + +files = ConcatDataset([StepsOf(f) for f in paths]) # one sample per time step +frames = DistDataset(files, "frames", comm, method=1) +starts = [row_of(files, k, t) for k, f in enumerate(paths) # windows inside each file + for t in range(0, len(files.datasets[k]) - 2 * dt)] +pairs = WindowedDataset(frames, window=3, dilation=dt, starts=starts) # (t, t+dt, t+2dt) +``` + +- **`DistDatasetReader(name, handshake_dir=None, n_core=None, device=None, decode=None)`**: the same dataset read by a separate `method=2` extra job; it learns the fields from a `{name}.meta.json` file the core group writes next to the handshake records. +- **`ThreadDataLoader(dataset, reuse_buffers=False, collate_copies=False, **DataLoader args)`**: a `DataLoader` whose workers are threads, not forked processes, so it is safe with MPI and GPU buffers. Each batch is fetched, collated and optionally pinned in a worker thread; random draws match `DataLoader`'s. As with `DataLoader`, `iter(loader)` returns a separate iterator for one epoch (`list(it)` or `islice(it, …)` after `next(it)` continue the epoch; iterators over one loader are independent), and while the training step holds a batch, `num_workers * prefetch_factor` more are being fetched, so that many plus one are in memory. One or two workers are enough: reads on one variable are serialized by its lock, and one batched read already keeps the network busy. + - `reuse_buffers=True` reads every batch into one of a fixed pool of `num_workers` buffer sets from `dataset.alloc(batch_size)`, registered once, instead of fresh buffers registered on every read (for large rows, registration can cost more than the transfer). A worker reads and collates its batch, then returns the set, so the collate must copy: it needs `batch_size` and the default `collate_fn`, or `collate_copies=True` to declare that your `collate_fn` copies; other setups raise. `loader.close()` (or deleting the loader) waits for running fetches and unregisters the pool. `DDSTORE_AFFINITY_WIDTH` / `DDSTORE_AFFINITY_OFFSET` pin worker threads to CPUs. + +[examples/vae/vae-ddp.py](https://github.com/ORNL/DDStore/blob/main/examples/vae/vae-ddp.py) trains a VAE with DDP on top of it: + +```bash +DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --num-workers=1 +DDSTORE_METHOD=1 DDSTORE_FABRIC=cxi mpirun -n 4 python examples/vae/vae-ddp.py --num-workers=1 --gpu-dest --gpu-source +``` + +- **`--num-workers`** (default 0): `0` uses PyTorch's standard `DataLoader`; `> 0` uses `ThreadDataLoader` (needs `method=1`/`2`). +- **`--gpu-dest` / `--gpu-source`**: `DistDataset`'s `device` / `add_device`. +- **`--replicate R`** repeats the training set R times (longer epochs); **`--image-scale S`** upscales images to (28·S)² so each row is S² larger. Both default to 1, the original example. +- The [method=2 split](backends.md#file-based-handshake-method2) variant is [vae_core_server.py](https://github.com/ORNL/DDStore/blob/main/examples/vae/vae_core_server.py) (a `DistDataset` core group) + [vae_extra_train.py](https://github.com/ORNL/DDStore/blob/main/examples/vae/vae_extra_train.py) (a `DistDatasetReader`), with the same options. diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..a75f64b --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,32 @@ +# Quick start + +```python +import numpy as np +from mpi4py import MPI +import pyddstore as dds + +comm = MPI.COMM_WORLD +rank = comm.Get_rank() + +# Each rank contributes its own shard +store = dds.PyDDStore(comm) # MPI RMA backend (default) +# store = dds.PyDDStore(comm, method=1) # libfabric RDMA backend + +data = np.random.rand(1024, 64).astype(np.float32) +store.add("features", data) # collective — all ranks must call + +# Read any global sample index +out = np.zeros((1, 64), dtype=np.float32) +store.epoch_begin() +store.get("features", out, start=2048) # global index across all shards +store.epoch_end() + +store.free() +``` + +Run with: +```bash +mpirun -n 4 python my_script.py +``` + +With PyTorch, [`pyddstore.torch.DistDataset`](pytorch.md) does the sharding, `add()` and batched reads for you. diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..d1cf123 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +sphinx>=7 +myst-parser>=2 +furo +numpy diff --git a/docs/results.md b/docs/results.md index f22b5ca..1d8176c 100644 --- a/docs/results.md +++ b/docs/results.md @@ -1,6 +1,6 @@ # DDStore measurements and findings -Measurements behind the recommendations in the [README](../README.md), +Measurements behind the recommendations in the [documentation](index.md), collected on the `check-thread` branch in October 2026. Unless noted: `method=1`, `DDSTORE_FABRIC=cxi`, `vae-ddp.py` with `VAE_PROFILE=1`, epoch times averaged over all epochs but the first. Epochs are short (0.1–0.6 s), diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..a1762fa --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,72 @@ +# Testing + +## Unit tests (pytest) + +Install test dependencies: + +```bash +pip install pytest pytest-mpi +``` + +**Single-rank** — no cluster required, covers all dtypes, `add`/`get`/`init`/`update`, and error cases: + +```bash +mpirun -n 1 python -m pytest test/test_single.py -v +``` + +**Multi-rank** — verifies remote reads across all rank pairs and sub-communicator grouping: + +```bash +mpirun -n 4 python -m pytest test/test_multirank.py -v +``` + +**Batched reads and the PyTorch layer** — method 0 everywhere; method 1, method 2 and GPU cases run inside a Slurm step with a CXI device (provider from `DDSTORE_FABRIC`, default `cxi`): + +```bash +mpirun -n 4 python -m pytest test/test_get_batch.py test/test_torch.py -v +``` + +**GPUDirect RDMA** — requires a live `cxi` fabric and a CUDA/HIP GPU per rank (skipped automatically otherwise); see [GPUDirect RDMA](gpudirect.md): + +```bash +DDSTORE_FABRIC=cxi mpirun -n 2 python -m pytest test/test_gpu_rdma.py -v +``` + +| Test file | Min ranks | What is tested | +|---|---|---| +| `test/test_single.py` | 1 | All dtypes, `add`/`get`, `init`/`update`/`get`, error handling, double `free()` | +| `test/test_multirank.py` | 2 (4 recommended) | Remote reads, shard boundaries, multiple variables, `ddstore_width` grouping | +| `test/test_gpu_rdma.py` | 2 | GPU-resident `add()`/`get()` in both directions, both libfabric methods, negative/error cases | +| `test/test_get_batch.py` | 2 (4 recommended) | `get_batch()`: shuffled indices across ranks with repeats, single row, dtypes, error recovery, GPU destination, concurrent threads, registered destination buffers, wide rows (with `DDSTORE_MAX_READ_BYTES=4096` they are read in pieces); method 0, plus method 1 over `cxi` inside a Slurm step | +| `test/test_torch.py` | 2 (4 recommended) | `pyddstore.torch`: tuple/dict/single samples of every field kind, numpy records, loaders vs the plain source, `ThreadDataLoader` iterators and prefetch depth, `read_rows`/`alloc` buffers, `reuse_buffers` (also with GPU buffers), `WindowedDataset`, `row_of`, `encode`/`decode`/`fields`, chunked loading, error handling, `ddstore_width`, GPU placement, `DistDatasetReader` | + +## Integration scripts + +```bash +# Basic functional test (libfabric, method=1) +mpirun -n 4 python examples/scripts/demo.py + +# Integration test with PyTorch DDP (libfabric, method=1) +mpirun -n 4 python examples/scripts/test.py +``` + +Optional arguments for `examples/scripts/demo.py` and `examples/scripts/test.py`: + +| Flag | Default | Description | +|---|---|---| +| `--num` | `1048576` | Rows per rank | +| `--dim` | `64` | Elements per row | +| `--nbatch` | `32` | Number of random reads | +| `--gloo` / `--nccl` | `--gloo` | `test.py` only: `torch.distributed` backend | + +## Method 2 (file-based handshake) + +Two separate launches sharing a handshake directory on a shared filesystem — not a single `mpirun`, since core and extra are independent jobs: + +```bash +# Terminal 1 — core (data-holding) side +mpirun -n 4 python test/test_method2_core.py /path/to/shared/ddstore_hs + +# Terminal 2 — extra (reader) side, after or while the core side is running +python test/test_method2_extra.py /path/to/shared/ddstore_hs 4 +``` diff --git a/include/ddstore.hpp b/include/ddstore.hpp index 7584661..0747152 100644 --- a/include/ddstore.hpp +++ b/include/ddstore.hpp @@ -102,7 +102,7 @@ class DDStore * caller's own device pointer directly. The caller must keep that GPU * allocation alive (not garbage-collected, not reused) for as long as * this variable stays registered, i.e. until free() or this DDStore's - * destruction. pyddstore.pyx enforces this for Python callers via a + * destruction. pyddstore/_core.pyx enforces this for Python callers via a * keepalive dict; direct C++ callers must manage it themselves. */ template void add(std::string name, T *buffer, long nrows, int disp, int hmem_iface = 0) @@ -449,7 +449,7 @@ class DDStore /* hmem_iface: 0 (FI_HMEM_SYSTEM) for a host buffer, or an fi_hmem_iface * value (FI_HMEM_CUDA, FI_HMEM_ROCR, ...) identifying what kind of GPU * memory `buffer` is. Left as a plain int (not the enum) so the Cython - * binding (pyddstore.pyx) can pass it without cimporting the enum; + * binding (pyddstore/_core.pyx) can pass it without cimporting the enum; * read_from_remote() in common.cxx casts it back before use. */ template void get(std::string name, long start, long count, T *buffer, int hmem_iface = 0) diff --git a/pyproject.toml b/pyproject.toml index 98b7461..cdb8d2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "PyDDStore" version = "2.0" description = "Distributed Data Store" -requires-python = ">=3.6" +requires-python = ">=3.9" dependencies = ["numpy", "mpi4py"] [project.optional-dependencies] diff --git a/src/cpu_nic_map.py b/src/cpu_nic_map.py index 7446b1f..7830022 100644 --- a/src/cpu_nic_map.py +++ b/src/cpu_nic_map.py @@ -9,7 +9,7 @@ - allocated_nics(): affinity-aware wrapper — which NIC(s) is *this process*, given its actual pinning (os.sched_getaffinity), closest to. - select_fabric_iface(): called automatically by PyDDStore.__cinit__ - (src/pyddstore.pyx) for method=1/2 to set FABRIC_IFACE if not already set. + (src/pyddstore/_core.pyx) for method=1/2 to set FABRIC_IFACE if not already set. Kernel NIC names are always hsnN under /sys/class/net, on Frontier and Perlmutter alike -- there is no per-system glob pattern to choose. The cxi @@ -253,7 +253,7 @@ def select_fabric_iface(nic_map=None): else DDSTORE_NIC_MAP when set, or a live hwloc-calc/lstopo query (build_map) against this process's real CPU affinity when neither is. - Called automatically by PyDDStore.__cinit__ (src/pyddstore.pyx) for + Called automatically by PyDDStore.__cinit__ (src/pyddstore/_core.pyx) for method=1/2. nic_map: an explicit precomputed map string (serialize_env()/--env diff --git a/test/test_gpu_rdma.py b/test/test_gpu_rdma.py index d955429..f836f01 100644 --- a/test/test_gpu_rdma.py +++ b/test/test_gpu_rdma.py @@ -316,7 +316,7 @@ def test_gpu_buffer_rejected_on_hsn(comm, monkeypatch): # Set up with cxi so that add() and init_fabric succeed (hsn is not # available on all machines, e.g. Perlmutter which is CXI-only). # Then switch DDSTORE_FABRIC to hsn before get() — the Python-level check - # in pyddstore.pyx reads the env var at get() time and rejects GPU buffers + # in pyddstore/_core.pyx reads the env var at get() time and rejects GPU buffers # with a clear error before touching the fabric. monkeypatch.setenv("DDSTORE_FABRIC", "cxi") store = dds.PyDDStore(comm, method=1) @@ -489,7 +489,7 @@ def test_add_from_gpu_tensor_gpu_dest_cxi_method2(comm, monkeypatch, tmp_path): def test_concurrent_get_thread_safety(comm, monkeypatch): """DDStore::get() releases the GIL for its blocking transfer (see the - `with nogil:` block in pyddstore.pyx), so multiple Python threads can + `with nogil:` block in pyddstore/_core.pyx), so multiple Python threads can genuinely be inside DDStore::get() at the same time. Without synchronization, concurrent calls on the same variable would race on the CQ poll loop and the recv-MR region cache in common.cxx (confirmed

ldTQBoIMiMU@)7mbF7`>uyzcL#cQI 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�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