Skip to content

File kokkos_p2p.h

File List > code_source > templat > include > TempLat > parallel > devices > kokkos > kokkos_p2p.h

Go to the documentation of this file

#ifndef TEMPLAT_PARALLEL_KOKKOS_P2P_H
#define TEMPLAT_PARALLEL_KOKKOS_P2P_H

/* This file is part of TempLat, available at https://cosmolattice.github.io/templat .
   Copyright 2021-2026 The TempLat authors, see AUTHORS.md.
   Released under the MIT license, see LICENSE.md. */

// File info: Main contributor(s): Franz R. Sattler, Year: 2026

#include "TempLat/util/exception.h"

#if (defined(KOKKOS_ENABLE_CUDA) || defined(KOKKOS_ENABLE_HIP))

#if defined(KOKKOS_ENABLE_CUDA)
#include <cuda_runtime.h>
#include <cstdio>
#include <dlfcn.h>
#elif defined(KOKKOS_ENABLE_HIP)
#include <hip/hip_runtime.h>
#endif

namespace TempLat::device_kokkos::p2p
{
  MakeException(GpuP2PException);

  // ============================================================
  // Backend dispatch: thin wrappers mapping CUDA <-> HIP uniformly
  // ============================================================

#if defined(KOKKOS_ENABLE_CUDA)

  using IpcMemHandle_t = cudaIpcMemHandle_t;
  static constexpr size_t IpcHandleSize = sizeof(cudaIpcMemHandle_t);

  inline int getDeviceId()
  {
    int dev;
    auto err = cudaGetDevice(&dev);
    if (err != cudaSuccess) throw GpuP2PException("cudaGetDevice failed: ", cudaGetErrorString(err));
    return dev;
  }

  inline bool canAccessPeer(int srcDevice, int dstDevice)
  {
    int canAccess = 0;
    cudaDeviceCanAccessPeer(&canAccess, srcDevice, dstDevice);
    return canAccess != 0;
  }

  inline void enablePeerAccess(int peerDevice)
  {
    cudaError_t err = cudaDeviceEnablePeerAccess(peerDevice, 0);
    if (err != cudaSuccess && err != cudaErrorPeerAccessAlreadyEnabled)
      throw GpuP2PException("cudaDeviceEnablePeerAccess failed for device ", peerDevice, ": ", cudaGetErrorString(err));
  }

  inline void ipcGetHandle(void *devPtr, void *handle)
  {
    auto err = cudaIpcGetMemHandle(reinterpret_cast<cudaIpcMemHandle_t *>(handle), devPtr);
    if (err != cudaSuccess) throw GpuP2PException("cudaIpcGetMemHandle failed: ", cudaGetErrorString(err));
  }

  inline void *ipcOpenHandle(const void *handle)
  {
    void *ptr;
    auto err = cudaIpcOpenMemHandle(&ptr, *reinterpret_cast<const cudaIpcMemHandle_t *>(handle),
                                    cudaIpcMemLazyEnablePeerAccess);
    if (err != cudaSuccess) throw GpuP2PException("cudaIpcOpenMemHandle failed: ", cudaGetErrorString(err));
    return ptr;
  }

  inline void ipcCloseHandle(void *ptr) { cudaIpcCloseMemHandle(ptr); }

  inline void memcpyAsync(void *dst, const void *src, size_t bytes)
  {
    auto err = cudaMemcpyAsync(dst, src, bytes, cudaMemcpyDefault, 0);
    if (err != cudaSuccess) throw GpuP2PException("cudaMemcpyAsync failed: ", cudaGetErrorString(err));
  }

  inline void streamSynchronize()
  {
    auto err = cudaStreamSynchronize(0);
    if (err != cudaSuccess) throw GpuP2PException("cudaStreamSynchronize failed: ", cudaGetErrorString(err));
  }

#elif defined(KOKKOS_ENABLE_HIP)

  using IpcMemHandle_t = hipIpcMemHandle_t;
  static constexpr size_t IpcHandleSize = sizeof(hipIpcMemHandle_t);

  inline int getDeviceId()
  {
    int dev;
    auto err = hipGetDevice(&dev);
    if (err != hipSuccess) throw GpuP2PException("hipGetDevice failed: ", hipGetErrorString(err));
    return dev;
  }

  inline bool canAccessPeer(int srcDevice, int dstDevice)
  {
    int canAccess = 0;
    hipDeviceCanAccessPeer(&canAccess, srcDevice, dstDevice);
    return canAccess != 0;
  }

  inline void enablePeerAccess(int peerDevice)
  {
    hipError_t err = hipDeviceEnablePeerAccess(peerDevice, 0);
    if (err != hipSuccess && err != hipErrorPeerAccessAlreadyEnabled)
      throw GpuP2PException("hipDeviceEnablePeerAccess failed for device ", peerDevice, ": ", hipGetErrorString(err));
  }

  inline void ipcGetHandle(void *devPtr, void *handle)
  {
    auto err = hipIpcGetMemHandle(reinterpret_cast<hipIpcMemHandle_t *>(handle), devPtr);
    if (err != hipSuccess) throw GpuP2PException("hipIpcGetMemHandle failed: ", hipGetErrorString(err));
  }

  inline void *ipcOpenHandle(const void *handle)
  {
    void *ptr;
    auto err =
        hipIpcOpenMemHandle(&ptr, *reinterpret_cast<const hipIpcMemHandle_t *>(handle), hipIpcMemLazyEnablePeerAccess);
    if (err != hipSuccess) throw GpuP2PException("hipIpcOpenMemHandle failed: ", hipGetErrorString(err));
    return ptr;
  }

  inline void ipcCloseHandle(void *ptr) { hipIpcCloseMemHandle(ptr); }

  inline void memcpyAsync(void *dst, const void *src, size_t bytes)
  {
    auto err = hipMemcpyAsync(dst, src, bytes, hipMemcpyDefault, 0);
    if (err != hipSuccess) throw GpuP2PException("hipMemcpyAsync failed: ", hipGetErrorString(err));
  }

  inline void streamSynchronize()
  {
    auto err = hipStreamSynchronize(0);
    if (err != hipSuccess) throw GpuP2PException("hipStreamSynchronize failed: ", hipGetErrorString(err));
  }

#endif

  // ============================================================
  // Raw GPU memory allocation (bypasses Kokkos header)
  // ============================================================

  // Kokkos::View.data() is offset from the cudaMalloc base by a SharedAllocationHeader
  // (128 bytes). cudaIpcGetMemHandle/cudaIpcOpenMemHandle operate on the allocation base.
  // To avoid this offset issue, IPC-exported buffers are allocated with raw cudaMalloc/hipMalloc
  // and wrapped as unmanaged Kokkos views. This gives us clean base pointers for IPC.

  inline void *rawDeviceMalloc(size_t bytes)
  {
    void *ptr = nullptr;
#if defined(KOKKOS_ENABLE_CUDA)
    auto err = cudaMalloc(&ptr, bytes);
    if (err != cudaSuccess)
      throw GpuP2PException("cudaMalloc failed for IPC buffer (", bytes, " bytes): ", cudaGetErrorString(err));
#elif defined(KOKKOS_ENABLE_HIP)
    auto err = hipMalloc(&ptr, bytes);
    if (err != hipSuccess)
      throw GpuP2PException("hipMalloc failed for IPC buffer (", bytes, " bytes): ", hipGetErrorString(err));
#endif
    return ptr;
  }

  inline void rawDeviceFree(void *ptr)
  {
    if (ptr == nullptr) return;
#if defined(KOKKOS_ENABLE_CUDA)
    cudaFree(ptr);
#elif defined(KOKKOS_ENABLE_HIP)
    hipFree(ptr);
#endif
  }

  // ============================================================
  // Link type detection: NVLink/xGMI (full-duplex) vs PCIe
  // ============================================================

#if defined(KOKKOS_ENABLE_CUDA)
  // CUDA runtime P2P attributes (cudaDevP2PAttrCudaArrayAccessSupported, etc.) are
  // unreliable for distinguishing NVLink from PCIe. Query NVML's NVLink port state
  // via dlopen — libnvidia-ml.so.1 is always present with the NVIDIA driver.
  inline bool isFullDuplexLink(int cudaDeviceA, int cudaDeviceB)
  {
    // Get PCI addresses from CUDA to match against NVML devices
    cudaDeviceProp propA{}, propB{};
    if (cudaGetDeviceProperties(&propA, cudaDeviceA) != cudaSuccess) return false;
    if (cudaGetDeviceProperties(&propB, cudaDeviceB) != cudaSuccess) return false;

    void *lib = dlopen("libnvidia-ml.so.1", RTLD_LAZY);
    if (!lib) return false; // NVML unavailable → assume PCIe (safe default)

    // NVML function signatures (avoid header dependency)
    using InitFn = unsigned int (*)();
    using ShutdownFn = unsigned int (*)();
    using HandleByPciFn = unsigned int (*)(const char *, void **);
    using NvLinkStateFn = unsigned int (*)(void *, unsigned int, unsigned int *);
    using NvLinkRemotePciFn = unsigned int (*)(void *, unsigned int, void *);
    using GetPciInfoFn = unsigned int (*)(void *, void *);

    auto fnInit = reinterpret_cast<InitFn>(dlsym(lib, "nvmlInit_v2"));
    auto fnShutdown = reinterpret_cast<ShutdownFn>(dlsym(lib, "nvmlShutdown"));
    auto fnHandleByPci = reinterpret_cast<HandleByPciFn>(dlsym(lib, "nvmlDeviceGetHandleByPciBusId_v2"));
    auto fnNvLinkState = reinterpret_cast<NvLinkStateFn>(dlsym(lib, "nvmlDeviceGetNvLinkState"));
    auto fnNvLinkRemotePci = reinterpret_cast<NvLinkRemotePciFn>(dlsym(lib, "nvmlDeviceGetNvLinkRemotePciInfo_v2"));
    auto fnGetPciInfo = reinterpret_cast<GetPciInfoFn>(dlsym(lib, "nvmlDeviceGetPciInfo_v3"));

    if (!fnInit || !fnShutdown || !fnHandleByPci || !fnNvLinkState || !fnNvLinkRemotePci || !fnGetPciInfo) {
      dlclose(lib);
      return false;
    }

    bool found = false;
    if (fnInit() == 0) { // NVML_SUCCESS = 0
      // Look up NVML device handles by PCI bus ID (handles CUDA_VISIBLE_DEVICES reordering)
      char busIdA[32], busIdB[32];
      snprintf(busIdA, sizeof(busIdA), "%08x:%02x:%02x.0", propA.pciDomainID, propA.pciBusID, propA.pciDeviceID);
      snprintf(busIdB, sizeof(busIdB), "%08x:%02x:%02x.0", propB.pciDomainID, propB.pciBusID, propB.pciDeviceID);

      void *devA = nullptr, *devB = nullptr;
      if (fnHandleByPci(busIdA, &devA) == 0 && fnHandleByPci(busIdB, &devB) == 0) {
        // Mirror the FULL nvmlPciInfo_t layout. NVML writes the whole struct, so
        // a truncated mirror (only the fields we read) lets it overflow the
        // stack. We compare only domain/bus/device but must size the struct
        // fully: busIdLegacy[16], uint domain/bus/device, uint pciDeviceId,
        // uint pciSubSystemId, busId[32].
        struct NvmlPciInfo {
          char busIdLegacy[16];
          unsigned int domain, bus, device;
          unsigned int pciDeviceId, pciSubSystemId;
          char busId[32];
        };
        NvmlPciInfo pciB{};
        fnGetPciInfo(devB, &pciB);

        // Enumerate NVLink ports on device A (up to 18 on recent hardware)
        for (unsigned int link = 0; link < 18 && !found; ++link) {
          unsigned int state = 0;                                   // nvmlEnableState_t
          if (fnNvLinkState(devA, link, &state) != 0 || state != 1) // NVML_FEATURE_ENABLED = 1
            continue;
          NvmlPciInfo remotePci{};
          if (fnNvLinkRemotePci(devA, link, &remotePci) != 0) continue;
          if (remotePci.domain == pciB.domain && remotePci.bus == pciB.bus && remotePci.device == pciB.device)
            found = true;
        }
      }
      fnShutdown();
    }
    dlclose(lib);
    return found;
  }

#elif defined(KOKKOS_ENABLE_HIP)
  inline bool isFullDuplexLink(int deviceA, int deviceB)
  {
    uint32_t linktype = 0, hopcount = 0;
    auto err = hipExtGetLinkTypeAndHopCount(deviceA, deviceB, &linktype, &hopcount);
    if (err != hipSuccess) return false;
    // HSA_AMD_LINK_INFO_TYPE_XGMI = 4 (AMD Infinity Fabric, full-duplex)
    return linktype == 4;
  }
#endif

  // ============================================================
  // IPC handle packet — POD struct sent via MPI_BYTE
  // ============================================================

  struct IpcHandlePacket {
    char handle[IpcHandleSize];
    int deviceId;
    uint64_t version;
  };

} // namespace TempLat::device_kokkos::p2p

#endif // (KOKKOS_ENABLE_CUDA || KOKKOS_ENABLE_HIP)

#endif