Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ The library provides some environment variables, which may be useful:
- `EP_GIN_GDAKI_DEBUG`: `0` or `1`, enable NCCL Gin GDAKI debugging output, `0` by default
- `EP_DEBUG_CHECKS`: debug checks compiled into the kernels, off when unset or `0`. `1` or `all` turns on every check, a comma-separated list of the names below turns on single checks
- `queue`: after a network wait of the unordered kernels times out, a short timed flush of the waiting warp's GIN context prints whether this rank's NIC had completed every write posted on it (`NIC drained: 1`) or still held some (`0`). GIN flush timeouts then also print the context id and wait one second before trapping
- `tokens`: checks that every dispatched and combined token holds valid numbers, and prints the rank and token of each invalid one (about 32 reports per second for each kernel). It adds latency, so use it only when you debug. It is best effort: corruption that leaves valid numbers, such as a bit flip, is not found
- `EP_USE_NVIDIA_TOOLS`: `0` or `1`, skip internal profiling when running under external NVIDIA tools, `0` by default
- `EP_DISABLE_BARRIER_PROFILING`: `0` or `1`, disable barrier-based communication profiling in benchmarks, `0` by default
- Build
Expand Down
5 changes: 4 additions & 1 deletion csrc/kernels/elastic/combine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ class CombineReduceEpilogueRuntime final : public jit::LaunchRuntime<CombineRedu
int num_max_tokens_per_rank;
int num_experts, num_topk;
int num_channels;
int debug_checks;

// Parameters
nv_bfloat16* combined_x;
Expand All @@ -285,7 +286,7 @@ class CombineReduceEpilogueRuntime final : public jit::LaunchRuntime<CombineRedu
using namespace deep_ep::elastic;

static void __instantiate_kernel() {{
auto ptr = reinterpret_cast<void*>(&combine_reduce_epilogue_impl<{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}>);
auto ptr = reinterpret_cast<void*>(&combine_reduce_epilogue_impl<{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}>);
}}
)", args.use_expanded_layout, args.allow_multiple_reduction,
args.launch_args.grid_dim.first,
Expand All @@ -295,6 +296,7 @@ static void __instantiate_kernel() {{
args.num_max_tokens_per_rank,
args.num_experts, args.num_topk,
args.num_channels,
args.debug_checks,
args.use_ordered_kernel);
}

Expand Down Expand Up @@ -342,6 +344,7 @@ static void launch_combine_reduce_epilogue(void* combined_x,
.num_max_tokens_per_rank = num_max_tokens_per_rank,
.num_experts = num_experts, .num_topk = num_topk,
.num_channels = num_channels,
.debug_checks = get_debug_check_mask(),
.combined_x = static_cast<nv_bfloat16*>(combined_x),
.combined_topk_weights = combined_topk_weights,
.combined_topk_idx = combined_topk_idx,
Expand Down
7 changes: 5 additions & 2 deletions csrc/kernels/elastic/dispatch.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ class DispatchCopyEpilogueRuntime final : public jit::LaunchRuntime<DispatchCopy
int num_hidden_bytes, num_sf_packs;
int num_max_tokens_per_rank;
int num_experts, num_topk, expert_alignment;
int debug_checks;

// Parameters
void *buffer, *workspace;
Expand All @@ -343,15 +344,16 @@ class DispatchCopyEpilogueRuntime final : public jit::LaunchRuntime<DispatchCopy
using namespace deep_ep::elastic;

static void __instantiate_kernel() {{
auto ptr = reinterpret_cast<void*>(&dispatch_copy_epilogue_impl<{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}>);
auto ptr = reinterpret_cast<void*>(&dispatch_copy_epilogue_impl<{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}>);
}}
)",
args.do_expand, args.cached_mode, args.do_zero_padding,
args.launch_args.grid_dim.first, args.num_channels, args.num_warps,
args.num_scaleout_ranks, args.num_scaleup_ranks,
args.num_hidden_bytes, args.num_sf_packs,
args.num_max_tokens_per_rank,
args.num_experts, args.num_topk, args.expert_alignment);
args.num_experts, args.num_topk, args.expert_alignment,
args.debug_checks);
}

static void launch_impl(const jit::KernelHandle& kernel, const jit::LaunchConfigHandle& config, Args args) {
Expand Down Expand Up @@ -401,6 +403,7 @@ static void launch_dispatch_copy_epilogue(void* buffer, void* workspace,
.num_hidden_bytes = num_hidden_bytes, .num_sf_packs = num_sf_packs,
.num_max_tokens_per_rank = num_max_tokens_per_rank,
.num_experts = num_experts, .num_topk = num_topk, .expert_alignment = expert_alignment,
.debug_checks = get_debug_check_mask(),
.buffer = buffer, .workspace = workspace,
.psum_num_recv_tokens_per_scaleup_rank = psum_num_recv_tokens_per_scaleup_rank,
.psum_num_recv_tokens_per_expert = psum_num_recv_tokens_per_expert,
Expand Down
7 changes: 7 additions & 0 deletions csrc/kernels/elastic/kernel_select.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,11 @@ static bool use_ordered_hybrid_kernel() {
return ordered;
}

// The mask of `EP_DEBUG_CHECKS` (see `deep_ep/common/compiled.cuh`), read once per process. The JIT
// generators pass it to the kernels as the template argument `kDebugChecks`
static int get_debug_check_mask() {
static const int mask = get_debug_checks();
return mask;
}

} // namespace deep_ep::elastic
3 changes: 2 additions & 1 deletion deep_ep/include/deep_ep/common/compiled.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,13 @@ constexpr bool kEnableSM90Features = false;
constexpr int kDebugChecks = EP_DEBUG_CHECKS;
constexpr int kDebugCheckGinQueue = 1 << 0;
constexpr bool kReportGinQueue = (kDebugChecks & kDebugCheckGinQueue) != 0;
constexpr int kDebugCheckTokens = 1 << 1; // Scan each token in the dispatch and combine epilogues

struct DebugCheckName {
const char* name;
int bit;
};
constexpr DebugCheckName kDebugCheckNames[] = {{"queue", kDebugCheckGinQueue}};
constexpr DebugCheckName kDebugCheckNames[] = {{"queue", kDebugCheckGinQueue}, {"tokens", kDebugCheckTokens}};

template <int kNumBits> struct int_with_bits;
template <> struct int_with_bits<8> { using type = int8_t; };
Expand Down
158 changes: 158 additions & 0 deletions deep_ep/include/deep_ep/common/diag.cuh
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#pragma once

// MIT License
//
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// Debug checks. The bits and names of `EP_DEBUG_CHECKS` are in `compiled.cuh`. The host
// JIT generators pass the mask to each kernel as the template argument `kDebugChecks`.
// Each call site is in an `if constexpr` block on its bit, so a check with a clear bit
// adds no instructions.

#include <cuda/atomic>
#include <cuda/ptx>

#include <deep_ep/common/exception.cuh>
#include <deep_ep/common/ptx.cuh>

namespace deep_ep::elastic::diag {

// Element type of a token payload. The kernels see the payload as bytes
static constexpr int kPayloadBF16 = 0;
static constexpr int kPayloadFP8E4M3 = 1;

// Dispatch takes BF16, or FP8 E4M3 with scale factors (see `ElasticBuffer.dispatch`),
// so the scale factors give the type. Other dtypes are outside the API: the scan reads
// them as BF16, and can miss non-finite values (FP16) or give false reports (FP32)
constexpr int get_dispatch_payload_type(const int& num_sf_packs) {
return num_sf_packs > 0 ? kPayloadFP8E4M3 : kPayloadBF16;
}

constexpr int get_payload_elem_bytes(const int& payload_type) {
return payload_type == kPayloadBF16 ? 2 : 1;
}

// Rate limit of the reports: `kNumMaxReportsPerWindow` lines in each window of 2^30 ns
// (about 1 s), for each kernel variant. One bad buffer can hold thousands of bad
// tokens, and each device printf is slow: 32 lines add about 110 us to a launch, and 32k
// lines add about 150 ms. Because the window is in time, not in launches, the reports
// start again in the next window, also under CUDA graph replay.
static constexpr unsigned int kNumMaxReportsPerWindow = 32;
static constexpr int kReportWindowShift = 30;

static __device__ unsigned long long report_window = 0;
static __device__ unsigned int num_window_reports = 0;

// Returns true when the caller can print one report. The first report of a new window
// prints the number of reports that the earlier window did not print. Exactly one thread
// wins the `atomicMax`, so the switch has no retry loop. Reports from other threads that
// land before its `atomicExch` count in the earlier window, so a window that starts during
// a burst of reports can print up to twice `kNumMaxReportsPerWindow` lines
__device__ __forceinline__ bool should_report() {
// `%globaltimer` is the same on all SMs, `clock64()` is not
const auto window = cuda::ptx::get_sreg_globaltimer() >> kReportWindowShift;
const auto current_window = cuda::atomic_ref<unsigned long long, cuda::thread_scope_device>(report_window)
.load(cuda::memory_order_relaxed);
if (current_window < window and atomicMax(&report_window, window) < window) {
const auto num_reports = atomicExch(&num_window_reports, 0u);
if (num_reports > kNumMaxReportsPerWindow)
printf("DeepEP debug check: %u more reports were not printed\n", num_reports - kNumMaxReportsPerWindow);
}
return atomicAdd(&num_window_reports, 1u) < kNumMaxReportsPerWindow;
}

// Use only in an `if constexpr` block on a check bit
#define EP_DIAG_REPORT(...) \
Comment thread
hershys-aws marked this conversation as resolved.
Resolved
do { if (::deep_ep::elastic::diag::should_report()) printf(__VA_ARGS__); } while (0)

// Result of the scan of one token. The values are the same in all lanes
struct NonFiniteScan {
int num_bad;
int first_bad_idx; // -1 when the token is clean
unsigned int first_bad_bits;
};

// Count the non-finite elements of one token and find the first one. A full warp
// calls this function. `ptr` is 16-byte aligned shared or global memory.
//
// A BF16 element `S EEEEEEEE MMMMMMM` is non-finite when its exponent bits are all
// ones: 0x7f80 is Inf, and larger values are NaN. An FP8 E4M3 element `S EEEE MMM`
// has no Inf and one NaN encoding: exponent and mantissa all ones. Both tests are
// "these bits are all ones". The test runs on all elements of a 32-bit word at
// once: mask the word to those bits, add one at their lowest bit, and keep the top
// bit of each element. The carry sets the top bit only when the bits were all ones.
// The mask drops the sign, so the sum is at most the top bit, and the carry cannot
// cross into the next element.
//
// BF16 example: NaN is 0x7fc0. 0x7fc0 & 0x7f80 = 0x7f80, + 0x0080 = 0x8000, so the
// top bit is set. 1.0 is 0x3f80. 0x3f80 & 0x7f80 = 0x3f80, + 0x0080 = 0x4000, so the
// top bit is clear
//
// The scan has two passes. The fast pass has no branches and no reductions. It
// only asks "did any lane see a hit". This is the full cost of a clean token. The
// slow pass counts and locates the bad elements. It runs only after a hit
template <int kNumBytes, int kPayloadType>
__device__ __forceinline__ NonFiniteScan scan_nonfinite(const void* ptr, const int& lane_idx) {
Comment thread
hershys-aws marked this conversation as resolved.
Resolved
EP_STATIC_ASSERT(kNumBytes % sizeof(int4) == 0, "Token must be a multiple of 16 bytes");
EP_STATIC_ASSERT(kPayloadType == kPayloadBF16 or kPayloadType == kPayloadFP8E4M3, "Unsupported payload type");
constexpr bool kIsBF16 = kPayloadType == kPayloadBF16;
constexpr int kElemBits = kIsBF16 ? 16 : 8, kNumElemsPerWord = 32 / kElemBits;
constexpr unsigned int kOnesMask = kIsBF16 ? 0x7f807f80u : 0x7f7f7f7fu; // The bits that must all be ones
constexpr unsigned int kOnesLsb = kIsBF16 ? 0x00800080u : 0x01010101u; // The lowest of these bits
constexpr unsigned int kTopBit = kIsBF16 ? 0x80008000u : 0x80808080u; // The top bit of each element
constexpr int kNumVecs = kNumBytes / static_cast<int>(sizeof(int4));
const auto* vec_ptr = static_cast<const uint4*>(ptr);

// One bit for each element, at the top bit of the element. The bit is set when
// the element is non-finite
const auto hits_in = [](const unsigned int& word) { return ((word & kOnesMask) + kOnesLsb) & kTopBit; };

// Fast pass: fully unrolled, no branches, no reductions
unsigned int any_hits = 0;
#pragma unroll
for (int it = 0; it < (kNumVecs + 31) / 32; ++ it) {
const int v = it * 32 + lane_idx;
if (kNumVecs % 32 == 0 or v < kNumVecs) {
const auto vec = vec_ptr[v];
any_hits |= hits_in(vec.x) | hits_in(vec.y) | hits_in(vec.z) | hits_in(vec.w);
}
}
if (not ptx::any(any_hits != 0))
return {0, -1, 0};

// Slow pass, only after a hit: count the bad elements, and find the first one and its bits
constexpr unsigned int kNone = 0x7fffffffu;
int num_bad = 0;
unsigned int first_bad_idx = kNone, first_bad_bits = 0;
for (int v = lane_idx; v < kNumVecs; v += 32) {
const auto vec = vec_ptr[v];
const unsigned int words[4] = {vec.x, vec.y, vec.z, vec.w};
#pragma unroll
for (int w = 0; w < 4; ++ w) {
const auto hits = hits_in(words[w]);
num_bad += __popc(hits);
if (hits != 0 and first_bad_idx == kNone) {
// Little-endian: the lowest set bit belongs to the element with the lowest index
const int e = ptx::ffs(hits) / kElemBits;
first_bad_idx = (v * 4 + w) * kNumElemsPerWord + e;
first_bad_bits = (words[w] >> (e * kElemBits)) & ((1u << kElemBits) - 1);
}
}
}
// Each lane saw only its own vectors. Combine them: the total count, the smallest
// index, and the bits from the lane that found that index
num_bad = ptx::reduce_add(num_bad);
const auto min_idx = __reduce_min_sync(0xffffffff, first_bad_idx);
const auto owner_lane_idx = ptx::ffs(ptx::gather(first_bad_idx == min_idx));
first_bad_bits = ptx::exchange(first_bad_bits, owner_lane_idx);
return {num_bad, min_idx == kNone ? -1 : static_cast<int>(min_idx), first_bad_bits};
}


} // namespace deep_ep::elastic::diag
14 changes: 14 additions & 0 deletions deep_ep/include/deep_ep/impls/combine_reduce_epilogue.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

#include <deep_ep/common/compiled.cuh>
#include <deep_ep/common/diag.cuh>
#include <deep_ep/common/ptx.cuh>
#include <deep_ep/common/layout.cuh>

Expand All @@ -28,6 +29,7 @@ template <bool kUseExpandedLayout, bool kAllowMultipleReduction,
int kNumMaxTokensPerRank,
int kNumExperts, int kNumTopk,
int kNumChannels,
int kDebugChecks,
// Ordered (upstream) recv layout: partials live at `token_idx` per rank buffer
// instead of the unordered per-(channel, slot) map.
bool kOrderedLayout = false,
Expand Down Expand Up @@ -163,6 +165,18 @@ combine_reduce_epilogue_impl(nv_bfloat16* combined_x,
ptx::tma_store_fence();
__syncwarp();

// Debug check. The reduced token is in shared memory now. Scan it before the store
if constexpr (kDebugChecks & kDebugCheckTokens) {
const auto scan = diag::scan_nonfinite<kNumHiddenBytes, diag::kPayloadBF16>(tma_buffer.get_base_ptr(), lane_idx);
if (scan.num_bad > 0 and ptx::elect_one_sync()) {
EP_DIAG_REPORT("DeepEP debug check: non-finite token in combine, rank: %d, token: %d/%d, "
"sources: %d, bad elements: %d/%d, first bad: %d (bits 0x%x)\n",
scaleout_rank_idx * kNumScaleupRanks + scaleup_rank_idx, token_idx, num_combined_tokens,
__popc(reduce_valid_mask), scan.num_bad, kHidden, scan.first_bad_idx, scan.first_bad_bits);
}
__syncwarp();
}

// Issue TMA copy
if (ptx::elect_one_sync()) {
ptx::tma_store_1d(output_buffer.get_token_buffer(token_idx).get_base_ptr(),
Expand Down
22 changes: 22 additions & 0 deletions deep_ep/include/deep_ep/impls/dispatch_copy_epilogue.cuh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <deep_ep/common/compiled.cuh>
#include <deep_ep/common/diag.cuh>
#include <deep_ep/common/layout.cuh>
#include <deep_ep/common/math.cuh>
#include <deep_ep/common/ptx.cuh>
Expand All @@ -15,6 +16,7 @@ template <bool kDoExpand, bool kCachedMode, bool kDoZeroPadding,
int kNumHiddenBytes, int kNumSFPacks,
int kNumMaxTokensPerRank,
int kNumExperts, int kNumTopk, int kExpertAlignment,
int kDebugChecks,
int kNumRanks = kNumScaleoutRanks * kNumScaleupRanks,
int kNumThreads = kNumWarps * 32,
int kNumMaxTokensPerChannel = math::constexpr_ceil_div(kNumMaxTokensPerRank, kNumChannels),
Expand Down Expand Up @@ -127,6 +129,26 @@ dispatch_copy_epilogue_impl(void* buffer, void* workspace,
ptx::mbarrier_wait_and_flip_phase(mbarrier_ptr, phase);
__syncwarp();

// Debug check. The full token is in shared memory now. Scan it before the store
if constexpr (kDebugChecks & kDebugCheckTokens) {
constexpr int kPayloadType = diag::get_dispatch_payload_type(kNumSFPacks);
const auto scan = diag::scan_nonfinite<kNumHiddenBytes, kPayloadType>(tma_buffer.get_hidden_ptr(), lane_idx);
if (scan.num_bad > 0) {
// `master_src_topk_idx` is -1 when no top-k entry is in range, for example on a corrupt token
const auto expert_idx = ptx::exchange(dst_expert_idx, master_src_topk_idx < 0 ? 0 : master_src_topk_idx);
if (ptx::elect_one_sync()) {
const auto src_token_global_idx = *tma_buffer.get_src_token_global_idx_ptr();
EP_DIAG_REPORT("DeepEP debug check: non-finite token in dispatch, rank: %d, recv token: %d/%d, "
"src rank: %d, src token: %d, local expert: %d, bad elements: %d/%d, first bad: %d (bits 0x%x)\n",
rank_idx, i, num_recv_tokens,
src_token_global_idx / kNumMaxTokensPerRank, src_token_global_idx % kNumMaxTokensPerRank,
expert_idx, scan.num_bad, kNumHiddenBytes / diag::get_payload_elem_bytes(kPayloadType),
scan.first_bad_idx, scan.first_bad_bits);
}
}
__syncwarp();
}

// Maintain linked list
if constexpr (kDoCreateLinkedList) {
if (ptx::elect_one_sync())
Expand Down
Loading