diff --git a/projects/rccl/src/include/amdsmi_wrap.h b/projects/rccl/src/include/amdsmi_wrap.h index 83319f69ef9a..c8f749582a9f 100644 --- a/projects/rccl/src/include/amdsmi_wrap.h +++ b/projects/rccl/src/include/amdsmi_wrap.h @@ -3,8 +3,12 @@ #ifndef AMDSMI_WRAP_H_ #define AMDSMI_WRAP_H_ +#include #include +#include #include +#include +#include #include "nccl.h" #include "device.h" @@ -462,6 +466,37 @@ amdsmi_status_t amdsmi_fabric_telem_id_to_string(uint64_t telem_id, const char** /** @} End rcclFabricCompat */ #endif // !AMDSMI_FABRIC_DIRECT +/************************************************************************* + * Fabric ABI guard + * + * amdsmi_get_gpu_fabric_info() is reached through dlopen, so it writes this + * struct using the shipped library's layout, not ours. If our declaration is + * smaller the library writes past the end of the caller's object, and any + * shift moves the fields we read. Neither shows up as a compiler diagnostic, + * so recognize the two shipped layouts here: a mismatch must be a deliberate + * update, not a silent memory bug. Some pre-release ROCm 7.14 snapshots used + * AMDSMI_FABRIC_MAX_LOCAL_GPUS=8, while final ROCm 7.14 and ROCm 7.15 use 16 + * without changing the amd_smi major. + ************************************************************************/ +constexpr size_t kAmdSmiFabricInfo8GpuSize = 288; +constexpr size_t kAmdSmiFabricInfo16GpuSize = 320; +constexpr size_t kAmdSmiFabricState8GpuOffset = 208; +constexpr size_t kAmdSmiFabricState16GpuOffset = 240; + +constexpr bool amdSmiFabricLayoutIs8Gpu = + sizeof(amdsmi_fabric_info_v1_t) == 212 && sizeof(amdsmi_fabric_info_t) == kAmdSmiFabricInfo8GpuSize && + offsetof(amdsmi_fabric_info_v1_t, addr_mode) == kAmdSmiFabricState8GpuOffset - sizeof(uint32_t) && + offsetof(amdsmi_fabric_info_v1_t, accel_state) == kAmdSmiFabricState8GpuOffset && + offsetof(amdsmi_fabric_info_t, reserved) == 224; + +constexpr bool amdSmiFabricLayoutIs16Gpu = + sizeof(amdsmi_fabric_info_v1_t) == 244 && sizeof(amdsmi_fabric_info_t) == kAmdSmiFabricInfo16GpuSize && + offsetof(amdsmi_fabric_info_v1_t, addr_mode) == kAmdSmiFabricState16GpuOffset - sizeof(uint32_t) && + offsetof(amdsmi_fabric_info_v1_t, accel_state) == kAmdSmiFabricState16GpuOffset && + offsetof(amdsmi_fabric_info_t, reserved) == 256; + +static_assert(amdSmiFabricLayoutIs8Gpu || amdSmiFabricLayoutIs16Gpu, "unsupported amdsmi fabric layout"); + /************************************************************************* * AMD SMI Fabric Info Cache * @@ -472,6 +507,120 @@ amdsmi_status_t amdsmi_fabric_telem_id_to_string(uint64_t telem_id, const char** #define AMDSMI_FABRIC_INFO_VERSION_1 1 #define AMDSMI_FABRIC_INFO_CURRENT_VERSION AMDSMI_FABRIC_INFO_VERSION_1 +// Sentinel amd_smi leaves in numeric fabric fields it could not source from sysfs +constexpr uint32_t kAmdSmiFabricVersionUnreported = 0xFFFFFFFFu; + +// amd_smi never populates amdsmi_fabric_info_t's version field, so an unreported value +// has to be read as v1 rather than as a reason to discard an otherwise valid payload. +// A version the library does claim, but that we cannot interpret, is still refused. +inline bool amdSmiFabricVersionUsable(uint32_t version) { + return version == kAmdSmiFabricVersionUnreported || version == AMDSMI_FABRIC_INFO_CURRENT_VERSION; +} + +// Fabric is usable only over a link type we drive, and only once the accelerator has +// reached a state where its vPoD can carry traffic. +// +// AMDSMI_FABRIC_TYPE_UALLINK is deprecated in favour of the correctly spelled +// AMDSMI_FABRIC_TYPE_UALINK, but only the former exists in amd_smi before 27.0 and +// the latter is an alias for it after, so the old name is the one that compiles +// against every header we support. +inline bool amdSmiFabricStateUsable(amdsmi_fabric_type_t type, amdsmi_fabric_accelerator_vpod_state_t state) { + return (type == AMDSMI_FABRIC_TYPE_UALOE || type == AMDSMI_FABRIC_TYPE_UALLINK) && + (state == AMDSMI_FABRIC_ACCELERATOR_VPOD_STATE_ACTIVE || state == AMDSMI_FABRIC_ACCELERATOR_VPOD_STATE_READY); +} + +/************************************************************************* + * amdsmi_fabric_info_t layout compatibility + * + * amd_smi 27.0 flattened this struct. The version moved from a nested + * fabric_info.version to a top-level fabric_version, and the payload from + * fabric_info.fabric_version.v1 to fabric_info.v1 -- note that fabric_version + * names different things either side of the change. The version and payload + * offsets did not move (bytes 8 and 12), so the shape change itself is only a + * source-level rename; the ABI guard above handles payload capacity separately. + * + * Detect the shape rather than test a version macro, so a header that carries + * the change under any version still resolves correctly. Only the flattened + * layout has a top-level integral fabric_version; in the nested layout that name + * exists solely inside fabric_info, as a union. + ************************************************************************/ +template +struct amdSmiFabricInfoIsFlat : std::false_type {}; + +template +struct amdSmiFabricInfoIsFlat().fabric_version)>> + : std::is_integral().fabric_version)>> {}; + +// Templates so that if constexpr discards the branch that does not compile against +// the header in use; in a non-template function both branches are type-checked. +template +inline uint32_t amdSmiFabricInfoVersion(const FabricInfoT& info) { + if constexpr (amdSmiFabricInfoIsFlat::value) { + return info.fabric_version; + } else { + return info.fabric_info.version; + } +} + +template +inline const amdsmi_fabric_info_v1_t* amdSmiFabricInfoV1(const FabricInfoT& info) { + if constexpr (amdSmiFabricInfoIsFlat::value) { + return &info.fabric_info.v1; + } else { + return &info.fabric_info.fabric_version.v1; + } +} + +// amd_smi 26.x used both supported payload sizes under the same SONAME. Use a +// maximum-sized canary buffer so either runtime can write safely, then identify +// how much it wrote. Both implementations value-initialize a local struct and +// assign the complete object, making the final 32 bytes zero only for the +// 16-GPU runtime. +constexpr unsigned char kAmdSmiFabricBufferCanary = 0xA5; + +struct alignas(amdsmi_fabric_info_t) amdSmiFabricInfoBuffer { + unsigned char bytes[kAmdSmiFabricInfo16GpuSize]; +}; + +enum class amdSmiFabricRuntimeLayout { + EightGpu, + SixteenGpu, + Unknown, +}; + +inline void amdSmiPrepareFabricInfoBuffer(amdSmiFabricInfoBuffer& buffer) { + memset(buffer.bytes, kAmdSmiFabricBufferCanary, sizeof(buffer.bytes)); +} + +inline amdsmi_fabric_info_t* amdSmiFabricInfoBufferAsInfo(amdSmiFabricInfoBuffer& buffer) { + return reinterpret_cast(buffer.bytes); +} + +inline amdSmiFabricRuntimeLayout amdSmiDetectFabricRuntimeLayout(const amdSmiFabricInfoBuffer& buffer) { + bool tailIsCanary = true; + bool tailIsZero = true; + for (size_t i = kAmdSmiFabricInfo8GpuSize; i < kAmdSmiFabricInfo16GpuSize; ++i) { + tailIsCanary &= buffer.bytes[i] == kAmdSmiFabricBufferCanary; + tailIsZero &= buffer.bytes[i] == 0; + } + if (tailIsCanary) return amdSmiFabricRuntimeLayout::EightGpu; + if (tailIsZero) return amdSmiFabricRuntimeLayout::SixteenGpu; + return amdSmiFabricRuntimeLayout::Unknown; +} + +// amd_smi 27.0 also changed amdsmi_fabric_telem_id_to_string from returning the name +// to writing it through an out-parameter. The two are not call-compatible and we reach +// it through dlopen, so the choice has to follow the version the loaded runtime +// reports. An unknown version must assume the out-parameter form: calling a 27+ +// implementation the old way leaves it writing through an uninitialized pointer, +// whereas calling an older one the new way merely leaves the name unwritten. +constexpr uint32_t kAmdSmiLibVersionUnknown = 0; +constexpr uint32_t kAmdSmiTelemOutParamMajor = 27; + +inline bool amdSmiTelemIdUsesOutParam(uint32_t libMajor) { + return libMajor == kAmdSmiLibVersionUnknown || libMajor >= kAmdSmiTelemOutParamMajor; +} + struct amdsmiFabricDeviceInfo { bool fabricSupported; //!< Whether UALoE fabric is available amdsmi_fabric_type_t fabricType; //!< Fabric type (UALOE or UALLINK) diff --git a/projects/rccl/src/misc/amdsmi_wrap.cc b/projects/rccl/src/misc/amdsmi_wrap.cc index 368b483fa020..7c2b27533f62 100644 --- a/projects/rccl/src/misc/amdsmi_wrap.cc +++ b/projects/rccl/src/misc/amdsmi_wrap.cc @@ -132,8 +132,17 @@ std::mutex fabricLock; // Thread safety for fabric operations bool fabricInitialized = false; thread_local bool threadFabricInitialized = false; ncclResult_t fabricInitResult = ncclSuccess; +std::mutex amdSmiInitLock; ncclResult_t amdSmiInitResult = ncclSuccess; -std::atomic amdSmiInitCalled{false}; // Track if amd_smi_init has been called +bool amdSmiInitCalled = false; +bool amdSmiLibInitialized = false; + +// Major version of the loaded amd_smi, as it reports itself. Selects the telemetry +// name call convention; see amdSmiTelemIdUsesOutParam(). +std::atomic amdSmiLibMajor{kAmdSmiLibVersionUnknown}; + +// Signature amdsmi_fabric_telem_id_to_string had before amd_smi 27.0 +using AmdSmiTelemIdToStringLegacyFn = const char* (*)(uint64_t); } // namespace /************************************************************************* @@ -141,7 +150,7 @@ std::atomic amdSmiInitCalled{false}; // Track if amd_smi_init has been cal ************************************************************************/ static ncclResult_t getProcessorHandle(uint32_t deviceIndex, amdsmi_processor_handle* procHandle) { if (!rcclParamUseAmdSmiLib()) { - return ncclSystemError; // Fabric only supported with amd_smi_lib + return ncclSystemError; // processor handles are amd_smi_lib-only } uint32_t socket_count = 0; @@ -179,10 +188,7 @@ static bool amd_smi_FabricFunctionsLoaded() { * Existing AMD SMI Wrapper Functions ************************************************************************/ -ncclResult_t amd_smi_init() { - // Ensure we only initialize once - if (amdSmiInitCalled.exchange(true)) return amdSmiInitResult; - +static ncclResult_t amd_smi_init_impl() { if (__atomic_load_n(&is_wsl2, __ATOMIC_ACQUIRE) == -1) __atomic_store_n(&is_wsl2, (access("/dev/dxg", F_OK) == -1) ? 0 : 1, __ATOMIC_RELEASE); if (__atomic_load_n(&is_wsl2, __ATOMIC_ACQUIRE)) { @@ -197,7 +203,6 @@ ncclResult_t amd_smi_init() { static void* libhandle = dlopen(RCCL_AMDSMI_LIBNAME, RTLD_NOW); if (libhandle == nullptr) { WARN("Failed to open %s: %s", RCCL_AMDSMI_LIBNAME, dlerror()); - amdSmiInitResult = ncclInternalError; return ncclInternalError; } @@ -235,32 +240,51 @@ ncclResult_t amd_smi_init() { // initialize amd-smi for AMD GPUs AMDSMITRY(amdsmi_init, AMDSMI_INIT_AMD_GPUS); + amdSmiLibInitialized = true; // get amd-smi version amdsmi_version_t version; AMDSMITRY(amdsmi_get_lib_version, &version); - INFO(NCCL_INIT, "amdsmi_lib: version %d.%d.%d.%s", version.major, version.minor, version.release, version.build); + INFO(NCCL_INIT, "amdsmi_lib: version %u.%u.%u (build %s)", version.major, version.minor, version.release, + version.build); + // Retained because the telemetry name call convention depends on it + amdSmiLibMajor.store(version.major, std::memory_order_release); } else { -#ifdef HIP_FABRIC_API - WARN("RCCL_USE_AMD_SMI_LIB not set, but HIP_FABRIC_API is defined. Fabric support is only available through AMD " - "SMI. Rerun with RCCL_USE_AMD_SMI_LIB=1 to enable AMD SMI and UALoE fabric support."); -#endif // initialize alternate rsmi ARSMICHECK(ARSMI_init()); - INFO(NCCL_INIT, "initialized internal alternative rsmi functionality"); + // RCCL_USE_AMD_SMI_LIB only selects who performs fabric *discovery*: amd_smi_lib, or the + // ualink sysfs nodes via ARSMI_get_fabric_info(). Both populate amdsmiFabricDevices + // identically, so UALoE/UALLink works on this path. + INFO(NCCL_INIT, "initialized internal alternative rsmi functionality; UALoE/UALLink fabric discovery uses sysfs " + "(set RCCL_USE_AMD_SMI_LIB=1 to use amd_smi_lib instead)"); } return ncclSuccess; } +ncclResult_t amd_smi_init() { + std::lock_guard lock(amdSmiInitLock); + if (amdSmiInitCalled) return amdSmiInitResult; + + // Cache failures too; shutdown explicitly resets this state for a retry. + amdSmiInitResult = amd_smi_init_impl(); + amdSmiInitCalled = true; + return amdSmiInitResult; +} + ncclResult_t amd_smi_shutdown() { - // Only shutdown if we actually initialized with amd_smi_lib - if (!rcclParamUseAmdSmiLib() || pfn_amdsmi_shut_down == nullptr) { - return ncclSuccess; + std::lock_guard lock(amdSmiInitLock); + + if (!amdSmiInitCalled) return ncclSuccess; + + // The backend may be initialized even if a later version query failed. + if (amdSmiLibInitialized) { + AMDSMITRY(amdsmi_shut_down); + amdSmiLibInitialized = false; } - amdSmiInitCalled.store(false); - amdSmiInitResult = ncclSuccess; - AMDSMITRY(amdsmi_shut_down); + amdSmiLibMajor.store(kAmdSmiLibVersionUnknown, std::memory_order_release); + amdSmiInitResult = ncclSuccess; + amdSmiInitCalled = false; return ncclSuccess; } @@ -602,6 +626,30 @@ ncclResult_t amd_smi_ensureFabricInitialized() { fabricInitResult = ncclInternalError; return fabricInitResult; } + + // amd_smi 26.x shipped both fabric payload sizes under the same SONAME, so + // the library version cannot identify a mixed header/runtime installation. + // Probe through an oversized canary buffer before using the typed structure. + // If the layouts differ (or cannot be identified), use the sysfs backend. + if (!useSysfs && amd_smi_FabricFunctionsLoaded()) { + amdsmi_processor_handle probeHandle; + if (getProcessorHandle(0, &probeHandle) == ncclSuccess) { + amdSmiFabricInfoBuffer probeBuffer; + amdSmiPrepareFabricInfoBuffer(probeBuffer); + pfn_amdsmi_get_gpu_fabric_info(probeHandle, amdSmiFabricInfoBufferAsInfo(probeBuffer)); + const amdSmiFabricRuntimeLayout runtimeLayout = amdSmiDetectFabricRuntimeLayout(probeBuffer); + const bool runtimeLayoutIs8Gpu = runtimeLayout == amdSmiFabricRuntimeLayout::EightGpu; + if (runtimeLayout == amdSmiFabricRuntimeLayout::Unknown) { + WARN("AMD SMI fabric: unable to verify the loaded library's fabric ABI; falling back to sysfs"); + useSysfs = true; + } else if (runtimeLayoutIs8Gpu != amdSmiFabricLayoutIs8Gpu) { + WARN("AMD SMI fabric ABI mismatch: RCCL was built for the %u-GPU layout, but the loaded library uses the " + "%u-GPU layout; falling back to sysfs", + amdSmiFabricLayoutIs8Gpu ? 8 : 16, runtimeLayoutIs8Gpu ? 8 : 16); + useSysfs = true; + } + } + } amdsmiFabricDeviceCount = (int)numDevs; for (uint32_t d = 0; d < numDevs; d++) { @@ -640,17 +688,15 @@ ncclResult_t amd_smi_ensureFabricInitialized() { devInfo->fabricSupported = false; continue; } - if (fabricInfo.fabric_version != AMDSMI_FABRIC_INFO_CURRENT_VERSION) { - WARN("AMD SMI fabric: unexpected fabric info version %u for device %u, expected %u", - fabricInfo.fabric_version, d, AMDSMI_FABRIC_INFO_CURRENT_VERSION); + const uint32_t fabricVersion = amdSmiFabricInfoVersion(fabricInfo); + if (!amdSmiFabricVersionUsable(fabricVersion)) { + WARN("AMD SMI fabric: unexpected fabric info version %u for device %u, expected %u", fabricVersion, d, + AMDSMI_FABRIC_INFO_CURRENT_VERSION); devInfo->fabricSupported = false; continue; } - const amdsmi_fabric_info_v1_t* v1 = &fabricInfo.fabric_info.v1; - devInfo->fabricSupported = - ((v1->fabric_type == AMDSMI_FABRIC_TYPE_UALOE || v1->fabric_type == AMDSMI_FABRIC_TYPE_UALLINK) && - (v1->accel_state == AMDSMI_FABRIC_ACCELERATOR_VPOD_STATE_ACTIVE || - v1->accel_state == AMDSMI_FABRIC_ACCELERATOR_VPOD_STATE_READY)); + const amdsmi_fabric_info_v1_t* v1 = amdSmiFabricInfoV1(fabricInfo); + devInfo->fabricSupported = amdSmiFabricStateUsable(v1->fabric_type, v1->accel_state); devInfo->fabricType = v1->fabric_type; devInfo->state = v1->accel_state; devInfo->acceleratorId = v1->accelerator_id; @@ -762,6 +808,13 @@ const char* amd_smi_fabricTelemIdToString(uint64_t telemId) { if (pfn_amdsmi_fabric_telem_id_to_string == nullptr) { return ARSMI_fabric_telem_id_to_string(telemId); } + if (!amdSmiTelemIdUsesOutParam(amdSmiLibMajor.load(std::memory_order_acquire))) { + // dlsym gave us an address, not a prototype: on a pre-27 runtime the symbol has + // to be called through its own signature or the extra argument is read as one. + auto legacyFn = reinterpret_cast(pfn_amdsmi_fabric_telem_id_to_string); + const char* legacyName = legacyFn(telemId); + return legacyName == nullptr ? "UNKNOWN" : legacyName; + } const char* telemName = nullptr; amdsmi_status_t ret = pfn_amdsmi_fabric_telem_id_to_string(telemId, &telemName); if (ret != AMDSMI_STATUS_SUCCESS || telemName == nullptr) { diff --git a/projects/rccl/test/AmdSmiFabricTests.cpp b/projects/rccl/test/AmdSmiFabricTests.cpp new file mode 100644 index 000000000000..fc59032188ab --- /dev/null +++ b/projects/rccl/test/AmdSmiFabricTests.cpp @@ -0,0 +1,306 @@ +/************************************************************************* + * Copyright (c) 2026 Advanced Micro Devices, Inc. All rights reserved. + * + * See LICENSE.txt for license information + ************************************************************************/ + +#include "amdsmi_wrap.h" + +#include +#include +#include + +namespace RcclUnitTesting +{ + +// --------------------------------------------------------------------------- +// Fabric info version acceptance +// +// amdsmi_get_gpu_fabric_info() returns SUCCESS with a fully populated payload +// but never writes the version field, leaving it at the sentinel amd_smi uses +// for anything it could not source from sysfs. Rejecting that sentinel made +// RCCL_USE_AMD_SMI_LIB=1 discard working UALoE fabric on every device. +// --------------------------------------------------------------------------- + +TEST(AmdSmiFabricVersion, AcceptsUnreportedSentinel) +{ + EXPECT_TRUE(amdSmiFabricVersionUsable(kAmdSmiFabricVersionUnreported)); +} + +TEST(AmdSmiFabricVersion, AcceptsCurrentVersion) +{ + EXPECT_TRUE(amdSmiFabricVersionUsable(AMDSMI_FABRIC_INFO_CURRENT_VERSION)); +} + +// A version the library actually claims, but whose union layout we do not +// implement, must still be refused rather than misparsed as v1. +TEST(AmdSmiFabricVersion, RejectsUnimplementedFutureVersion) +{ + EXPECT_FALSE(amdSmiFabricVersionUsable(AMDSMI_FABRIC_INFO_CURRENT_VERSION + 1)); +} + +// Distinct from the sentinel: zero means the library returned without writing +// the field at all, which contradicts a SUCCESS status and is worth surfacing. +TEST(AmdSmiFabricVersion, RejectsZero) +{ + EXPECT_FALSE(amdSmiFabricVersionUsable(0)); +} + +// --------------------------------------------------------------------------- +// Fabric type / accelerator state eligibility +// +// This is the decision the version check was gating: only a link type we drive +// combined with a state whose vPoD can carry traffic counts as usable. +// --------------------------------------------------------------------------- + +TEST(AmdSmiFabricState, UaloeReadyIsUsable) +{ + EXPECT_TRUE(amdSmiFabricStateUsable(AMDSMI_FABRIC_TYPE_UALOE, + AMDSMI_FABRIC_ACCELERATOR_VPOD_STATE_READY)); +} + +TEST(AmdSmiFabricState, UaloeActiveIsUsable) +{ + EXPECT_TRUE(amdSmiFabricStateUsable(AMDSMI_FABRIC_TYPE_UALOE, + AMDSMI_FABRIC_ACCELERATOR_VPOD_STATE_ACTIVE)); +} + +TEST(AmdSmiFabricState, UalinkReadyIsUsable) +{ + EXPECT_TRUE(amdSmiFabricStateUsable(AMDSMI_FABRIC_TYPE_UALLINK, + AMDSMI_FABRIC_ACCELERATOR_VPOD_STATE_READY)); +} + +TEST(AmdSmiFabricState, ConfiguredButNotReadyIsNotUsable) +{ + EXPECT_FALSE(amdSmiFabricStateUsable(AMDSMI_FABRIC_TYPE_UALOE, + AMDSMI_FABRIC_ACCELERATOR_VPOD_STATE_CONFIGURED)); +} + +TEST(AmdSmiFabricState, UnconfiguredIsNotUsable) +{ + EXPECT_FALSE(amdSmiFabricStateUsable(AMDSMI_FABRIC_TYPE_UALOE, + AMDSMI_FABRIC_ACCELERATOR_VPOD_STATE_UNCONFIGURED)); +} + +TEST(AmdSmiFabricState, ErrorStateIsNotUsable) +{ + EXPECT_FALSE(amdSmiFabricStateUsable(AMDSMI_FABRIC_TYPE_UALOE, + AMDSMI_FABRIC_ACCELERATOR_VPOD_STATE_ERROR)); +} + +TEST(AmdSmiFabricState, UnknownLinkTypeIsNotUsable) +{ + EXPECT_FALSE(amdSmiFabricStateUsable(AMDSMI_FABRIC_TYPE_UNKNOWN, + AMDSMI_FABRIC_ACCELERATOR_VPOD_STATE_ACTIVE)); +} + +// The combination this node actually reports, and that the version check used +// to throw away: UALoE + READY reached through the full struct. +TEST(AmdSmiFabricState, PayloadFromSysfsBackedDeviceIsUsable) +{ + amdsmi_fabric_info_v1_t v1{}; + v1.fabric_type = AMDSMI_FABRIC_TYPE_UALOE; + v1.accel_state = AMDSMI_FABRIC_ACCELERATOR_VPOD_STATE_READY; + v1.accelerator_id = 3; + v1.ppod_size = 8; + v1.vpod_id = 1; + v1.vpod_size = 4; + + EXPECT_TRUE(amdSmiFabricVersionUsable(kAmdSmiFabricVersionUnreported)); + EXPECT_TRUE(amdSmiFabricStateUsable(v1.fabric_type, v1.accel_state)); +} + +// --------------------------------------------------------------------------- +// Struct layout +// +// The library writes these structs through dlopen using its own layout, so a +// declaration that disagrees overflows the caller's object and shifts the +// fields we read. amdsmi_wrap.h accepts the coherent 8-GPU layout used by some +// pre-release ROCm 7.14 snapshots and the 16-GPU layout used by final ROCm 7.14 +// and ROCm 7.15, but rejects any other combination. +// --------------------------------------------------------------------------- + +TEST(AmdSmiFabricLayout, MatchesShippedLibraryAbi) +{ + EXPECT_TRUE(amdSmiFabricLayoutIs8Gpu || amdSmiFabricLayoutIs16Gpu); + EXPECT_NE(amdSmiFabricLayoutIs8Gpu, amdSmiFabricLayoutIs16Gpu); +} + +TEST(AmdSmiFabricLayout, TrailingFieldsAreNotShifted) +{ + const size_t expectedAddrMode = amdSmiFabricLayoutIs8Gpu ? 204u : 236u; + EXPECT_EQ(offsetof(amdsmi_fabric_info_v1_t, addr_mode), expectedAddrMode); + EXPECT_EQ(offsetof(amdsmi_fabric_info_v1_t, accel_state), expectedAddrMode + sizeof(uint32_t)); +} + +TEST(AmdSmiFabricRuntimeLayout, DetectsEightGpuWriter) +{ + amdSmiFabricInfoBuffer buffer; + amdSmiPrepareFabricInfoBuffer(buffer); + memset(buffer.bytes, 0, kAmdSmiFabricInfo8GpuSize); + + EXPECT_EQ(amdSmiDetectFabricRuntimeLayout(buffer), amdSmiFabricRuntimeLayout::EightGpu); +} + +TEST(AmdSmiFabricRuntimeLayout, DetectsSixteenGpuWriter) +{ + amdSmiFabricInfoBuffer buffer; + amdSmiPrepareFabricInfoBuffer(buffer); + memset(buffer.bytes, 0, kAmdSmiFabricInfo16GpuSize); + + EXPECT_EQ(amdSmiDetectFabricRuntimeLayout(buffer), amdSmiFabricRuntimeLayout::SixteenGpu); +} + +TEST(AmdSmiFabricRuntimeLayout, RejectsUnknownWriter) +{ + amdSmiFabricInfoBuffer buffer; + amdSmiPrepareFabricInfoBuffer(buffer); + buffer.bytes[kAmdSmiFabricInfo8GpuSize] = 0; + + EXPECT_EQ(amdSmiDetectFabricRuntimeLayout(buffer), amdSmiFabricRuntimeLayout::Unknown); +} + +// --------------------------------------------------------------------------- +// Both amdsmi_fabric_info_t shapes +// +// amd_smi 27.0 flattened the struct, so RCCL has to read a version and a v1 +// payload out of either shape. Only one of them exists in any given build, so +// stand both up as mocks here: that is the only way to compile and exercise the +// branch the installed header does not select. +// --------------------------------------------------------------------------- + +namespace +{ +union MockFabricPayload +{ + amdsmi_fabric_info_v1_t v1; +}; + +// Pre-27: version and payload sit inside fabric_info, where the union carries +// the fabric_version name. +struct MockNestedFabricVer +{ + uint32_t version; + MockFabricPayload fabric_version; +}; + +struct MockNestedFabricInfo +{ + amdsmi_bdf_t bdf; + MockNestedFabricVer fabric_info; + uint32_t reserved[15]; +}; + +// 27.0 and later: version hoisted to the top level, payload reachable directly. +struct MockFlatFabricInfo +{ + amdsmi_bdf_t bdf; + uint32_t fabric_version; + MockFabricPayload fabric_info; + uint32_t reserved[15]; +}; + +// A top-level fabric_version that is not the version number, which is what the +// name means in the nested shape. Detection keys on the type, not the name. +struct MockHoistedUnionFabricInfo +{ + amdsmi_bdf_t bdf; + MockFabricPayload fabric_version; +}; +} // namespace + +TEST(AmdSmiFabricLayoutCompat, DetectsEachShape) +{ + EXPECT_FALSE(amdSmiFabricInfoIsFlat::value); + EXPECT_TRUE(amdSmiFabricInfoIsFlat::value); + EXPECT_FALSE(amdSmiFabricInfoIsFlat::value); +} + +TEST(AmdSmiFabricLayoutCompat, ReadsVersionFromNestedShape) +{ + MockNestedFabricInfo info{}; + info.fabric_info.version = kAmdSmiFabricVersionUnreported; + + EXPECT_EQ(amdSmiFabricInfoVersion(info), kAmdSmiFabricVersionUnreported); + EXPECT_TRUE(amdSmiFabricVersionUsable(amdSmiFabricInfoVersion(info))); +} + +TEST(AmdSmiFabricLayoutCompat, ReadsVersionFromFlatShape) +{ + MockFlatFabricInfo info{}; + info.fabric_version = kAmdSmiFabricVersionUnreported; + + EXPECT_EQ(amdSmiFabricInfoVersion(info), kAmdSmiFabricVersionUnreported); + EXPECT_TRUE(amdSmiFabricVersionUsable(amdSmiFabricInfoVersion(info))); +} + +TEST(AmdSmiFabricLayoutCompat, ReadsPayloadFromNestedShape) +{ + MockNestedFabricInfo info{}; + info.fabric_info.fabric_version.v1.fabric_type = AMDSMI_FABRIC_TYPE_UALOE; + info.fabric_info.fabric_version.v1.accel_state = AMDSMI_FABRIC_ACCELERATOR_VPOD_STATE_READY; + info.fabric_info.fabric_version.v1.accelerator_id = 3; + + const amdsmi_fabric_info_v1_t* v1 = amdSmiFabricInfoV1(info); + ASSERT_NE(v1, nullptr); + EXPECT_EQ(v1->accelerator_id, 3u); + EXPECT_TRUE(amdSmiFabricStateUsable(v1->fabric_type, v1->accel_state)); +} + +TEST(AmdSmiFabricLayoutCompat, ReadsPayloadFromFlatShape) +{ + MockFlatFabricInfo info{}; + info.fabric_info.v1.fabric_type = AMDSMI_FABRIC_TYPE_UALOE; + info.fabric_info.v1.accel_state = AMDSMI_FABRIC_ACCELERATOR_VPOD_STATE_READY; + info.fabric_info.v1.accelerator_id = 3; + + const amdsmi_fabric_info_v1_t* v1 = amdSmiFabricInfoV1(info); + ASSERT_NE(v1, nullptr); + EXPECT_EQ(v1->accelerator_id, 3u); + EXPECT_TRUE(amdSmiFabricStateUsable(v1->fabric_type, v1->accel_state)); +} + +// The rename did not move anything, which is why one set of ABI asserts covers +// both headers. If a future layout change breaks this, the accessors are no +// longer enough on their own. +TEST(AmdSmiFabricLayoutCompat, BothShapesShareOneAbi) +{ + EXPECT_EQ(sizeof(MockNestedFabricInfo), sizeof(MockFlatFabricInfo)); + EXPECT_EQ(sizeof(MockFlatFabricInfo), sizeof(amdsmi_fabric_info_t)); + + EXPECT_EQ(offsetof(MockNestedFabricInfo, fabric_info) + offsetof(MockNestedFabricVer, version), + offsetof(MockFlatFabricInfo, fabric_version)); + EXPECT_EQ(offsetof(MockNestedFabricInfo, fabric_info) + offsetof(MockNestedFabricVer, fabric_version), + offsetof(MockFlatFabricInfo, fabric_info)); + EXPECT_EQ(offsetof(MockNestedFabricInfo, reserved), offsetof(MockFlatFabricInfo, reserved)); +} + +// --------------------------------------------------------------------------- +// Telemetry name call convention +// +// amdsmi_fabric_telem_id_to_string swapped its return value for an out-parameter +// in 27.0, and we reach it through dlopen, so the version the loaded runtime +// reports decides how it may be called. +// --------------------------------------------------------------------------- + +TEST(AmdSmiFabricTelemAbi, PreflattenRuntimeUsesReturnValue) +{ + EXPECT_FALSE(amdSmiTelemIdUsesOutParam(26)); +} + +TEST(AmdSmiFabricTelemAbi, FlattenedRuntimeUsesOutParam) +{ + EXPECT_TRUE(amdSmiTelemIdUsesOutParam(kAmdSmiTelemOutParamMajor)); + EXPECT_TRUE(amdSmiTelemIdUsesOutParam(kAmdSmiTelemOutParamMajor + 1)); +} + +// Guessing wrong is not symmetric: the out-parameter form called on an older +// runtime just leaves the name unwritten, while the older form called on a newer +// runtime hands it an uninitialized pointer to write through. +TEST(AmdSmiFabricTelemAbi, UnknownVersionTakesTheSaferConvention) +{ + EXPECT_TRUE(amdSmiTelemIdUsesOutParam(kAmdSmiLibVersionUnknown)); +} + +} // namespace RcclUnitTesting diff --git a/projects/rccl/test/AmdSmiLifecycleStub.cpp b/projects/rccl/test/AmdSmiLifecycleStub.cpp new file mode 100644 index 000000000000..f6d50fedf941 --- /dev/null +++ b/projects/rccl/test/AmdSmiLifecycleStub.cpp @@ -0,0 +1,85 @@ +/************************************************************************* + * Copyright (c) 2026 Advanced Micro Devices, Inc. All rights reserved. + * + * See LICENSE.txt for license information + ************************************************************************/ + +#include +#include +#include +#include +#include + +namespace +{ + +constexpr int kAmdSmiSuccess = 0; +constexpr int kAmdSmiUnknownError = -1; + +struct AmdSmiVersion +{ + uint32_t major; + uint32_t minor; + uint32_t release; + const char* build; +}; + +std::atomic initCount{0}; +std::atomic versionCount{0}; +std::atomic shutdownCount{0}; + +bool envEnabled(const char* name) +{ + const char* value = std::getenv(name); + return value != nullptr && value[0] == '1'; +} + +} // namespace + +extern "C" int amdsmi_init(uint64_t) +{ + ++initCount; + if(envEnabled("RCCL_AMDSMI_TEST_DELAY_INIT")) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + return envEnabled("RCCL_AMDSMI_TEST_FAIL_INIT") ? kAmdSmiUnknownError : kAmdSmiSuccess; +} + +extern "C" int amdsmi_get_lib_version(AmdSmiVersion* version) +{ + ++versionCount; + if(envEnabled("RCCL_AMDSMI_TEST_FAIL_VERSION")) + return kAmdSmiUnknownError; + + version->major = 0; + version->minor = 0; + version->release = 0; + version->build = "test"; + return kAmdSmiSuccess; +} + +extern "C" int amdsmi_shut_down() +{ + ++shutdownCount; + return kAmdSmiSuccess; +} + +extern "C" int amdsmi_status_code_to_string(int, const char** statusString) +{ + *statusString = "injected amd_smi failure"; + return kAmdSmiSuccess; +} + +extern "C" unsigned amdsmi_test_init_count() +{ + return initCount.load(); +} + +extern "C" unsigned amdsmi_test_version_count() +{ + return versionCount.load(); +} + +extern "C" unsigned amdsmi_test_shutdown_count() +{ + return shutdownCount.load(); +} diff --git a/projects/rccl/test/AmdSmiWrapTests.cpp b/projects/rccl/test/AmdSmiWrapTests.cpp new file mode 100644 index 000000000000..3700d0d186ff --- /dev/null +++ b/projects/rccl/test/AmdSmiWrapTests.cpp @@ -0,0 +1,343 @@ +/************************************************************************* + * Copyright (c) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. + * + * See LICENSE.txt for license information + ************************************************************************/ + +// Exercises the amdsmi_wrap API against the machine's real GPUs. +// +// These call into librccl rather than recompiling the wrapper, so they only +// link in Debug: src/CMakeLists.txt exports the internal amd_smi_* symbols +// under -fvisibility=default there and hides them everywhere else. +// +// Every case skips rather than fails when what it needs is absent, so the +// suite stays meaningful on a machine with no GPUs, no amd_smi library, or no +// fabric. + +#include "amdsmi_wrap.h" +#include "alt_rsmi.h" +#include "common/ProcessIsolatedTestRunner.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RcclUnitTesting +{ + +namespace +{ + +using StubCounterFn = unsigned (*)(); + +std::string lifecycleStubLibraryPath() +{ + std::string path = AMDSMI_TEST_STUB_DIR; + if(const char* inheritedPath = std::getenv("LD_LIBRARY_PATH")) + path += ":" + std::string(inheritedPath); + return path; +} + +unsigned lifecycleStubCounter(const char* symbol) +{ + void* handle = dlopen(AMDSMI_TEST_STUB_SONAME, RTLD_NOW | RTLD_NOLOAD); + if(handle == nullptr) + { + ADD_FAILURE() << "lifecycle stub is not loaded: " << dlerror(); + return 0; + } + + dlerror(); + auto counter = reinterpret_cast(dlsym(handle, symbol)); + if(const char* error = dlerror()) + { + ADD_FAILURE() << "failed to resolve " << symbol << ": " << error; + dlclose(handle); + return 0; + } + + const unsigned value = counter(); + dlclose(handle); + return value; +} + +// amd_smi_init() succeeds on the sysfs fallback path too, so a failure here +// means neither backend is usable and there is nothing to assert against. +class AmdSmiWrapTest : public ::testing::Test +{ +protected: + void SetUp() override + { + if(amd_smi_init() != ncclSuccess) + GTEST_SKIP() << "amd_smi_init() failed: no usable SMI backend"; + initialized_ = true; + + if(amd_smi_getNumDevice(&numDevices_) != ncclSuccess) + GTEST_SKIP() << "amd_smi_getNumDevice() failed"; + } + + void TearDown() override + { + if(initialized_) + EXPECT_EQ(amd_smi_shutdown(), ncclSuccess); + } + + void requireDevices(uint32_t count) + { + if(numDevices_ < count) + GTEST_SKIP() << "needs " << count << " device(s), found " << numDevices_; + } + + uint32_t numDevices_ = 0; + bool initialized_ = false; +}; + +} // namespace + +TEST(AmdSmiWrapLifecycle, InitAndShutdownSucceed) +{ + if(amd_smi_init() != ncclSuccess) + GTEST_SKIP() << "amd_smi_init() failed: no usable SMI backend"; + EXPECT_EQ(amd_smi_shutdown(), ncclSuccess); +} + +// Re-initializing after a shutdown has to work: RCCL brings the wrapper up per +// communicator, so a one-shot init would break every rank after the first. +TEST(AmdSmiWrapLifecycle, InitIsRepeatable) +{ + if(amd_smi_init() != ncclSuccess) + GTEST_SKIP() << "amd_smi_init() failed: no usable SMI backend"; + ASSERT_EQ(amd_smi_shutdown(), ncclSuccess); + + EXPECT_EQ(amd_smi_init(), ncclSuccess); + EXPECT_EQ(amd_smi_shutdown(), ncclSuccess); +} + +TEST(AmdSmiWrapLifecycle, ConcurrentCallersReceiveCachedFailure) +{ + RUN_ISOLATED_TEST_WITH_ENV( + "ConcurrentCallersReceiveCachedFailure", + []() { + constexpr size_t kThreadCount = 16; + std::vector results(kThreadCount, ncclSuccess); + std::vector threads; + threads.reserve(kThreadCount); + + for(size_t i = 0; i < kThreadCount; ++i) + threads.emplace_back([&, i]() { results[i] = amd_smi_init(); }); + for(auto& thread : threads) + thread.join(); + + for(size_t i = 0; i < results.size(); ++i) + EXPECT_EQ(results[i], ncclInternalError) << "caller " << i; + + EXPECT_EQ(lifecycleStubCounter("amdsmi_test_init_count"), 1u); + EXPECT_EQ(amd_smi_init(), ncclInternalError); + EXPECT_EQ(lifecycleStubCounter("amdsmi_test_init_count"), 1u); + EXPECT_EQ(amd_smi_shutdown(), ncclSuccess); + EXPECT_EQ(lifecycleStubCounter("amdsmi_test_shutdown_count"), 0u); + }, + {{"RCCL_USE_AMD_SMI_LIB", "1"}, + {"RCCL_AMDSMI_TEST_DELAY_INIT", "1"}, + {"RCCL_AMDSMI_TEST_FAIL_INIT", "1"}, + {"LD_LIBRARY_PATH", lifecycleStubLibraryPath()}} + ); +} + +TEST(AmdSmiWrapLifecycle, ShutdownCleansUpAfterVersionFailure) +{ + RUN_ISOLATED_TEST_WITH_ENV( + "ShutdownCleansUpAfterVersionFailure", + []() { + EXPECT_EQ(amd_smi_init(), ncclInternalError); + EXPECT_EQ(amd_smi_init(), ncclInternalError); + EXPECT_EQ(lifecycleStubCounter("amdsmi_test_init_count"), 1u); + EXPECT_EQ(lifecycleStubCounter("amdsmi_test_version_count"), 1u); + EXPECT_EQ(lifecycleStubCounter("amdsmi_test_shutdown_count"), 0u); + + EXPECT_EQ(amd_smi_shutdown(), ncclSuccess); + EXPECT_EQ(lifecycleStubCounter("amdsmi_test_shutdown_count"), 1u); + + // Shutdown clears the cached failure and permits a fresh attempt. + EXPECT_EQ(amd_smi_init(), ncclInternalError); + EXPECT_EQ(lifecycleStubCounter("amdsmi_test_init_count"), 2u); + EXPECT_EQ(lifecycleStubCounter("amdsmi_test_version_count"), 2u); + EXPECT_EQ(amd_smi_shutdown(), ncclSuccess); + EXPECT_EQ(lifecycleStubCounter("amdsmi_test_shutdown_count"), 2u); + }, + {{"RCCL_USE_AMD_SMI_LIB", "1"}, + {"RCCL_AMDSMI_TEST_FAIL_VERSION", "1"}, + {"LD_LIBRARY_PATH", lifecycleStubLibraryPath()}} + ); +} + +TEST_F(AmdSmiWrapTest, PciBusIdIsPopulatedForEveryDevice) +{ + requireDevices(1); + + for(uint32_t i = 0; i < numDevices_; i++) + { + char busId[32] = {0}; + ASSERT_EQ(amd_smi_getDevicePciBusIdString(i, busId, sizeof(busId)), ncclSuccess) + << "device " << i; + EXPECT_GT(strlen(busId), 0u) << "device " << i << " reported an empty bus ID"; + } +} + +// The topology code identifies GPUs by bus ID, so the mapping has to be a +// bijection: every device's own bus ID must resolve back to that same index. +TEST_F(AmdSmiWrapTest, PciBusIdRoundTripsToDeviceIndex) +{ + requireDevices(1); + + for(uint32_t i = 0; i < numDevices_; i++) + { + char busId[32] = {0}; + ASSERT_EQ(amd_smi_getDevicePciBusIdString(i, busId, sizeof(busId)), ncclSuccess); + + uint32_t deviceIndex = 0xFFFFFFFFu; + ASSERT_EQ(amd_smi_getDeviceIndexByPciBusId(busId, &deviceIndex), ncclSuccess) + << "bus ID " << busId; + EXPECT_EQ(deviceIndex, i) << "bus ID " << busId << " resolved to the wrong device"; + } +} + +TEST_F(AmdSmiWrapTest, LinkInfoIsReportedBetweenTwoDevices) +{ + requireDevices(2); + + amdsmi_link_type_t linkType; + int hops = -1; + int count = -1; + ASSERT_EQ(amd_smi_getLinkInfo(0, 1, &linkType, &hops, &count), ncclSuccess); + + EXPECT_GE(hops, 0); + EXPECT_GE(count, 0); +} + +TEST_F(AmdSmiWrapTest, FirmwareVersionQuerySucceeds) +{ + requireDevices(1); + + // Reported as 0 when the backend cannot source it; only the call contract + // is under test here. + uint64_t fwVersion = 0; + EXPECT_EQ(amd_smi_getFirmwareVersion(0, &fwVersion), ncclSuccess); +} + +// amd_smi_isFabricSupported() and amd_smi_getFabricBandwidth() both read the +// cache that amd_smi_ensureFabricInitialized() fills, so they must agree with +// the cached record rather than each deriving their own answer. +TEST_F(AmdSmiWrapTest, FabricViewsAgreeWithCachedDeviceInfo) +{ + requireDevices(1); + ASSERT_EQ(amd_smi_ensureFabricInitialized(), ncclSuccess); + + for(uint32_t i = 0; i < numDevices_; i++) + { + struct amdsmiFabricDeviceInfo devInfo; + memset(&devInfo, 0, sizeof(devInfo)); + ASSERT_EQ(amd_smi_getFabricDeviceInfo(i, &devInfo), ncclSuccess) << "device " << i; + + bool supported = false; + ASSERT_EQ(amd_smi_isFabricSupported(i, &supported), ncclSuccess) << "device " << i; + EXPECT_EQ(supported, devInfo.fabricSupported) << "device " << i; + + uint32_t bandwidth = 0; + ASSERT_EQ(amd_smi_getFabricBandwidth(i, &bandwidth), ncclSuccess) << "device " << i; + + if(!supported) + { + // Documented contract: 0 tells the caller to fall back to + // arch-based defaults instead of trusting a fabric number. + EXPECT_EQ(bandwidth, 0u) << "device " << i << " reported bandwidth without fabric"; + continue; + } + + EXPECT_EQ(bandwidth, devInfo.bandwidth) << "device " << i; + // A device is only marked supported when it passed this predicate, so + // the cached type/state must still satisfy it. + EXPECT_TRUE(amdSmiFabricStateUsable(devInfo.fabricType, devInfo.state)) + << "device " << i << " marked supported with type " << devInfo.fabricType + << " state " << devInfo.state; + EXPECT_GT(devInfo.vpodSize, 0u) << "device " << i; + } +} + +// amd_smi_fabricTelemIdToString dispatches on the loaded runtime's major +// version between the pre-27 return-value signature and the 27+ out-param +// signature (see amdSmiTelemIdUsesOutParam in amdsmi_wrap.h). The predicate +// itself is unit-tested in AmdSmiFabricTests; this drives the actual dispatch +// against whichever runtime is really loaded and pins the contract its callers +// depend on: for any id it returns a printable string, never nullptr. A +// regression in the reinterpret_cast legacy path (or the out-param path) would +// otherwise only surface as a crash or garbage log line on real hardware. +TEST_F(AmdSmiWrapTest, FabricTelemIdToStringNeverReturnsNull) +{ + requireDevices(1); + if(amd_smi_ensureFabricInitialized() != ncclSuccess) + GTEST_SKIP() << "fabric not available on this machine"; + + // An id the runtime does not map still has to come back as a printable + // string, because callers log it unconditionally. Whichever call + // convention matched the loaded library, both paths collapse a missing + // name to "UNKNOWN" rather than nullptr. + const char* unknownName = amd_smi_fabricTelemIdToString(~static_cast(0)); + ASSERT_NE(unknownName, nullptr); + EXPECT_GT(std::strlen(unknownName), 0u); +} + +// The regression this suite exists for: RCCL_USE_AMD_SMI_LIB selects between +// amdsmi_get_gpu_fabric_info() and the ualink sysfs nodes, and the two are +// meant to be interchangeable views of one fabric. They silently diverged when +// the library returned SUCCESS without ever writing fabric_info.version and +// RCCL rejected the payload on that basis, so the library path reported no +// fabric on hardware where sysfs plainly showed it. +// +// Runs isolated because RCCL_PARAM caches the env var in a static on first +// read, so the value has to be set before this process starts. +TEST(AmdSmiWrapFabricPaths, LibraryPathMatchesSysfs) +{ + RUN_ISOLATED_TEST_WITH_ENV( + "LibraryPathMatchesSysfs", + []() { + if(amd_smi_init() != ncclSuccess) + GTEST_SKIP() << "amd_smi_init() failed: no usable SMI backend"; + + // Read sysfs first: it decides whether this machine has fabric at + // all, and so whether there is anything to compare against. + ASSERT_EQ(ARSMI_init(), 0); + struct ARSMI_fabricInfo sysfsInfo; + memset(&sysfsInfo, 0, sizeof(sysfsInfo)); + const int sysfsResult = ARSMI_get_fabric_info(0, &sysfsInfo); + if(sysfsResult == ENODEV) + GTEST_SKIP() << "device 0 has no ualink sysfs node: no fabric on this machine"; + ASSERT_EQ(sysfsResult, 0); + + ASSERT_EQ(amd_smi_ensureFabricInitialized(), ncclSuccess); + struct amdsmiFabricDeviceInfo libInfo; + memset(&libInfo, 0, sizeof(libInfo)); + ASSERT_EQ(amd_smi_getFabricDeviceInfo(0, &libInfo), ncclSuccess); + + EXPECT_EQ(libInfo.fabricSupported, sysfsInfo.supported == 1) + << "amd_smi and sysfs disagree on whether device 0 has fabric"; + EXPECT_EQ(static_cast(libInfo.fabricType), static_cast(sysfsInfo.fabric_type)); + EXPECT_EQ(static_cast(libInfo.state), static_cast(sysfsInfo.accel_state)); + EXPECT_EQ(libInfo.acceleratorId, sysfsInfo.accel_id); + EXPECT_EQ(libInfo.ppodSize, sysfsInfo.ppod_size); + EXPECT_EQ(libInfo.cliqueId, sysfsInfo.vpod_id); + EXPECT_EQ(libInfo.vpodSize, sysfsInfo.vpod_size); + + EXPECT_EQ(amd_smi_shutdown(), ncclSuccess); + }, + {{"RCCL_USE_AMD_SMI_LIB", "1"}} + ); +} + +} // namespace RcclUnitTesting diff --git a/projects/rccl/test/CMakeLists.txt b/projects/rccl/test/CMakeLists.txt index cd79faaeba78..632a5924bcc5 100644 --- a/projects/rccl/test/CMakeLists.txt +++ b/projects/rccl/test/CMakeLists.txt @@ -428,6 +428,82 @@ if(BUILD_TESTS) endif() endif() + # rccl-UnitTestsAmdSmi: tests for the amdsmi_wrap layer. + # + # AmdSmiFabricTests.cpp covers the fabric predicates and struct layout pinned in + # amdsmi_wrap.h. Those are header-inline, so they need no librccl.so symbols and + # run in both Release and Debug. + # + # AmdSmiWrapTests.cpp drives the amd_smi_* API itself, so it needs symbols that + # src/CMakeLists.txt only exports under -fvisibility=default in Debug. Adding it + # in Release would fail to link, so it joins the target only where they exist. + if (ROCM_VERSION VERSION_GREATER_EQUAL "60400") + list(APPEND RCCL_TEST_EXECUTABLES rccl-UnitTestsAmdSmi) + + # main_altrsmi.cpp is a plain InitGoogleTest/RUN_ALL_TESTS entry point, reused here. + set(TEST_AMDSMI_SOURCE_FILES + AmdSmiFabricTests.cpp + common/main_altrsmi.cpp + ) + + if(CMAKE_BUILD_TYPE MATCHES "Debug") + list(APPEND TEST_AMDSMI_SOURCE_FILES + AmdSmiWrapTests.cpp + common/ProcessIsolatedTestRunner.cpp + ) + + # Loaded only by isolated lifecycle tests, never by the normal test process. + add_library(rccl-AmdSmiLifecycleStub SHARED AmdSmiLifecycleStub.cpp) + set_target_properties(rccl-AmdSmiLifecycleStub PROPERTIES + OUTPUT_NAME "amd_smi" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/amdsmi_lifecycle_stub" + ) + + # Match the versioned name used by amdsmi_wrap when headers are present. + # Older ROCm configurations may not define SMI_INCLUDE_DIR, so also check + # ROCM_PATH; without either header amdsmi_wrap uses the unversioned name. + if(EXISTS "${SMI_INCLUDE_DIR}/amd_smi/amdsmi.h") + set(AMDSMI_TEST_HEADER "${SMI_INCLUDE_DIR}/amd_smi/amdsmi.h") + elseif(EXISTS "${ROCM_PATH}/include/amd_smi/amdsmi.h") + set(AMDSMI_TEST_HEADER "${ROCM_PATH}/include/amd_smi/amdsmi.h") + endif() + if(AMDSMI_TEST_HEADER) + file(STRINGS "${AMDSMI_TEST_HEADER}" AMDSMI_VERSION_MAJOR_LINE + REGEX "^#define AMDSMI_LIB_VERSION_MAJOR [0-9]+" + LIMIT_COUNT 1 + ) + string(REGEX REPLACE ".* ([0-9]+)$" "\\1" + AMDSMI_TEST_SOVERSION "${AMDSMI_VERSION_MAJOR_LINE}" + ) + set_target_properties(rccl-AmdSmiLifecycleStub PROPERTIES + SOVERSION "${AMDSMI_TEST_SOVERSION}" + ) + endif() + endif() + + add_executable(rccl-UnitTestsAmdSmi ${TEST_AMDSMI_SOURCE_FILES}) + + if(CMAKE_BUILD_TYPE MATCHES "Debug") + add_dependencies(rccl-UnitTestsAmdSmi rccl-AmdSmiLifecycleStub) + target_compile_definitions(rccl-UnitTestsAmdSmi PRIVATE + AMDSMI_TEST_STUB_DIR="$" + AMDSMI_TEST_STUB_SONAME="$" + ) + endif() + + # amdsmi_wrap.h needs both halves of this pairing: the include path so its + # __has_include finds amdsmi, and AMDSMI_FABRIC_DIRECT, which is PRIVATE to the + # rccl target and so does not reach test TUs. With the headers reachable but the + # define absent, the header's fabric compat block compiles on top of the real + # declarations and every fabric type collides. + if(DEFINED SMI_INCLUDE_DIR) + target_include_directories(rccl-UnitTestsAmdSmi PRIVATE ${SMI_INCLUDE_DIR}) + endif() + if(AMDSMI_FABRIC_API) + target_compile_definitions(rccl-UnitTestsAmdSmi PRIVATE AMDSMI_FABRIC_DIRECT) + endif() + endif() + # Create separate MPI test binary if MPI tests are enabled if(ENABLE_MPI_TESTS) set(MPI_TEST_SOURCE_FILES @@ -589,6 +665,23 @@ if(BUILD_TESTS) else() message(WARNING "Skipping test categories for rccl-UnitTestsFixtures: missing ${_rccl_test_categories_fixtures_yaml}") endif() + + # Registered outside the Debug-only block below: this target's fabric predicate + # and layout cases need no librccl symbols and so run in Release too. The + # AmdSmiWrap* patterns in the yaml match nothing in a Release binary, where + # that file is left out for lack of exported amd_smi_* symbols. + set(_rccl_test_categories_amdsmi_yaml "${CMAKE_CURRENT_SOURCE_DIR}/test_categories_amdsmi.yaml") + if(EXISTS "${_rccl_test_categories_amdsmi_yaml}") + message(STATUS "Applying test categories for rccl-UnitTestsAmdSmi") + apply_test_category_labels( + rccl-UnitTestsAmdSmi + "${_rccl_test_categories_amdsmi_yaml}" + "${PROJECT_BINARY_DIR}" + "${INSTALL_TEST_FILE}" + ) + else() + message(WARNING "Skipping test categories for rccl-UnitTestsAmdSmi: missing ${_rccl_test_categories_amdsmi_yaml}") + endif() endif() if(ROCM_VERSION VERSION_GREATER_EQUAL "60400" AND CMAKE_BUILD_TYPE MATCHES "Debug") diff --git a/projects/rccl/test/test_categories_amdsmi.yaml b/projects/rccl/test/test_categories_amdsmi.yaml new file mode 100644 index 000000000000..3cdaadcde4ea --- /dev/null +++ b/projects/rccl/test/test_categories_amdsmi.yaml @@ -0,0 +1,62 @@ +# rccl-UnitTestsAmdSmi — shared/ctest schema (ROCm 6.4+, Release and Debug). +# RCCL does not support Windows: exclude_windows matches all tests so generated suites are empty on Windows. +# +# Suites: +# AmdSmiFabric* — pure predicate and struct layout checks, no hardware needed. +# Present in every build type. +# AmdSmiWrap* — drives the amd_smi_* API against real GPUs; each case skips +# when the devices, amd_smi library, or fabric it needs is absent. +# Debug builds only: the internal amd_smi_* symbols are hidden in +# Release, so these patterns match nothing there. +# +# Tiers: +# quick — canonical anchor; NCCL_DEBUG=INFO for library logging. +# standard — merge quick; override labels. +# comprehensive — merge quick; override labels. +# full — merge quick; override labels. + +test_categories: + quick: &amdsmi_quick + description: "Smoke — amdsmi_wrap fabric predicates, struct layout, and wrapper API, short timeout." + test_patterns: + - "AmdSmiFabric*.*" + - "AmdSmiWrap*.*" + exclude_windows: + - "*" + labels: + - "rccl" + - "quick" + - "smoke" + - "precheck" + - "amdsmi" + env_variables: + - "NCCL_DEBUG=INFO" + standard: + <<: *amdsmi_quick + labels: + - "rccl" + - "standard" + - "regression" + - "amdsmi" + comprehensive: + <<: *amdsmi_quick + labels: + - "rccl" + - "comprehensive" + - "comprehensive_regression" + - "amdsmi" + full: + <<: *amdsmi_quick + labels: + - "rccl" + - "full" + - "full_regression" + - "amdsmi" + +execution_settings: + timeout_multiplier: 1 + category_timeouts: + quick: 600 + standard: 600 + comprehensive: 600 + full: 600 diff --git a/projects/rccl/tools/topo_expl/Makefile b/projects/rccl/tools/topo_expl/Makefile index 7ac28afb0435..d15c634fdbef 100644 --- a/projects/rccl/tools/topo_expl/Makefile +++ b/projects/rccl/tools/topo_expl/Makefile @@ -28,14 +28,34 @@ else NCCL_OS_DEFINE := -DNCCL_OS_WINDOWS endif +# topo_expl bypasses RCCL's CMake configuration, so reproduce the two +# compile-time feature probes needed by headers it imports. +HAVE_HIP_FABRIC_API := $(shell printf '%s\n' \ + '#include ' \ + 'int main() { hipMemFabricHandle_t h{}; (void)h; auto fn = &hipMemImportFromShareableHandle; (void)fn; }' | \ + $(HIPCC) -x c++ -std=c++17 -fsyntax-only -D__HIP_PLATFORM_AMD__ -I$(ROCM_PATH)/include - \ + >/dev/null 2>&1 && echo yes || echo no) +ifeq ($(HAVE_HIP_FABRIC_API),yes) +FEATURE_DEFINES += -DHIP_FABRIC_API +endif + +HAVE_AMDSMI_FABRIC_API := $(shell printf '%s\n' \ + '#include ' \ + 'int main() { amdsmi_fabric_info_t info{}; (void)info; }' | \ + $(HIPCC) -x c++ -std=c++17 -fsyntax-only -I$(ROCM_PATH)/include - \ + >/dev/null 2>&1 && echo yes || echo no) +ifeq ($(HAVE_AMDSMI_FABRIC_API),yes) +FEATURE_DEFINES += -DAMDSMI_FABRIC_DIRECT +endif + CXXFLAGS = -g -ffunction-sections -fdata-sections -DROCM_VERSION=$(ROCM_VERSION) \ -std=c++17 \ -Wl,--gc-sections -fgpu-rdc \ -Iinclude -Itest -Ihipify_rccl/include -Ihipify_rccl/include/plugin \ -Ihipify_rccl/include/nccl_device \ - -Ihipify_rccl/src/device/include -Ihipify_rccl/graph -I/opt/rocm/include/ \ + -Ihipify_rccl/src/device/include -Ihipify_rccl/graph -I$(ROCM_PATH)/include/ \ -DTOPO_EXPL -DENABLE_TRACE -DENABLE_LL128 -DNVTX_NO_IMPL -DRCCL_EXPOSE_STATIC \ - $(NCCL_OS_DEFINE) \ + $(NCCL_OS_DEFINE) $(FEATURE_DEFINES) \ -lpthread TOPO_EXPL_FILES = src/$(EXE).cpp src/topo_expl_api.cpp src/model.cpp src/topo_expl_impl.cpp src/utils.cpp src/stubs.cc test/topo_expl_tests.cpp