[AMD] [sgl-kernel] Bypass caches for peer traffic in ROCm custom all-reduce (#32832)
Co-authored-by: Hubert Lu <Hubert.Lu@amd.com>
This commit is contained in:
@@ -224,14 +224,101 @@ DINLINE void multi_gpu_barrier(const RankSignals& sg, Signal* self_sg, int rank)
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_ROCM
|
||||||
|
// Global cache-bypassing 128-bit load/store, modeled on RCCL's op128.h
|
||||||
|
// load128/store128
|
||||||
|
// (https://github.com/ROCm/rocm-systems/blob/develop/projects/rccl/src/device/op128.h).
|
||||||
|
// The custom all-reduce reads every peer's buffer over xGMI and writes results
|
||||||
|
// that peers consume once; routing this traffic around the caches avoids
|
||||||
|
// polluting L2 with single-use data and keeps peer reads/writes going straight
|
||||||
|
// to HBM.
|
||||||
|
//
|
||||||
|
// Primary path (gfx942/gfx950): the __builtin_amdgcn_global_{load,store}_b128
|
||||||
|
// builtins with a system syncscope ("") lower to global_{load,store}_dwordx4
|
||||||
|
// with the sc0 and sc1 cache-bypass bits set. Fallback path: nontemporal
|
||||||
|
// (streaming) 64-bit accesses, which set the SLC bit -- op128.h's builtin-free
|
||||||
|
// path -- for toolchains/arches without the b128 builtins.
|
||||||
|
#ifndef ROCM_HAVE_GLOBAL_DWORDX4_BUILTINS
|
||||||
|
#if defined(__has_builtin) && __has_builtin(__builtin_amdgcn_global_load_b128) && \
|
||||||
|
__has_builtin(__builtin_amdgcn_global_store_b128)
|
||||||
|
#define ROCM_HAVE_GLOBAL_DWORDX4_BUILTINS 1
|
||||||
|
#else
|
||||||
|
#define ROCM_HAVE_GLOBAL_DWORDX4_BUILTINS 0
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if ROCM_HAVE_GLOBAL_DWORDX4_BUILTINS
|
||||||
|
typedef unsigned int sgl_v4u __attribute__((ext_vector_type(4)));
|
||||||
|
typedef __attribute__((address_space(1))) sgl_v4u* sgl_v4u_gptr;
|
||||||
|
// An empty syncscope string denotes system scope in the AMDGPU backend, which
|
||||||
|
// is what forces both cache-bypass bits (sc0 + sc1) on the dwordx4 access.
|
||||||
|
#define SGL_SYSTEM_SYNCSCOPE ""
|
||||||
|
#endif
|
||||||
|
|
||||||
|
template <typename P>
|
||||||
|
DINLINE P load_bypass(const P* ptr) {
|
||||||
|
static_assert(sizeof(P) == 16, "load_bypass expects a 16-byte packed type");
|
||||||
|
#if ROCM_HAVE_GLOBAL_DWORDX4_BUILTINS
|
||||||
|
union {
|
||||||
|
P p;
|
||||||
|
sgl_v4u v;
|
||||||
|
} u;
|
||||||
|
u.v = __builtin_amdgcn_global_load_b128((sgl_v4u_gptr)ptr, SGL_SYSTEM_SYNCSCOPE);
|
||||||
|
return u.p;
|
||||||
|
#else
|
||||||
|
union {
|
||||||
|
P p;
|
||||||
|
uint64_t u64[2];
|
||||||
|
} u;
|
||||||
|
const uint64_t* addr = reinterpret_cast<const uint64_t*>(ptr);
|
||||||
|
u.u64[0] = __builtin_nontemporal_load(addr);
|
||||||
|
u.u64[1] = __builtin_nontemporal_load(addr + 1);
|
||||||
|
return u.p;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename P>
|
||||||
|
DINLINE void store_bypass(P* ptr, const P& val) {
|
||||||
|
static_assert(sizeof(P) == 16, "store_bypass expects a 16-byte packed type");
|
||||||
|
#if ROCM_HAVE_GLOBAL_DWORDX4_BUILTINS
|
||||||
|
union {
|
||||||
|
P p;
|
||||||
|
sgl_v4u v;
|
||||||
|
} u;
|
||||||
|
u.p = val;
|
||||||
|
__builtin_amdgcn_global_store_b128((sgl_v4u_gptr)ptr, u.v, SGL_SYSTEM_SYNCSCOPE);
|
||||||
|
#else
|
||||||
|
union {
|
||||||
|
P p;
|
||||||
|
uint64_t u64[2];
|
||||||
|
} u;
|
||||||
|
u.p = val;
|
||||||
|
uint64_t* addr = reinterpret_cast<uint64_t*>(ptr);
|
||||||
|
__builtin_nontemporal_store(u.u64[0], addr);
|
||||||
|
__builtin_nontemporal_store(u.u64[1], addr + 1);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
template <typename P, int ngpus, typename A>
|
template <typename P, int ngpus, typename A>
|
||||||
DINLINE P packed_reduce(const P* ptrs[], int idx) {
|
DINLINE P packed_reduce(const P* ptrs[], int idx) {
|
||||||
|
#ifdef USE_ROCM
|
||||||
|
// Read each peer's packed element with a cache-bypassing load so the reduction
|
||||||
|
// consumes fresh remote data without allocating it in L2 (see load_bypass).
|
||||||
|
A tmp = upcast(load_bypass(&ptrs[0][idx]));
|
||||||
|
#pragma unroll
|
||||||
|
for (int i = 1; i < ngpus; i++) {
|
||||||
|
packed_assign_add(tmp, upcast(load_bypass(&ptrs[i][idx])));
|
||||||
|
}
|
||||||
|
return downcast<P>(tmp);
|
||||||
|
#else
|
||||||
A tmp = upcast(ptrs[0][idx]);
|
A tmp = upcast(ptrs[0][idx]);
|
||||||
#pragma unroll
|
#pragma unroll
|
||||||
for (int i = 1; i < ngpus; i++) {
|
for (int i = 1; i < ngpus; i++) {
|
||||||
packed_assign_add(tmp, upcast(ptrs[i][idx]));
|
packed_assign_add(tmp, upcast(ptrs[i][idx]));
|
||||||
}
|
}
|
||||||
return downcast<P>(tmp);
|
return downcast<P>(tmp);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T, int ngpus>
|
template <typename T, int ngpus>
|
||||||
@@ -245,7 +332,11 @@ __global__ void __launch_bounds__(kMaxThreadsPerBlock, 1) cross_device_reduce_1s
|
|||||||
multi_gpu_barrier<ngpus, true>(sg, self_sg, rank);
|
multi_gpu_barrier<ngpus, true>(sg, self_sg, rank);
|
||||||
// do the actual reduction
|
// do the actual reduction
|
||||||
for (int idx = blockIdx.x * blockDim.x + threadIdx.x; idx < size; idx += gridDim.x * blockDim.x) {
|
for (int idx = blockIdx.x * blockDim.x + threadIdx.x; idx < size; idx += gridDim.x * blockDim.x) {
|
||||||
|
#ifdef USE_ROCM
|
||||||
|
store_bypass(&((P*)result)[idx], packed_reduce<P, ngpus, A>((const P**)&dp.ptrs[0], idx));
|
||||||
|
#else
|
||||||
((P*)result)[idx] = packed_reduce<P, ngpus, A>((const P**)&dp.ptrs[0], idx);
|
((P*)result)[idx] = packed_reduce<P, ngpus, A>((const P**)&dp.ptrs[0], idx);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
multi_gpu_barrier<ngpus, false>(sg, self_sg, rank);
|
multi_gpu_barrier<ngpus, false>(sg, self_sg, rank);
|
||||||
}
|
}
|
||||||
@@ -402,7 +493,13 @@ __global__ void __launch_bounds__(kMaxThreadsPerBlock, 1) cross_device_reduce_2s
|
|||||||
multi_gpu_barrier<ngpus, true>(sg, self_sg, rank);
|
multi_gpu_barrier<ngpus, true>(sg, self_sg, rank);
|
||||||
// stage 1: reduce scatter
|
// stage 1: reduce scatter
|
||||||
for (int idx = start + tid; idx < end; idx += stride) {
|
for (int idx = start + tid; idx < end; idx += stride) {
|
||||||
|
#ifdef USE_ROCM
|
||||||
|
// tmp_out lives in this rank's IPC-shared buffer and is read by peers in
|
||||||
|
// stage 2, so bypass the cache on the write as well.
|
||||||
|
store_bypass(&tmp_out[idx - start], packed_reduce<P, ngpus, A>(ptrs, idx));
|
||||||
|
#else
|
||||||
tmp_out[idx - start] = packed_reduce<P, ngpus, A>(ptrs, idx);
|
tmp_out[idx - start] = packed_reduce<P, ngpus, A>(ptrs, idx);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
multi_gpu_barrier<ngpus, false, true>(sg, self_sg, rank);
|
multi_gpu_barrier<ngpus, false, true>(sg, self_sg, rank);
|
||||||
|
|
||||||
@@ -417,7 +514,13 @@ __global__ void __launch_bounds__(kMaxThreadsPerBlock, 1) cross_device_reduce_2s
|
|||||||
int gather_from_rank = ((rank + i) % ngpus);
|
int gather_from_rank = ((rank + i) % ngpus);
|
||||||
if (gather_from_rank == ngpus - 1 || idx < part) {
|
if (gather_from_rank == ngpus - 1 || idx < part) {
|
||||||
int dst_idx = gather_from_rank * part + idx;
|
int dst_idx = gather_from_rank * part + idx;
|
||||||
|
#ifdef USE_ROCM
|
||||||
|
// Gather each peer's reduced chunk with a cache-bypassing load and write
|
||||||
|
// the local result with a cache-bypassing store.
|
||||||
|
store_bypass(&((P*)result)[dst_idx], load_bypass(&tmps[i][idx]));
|
||||||
|
#else
|
||||||
((P*)result)[dst_idx] = tmps[i][idx];
|
((P*)result)[dst_idx] = tmps[i][idx];
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,14 +247,101 @@ DINLINE void end_sync(
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_ROCM
|
||||||
|
// Global cache-bypassing 128-bit load/store, modeled on RCCL's op128.h
|
||||||
|
// load128/store128
|
||||||
|
// (https://github.com/ROCm/rocm-systems/blob/develop/projects/rccl/src/device/op128.h).
|
||||||
|
// The custom all-reduce reads every peer's buffer over xGMI and writes results
|
||||||
|
// that peers consume once; routing this traffic around the caches avoids
|
||||||
|
// polluting L2 with single-use data and keeps peer reads/writes going straight
|
||||||
|
// to HBM.
|
||||||
|
//
|
||||||
|
// Primary path (gfx942/gfx950): the __builtin_amdgcn_global_{load,store}_b128
|
||||||
|
// builtins with a system syncscope ("") lower to global_{load,store}_dwordx4
|
||||||
|
// with the sc0 and sc1 cache-bypass bits set. Fallback path: nontemporal
|
||||||
|
// (streaming) 64-bit accesses, which set the SLC bit -- op128.h's builtin-free
|
||||||
|
// path -- for toolchains/arches without the b128 builtins.
|
||||||
|
#ifndef ROCM_HAVE_GLOBAL_DWORDX4_BUILTINS
|
||||||
|
#if defined(__has_builtin) && __has_builtin(__builtin_amdgcn_global_load_b128) && \
|
||||||
|
__has_builtin(__builtin_amdgcn_global_store_b128)
|
||||||
|
#define ROCM_HAVE_GLOBAL_DWORDX4_BUILTINS 1
|
||||||
|
#else
|
||||||
|
#define ROCM_HAVE_GLOBAL_DWORDX4_BUILTINS 0
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if ROCM_HAVE_GLOBAL_DWORDX4_BUILTINS
|
||||||
|
typedef unsigned int sgl_v4u __attribute__((ext_vector_type(4)));
|
||||||
|
typedef __attribute__((address_space(1))) sgl_v4u* sgl_v4u_gptr;
|
||||||
|
// An empty syncscope string denotes system scope in the AMDGPU backend, which
|
||||||
|
// is what forces both cache-bypass bits (sc0 + sc1) on the dwordx4 access.
|
||||||
|
#define SGL_SYSTEM_SYNCSCOPE ""
|
||||||
|
#endif
|
||||||
|
|
||||||
|
template <typename P>
|
||||||
|
DINLINE P load_bypass(const P* ptr) {
|
||||||
|
static_assert(sizeof(P) == 16, "load_bypass expects a 16-byte packed type");
|
||||||
|
#if ROCM_HAVE_GLOBAL_DWORDX4_BUILTINS
|
||||||
|
union {
|
||||||
|
P p;
|
||||||
|
sgl_v4u v;
|
||||||
|
} u;
|
||||||
|
u.v = __builtin_amdgcn_global_load_b128((sgl_v4u_gptr)ptr, SGL_SYSTEM_SYNCSCOPE);
|
||||||
|
return u.p;
|
||||||
|
#else
|
||||||
|
union {
|
||||||
|
P p;
|
||||||
|
uint64_t u64[2];
|
||||||
|
} u;
|
||||||
|
const uint64_t* addr = reinterpret_cast<const uint64_t*>(ptr);
|
||||||
|
u.u64[0] = __builtin_nontemporal_load(addr);
|
||||||
|
u.u64[1] = __builtin_nontemporal_load(addr + 1);
|
||||||
|
return u.p;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename P>
|
||||||
|
DINLINE void store_bypass(P* ptr, const P& val) {
|
||||||
|
static_assert(sizeof(P) == 16, "store_bypass expects a 16-byte packed type");
|
||||||
|
#if ROCM_HAVE_GLOBAL_DWORDX4_BUILTINS
|
||||||
|
union {
|
||||||
|
P p;
|
||||||
|
sgl_v4u v;
|
||||||
|
} u;
|
||||||
|
u.p = val;
|
||||||
|
__builtin_amdgcn_global_store_b128((sgl_v4u_gptr)ptr, u.v, SGL_SYSTEM_SYNCSCOPE);
|
||||||
|
#else
|
||||||
|
union {
|
||||||
|
P p;
|
||||||
|
uint64_t u64[2];
|
||||||
|
} u;
|
||||||
|
u.p = val;
|
||||||
|
uint64_t* addr = reinterpret_cast<uint64_t*>(ptr);
|
||||||
|
__builtin_nontemporal_store(u.u64[0], addr);
|
||||||
|
__builtin_nontemporal_store(u.u64[1], addr + 1);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
template <typename P, int ngpus, typename A>
|
template <typename P, int ngpus, typename A>
|
||||||
DINLINE P packed_reduce(const P* ptrs[], int idx) {
|
DINLINE P packed_reduce(const P* ptrs[], int idx) {
|
||||||
|
#ifdef USE_ROCM
|
||||||
|
// Read each peer's packed element with a cache-bypassing load so the reduction
|
||||||
|
// consumes fresh remote data without allocating it in L2 (see load_bypass).
|
||||||
|
A tmp = upcast(load_bypass(&ptrs[0][idx]));
|
||||||
|
#pragma unroll
|
||||||
|
for (int i = 1; i < ngpus; i++) {
|
||||||
|
packed_assign_add(tmp, upcast(load_bypass(&ptrs[i][idx])));
|
||||||
|
}
|
||||||
|
return downcast<P>(tmp);
|
||||||
|
#else
|
||||||
A tmp = upcast(ptrs[0][idx]);
|
A tmp = upcast(ptrs[0][idx]);
|
||||||
#pragma unroll
|
#pragma unroll
|
||||||
for (int i = 1; i < ngpus; i++) {
|
for (int i = 1; i < ngpus; i++) {
|
||||||
packed_assign_add(tmp, upcast(ptrs[i][idx]));
|
packed_assign_add(tmp, upcast(ptrs[i][idx]));
|
||||||
}
|
}
|
||||||
return downcast<P>(tmp);
|
return downcast<P>(tmp);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T, int ngpus>
|
template <typename T, int ngpus>
|
||||||
@@ -276,7 +363,11 @@ __global__ void __launch_bounds__(512, 1) cross_device_reduce_1stage(
|
|||||||
start_sync<ngpus>(sg, self_sg, rank);
|
start_sync<ngpus>(sg, self_sg, rank);
|
||||||
// do the actual reduction
|
// do the actual reduction
|
||||||
for (int idx = blockIdx.x * blockDim.x + threadIdx.x; idx < size; idx += gridDim.x * blockDim.x) {
|
for (int idx = blockIdx.x * blockDim.x + threadIdx.x; idx < size; idx += gridDim.x * blockDim.x) {
|
||||||
|
#ifdef USE_ROCM
|
||||||
|
store_bypass(&((P*)result)[idx], packed_reduce<P, ngpus, A>((const P**)&dp.ptrs[0], idx));
|
||||||
|
#else
|
||||||
((P*)result)[idx] = packed_reduce<P, ngpus, A>((const P**)&dp.ptrs[0], idx);
|
((P*)result)[idx] = packed_reduce<P, ngpus, A>((const P**)&dp.ptrs[0], idx);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
end_sync<ngpus, true>(sg, self_sg, rank);
|
end_sync<ngpus, true>(sg, self_sg, rank);
|
||||||
}
|
}
|
||||||
@@ -321,7 +412,13 @@ __global__ void __launch_bounds__(512, 1) cross_device_reduce_2stage(
|
|||||||
start_sync<ngpus>(sg, self_sg, rank);
|
start_sync<ngpus>(sg, self_sg, rank);
|
||||||
// stage 1: reduce scatter
|
// stage 1: reduce scatter
|
||||||
for (int idx = start + tid; idx < end; idx += stride) {
|
for (int idx = start + tid; idx < end; idx += stride) {
|
||||||
|
#ifdef USE_ROCM
|
||||||
|
// tmp_out lives in this rank's IPC-shared buffer and is read by peers in
|
||||||
|
// stage 2, so bypass the cache on the write as well.
|
||||||
|
store_bypass(&tmp_out[idx - start], packed_reduce<P, ngpus, A>(ptrs, idx));
|
||||||
|
#else
|
||||||
tmp_out[idx - start] = packed_reduce<P, ngpus, A>(ptrs, idx);
|
tmp_out[idx - start] = packed_reduce<P, ngpus, A>(ptrs, idx);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
end_sync<ngpus>(sg, self_sg, rank);
|
end_sync<ngpus>(sg, self_sg, rank);
|
||||||
|
|
||||||
@@ -336,7 +433,13 @@ __global__ void __launch_bounds__(512, 1) cross_device_reduce_2stage(
|
|||||||
int gather_from_rank = ((rank + i) % ngpus);
|
int gather_from_rank = ((rank + i) % ngpus);
|
||||||
if (gather_from_rank == ngpus - 1 || idx < part) {
|
if (gather_from_rank == ngpus - 1 || idx < part) {
|
||||||
int dst_idx = gather_from_rank * part + idx;
|
int dst_idx = gather_from_rank * part + idx;
|
||||||
|
#ifdef USE_ROCM
|
||||||
|
// Gather each peer's reduced chunk with a cache-bypassing load and write
|
||||||
|
// the local result with a cache-bypassing store.
|
||||||
|
store_bypass(&((P*)result)[dst_idx], load_bypass(&tmps[i][idx]));
|
||||||
|
#else
|
||||||
((P*)result)[dst_idx] = tmps[i][idx];
|
((P*)result)[dst_idx] = tmps[i][idx];
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user