/************************************************************************* * Copyright (c) 2026 Advanced Micro Devices, Inc. All rights reserved. * * See LICENSE.txt for license information ************************************************************************/ // Single-GPU device-leaf tests for the GIN proxy backend. #include "DeviceTestBase.hpp" // gin_proxy.h declares specializations of templates (ncclGinApi_Put, ...) and // references ncclGinCtx / ncclGinDescriptorSmem; their primary declarations // live in gin_device_common.h, which must be included first. #include "nccl_device/coop.h" #include "nccl_device/impl/core__funcs.h" #include "nccl_device/gin/gin_device_host_common.h" #include "nccl_device/gin/gin_device_common.h" #include "nccl_device/gin/proxy/gin_proxy.h" #include "nccl_device/gin/proxy/gin_proxy_device_host_common.h" #include namespace RcclUnitTesting { class GinDeviceTest : public DeviceTestBase {}; // --------------------------------------------------------------------------- // ConstructProxyOp: pack (hasInline, hasSignal, signalOp, hasCounter) -> op byte // --------------------------------------------------------------------------- struct OpCase { bool hasInline; bool hasSignal; uint32_t signalOp; // unused when hasSignal == false bool hasCounter; }; // Host-side reference encoding. static uint8_t expectedOp(const OpCase& c) { uint8_t op = static_cast(ncclGinProxyOpPut); if (c.hasInline) op |= static_cast(ncclGinProxyOpWithInline); if (c.hasCounter) op |= static_cast(ncclGinProxyOpWithCounter); if (c.hasSignal) { if (c.signalOp == static_cast(ncclGinSignalInc)) op |= static_cast(ncclGinProxyOpWithSignalInc); else op |= static_cast(ncclGinProxyOpWithSignalAdd); } return op; } __global__ void kernelConstructProxyOp(const OpCase* __restrict__ in, uint8_t* __restrict__ out, int n) { int i = blockIdx.x * blockDim.x + threadIdx.x; if (i >= n) return; ncclGinProxyOp_t op; nccl::gin::proxy::constructProxyOp( op, /*isGet=*/false, /*isFlush=*/false, in[i].hasInline, in[i].hasSignal ? NCCL_GIN_SIGNAL_TYPE_INDEXED : NCCL_GIN_SIGNAL_TYPE_NONE, static_cast(in[i].signalOp), in[i].hasCounter); out[i] = static_cast(op); } TEST_F(GinDeviceTest, ConstructProxyOp) { // 12 effective cases: 2 (hasInline) * 2 (hasCounter) * 3 (no-signal | Inc | Add). std::vector cases; cases.reserve(12); for (bool hasInline : {false, true}) { for (bool hasCounter : {false, true}) { cases.push_back({hasInline, /*hasSignal=*/false, static_cast(ncclGinSignalInc), hasCounter}); cases.push_back({hasInline, /*hasSignal=*/true, static_cast(ncclGinSignalInc), hasCounter}); cases.push_back({hasInline, /*hasSignal=*/true, static_cast(ncclGinSignalAdd), hasCounter}); } } const int N = static_cast(cases.size()); ASSERT_EQ(N, 12); DeviceBuffer d_in(N); DeviceBuffer d_out(N); d_in.copyFrom(cases); d_out.zero(); kernelConstructProxyOp<<>>(d_in.ptr, d_out.ptr, N); syncAndCheck(); std::vector h_out = d_out.copyTo(); for (int i = 0; i < N; i++) { const uint8_t got = h_out[i]; const uint8_t exp = expectedOp(cases[i]); EXPECT_EQ(got, exp) << "case " << i << ": hasInline=" << cases[i].hasInline << ", hasSignal=" << cases[i].hasSignal << ", signalOp=" << cases[i].signalOp << ", hasCounter=" << cases[i].hasCounter << "; got=0x" << std::hex << static_cast(got) << ", expected=0x" << std::hex << static_cast(exp); } // All-flags-off case must equal Put exactly (no stray bits OR'd in). for (int i = 0; i < N; i++) { if (!cases[i].hasInline && !cases[i].hasSignal && !cases[i].hasCounter) { EXPECT_EQ(h_out[i], static_cast(ncclGinProxyOpPut)) << "Base case (no flags) at index " << i << " is not ncclGinProxyOpPut"; } } } // --------------------------------------------------------------------------- // BuildGfd_PutOnly: pack a 128-byte GFD for a non-inline put (no signal, no counter) // --------------------------------------------------------------------------- __global__ void kernelBuildGfdPutOnly(ncclGinProxyGfd_t* gfd, uint64_t srcOff, uint64_t srcHandle, uint64_t dstOff, uint64_t dstHandle, uint64_t size) { if (threadIdx.x != 0 || blockIdx.x != 0) return; nccl::gin::proxy::buildGfd( gfd, ncclGinProxyOpPut, /*srcVal=*/0ULL, // unused (hasInline=false) /*hasInline=*/false, /*srcOff=*/srcOff, /*srcHandle=*/reinterpret_cast(srcHandle), /*dstOff=*/dstOff, /*dstHandle=*/reinterpret_cast(dstHandle), /*size=*/size, /*counterId=*/0, /*signalId=*/0, /*signalVal=*/0, /*signalWindow=*/nullptr, /*signalOff=*/0); } TEST_F(GinDeviceTest, BuildGfd_PutOnly) { static_assert(sizeof(ncclGinProxyGfd_t) == 128, "GFD must be 128 bytes"); // Distinctive 48-bit values so bit-corruption in any qword is visible. // Each constant is a rotation of 0x0123456789ABCDEF truncated to 48 bits, // so every byte differs across the four constants. constexpr uint64_t kSrcOff = 0x0000123456789ABCULL; constexpr uint64_t kSrcHandle = 0x000056789ABCDEF0ULL; constexpr uint64_t kDstOff = 0x00009ABCDEF01234ULL; constexpr uint64_t kDstHandle = 0x0000DEF012345678ULL; constexpr uint64_t kSize = 4096; DeviceBuffer d_gfd(1); d_gfd.zero(); // start from zeros so we know exactly what buildGfd wrote kernelBuildGfdPutOnly<<<1, 1>>>(d_gfd.ptr, kSrcOff, kSrcHandle, kDstOff, kDstHandle, kSize); syncAndCheck(); const ncclGinProxyGfd_t gfd = d_gfd.download(); // Header qword: flag, size (op now lives in the headerExt qword). EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdHeader].header.flag), 1ULL); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdHeaderExt].headerExt.op), static_cast(ncclGinProxyOpPut)); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdHeader].header.size), kSize); // Source address qwords (only valid in the hasInline=false branch). EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSrcOff].srcOff.flag), 1ULL); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSrcOff].srcOff.srcOff), kSrcOff); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSrcHandle].srcHandle.flag), 1ULL); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSrcHandle].srcHandle.srcHandle), kSrcHandle); // Destination address qwords. EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdDstOff].dstOff.flag), 1ULL); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdDstOff].dstOff.dstOff), kDstOff); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdDstHandle].dstHandle.flag), 1ULL); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdDstHandle].dstHandle.dstHandle), kDstHandle); // Completion qword: flag set, ids and signal-value-low zero (no signal/counter). EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdCompletion].completion.flag), 1ULL); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdCompletion].completion.counterId), 0u); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdCompletion].completion.signalId), 0u); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdCompletion].completion.signalValLow), 0u); // SignalVal qword: flag set, high halves of signalVal zero (no signal). EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSignalVal].signalVal.flag), 1ULL); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSignalVal].signalVal.signalValLow2), 0u); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSignalVal].signalVal.signalValHigh), 0u); // buildGfd flags every qword; the trailing qword is the end-of-GFD marker. EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdQwords - 1].flag.v), 1ULL); } // --------------------------------------------------------------------------- // BuildGfd_Inline: pack a 128-byte GFD for an inline put (hasInline=true). // T=uint32_t -> only inlineValLow holds data; sizeof(T)>4/>6 branches stay quiet. // T=uint64_t -> value splits as 32 (Low) + 16 (Low2) + 16 (High) bits. // --------------------------------------------------------------------------- template __global__ void kernelBuildGfdInline(ncclGinProxyGfd_t* gfd, T srcVal, uint64_t dstOff, uint64_t dstHandle) { if (threadIdx.x != 0 || blockIdx.x != 0) return; nccl::gin::proxy::buildGfd( gfd, ncclGinProxyOpPut, srcVal, /*hasInline=*/true, /*srcOff=*/0, // unused (hasInline=true) /*srcHandle=*/reinterpret_cast(0), // unused (hasInline=true) /*dstOff=*/dstOff, /*dstHandle=*/reinterpret_cast(dstHandle), /*size=*/sizeof(T), /*counterId=*/0, /*signalId=*/0, /*signalVal=*/0, /*signalWindow=*/nullptr, /*signalOff=*/0); } TEST_F(GinDeviceTest, BuildGfd_Inline) { static_assert(sizeof(ncclGinProxyGfd_t) == 128, "GFD must be 128 bytes"); constexpr uint64_t kDstOff = 0x00009ABCDEF01234ULL; constexpr uint64_t kDstHandle = 0x0000DEF012345678ULL; constexpr uint32_t kValU32 = 0xA5B6C7D8u; constexpr uint64_t kValU64 = 0x0123456789ABCDEFULL; // ---- uint32_t (4-byte): only inlineValLow set; Low2 and High stay zero ---- { DeviceBuffer d_gfd(1); d_gfd.zero(); kernelBuildGfdInline<<<1, 1>>>(d_gfd.ptr, kValU32, kDstOff, kDstHandle); syncAndCheck(); const ncclGinProxyGfd_t gfd = d_gfd.download(); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdHeader].header.flag), 1ULL) << "u32"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdHeaderExt].headerExt.op), static_cast(ncclGinProxyOpPut)) << "u32"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdHeader].header.size), static_cast(sizeof(uint32_t))) << "u32"; // Inline qwords (slots 1+2): Low holds the whole 32-bit value; Low2 and High must stay 0. EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdInlineLow].inlineLow.flag), 1u) << "u32"; EXPECT_EQ(gfd.qword[ncclGinProxyGfdInlineLow].inlineLow.inlineValLow, kValU32) << "u32"; EXPECT_EQ(gfd.qword[ncclGinProxyGfdInlineLow].inlineLow.inlineValLow2, 0u) << "u32 must skip Low2 (sizeof<=4)"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdInlineHigh].inlineHigh.flag), 1u) << "u32"; EXPECT_EQ(gfd.qword[ncclGinProxyGfdInlineHigh].inlineHigh.inlineValHigh, 0u) << "u32 must skip High (sizeof<=6)"; // Destination address qwords. EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdDstOff].dstOff.flag), 1ULL) << "u32"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdDstOff].dstOff.dstOff), kDstOff) << "u32"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdDstHandle].dstHandle.flag), 1ULL) << "u32"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdDstHandle].dstHandle.dstHandle), kDstHandle) << "u32"; // Completion / SignalVal: no signal, no counter -> id and value fields zero. EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdCompletion].completion.flag), 1ULL) << "u32"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdCompletion].completion.counterId), 0u) << "u32"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdCompletion].completion.signalId), 0u) << "u32"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdCompletion].completion.signalValLow), 0u) << "u32"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSignalVal].signalVal.flag), 1ULL) << "u32"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSignalVal].signalVal.signalValLow2), 0u) << "u32"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSignalVal].signalVal.signalValHigh), 0u) << "u32"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdQwords - 1].flag.v), 1ULL) << "u32"; } // ---- uint64_t (8-byte): value splits across Low (32) + Low2 (16) + High (16) ---- { DeviceBuffer d_gfd(1); d_gfd.zero(); kernelBuildGfdInline<<<1, 1>>>(d_gfd.ptr, kValU64, kDstOff, kDstHandle); syncAndCheck(); const ncclGinProxyGfd_t gfd = d_gfd.download(); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdHeader].header.flag), 1ULL) << "u64"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdHeaderExt].headerExt.op), static_cast(ncclGinProxyOpPut)) << "u64"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdHeader].header.size), static_cast(sizeof(uint64_t))) << "u64"; // Inline split: Low = bits[0:32), Low2 = bits[32:48), High = bits[48:64). constexpr uint32_t expectLow = static_cast(kValU64); // 0x89ABCDEF constexpr uint16_t expectLow2 = static_cast(kValU64 >> 32); // 0x4567 constexpr uint16_t expectHigh = static_cast(kValU64 >> 48); // 0x0123 EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdInlineLow].inlineLow.flag), 1u) << "u64"; EXPECT_EQ(gfd.qword[ncclGinProxyGfdInlineLow].inlineLow.inlineValLow, expectLow) << "u64"; EXPECT_EQ(gfd.qword[ncclGinProxyGfdInlineLow].inlineLow.inlineValLow2, expectLow2) << "u64"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdInlineHigh].inlineHigh.flag), 1u) << "u64"; EXPECT_EQ(gfd.qword[ncclGinProxyGfdInlineHigh].inlineHigh.inlineValHigh, expectHigh) << "u64"; // Round-trip: reassemble the 64-bit value from the three pieces. const uint64_t roundtrip = static_cast(gfd.qword[ncclGinProxyGfdInlineLow].inlineLow.inlineValLow) | (static_cast(gfd.qword[ncclGinProxyGfdInlineLow].inlineLow.inlineValLow2) << 32) | (static_cast(gfd.qword[ncclGinProxyGfdInlineHigh].inlineHigh.inlineValHigh) << 48); EXPECT_EQ(roundtrip, kValU64) << "u64 inline value round-trip"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdDstOff].dstOff.flag), 1ULL) << "u64"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdDstOff].dstOff.dstOff), kDstOff) << "u64"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdDstHandle].dstHandle.flag), 1ULL) << "u64"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdDstHandle].dstHandle.dstHandle), kDstHandle) << "u64"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdCompletion].completion.flag), 1ULL) << "u64"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdCompletion].completion.counterId), 0u) << "u64"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdCompletion].completion.signalId), 0u) << "u64"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdCompletion].completion.signalValLow), 0u) << "u64"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSignalVal].signalVal.flag), 1ULL) << "u64"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSignalVal].signalVal.signalValLow2), 0u) << "u64"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSignalVal].signalVal.signalValHigh), 0u) << "u64"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdQwords - 1].flag.v), 1ULL) << "u64"; } } // --------------------------------------------------------------------------- // BuildGfd_SignalAndCounter: signalId + counterId set; full 64-bit signalVal split as // completion.signalValLow = bits[ 0:16) // signalVal.signalValLow2 = bits[16:32) // signalVal.signalValHigh = bits[32:64) // --------------------------------------------------------------------------- __global__ void kernelBuildGfdSignalAndCounter(ncclGinProxyGfd_t* gfd, uint64_t srcOff, uint64_t srcHandle, uint64_t dstOff, uint64_t dstHandle, uint64_t size, uint32_t counterId, uint32_t signalId, uint64_t signalVal) { if (threadIdx.x != 0 || blockIdx.x != 0) return; nccl::gin::proxy::buildGfd( gfd, static_cast(static_cast(ncclGinProxyOpPut) | static_cast(ncclGinProxyOpWithSignalAdd) | static_cast(ncclGinProxyOpWithCounter)), /*srcVal=*/0ULL, // unused (hasInline=false) /*hasInline=*/false, /*srcOff=*/srcOff, /*srcHandle=*/reinterpret_cast(srcHandle), /*dstOff=*/dstOff, /*dstHandle=*/reinterpret_cast(dstHandle), /*size=*/size, /*counterId=*/counterId, /*signalId=*/signalId, /*signalVal=*/signalVal, /*signalWindow=*/nullptr, /*signalOff=*/0); } TEST_F(GinDeviceTest, BuildGfd_SignalAndCounter) { constexpr uint64_t kSrcOff = 0x0000123456789ABCULL; constexpr uint64_t kSrcHandle = 0x000056789ABCDEF0ULL; constexpr uint64_t kDstOff = 0x00009ABCDEF01234ULL; constexpr uint64_t kDstHandle = 0x0000DEF012345678ULL; constexpr uint64_t kSize = 4096; constexpr uint16_t kSignalId = 0x1111; constexpr uint16_t kCounterId = 0x2222; constexpr uint64_t kSignalVal = 0x0123456789ABCDEFULL; constexpr uint16_t kSigValLow = static_cast(kSignalVal); // 0xCDEF constexpr uint16_t kSigValLow2 = static_cast(kSignalVal >> 16); // 0x89AB constexpr uint32_t kSigValHigh = static_cast(kSignalVal >> 32); // 0x01234567 DeviceBuffer d_gfd(1); d_gfd.zero(); kernelBuildGfdSignalAndCounter<<<1, 1>>>(d_gfd.ptr, kSrcOff, kSrcHandle, kDstOff, kDstHandle, kSize, kCounterId, kSignalId, kSignalVal); syncAndCheck(); const ncclGinProxyGfd_t gfd = d_gfd.download(); // Header: buildGfd stores the op byte unchanged (verify with the same OR we passed in). const uint64_t expectedOp = static_cast(ncclGinProxyOpPut) | static_cast(ncclGinProxyOpWithSignalAdd) | static_cast(ncclGinProxyOpWithCounter); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdHeader].header.flag), 1ULL); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdHeaderExt].headerExt.op), expectedOp); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdHeader].header.size), kSize); // Source + destination address qwords (non-inline path). EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSrcOff].srcOff.flag), 1ULL); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSrcOff].srcOff.srcOff), kSrcOff); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSrcHandle].srcHandle.flag), 1ULL); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSrcHandle].srcHandle.srcHandle), kSrcHandle); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdDstOff].dstOff.flag), 1ULL); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdDstOff].dstOff.dstOff), kDstOff); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdDstHandle].dstHandle.flag), 1ULL); EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdDstHandle].dstHandle.dstHandle), kDstHandle); // Completion qword: ids and low 16 bits of signalVal. EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdCompletion].completion.flag), 1ULL); EXPECT_EQ(gfd.qword[ncclGinProxyGfdCompletion].completion.counterId, kCounterId); EXPECT_EQ(gfd.qword[ncclGinProxyGfdCompletion].completion.signalId, kSignalId); EXPECT_EQ(gfd.qword[ncclGinProxyGfdCompletion].completion.signalValLow, kSigValLow); // SignalVal qword: bits[16:32) and bits[32:64) of signalVal. EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdSignalVal].signalVal.flag), 1ULL); EXPECT_EQ(gfd.qword[ncclGinProxyGfdSignalVal].signalVal.signalValLow2, kSigValLow2); EXPECT_EQ(gfd.qword[ncclGinProxyGfdSignalVal].signalVal.signalValHigh, kSigValHigh); // Round-trip: reassemble the 64-bit signal value from the 16+16+32 pieces. const uint64_t signalValRoundtrip = static_cast(gfd.qword[ncclGinProxyGfdCompletion].completion.signalValLow) | (static_cast(gfd.qword[ncclGinProxyGfdSignalVal].signalVal.signalValLow2) << 16) | (static_cast(gfd.qword[ncclGinProxyGfdSignalVal].signalVal.signalValHigh) << 32); EXPECT_EQ(signalValRoundtrip, kSignalVal) << "signalVal 16+16+32 split round-trip"; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdQwords - 1].flag.v), 1ULL); } // --------------------------------------------------------------------------- // BuildGfd_SizeClasses: hasInline=true with T in {u8, u16, u32, u64}. // The `sizeof(T) > 4` and `sizeof(T) > 6` branches in buildGfd must stay quiet // for T <= 4 bytes (Low2 and High remain 0) and must fire for T = u64. // --------------------------------------------------------------------------- template __global__ void kernelBuildGfdSizeClass(ncclGinProxyGfd_t* gfd, T srcVal) { if (threadIdx.x != 0 || blockIdx.x != 0) return; nccl::gin::proxy::buildGfd( gfd, ncclGinProxyOpPut, srcVal, /*hasInline=*/true, /*srcOff=*/0, // unused /*srcHandle=*/reinterpret_cast(0), // unused /*dstOff=*/0, /*dstHandle=*/reinterpret_cast(0), /*size=*/sizeof(T), /*counterId=*/0, /*signalId=*/0, /*signalVal=*/0, /*signalWindow=*/nullptr, /*signalOff=*/0); } TEST_F(GinDeviceTest, BuildGfd_SizeClasses) { // Each srcVal sets the upper bits non-zero so any spurious write to Low2/High // (which must stay 0 for T <= 4 bytes) would surface as a non-zero readback. // Expected Low2/High depend on sizeof(T): they must remain 0 unless the // sizeof(T)>4 / sizeof(T)>6 branches fire (i.e. only for uint64_t). auto run = [this](auto srcVal, const char* label) { using T = decltype(srcVal); DeviceBuffer d_gfd(1); d_gfd.zero(); kernelBuildGfdSizeClass<<<1, 1>>>(d_gfd.ptr, srcVal); syncAndCheck(); const ncclGinProxyGfd_t gfd = d_gfd.download(); const uint16_t expectLow2 = (sizeof(T) > 4) ? static_cast(static_cast(srcVal) >> 32) : uint16_t{0}; const uint16_t expectHigh = (sizeof(T) > 6) ? static_cast(static_cast(srcVal) >> 48) : uint16_t{0}; EXPECT_EQ(static_cast(gfd.qword[ncclGinProxyGfdHeader].header.size), static_cast(sizeof(T))) << label; EXPECT_EQ(gfd.qword[ncclGinProxyGfdInlineLow].inlineLow.inlineValLow, static_cast(srcVal)) << label; EXPECT_EQ(gfd.qword[ncclGinProxyGfdInlineLow].inlineLow.inlineValLow2, expectLow2) << label << " Low2"; EXPECT_EQ(gfd.qword[ncclGinProxyGfdInlineHigh].inlineHigh.inlineValHigh, expectHigh) << label << " High"; }; run(uint8_t {0xA5u}, "u8"); run(uint16_t{0xA5B6u}, "u16"); run(uint32_t{0xA5B6C7D8u}, "u32"); run(uint64_t{0xA5B6C7D8E9FA0B1CULL}, "u64"); } // --------------------------------------------------------------------------- // PostGfd_OneSlot: a single postGfd writes the input GFD into slot 0 of peer 0's // ring, advances pis[peer] from 0 -> 1, and leaves cis untouched. // --------------------------------------------------------------------------- __global__ void kernelPostGfdOneSlot(ncclGinProxyGpuCtx_t* ctx, ncclGinProxyGfd_t* gfdIn, uint32_t peer) { nccl::gin::proxy::postGfd(ncclCoopCta{}, ctx, gfdIn, peer); } TEST_F(GinDeviceTest, PostGfd_OneSlot) { static_assert(sizeof(ncclGinProxyGfd_t) == 128, "GFD must be 128 bytes"); constexpr uint32_t kNranks = 2; constexpr uint32_t kQueueSize = 4; // power of two: postGfd uses idx & (queueSize-1) constexpr uint32_t kPeer = 0; // Device-side ring + PI/CI arrays + input GFD + the ctx struct itself. DeviceBuffer d_queues(kNranks * kQueueSize); DeviceBuffer d_pis(kNranks); DeviceBuffer d_cis(kNranks); DeviceBuffer d_gfdIn(1); DeviceBuffer d_ctx(1); d_queues.zero(); d_pis.zero(); d_cis.zero(); // Input GFD: byte ramp 0x10.. across the whole descriptor (unique non-zero // bytes). Any byte the kernel fails to write will read back as 0, // distinguishable from the input. ncclGinProxyGfd_t hostGfd{}; uint8_t* gfdBytes = reinterpret_cast(&hostGfd); for (size_t i = 0; i < sizeof(ncclGinProxyGfd_t); i++) gfdBytes[i] = static_cast(0x10 + i); d_gfdIn.upload(hostGfd); // Ctx struct: aliases the device buffers above. counters/signals unused by postGfd. ncclGinProxyGpuCtx_t hostCtx{}; hostCtx.nranks = static_cast(kNranks); hostCtx.queueSize = kQueueSize; hostCtx.queues = d_queues.ptr; hostCtx.pis = d_pis.ptr; hostCtx.cis = d_cis.ptr; hostCtx.counters = nullptr; hostCtx.signals = nullptr; d_ctx.upload(hostCtx); kernelPostGfdOneSlot<<<1, 1>>>(d_ctx.ptr, d_gfdIn.ptr, kPeer); syncAndCheck(); std::vector pis = d_pis.copyTo(); std::vector cis = d_cis.copyTo(); std::vector queues = d_queues.copyTo(); // PI for peer 0 advanced exactly once; PI for peer 1 untouched. EXPECT_EQ(pis[0], 1u) << "peer 0 PI must advance 0 -> 1"; EXPECT_EQ(pis[1], 0u) << "peer 1 PI must be untouched"; // CIs are consumer-side; postGfd must not write them. EXPECT_EQ(cis[0], 0u); EXPECT_EQ(cis[1], 0u); // Slot 0 of peer 0's ring (queues[peer * queueSize + 0] = queues[0]) must hold // the input GFD byte-for-byte. const uint8_t* slotBytes = reinterpret_cast(&queues[0]); for (size_t i = 0; i < sizeof(ncclGinProxyGfd_t); i++) { EXPECT_EQ(slotBytes[i], gfdBytes[i]) << "slot 0 byte " << i << " mismatch"; } // No other slot was written: peer 0 slots 1..3, plus all of peer 1. for (size_t s = 1; s < queues.size(); s++) { const uint8_t* zeroBytes = reinterpret_cast(&queues[s]); for (size_t i = 0; i < sizeof(ncclGinProxyGfd_t); i++) { EXPECT_EQ(zeroBytes[i], 0u) << "slot " << s << " byte " << i << " was unexpectedly written"; } } } // --------------------------------------------------------------------------- // PostGfd_MultiPeer: post one GFD per peer; per-peer ring base addressing // (queues[pe * queueSize], pis[pe], cis[pe]) must keep peers fully isolated. // --------------------------------------------------------------------------- __global__ void kernelPostGfdMultiPeer(ncclGinProxyGpuCtx_t* ctx, ncclGinProxyGfd_t* gfdsIn, uint32_t nranks) { for (uint32_t p = 0; p < nranks; p++) { nccl::gin::proxy::postGfd(ncclCoopCta{}, ctx, &gfdsIn[p], p); } } TEST_F(GinDeviceTest, PostGfd_MultiPeer) { constexpr uint32_t kNranks = 4; constexpr uint32_t kQueueSize = 4; DeviceBuffer d_queues(kNranks * kQueueSize); DeviceBuffer d_pis(kNranks); DeviceBuffer d_cis(kNranks); DeviceBuffer d_gfdsIn(kNranks); DeviceBuffer d_ctx(1); d_queues.zero(); d_pis.zero(); d_cis.zero(); // gfdsIn[p].header.size = p so each peer's slot is uniquely identifiable. std::vector hostGfds(kNranks); std::memset(hostGfds.data(), 0, hostGfds.size() * sizeof(ncclGinProxyGfd_t)); for (uint32_t p = 0; p < kNranks; p++) { hostGfds[p].qword[ncclGinProxyGfdHeader].header.size = p; } d_gfdsIn.copyFrom(hostGfds); ncclGinProxyGpuCtx_t hostCtx{}; hostCtx.nranks = static_cast(kNranks); hostCtx.queueSize = kQueueSize; hostCtx.queues = d_queues.ptr; hostCtx.pis = d_pis.ptr; hostCtx.cis = d_cis.ptr; hostCtx.counters = nullptr; hostCtx.signals = nullptr; d_ctx.upload(hostCtx); kernelPostGfdMultiPeer<<<1, 1>>>(d_ctx.ptr, d_gfdsIn.ptr, kNranks); syncAndCheck(); std::vector pis = d_pis.copyTo(); std::vector cis = d_cis.copyTo(); std::vector queues = d_queues.copyTo(); // Each peer's PI advanced exactly once; each peer's CI untouched. for (uint32_t p = 0; p < kNranks; p++) { EXPECT_EQ(pis[p], 1u) << "pis[" << p << "] must advance 0 -> 1"; EXPECT_EQ(cis[p], 0u) << "cis[" << p << "] must be untouched"; } // Slot 0 of each peer's ring received its own GFD; other slots stay zero. // No cross-peer contamination -- peer p's GFD lands at queues[p * queueSize + 0]. for (uint32_t p = 0; p < kNranks; p++) { const auto& slot0 = queues[p * kQueueSize + 0]; EXPECT_EQ(static_cast(slot0.qword[ncclGinProxyGfdHeader].header.size), static_cast(p)) << "peer " << p << " slot 0 header.size mismatch"; for (uint32_t s = 1; s < kQueueSize; s++) { const auto& slotN = queues[p * kQueueSize + s]; EXPECT_EQ(static_cast(slotN.qword[ncclGinProxyGfdHeader].header.size), 0ULL) << "peer " << p << " slot " << s << " was unexpectedly written"; } } } // --------------------------------------------------------------------------- // PostGfd_Wrap: pre-position pi=ci=queueSize-1 so 4 sequential posts straddle // the queueSize=4 boundary on iter 1 (idx=4 -> slot 0). Tests the // `idx & (queueSize-1)` wrap arithmetic at gin_proxy.h:57 deterministically. // // Note: the credit-wait `while (queueSize <= idx - ci)` uses UNSIGNED // subtraction, so a single producer can advance idx past ci by at most // queueSize-1 before blocking. A more aggressive 8-post wrap test would // need a concurrent consumer to bump ci, which isn't worth the complexity. // Pre-positioning pi/ci near the wrap point still exercises the wrap. // --------------------------------------------------------------------------- __global__ void kernelPostGfdWrap(ncclGinProxyGpuCtx_t* ctx, ncclGinProxyGfd_t* gfdsIn, uint32_t n, uint32_t peer) { for (uint32_t i = 0; i < n; i++) { nccl::gin::proxy::postGfd(ncclCoopCta{}, ctx, &gfdsIn[i], peer); } } TEST_F(GinDeviceTest, PostGfd_Wrap) { constexpr uint32_t kNranks = 1; constexpr uint32_t kQueueSize = 4; constexpr uint32_t kPeer = 0; constexpr uint32_t kPiPreset = 3; // queueSize - 1: iter 1 hits idx=4 (wrap) constexpr uint32_t kCiPreset = 3; // = pi: idx-ci stays in [0, queueSize-1] constexpr uint32_t kNumPosts = 4; DeviceBuffer d_queues(kNranks * kQueueSize); DeviceBuffer d_pis(kNranks); DeviceBuffer d_cis(kNranks); DeviceBuffer d_gfdsIn(kNumPosts); DeviceBuffer d_ctx(1); d_queues.zero(); d_pis.copyFrom(std::vector{kPiPreset}); d_cis.copyFrom(std::vector{kCiPreset}); std::vector hostGfds(kNumPosts); std::memset(hostGfds.data(), 0, hostGfds.size() * sizeof(ncclGinProxyGfd_t)); for (uint32_t i = 0; i < kNumPosts; i++) { hostGfds[i].qword[ncclGinProxyGfdHeader].header.size = i; } d_gfdsIn.copyFrom(hostGfds); ncclGinProxyGpuCtx_t hostCtx{}; hostCtx.nranks = static_cast(kNranks); hostCtx.queueSize = kQueueSize; hostCtx.queues = d_queues.ptr; hostCtx.pis = d_pis.ptr; hostCtx.cis = d_cis.ptr; hostCtx.counters = nullptr; hostCtx.signals = nullptr; d_ctx.upload(hostCtx); kernelPostGfdWrap<<<1, 1>>>(d_ctx.ptr, d_gfdsIn.ptr, kNumPosts, kPeer); syncAndCheck(); std::vector pis = d_pis.copyTo(); std::vector cis = d_cis.copyTo(); std::vector queues = d_queues.copyTo(); // PI advanced by kNumPosts; CI unchanged. EXPECT_EQ(pis[0], kPiPreset + kNumPosts) << "PI must advance 3 -> 7"; EXPECT_EQ(cis[0], kCiPreset) << "CI must be untouched"; // Slot mapping under `idx & (queueSize-1)`: // iter 0: idx=3 -> slot 3 (gfd 0) // iter 1: idx=4 -> slot 0 (gfd 1) <-- WRAP // iter 2: idx=5 -> slot 1 (gfd 2) // iter 3: idx=6 -> slot 2 (gfd 3) EXPECT_EQ(static_cast(queues[3].qword[ncclGinProxyGfdHeader].header.size), 0ULL) << "iter 0 -> slot 3"; EXPECT_EQ(static_cast(queues[0].qword[ncclGinProxyGfdHeader].header.size), 1ULL) << "iter 1 -> slot 0 (wrap)"; EXPECT_EQ(static_cast(queues[1].qword[ncclGinProxyGfdHeader].header.size), 2ULL) << "iter 2 -> slot 1"; EXPECT_EQ(static_cast(queues[2].qword[ncclGinProxyGfdHeader].header.size), 3ULL) << "iter 3 -> slot 2"; } // --------------------------------------------------------------------------- // PostGfd_PiCiOverflow: pre-position pi=ci=0xFFFFFFFE so 4 sequential posts // straddle the uint32 wrap. Verifies that: // 1. `idx = pi.fetch_add(1)` correctly wraps the producer index past 2^32. // 2. `idx & (queueSize-1)` produces the right slot post-wrap. // 3. `queueSize <= idx - ci.load()` (unsigned subtraction) keeps the // credit-wait dormant when both pi and ci cross the boundary together. // --------------------------------------------------------------------------- __global__ void kernelPostGfdOverflow(ncclGinProxyGpuCtx_t* ctx, ncclGinProxyGfd_t* gfdsIn, uint32_t n, uint32_t peer) { for (uint32_t i = 0; i < n; i++) { nccl::gin::proxy::postGfd(ncclCoopCta{}, ctx, &gfdsIn[i], peer); } } TEST_F(GinDeviceTest, PostGfd_PiCiOverflow) { constexpr uint32_t kNranks = 2; constexpr uint32_t kQueueSize = 4; constexpr uint32_t kPeer = 0; constexpr uint32_t kPreset = 0xFFFFFFFEu; // 4 puts straddle the uint32 wrap constexpr uint32_t kNumPosts = 4; DeviceBuffer d_queues(kNranks * kQueueSize); DeviceBuffer d_pis(kNranks); DeviceBuffer d_cis(kNranks); DeviceBuffer d_gfdsIn(kNumPosts); DeviceBuffer d_ctx(1); d_queues.zero(); // pis[0] = cis[0] = 0xFFFFFFFE; pis[1] = cis[1] = 0 (peer 1 acts as a sentinel). d_pis.copyFrom(std::vector{kPreset, 0u}); d_cis.copyFrom(std::vector{kPreset, 0u}); std::vector hostGfds(kNumPosts); std::memset(hostGfds.data(), 0, hostGfds.size() * sizeof(ncclGinProxyGfd_t)); for (uint32_t i = 0; i < kNumPosts; i++) { hostGfds[i].qword[ncclGinProxyGfdHeader].header.size = i; } d_gfdsIn.copyFrom(hostGfds); ncclGinProxyGpuCtx_t hostCtx{}; hostCtx.nranks = static_cast(kNranks); hostCtx.queueSize = kQueueSize; hostCtx.queues = d_queues.ptr; hostCtx.pis = d_pis.ptr; hostCtx.cis = d_cis.ptr; hostCtx.counters = nullptr; hostCtx.signals = nullptr; d_ctx.upload(hostCtx); kernelPostGfdOverflow<<<1, 1>>>(d_ctx.ptr, d_gfdsIn.ptr, kNumPosts, kPeer); syncAndCheck(); std::vector pis = d_pis.copyTo(); std::vector cis = d_cis.copyTo(); std::vector queues = d_queues.copyTo(); // PI started at 0xFFFFFFFE; 4 fetch_adds wrap the uint32 to (0xFFFFFFFE + 4) mod 2^32 = 2. EXPECT_EQ(pis[0], static_cast(kPreset + kNumPosts)) << "PI must wrap through 0xFFFFFFFE -> 0xFFFFFFFF -> 0 -> 1 -> 2"; EXPECT_EQ(pis[1], 0u) << "peer 1 PI must be untouched"; EXPECT_EQ(cis[0], kPreset) << "CI must be untouched"; EXPECT_EQ(cis[1], 0u) << "peer 1 CI must be untouched"; // Slot mapping (idx is the OLD pi value returned by fetch_add): // iter 0: idx=0xFFFFFFFE -> slot 2 (gfd 0) // iter 1: idx=0xFFFFFFFF -> slot 3 (gfd 1) // iter 2: idx=0x00000000 -> slot 0 (gfd 2) // iter 3: idx=0x00000001 -> slot 1 (gfd 3) EXPECT_EQ(static_cast(queues[2].qword[ncclGinProxyGfdHeader].header.size), 0ULL) << "iter 0 -> slot 2"; EXPECT_EQ(static_cast(queues[3].qword[ncclGinProxyGfdHeader].header.size), 1ULL) << "iter 1 -> slot 3"; EXPECT_EQ(static_cast(queues[0].qword[ncclGinProxyGfdHeader].header.size), 2ULL) << "iter 2 -> slot 0 (wrap)"; EXPECT_EQ(static_cast(queues[1].qword[ncclGinProxyGfdHeader].header.size), 3ULL) << "iter 3 -> slot 1"; // Peer 1's ring must remain entirely zero (no cross-peer contamination near the wrap). for (uint32_t s = 0; s < kQueueSize; s++) { EXPECT_EQ(static_cast(queues[kQueueSize + s].qword[ncclGinProxyGfdHeader].header.size), 0ULL) << "peer 1 slot " << s << " was unexpectedly written"; } } // --------------------------------------------------------------------------- // ResetSignal: ncclGinApi_ResetSignal::call writes // 0 to signals[signalId] via base + offset; verify only the targeted cell // is touched and the rest of the pool stays intact. // --------------------------------------------------------------------------- __global__ void kernelResetSignal(ncclGinCtx ctx, ncclGinSignal_t signalId) { if (threadIdx.x != 0 || blockIdx.x != 0) return; ncclGinSignalDescriptor signal{}; signal.type = NCCL_GIN_SIGNAL_TYPE_INDEXED; signal.indexedSignal.signalId = signalId; ncclGinApi_ResetSignal::call(ctx, signal); } TEST_F(GinDeviceTest, ResetSignal) { constexpr uint32_t kNumSignals = 8; constexpr ncclGinSignal_t kTargetId = 3; constexpr uint64_t kPattern = 0xA000ULL; // signals[i] = 0xA000 + i DeviceBuffer d_signals(kNumSignals); DeviceBuffer d_proxyCtx(1); // Pre-fill with non-zero pattern so any spurious zeroing surfaces as a mismatch. std::vector hostSignals(kNumSignals); for (uint32_t i = 0; i < kNumSignals; i++) { hostSignals[i] = kPattern + i; } d_signals.copyFrom(hostSignals); // ResetSignal only reads proxyCtx->signals; the rest of the struct is untouched. ncclGinProxyGpuCtx_t hostProxyCtx{}; hostProxyCtx.signals = d_signals.ptr; d_proxyCtx.upload(hostProxyCtx); // Wrap the proxy ctx in an ncclGinCtx; the leaf only dereferences ctx.handle. ncclGinCtx ctx{}; ctx.backend = NCCL_NET_DEVICE_GIN_PROXY; ctx.handle = d_proxyCtx.ptr; kernelResetSignal<<<1, 1>>>(ctx, kTargetId); syncAndCheck(); std::vector result = d_signals.copyTo(); // Target cell zeroed; all other cells keep their pre-filled value. EXPECT_EQ(result[kTargetId], 0ULL) << "target signal " << kTargetId << " must be zeroed"; for (uint32_t i = 0; i < kNumSignals; i++) { if (i == kTargetId) continue; EXPECT_EQ(result[i], kPattern + i) << "signal " << i << " unexpectedly modified (expected 0x" << std::hex << (kPattern + i) << ")"; } } // --------------------------------------------------------------------------- // ResetCounter: ncclGinApi_ResetCounter::call writes // 0 to counters[counterId] via base + offset. Mirror of ResetSignal but on // the separate counter pool (different base pointer, same failure shape). // --------------------------------------------------------------------------- __global__ void kernelResetCounter(ncclGinCtx ctx, ncclGinCounter_t counterId) { if (threadIdx.x != 0 || blockIdx.x != 0) return; ncclGinApi_ResetCounter::call(ctx, counterId); } TEST_F(GinDeviceTest, ResetCounter) { constexpr uint32_t kNumCounters = 8; constexpr ncclGinCounter_t kTargetId = 5; constexpr uint64_t kPattern = 0xB000ULL; // counters[i] = 0xB000 + i DeviceBuffer d_counters(kNumCounters); DeviceBuffer d_proxyCtx(1); // Pre-fill with non-zero pattern so any spurious zeroing surfaces as a mismatch. std::vector hostCounters(kNumCounters); for (uint32_t i = 0; i < kNumCounters; i++) { hostCounters[i] = kPattern + i; } d_counters.copyFrom(hostCounters); // ResetCounter only reads proxyCtx->counters; the rest of the struct is untouched. ncclGinProxyGpuCtx_t hostProxyCtx{}; hostProxyCtx.counters = d_counters.ptr; d_proxyCtx.upload(hostProxyCtx); // Wrap the proxy ctx in an ncclGinCtx; the leaf only dereferences ctx.handle. ncclGinCtx ctx{}; ctx.backend = NCCL_NET_DEVICE_GIN_PROXY; ctx.handle = d_proxyCtx.ptr; kernelResetCounter<<<1, 1>>>(ctx, kTargetId); syncAndCheck(); std::vector result = d_counters.copyTo(); // Target cell zeroed; all other cells keep their pre-filled value. EXPECT_EQ(result[kTargetId], 0ULL) << "target counter " << kTargetId << " must be zeroed"; for (uint32_t i = 0; i < kNumCounters; i++) { if (i == kTargetId) continue; EXPECT_EQ(result[i], kPattern + i) << "counter " << i << " unexpectedly modified (expected 0x" << std::hex << (kPattern + i) << ")"; } } } // namespace RcclUnitTesting