1383 lines
71 KiB
C++
1383 lines
71 KiB
C++
#pragma once
|
|
|
|
#include <cuda_runtime.h>
|
|
#include <memory>
|
|
#include <numeric>
|
|
#include <vector>
|
|
#include <pybind11/functional.h>
|
|
|
|
#include <deep_ep/common/layout.cuh>
|
|
#include <deep_ep/common/compiled.cuh>
|
|
|
|
#include "../kernels/backend/api.cuh"
|
|
#include "../kernels/elastic/api.hpp"
|
|
#include "../utils/event.hpp"
|
|
#include "utils.hpp"
|
|
|
|
namespace deep_ep::elastic {
|
|
|
|
class ElasticBuffer {
|
|
// Buffer bytes = GPU buffer + CPU buffer (excludes workspace)
|
|
// Memory layout: [[[Workspace] GPU buffer] CPU buffer]
|
|
int64_t num_buffer_bytes;
|
|
int64_t num_gpu_buffer_bytes;
|
|
int64_t num_cpu_buffer_bytes;
|
|
void* buffer;
|
|
|
|
// Destructor settings
|
|
bool explicitly_destroy;
|
|
bool destroyed = false;
|
|
|
|
// Workspace
|
|
// NOTES: for all workspace, we must keep them as zeros
|
|
void *workspace;
|
|
void *host_workspace, *mapped_host_workspace;
|
|
std::shared_ptr<layout::WorkspaceLayout> workspace_layout_wo_expert;
|
|
|
|
// CUDA streams
|
|
at::cuda::CUDAStream comm_stream;
|
|
|
|
// Whether to use hybrid mode (scale-out with scale-up)
|
|
bool allow_hybrid_mode;
|
|
|
|
// Whether to allow multiple reductions
|
|
bool allow_multiple_reduction;
|
|
|
|
// Whether to prefer overlapping communication with compute (use more SMs and channels if false)
|
|
bool prefer_overlap_with_compute;
|
|
|
|
// Timeout settings
|
|
int num_cpu_timeout_secs;
|
|
int64_t num_gpu_timeout_cycles;
|
|
|
|
// NCCL context
|
|
std::shared_ptr<nccl::NCCLSymmetricMemoryContext> nccl_context;
|
|
|
|
// Some EP hybrid mode settings
|
|
static constexpr int kNumMaxChannelsPerSM = 8;
|
|
static constexpr int kNumMaxSMs = 160;
|
|
static constexpr int kNumMaxChannels = kNumMaxChannelsPerSM * kNumMaxSMs;
|
|
|
|
// Some Engram storage settings
|
|
int num_engram_entries = 0, engram_hidden = 0;
|
|
std::optional<torch::Tensor> engram_sf;
|
|
|
|
// PP settings
|
|
int prev_rank_idx = 0, next_rank_idx = 0;
|
|
int64_t num_max_pp_tensor_bytes = 0;
|
|
int num_max_pp_inflight_tensors = 0;
|
|
|
|
// AGRS session settings
|
|
int64_t num_max_agrs_session_bytes = 0;
|
|
int num_max_agrs_per_session = 0;
|
|
int agrs_session_idx = 0;
|
|
bool agrs_in_session = false;
|
|
|
|
// AGRS in-session settings
|
|
int64_t agrs_buffer_offset = 0;
|
|
int agrs_buffer_slot_idx = 0;
|
|
|
|
public:
|
|
ElasticBuffer(const int& rank_idx, const int& num_ranks,
|
|
const int64_t& nccl_comm, const symmetric::cpu_comm_t& cpu_comm,
|
|
const int64_t& num_buffer_bytes, const int64_t& num_cpu_buffer_bytes,
|
|
const bool& allow_hybrid_mode,
|
|
const bool& allow_multiple_reduction,
|
|
const bool& prefer_overlap_with_compute,
|
|
const int& sl_idx, const int& num_allocated_qps,
|
|
const int& num_cpu_timeout_secs, const int& num_gpu_timeout_secs,
|
|
const bool& explicitly_destroy):
|
|
num_buffer_bytes(num_buffer_bytes),
|
|
num_cpu_buffer_bytes(num_cpu_buffer_bytes),
|
|
explicitly_destroy(explicitly_destroy),
|
|
comm_stream(get_global_comm_stream()),
|
|
allow_hybrid_mode(allow_hybrid_mode),
|
|
allow_multiple_reduction(allow_multiple_reduction),
|
|
prefer_overlap_with_compute(prefer_overlap_with_compute) {
|
|
// Check buffer bytes alignment (2 MB)
|
|
EP_HOST_ASSERT(num_buffer_bytes > 0 and num_buffer_bytes % symmetric::kNumAlignmentBytes == 0);
|
|
EP_HOST_ASSERT(num_cpu_buffer_bytes >= 0 and num_cpu_buffer_bytes % symmetric::kNumAlignmentBytes == 0);
|
|
EP_HOST_ASSERT(num_cpu_buffer_bytes <= num_buffer_bytes);
|
|
num_gpu_buffer_bytes = num_buffer_bytes - num_cpu_buffer_bytes;
|
|
|
|
// Workspace is aligned to 2 MB so that it sits cleanly at the front of the GPU segment
|
|
const auto num_workspace_bytes = math::align<int64_t>(
|
|
layout::WorkspaceLayout::get_num_bytes(), symmetric::kNumAlignmentBytes);
|
|
|
|
// Create NCCL symmetric memory context
|
|
// Symmetric memory layout: [[[Workspace] GPU buffer] CPU buffer]
|
|
// sym.num_bytes = workspace + buffer, sym.num_cpu_bytes = CPU buffer
|
|
const auto num_sym_bytes = num_workspace_bytes + num_buffer_bytes;
|
|
this->nccl_context = std::make_shared<nccl::NCCLSymmetricMemoryContext>(
|
|
nccl_comm, cpu_comm, num_ranks, rank_idx,
|
|
num_sym_bytes, num_cpu_buffer_bytes,
|
|
allow_hybrid_mode, sl_idx, num_allocated_qps);
|
|
|
|
// Verify the symmetric memory layout matches our expectations
|
|
EP_HOST_ASSERT(num_workspace_bytes + num_gpu_buffer_bytes == nccl_context->num_gpu_bytes);
|
|
EP_HOST_ASSERT(num_cpu_buffer_bytes == nccl_context->num_cpu_bytes);
|
|
|
|
// Timeout
|
|
this->num_cpu_timeout_secs = num_cpu_timeout_secs;
|
|
this->num_gpu_timeout_cycles = static_cast<int64_t>(num_gpu_timeout_secs);
|
|
this->num_gpu_timeout_cycles *= jit::device_runtime->get_clock_rate();
|
|
|
|
// Assign workspaces and buffers
|
|
workspace = this->nccl_context->mapped_window_ptr;
|
|
workspace_layout_wo_expert = std::make_shared<layout::WorkspaceLayout>(
|
|
workspace, nccl_context->num_scaleout_ranks, nccl_context->num_scaleup_ranks, 0);
|
|
buffer = static_cast<uint8_t*>(workspace) + num_workspace_bytes;
|
|
CUDA_RUNTIME_CHECK(cudaMemset(workspace, 0, num_workspace_bytes));
|
|
|
|
// Allocate host workspaces
|
|
CUDA_RUNTIME_CHECK(cudaMallocHost(&host_workspace, layout::WorkspaceLayout::get_num_bytes(), cudaHostAllocMapped));
|
|
CUDA_RUNTIME_CHECK(cudaHostGetDevicePointer(&mapped_host_workspace, host_workspace, 0));
|
|
std::memset(host_workspace, 0, layout::WorkspaceLayout::get_num_bytes());
|
|
|
|
// We should call a barrier at the end
|
|
// The barrier should be called by Python `dist.barrier`
|
|
// NOTES: do not call our barrier, as the workspace is not ready yet
|
|
}
|
|
|
|
~ElasticBuffer() noexcept(false) {
|
|
if (not explicitly_destroy)
|
|
destroy();
|
|
|
|
if (not destroyed) {
|
|
printf("`destroy()` is not called before DeepEP elastic buffer destruction, which can leak resources.\n");
|
|
fflush(stdout);
|
|
}
|
|
}
|
|
|
|
void destroy() {
|
|
EP_HOST_ASSERT(not destroyed);
|
|
|
|
// Finish all works on all GPUs
|
|
barrier(true, true);
|
|
|
|
// Deallocate host workspaces
|
|
CUDA_RUNTIME_CHECK(cudaFreeHost(host_workspace));
|
|
|
|
// Destroy NCCL context
|
|
nccl_context->finalize();
|
|
|
|
// Cannot use anymore
|
|
destroyed = true;
|
|
}
|
|
|
|
torch::Stream get_comm_stream() const {
|
|
return comm_stream;
|
|
}
|
|
|
|
std::tuple<int, int> get_physical_domain_size() const {
|
|
return {nccl_context->num_rdma_ranks, nccl_context->num_nvl_ranks};
|
|
}
|
|
|
|
std::tuple<int, int> get_logical_domain_size() const {
|
|
return {nccl_context->num_scaleout_ranks, nccl_context->num_scaleup_ranks};
|
|
}
|
|
|
|
// ReSharper disable once CppMemberFunctionMayBeStatic
|
|
void barrier(const bool& use_comm_stream, const bool& with_cpu_sync, const bool& sequential = true) const {
|
|
const auto compute_stream = at::cuda::getCurrentCUDAStream();
|
|
const auto stream = use_comm_stream ? comm_stream : compute_stream;
|
|
if (use_comm_stream)
|
|
stream_wait(comm_stream, compute_stream);
|
|
|
|
// Wait all streams to finish on this GPU
|
|
if (with_cpu_sync)
|
|
CUDA_RUNTIME_CHECK(cudaDeviceSynchronize());
|
|
|
|
// Launch GPU barrier
|
|
launch_barrier(nccl_context->dev_comm, nccl_context->window,
|
|
workspace,
|
|
nccl_context->scaleout_rank_idx, nccl_context->scaleup_rank_idx,
|
|
nccl_context->num_scaleout_ranks, nccl_context->num_scaleup_ranks,
|
|
num_gpu_timeout_cycles,
|
|
nccl_context->is_scaleup_nvlink,
|
|
sequential,
|
|
stream);
|
|
|
|
// Let CPU wait
|
|
if (with_cpu_sync)
|
|
CUDA_RUNTIME_CHECK(cudaDeviceSynchronize());
|
|
|
|
// Compute stream should also wait for the barrier
|
|
if (use_comm_stream)
|
|
stream_wait(compute_stream, comm_stream);
|
|
}
|
|
|
|
void engram_write(const torch::Tensor& storage, const std::optional<torch::Tensor>& sf) {
|
|
// Ensure previous fetch are finished
|
|
barrier(false, true);
|
|
|
|
const auto compute_stream = at::cuda::getCurrentCUDAStream();
|
|
|
|
// Check storage
|
|
const auto [num_entries, hidden] = get_shape<2>(storage);
|
|
EP_HOST_ASSERT(storage.scalar_type() == torch::kBFloat16 or
|
|
storage.scalar_type() == torch::kFloat8_e4m3fn);
|
|
EP_HOST_ASSERT(storage.is_cuda() and storage.is_contiguous());
|
|
num_engram_entries = num_entries, engram_hidden = hidden;
|
|
|
|
// Store globally-replicated FP8 scaling factors for local gather during fetch.
|
|
if (sf.has_value())
|
|
EP_HOST_ASSERT(sf->dim() == 2 and sf->is_cuda() and sf->is_contiguous() and
|
|
sf->element_size() == sizeof(sf_pack_t));
|
|
engram_sf = sf;
|
|
|
|
// Write storage to CPU segment at back of buffer
|
|
EP_HOST_ASSERT(storage.nbytes() <= num_cpu_buffer_bytes and "Engram storage exceeds CPU buffer size");
|
|
const auto cpu_write_offset = allow_hybrid_mode
|
|
? static_cast<int64_t>(nccl_context->scaleup_rank_idx) * num_cpu_buffer_bytes : 0;
|
|
CUDA_RUNTIME_CHECK(cudaMemcpyAsync(
|
|
math::advance_ptr(buffer, num_gpu_buffer_bytes + cpu_write_offset),
|
|
storage.data_ptr(), storage.nbytes(),
|
|
cudaMemcpyDeviceToDevice, compute_stream));
|
|
|
|
// Ensure data is visible for all ranks
|
|
barrier(false, true);
|
|
}
|
|
|
|
std::function<std::tuple<torch::Tensor, std::optional<torch::Tensor>>()>
|
|
engram_fetch(const torch::Tensor& indices, int num_qps, const bool& use_tma_aligned_col_major_sf) const {
|
|
const auto use_fp8 = engram_sf.has_value();
|
|
const auto fetched_dtype = use_fp8 ? torch::kFloat8_e4m3fn : torch::kBFloat16;
|
|
const int elem_size = use_fp8 ? sizeof(__nv_fp8_e4m3) : sizeof(nv_bfloat16);
|
|
const auto [num_tokens, num_entries_per_token] = get_shape<2>(indices);
|
|
EP_HOST_ASSERT(indices.scalar_type() == torch::kInt);
|
|
EP_HOST_ASSERT(indices.is_cuda() and indices.is_contiguous());
|
|
EP_HOST_ASSERT(num_tokens * num_entries_per_token * engram_hidden * elem_size <= num_gpu_buffer_bytes);
|
|
|
|
// Calculate a QP count
|
|
if (num_qps == 0)
|
|
num_qps = nccl_context->num_allocated_qps;
|
|
|
|
// Return tensor from the raw buffer: each token's entries are concatenated along hidden
|
|
EP_HOST_ASSERT(num_engram_entries > 0);
|
|
const auto fetched = torch::from_blob(
|
|
buffer,
|
|
{num_tokens, engram_hidden * num_entries_per_token},
|
|
torch::TensorOptions().dtype(fetched_dtype).device(torch::kCUDA)
|
|
);
|
|
|
|
auto fetched_sf = std::optional<torch::Tensor>();
|
|
void* sf_table_ptr = nullptr;
|
|
void* fetched_sf_ptr = nullptr;
|
|
int num_sf_packs = 0, sf_token_stride = 0, sf_hidden_stride = 0;
|
|
if (use_fp8) {
|
|
num_sf_packs = static_cast<int>(engram_sf->size(1));
|
|
if (use_tma_aligned_col_major_sf) {
|
|
// TMA-aligned column-major layout for the next GEMM input
|
|
sf_token_stride = 1, sf_hidden_stride = math::align(num_tokens, kNumAlignedSFPacks);
|
|
} else {
|
|
sf_token_stride = num_entries_per_token * num_sf_packs, sf_hidden_stride = 1;
|
|
}
|
|
fetched_sf = torch::empty_strided({num_tokens, num_entries_per_token * num_sf_packs},
|
|
{sf_token_stride, sf_hidden_stride}, engram_sf->options());
|
|
sf_table_ptr = engram_sf->data_ptr();
|
|
fetched_sf_ptr = fetched_sf->data_ptr();
|
|
}
|
|
|
|
// Last issued Gin requests
|
|
const auto num_gin_ranks = allow_hybrid_mode
|
|
? nccl_context->num_scaleout_ranks
|
|
: nccl_context->num_ranks;
|
|
const auto last_gin_requests = torch::empty(
|
|
{num_gin_ranks * num_qps, sizeof(ncclGinRequest_t)},
|
|
torch::TensorOptions().dtype(torch::kByte).device(torch::kCUDA)
|
|
);
|
|
|
|
// Launch the fetch kernel
|
|
launch_engram_fetch(
|
|
nccl_context->dev_comm, nccl_context->window,
|
|
math::advance_ptr(buffer, num_gpu_buffer_bytes),
|
|
fetched.data_ptr(),
|
|
indices.data_ptr<int>(),
|
|
static_cast<ncclGinRequest_t*>(last_gin_requests.data_ptr()),
|
|
sf_table_ptr, fetched_sf_ptr, sf_token_stride, sf_hidden_stride,
|
|
num_engram_entries,
|
|
engram_hidden, elem_size, num_sf_packs,
|
|
num_entries_per_token,
|
|
num_tokens,
|
|
nccl_context->num_scaleout_ranks,
|
|
nccl_context->num_scaleup_ranks,
|
|
num_cpu_buffer_bytes,
|
|
num_qps,
|
|
allow_hybrid_mode,
|
|
at::cuda::getCurrentCUDAStream()
|
|
);
|
|
|
|
return [=, this]() -> std::tuple<torch::Tensor, std::optional<torch::Tensor>> {
|
|
// Wait for all RDMA gets to complete
|
|
launch_engram_fetch_wait(
|
|
static_cast<ncclGinRequest_t*>(last_gin_requests.data_ptr()),
|
|
nccl_context->dev_comm,
|
|
nccl_context->window,
|
|
nccl_context->num_scaleout_ranks,
|
|
nccl_context->num_scaleup_ranks,
|
|
num_qps,
|
|
allow_hybrid_mode,
|
|
at::cuda::getCurrentCUDAStream()
|
|
);
|
|
return {fetched, fetched_sf};
|
|
};
|
|
}
|
|
|
|
void pp_set_config(const int64_t& num_max_tensor_bytes, const int& num_max_inflight_tensors) {
|
|
// Flush previous operations
|
|
barrier(false, true);
|
|
|
|
EP_HOST_ASSERT(num_max_tensor_bytes > 0 and num_max_inflight_tensors > 0);
|
|
EP_HOST_ASSERT(num_max_tensor_bytes * num_max_inflight_tensors * 2 * 2 <= num_buffer_bytes);
|
|
this->prev_rank_idx = (nccl_context->rank_idx + nccl_context->num_ranks - 1) % nccl_context->num_ranks;
|
|
this->next_rank_idx = (nccl_context->rank_idx + 1) % nccl_context->num_ranks;
|
|
this->num_max_pp_tensor_bytes = math::align<int64_t>(num_max_tensor_bytes, 32);
|
|
this->num_max_pp_inflight_tensors = num_max_inflight_tensors;
|
|
}
|
|
|
|
void pp_send(const torch::Tensor& x, const int& dst_rank_idx, const int& num_sms) const {
|
|
EP_HOST_ASSERT(num_max_pp_tensor_bytes > 0 and num_max_pp_inflight_tensors > 0);
|
|
EP_HOST_ASSERT(x.is_cuda() and x.is_contiguous() and x.nbytes() <= num_max_pp_tensor_bytes);
|
|
EP_HOST_ASSERT(dst_rank_idx == prev_rank_idx or dst_rank_idx == next_rank_idx);
|
|
|
|
launch_pp_send(
|
|
nccl_context->dev_comm, nccl_context->window,
|
|
x.data_ptr(), x.nbytes(),
|
|
buffer, workspace,
|
|
nccl_context->rank_idx, dst_rank_idx, nccl_context->num_ranks,
|
|
num_max_pp_tensor_bytes,
|
|
num_max_pp_inflight_tensors,
|
|
num_sms == 0 ? jit::device_runtime->get_num_sms() : num_sms,
|
|
num_gpu_timeout_cycles,
|
|
jit::device_runtime->get_num_smem_bytes(),
|
|
at::cuda::getCurrentCUDAStream()
|
|
);
|
|
}
|
|
|
|
void pp_recv(const torch::Tensor& x, const int& src_rank_idx, const int& num_sms) const {
|
|
EP_HOST_ASSERT(num_max_pp_tensor_bytes > 0 and num_max_pp_inflight_tensors > 0);
|
|
EP_HOST_ASSERT(x.is_cuda() and x.is_contiguous() and x.nbytes() <= num_max_pp_tensor_bytes);
|
|
EP_HOST_ASSERT(src_rank_idx == prev_rank_idx or src_rank_idx == next_rank_idx);
|
|
|
|
launch_pp_recv(
|
|
nccl_context->dev_comm, nccl_context->window,
|
|
x.data_ptr(), x.nbytes(),
|
|
buffer, workspace,
|
|
nccl_context->rank_idx, src_rank_idx, nccl_context->num_ranks,
|
|
num_max_pp_tensor_bytes,
|
|
num_max_pp_inflight_tensors,
|
|
num_sms == 0 ? jit::device_runtime->get_num_sms() : num_sms,
|
|
num_gpu_timeout_cycles,
|
|
jit::device_runtime->get_num_smem_bytes(),
|
|
at::cuda::getCurrentCUDAStream()
|
|
);
|
|
}
|
|
|
|
void agrs_set_config(const int64_t& num_max_session_bytes,
|
|
const int& new_num_max_agrs_per_session) {
|
|
// Flush previous operations
|
|
barrier(true, true);
|
|
|
|
EP_HOST_ASSERT(nccl_context->num_ranks > 1);
|
|
EP_HOST_ASSERT(num_max_session_bytes > 0 and new_num_max_agrs_per_session > 0);
|
|
EP_HOST_ASSERT(num_max_session_bytes <= num_buffer_bytes);
|
|
EP_HOST_ASSERT(new_num_max_agrs_per_session <= layout::WorkspaceLayout::kNumMaxInflightAGRS);
|
|
EP_HOST_ASSERT(nccl_context->num_nvl_ranks == nccl_context->num_ranks);
|
|
this->num_max_agrs_session_bytes = math::align<int64_t>(num_max_session_bytes, 32);
|
|
this->num_max_agrs_per_session = new_num_max_agrs_per_session;
|
|
}
|
|
|
|
void create_agrs_session() {
|
|
EP_HOST_ASSERT(not agrs_in_session);
|
|
agrs_in_session = true;
|
|
agrs_buffer_offset = 0;
|
|
agrs_buffer_slot_idx = 0;
|
|
agrs_session_idx += 1;
|
|
}
|
|
|
|
void destroy_agrs_session() {
|
|
// Must be in a session
|
|
EP_HOST_ASSERT(agrs_in_session);
|
|
agrs_in_session = false;
|
|
|
|
// Wait compute stream
|
|
stream_wait(comm_stream, at::cuda::getCurrentCUDAStream());
|
|
|
|
// Notify that the buffer is now available & Wait for the buffer to be ready
|
|
// NOTES: self-wait is guaranteed by in-stream order
|
|
std::vector<void*> write_ptrs(nccl_context->num_ranks - 1);
|
|
std::vector<void*> wait_ptrs(nccl_context->num_ranks - 1);
|
|
for (int i = 0; i < nccl_context->num_ranks - 1; ++ i) {
|
|
const auto dst_rank_idx = (nccl_context->rank_idx + i + 1) % nccl_context->num_ranks;
|
|
write_ptrs[i] = static_cast<int*>(
|
|
nccl_context->get_sym_ptr(workspace_layout_wo_expert->get_agrs_session_signal_ptr(nccl_context->rank_idx), dst_rank_idx));
|
|
wait_ptrs[i] = workspace_layout_wo_expert->get_agrs_session_signal_ptr(dst_rank_idx);
|
|
}
|
|
cuda_driver::batched_write_and_wait(comm_stream, write_ptrs, wait_ptrs, agrs_session_idx);
|
|
}
|
|
|
|
std::vector<torch::Tensor> agrs_get_inplace_tensor(const std::vector<int64_t>& num_bytes_list) const {
|
|
EP_HOST_ASSERT(num_bytes_list.size() >= 1);
|
|
EP_HOST_ASSERT(num_max_agrs_session_bytes > 0 and num_max_agrs_per_session > 0 and agrs_in_session);
|
|
|
|
std::vector<torch::Tensor> out;
|
|
out.reserve(num_bytes_list.size());
|
|
int64_t offset = agrs_buffer_offset;
|
|
for (const auto& num_bytes: num_bytes_list) {
|
|
EP_HOST_ASSERT(offset + num_bytes * nccl_context->num_ranks <= num_max_agrs_session_bytes and
|
|
agrs_buffer_slot_idx < num_max_agrs_per_session and
|
|
"Not enough session buffer size. Did you forget to flush session?");
|
|
out.push_back(torch::from_blob(math::advance_ptr(buffer, offset + num_bytes * nccl_context->rank_idx),
|
|
{num_bytes}, torch::TensorOptions().dtype(torch::kByte).device(torch::kCUDA)));
|
|
offset += math::align<int64_t>(num_bytes * nccl_context->num_ranks, 32);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::pair<std::vector<torch::Tensor>, std::function<void()>>
|
|
all_gather(const std::vector<torch::Tensor>& tensors) {
|
|
const int num_tensors = tensors.size();
|
|
EP_HOST_ASSERT(num_max_agrs_session_bytes > 0 and num_max_agrs_per_session > 0 and agrs_in_session);
|
|
EP_HOST_ASSERT(num_tensors >= 1);
|
|
|
|
int num_copies = 0;
|
|
std::vector<int64_t> offset(num_tensors);
|
|
for (int i = 0; i < num_tensors; ++ i) {
|
|
const auto& x = tensors[i];
|
|
EP_HOST_ASSERT(x.is_contiguous());
|
|
|
|
const auto x_offset = math::ptr_diff(x.data_ptr(), buffer);
|
|
const bool is_inplace = 0 <= x_offset and x_offset < num_max_agrs_session_bytes;
|
|
offset[i] = agrs_buffer_offset;
|
|
num_copies += nccl_context->num_ranks - is_inplace;
|
|
agrs_buffer_offset += math::align<int64_t>(x.nbytes() * nccl_context->num_ranks, 32);
|
|
EP_HOST_ASSERT(not is_inplace or x.data_ptr() == math::advance_ptr(buffer, offset[i] + x.nbytes() * nccl_context->rank_idx));
|
|
}
|
|
EP_HOST_ASSERT(agrs_buffer_offset <= num_max_agrs_session_bytes and
|
|
agrs_buffer_slot_idx < num_max_agrs_per_session and
|
|
"Not enough session buffer size. Did you forget to flush session?");
|
|
|
|
// Wait compute stream
|
|
const auto compute_stream = at::cuda::getCurrentCUDAStream();
|
|
stream_wait(comm_stream, compute_stream);
|
|
|
|
// Send data to all ranks
|
|
std::vector<size_t> sizes(num_copies);
|
|
std::vector<void*> dst_ptrs(num_copies), src_ptrs(num_copies);
|
|
int count = 0;
|
|
for (int i = 0; i < nccl_context->num_ranks; ++ i) {
|
|
for (int j = 0; j < num_tensors; ++ j) {
|
|
const auto& x = tensors[j];
|
|
const auto dst_rank_idx = (nccl_context->rank_idx + i) % nccl_context->num_ranks;
|
|
void* src_ptr = x.data_ptr();
|
|
void* dst_ptr =
|
|
nccl_context->get_sym_ptr(math::advance_ptr(buffer, offset[j] + x.nbytes() * nccl_context->rank_idx), dst_rank_idx);
|
|
if (src_ptr != dst_ptr) {
|
|
src_ptrs[count] = src_ptr;
|
|
dst_ptrs[count] = dst_ptr;
|
|
sizes[count] = x.nbytes();
|
|
count += 1;
|
|
}
|
|
}
|
|
}
|
|
cudaMemcpyAttributes attrs = {
|
|
.srcAccessOrder = cudaMemcpySrcAccessOrderStream,
|
|
.flags = cudaMemcpyFlagPreferOverlapWithCompute
|
|
};
|
|
#if defined(CUDART_VERSION) and CUDART_VERSION >= 13000
|
|
CUDA_RUNTIME_CHECK(cudaMemcpyBatchAsync(dst_ptrs.data(), src_ptrs.data(), sizes.data(), num_copies, attrs, comm_stream));
|
|
#else
|
|
CUDA_RUNTIME_CHECK(cudaMemcpyBatchAsync(dst_ptrs.data(), src_ptrs.data(), sizes.data(), num_copies, attrs, nullptr, comm_stream));
|
|
#endif
|
|
|
|
// Wait for data from other ranks
|
|
const int current_session = agrs_session_idx;
|
|
const int slot_idx = agrs_buffer_slot_idx;
|
|
agrs_buffer_slot_idx += 1;
|
|
std::vector<void*> write_ptrs(nccl_context->num_ranks - 1);
|
|
std::vector<void*> wait_ptrs(nccl_context->num_ranks - 1);
|
|
for (int i = 0; i < nccl_context->num_ranks - 1; ++ i) {
|
|
const auto dst_rank_idx = (nccl_context->rank_idx + i + 1) % nccl_context->num_ranks;
|
|
write_ptrs[i] = nccl_context->get_sym_ptr(
|
|
workspace_layout_wo_expert->get_agrs_recv_signal_ptr(slot_idx, nccl_context->rank_idx), dst_rank_idx);
|
|
wait_ptrs[i] = workspace_layout_wo_expert->get_agrs_recv_signal_ptr(slot_idx, dst_rank_idx);
|
|
}
|
|
cuda_driver::batched_write_and_wait(comm_stream, write_ptrs, wait_ptrs, current_session);
|
|
|
|
// Build output tensors eagerly
|
|
std::vector<torch::Tensor> out(num_tensors);
|
|
for (int i = 0; i < num_tensors; ++ i) {
|
|
auto shape = tensors[i].sizes().vec();
|
|
shape.insert(shape.begin(), nccl_context->num_ranks);
|
|
out[i] = torch::from_blob(math::advance_ptr(buffer, offset[i]), shape, tensors[i].options());
|
|
}
|
|
|
|
// Return tensors and a handle to wait for data arrival
|
|
const auto event = EventHandle(comm_stream);
|
|
auto handle = [=, this]() {
|
|
EP_HOST_ASSERT(compute_stream == at::cuda::getCurrentCUDAStream());
|
|
EP_HOST_ASSERT(agrs_in_session and current_session == this->agrs_session_idx);
|
|
stream_wait(compute_stream, event);
|
|
};
|
|
return {std::move(out), std::move(handle)};
|
|
}
|
|
|
|
torch::cuda::CUDAStream stream_control_prologue(const std::optional<EventHandle>& previous_event,
|
|
const bool& allocate_on_comm_stream,
|
|
const bool& async_with_compute_stream) const {
|
|
// Allocate all tensors on communication stream if set
|
|
// NOTES: do not allocate tensors upfront!
|
|
const auto compute_stream = at::cuda::getCurrentCUDAStream();
|
|
if (allocate_on_comm_stream)
|
|
at::cuda::setCurrentCUDAStream(comm_stream);
|
|
|
|
// Assertion for safety
|
|
// `previous_event` implicitly means the overlapping computation kernels are launched first,
|
|
// in order not to use the memory on the compute stream, we must allocate on the communication stream.
|
|
// If you launch the communication kernels firstly, then `previous_event` must be unnecessary.
|
|
if (previous_event.has_value())
|
|
EP_HOST_ASSERT(allocate_on_comm_stream);
|
|
|
|
// Wait previous tasks to finish
|
|
if (previous_event.has_value()) {
|
|
stream_wait(comm_stream, previous_event.value());
|
|
} else {
|
|
stream_wait(comm_stream, compute_stream);
|
|
}
|
|
return compute_stream;
|
|
}
|
|
|
|
void stream_control_before_epilogue(const std::optional<EventHandle>& previous_event_before_epilogue) const {
|
|
if (previous_event_before_epilogue.has_value())
|
|
stream_wait(comm_stream, previous_event_before_epilogue.value());
|
|
}
|
|
|
|
std::optional<EventHandle> stream_control_epilogue(const std::vector<std::optional<torch::Tensor>>& tensors,
|
|
const at::cuda::CUDAStream& compute_stream,
|
|
const bool& allocate_on_comm_stream,
|
|
const bool& async_with_compute_stream) const {
|
|
// Ensure memory access safety between two streams
|
|
std::optional<EventHandle> event;
|
|
if (async_with_compute_stream) {
|
|
event = EventHandle(comm_stream);
|
|
|
|
// NOTES: this environment only applies to V2 APIs
|
|
if (get_env<int>("EP_AVOID_RECORD_STREAM", 0)) {
|
|
event->tensors_to_record = tensors;
|
|
} else {
|
|
for (auto& t: tensors) if (t.has_value()) {
|
|
t->record_stream(compute_stream);
|
|
t->record_stream(comm_stream);
|
|
}
|
|
}
|
|
} else {
|
|
stream_wait(compute_stream, comm_stream);
|
|
}
|
|
|
|
// Switch back compute stream
|
|
if (allocate_on_comm_stream)
|
|
at::cuda::setCurrentCUDAStream(compute_stream);
|
|
|
|
// The CUDA event marking the finishing
|
|
return event;
|
|
}
|
|
|
|
static int64_t get_dispatch_buffer_size(const int& num_max_tokens_per_rank,
|
|
const int& hidden, const int& num_sf_packs, const int& num_topk,
|
|
const int& elem_size,
|
|
const int& num_scaleout_ranks, const int& num_scaleup_ranks,
|
|
const bool& is_scaleup_nvlink) {
|
|
const auto num_ranks = num_scaleup_ranks * num_scaleout_ranks;
|
|
const auto token_layout = get_dispatch_token_layout(hidden, elem_size, num_sf_packs, num_topk);
|
|
|
|
if (num_scaleout_ranks == 1) {
|
|
// Direct dispatch
|
|
const auto send_buffer_layout = layout::BufferLayout<false>(
|
|
token_layout, is_scaleup_nvlink ? 0 : 1, num_max_tokens_per_rank);
|
|
const auto recv_buffer_layout = layout::BufferLayout<false>(
|
|
token_layout, num_ranks, num_max_tokens_per_rank);
|
|
return send_buffer_layout.get_num_bytes() + recv_buffer_layout.get_num_bytes();
|
|
} else {
|
|
// Hybrid dispatch
|
|
const auto scaleup_recv_buffer = layout::BufferLayout<false>(
|
|
token_layout, num_scaleup_ranks, num_scaleout_ranks * num_max_tokens_per_rank);
|
|
const auto scaleout_send_buffer = layout::BufferLayout<false>(
|
|
token_layout, 1, num_max_tokens_per_rank);
|
|
const auto scaleout_recv_buffer = layout::BufferLayout<false>(
|
|
token_layout, num_scaleout_ranks,
|
|
/* kNumChannels * kNumMaxTokensPerChannel */ num_max_tokens_per_rank + kNumMaxChannels);
|
|
return scaleup_recv_buffer.get_num_bytes() +
|
|
scaleout_send_buffer.get_num_bytes() +
|
|
scaleout_recv_buffer.get_num_bytes();
|
|
}
|
|
}
|
|
|
|
static int64_t get_combine_buffer_size(const int& num_max_tokens_per_rank, const int& hidden, const int& num_topk,
|
|
const int& num_scaleout_ranks, const int& num_scaleup_ranks,
|
|
const bool& is_scaleup_nvlink,
|
|
const bool& allow_multiple_reduction) {
|
|
const auto num_ranks = num_scaleup_ranks * num_scaleout_ranks;
|
|
const auto token_layout = get_combine_token_layout(hidden, sizeof(nv_bfloat16), num_topk);
|
|
|
|
if (num_scaleout_ranks == 1) {
|
|
// Direct combine
|
|
const auto num_tokens_in_layout = allow_multiple_reduction ? std::min(num_ranks, num_topk) : num_topk;
|
|
const auto send_buffer_layout = layout::BufferLayout<false>(
|
|
token_layout, is_scaleup_nvlink ? 0 : num_ranks,
|
|
// For single reduction cases, the maximum number of received tokens is
|
|
// `num_ranks * num_topk * num_max_tokens_per_rank` (we assume the bad case of `do_expand=True`)
|
|
num_max_tokens_per_rank * (allow_multiple_reduction ? 1 : num_topk));
|
|
const auto recv_buffer_layout = layout::BufferLayout<false>(
|
|
token_layout, num_tokens_in_layout, num_max_tokens_per_rank);
|
|
return send_buffer_layout.get_num_bytes() + recv_buffer_layout.get_num_bytes();
|
|
} else {
|
|
// Hybrid combine
|
|
const int num_tokens_in_scaleup_layout = allow_multiple_reduction ? std::min(num_scaleup_ranks, num_topk) : num_topk;
|
|
const int num_tokens_in_scaleout_layout = allow_multiple_reduction ? std::min(num_scaleout_ranks, num_topk) : num_topk;
|
|
const auto scaleup_recv_buffer = layout::BufferLayout<false>(
|
|
token_layout, num_tokens_in_scaleup_layout, num_scaleout_ranks * num_max_tokens_per_rank);
|
|
const auto scaleout_recv_buffer = layout::BufferLayout<false>(
|
|
token_layout, num_tokens_in_scaleout_layout, num_max_tokens_per_rank);
|
|
const auto scaleout_send_buffer = layout::BufferLayout<false>(
|
|
token_layout, allow_multiple_reduction ? 1 : num_topk,
|
|
/* kNumChannels * num_scaleout_ranks * kNumMaxTokensPerChannel */
|
|
num_scaleout_ranks * (num_max_tokens_per_rank + kNumMaxChannels));
|
|
return scaleup_recv_buffer.get_num_bytes() +
|
|
scaleout_send_buffer.get_num_bytes() +
|
|
scaleout_recv_buffer.get_num_bytes();
|
|
}
|
|
}
|
|
|
|
static int64_t calculate_buffer_size(const int64_t& nccl_comm,
|
|
const int& num_max_tokens_per_rank, const int& hidden,
|
|
int num_topk, const bool& use_fp8_dispatch,
|
|
const bool& allow_hybrid_mode,
|
|
const bool& allow_multiple_reduction) {
|
|
EP_HOST_ASSERT(num_max_tokens_per_rank > 0 and hidden > 0);
|
|
|
|
// The worst case SF bytes must be less than the main part
|
|
EP_HOST_ASSERT(math::ceil_div(hidden, 32) * sizeof(float) <= hidden);
|
|
|
|
// NOTES: there are lots of `kNumTopk <= 32` restrictions, so we use 32 to calculate token size
|
|
num_topk = num_topk == 0 ? 32 : num_topk;
|
|
|
|
// Topology
|
|
const auto [num_rdma_ranks, num_nvl_ranks] = nccl::get_physical_domain_size(nccl_comm);
|
|
const auto [num_scaleout_ranks, num_scaleup_ranks] = nccl::get_logical_domain_size(nccl_comm, allow_hybrid_mode);
|
|
const auto is_scaleup_nvlink = num_scaleup_ranks == num_nvl_ranks;
|
|
|
|
// Dispatch size
|
|
const auto elem_size = use_fp8_dispatch ? sizeof(__nv_fp8_e4m3) : sizeof(nv_bfloat16);
|
|
const auto num_sf_packs = use_fp8_dispatch ? math::ceil_div(hidden, 32) : 0; // An approximation for number of SF packs
|
|
const auto num_dispatch_bytes = get_dispatch_buffer_size(
|
|
num_max_tokens_per_rank, hidden, num_sf_packs, num_topk, elem_size,
|
|
num_scaleout_ranks, num_scaleup_ranks,
|
|
is_scaleup_nvlink);
|
|
|
|
// Combine layout
|
|
const auto num_combine_bytes = get_combine_buffer_size(
|
|
num_max_tokens_per_rank, hidden, num_topk,
|
|
num_scaleout_ranks, num_scaleup_ranks,
|
|
is_scaleup_nvlink, allow_multiple_reduction);
|
|
|
|
// Return the maximum of those layouts, aligned to 2 MB
|
|
return math::align(std::max(num_dispatch_bytes, num_combine_bytes), symmetric::kNumAlignmentBytes);
|
|
}
|
|
|
|
static symmetric::cpu_handle_t create_cpu_handle(const int64_t& num_cpu_bytes) {
|
|
EP_HOST_ASSERT(num_cpu_bytes > 0 and num_cpu_bytes % symmetric::kNumAlignmentBytes == 0);
|
|
return symmetric::HybridElasticSymmetricMemory::create_cpu_handle(num_cpu_bytes);
|
|
}
|
|
|
|
std::tuple<torch::Tensor, std::optional<torch::Tensor>,
|
|
std::optional<torch::Tensor>, std::optional<torch::Tensor>,
|
|
std::optional<torch::Tensor>,
|
|
int, int,
|
|
std::vector<int>,
|
|
torch::Tensor, torch::Tensor, torch::Tensor,
|
|
torch::Tensor, torch::Tensor,
|
|
std::optional<torch::Tensor>, std::optional<torch::Tensor>,
|
|
std::optional<EventHandle>>
|
|
dispatch(const torch::Tensor& x,
|
|
const std::optional<torch::Tensor>& sf,
|
|
const torch::Tensor& topk_idx,
|
|
const std::optional<torch::Tensor>& topk_weights,
|
|
const std::optional<torch::Tensor>& cumulative_local_expert_recv_stats,
|
|
const std::optional<int>& cached_num_recv_tokens,
|
|
const std::optional<int>& cached_num_expanded_tokens,
|
|
const std::optional<std::vector<int>>& cached_num_recv_tokens_per_expert_list,
|
|
const std::optional<torch::Tensor>& cached_psum_num_recv_tokens_per_scaleup_rank,
|
|
const std::optional<torch::Tensor>& cached_psum_num_recv_tokens_per_expert,
|
|
const std::optional<torch::Tensor>& cached_num_unaligned_recv_tokens_per_expert,
|
|
const std::optional<torch::Tensor>& cached_dst_buffer_slot_idx,
|
|
const std::optional<torch::Tensor>& cached_token_metadata_at_forward,
|
|
const std::optional<torch::Tensor>& cached_recv_src_metadata,
|
|
const std::optional<torch::Tensor>& cached_channel_linked_list,
|
|
const int& num_max_tokens_per_rank,
|
|
const int& num_experts, const int& expert_alignment,
|
|
const int& num_sms, const int& num_qps,
|
|
const std::optional<EventHandle>& previous_event,
|
|
const std::optional<EventHandle>& previous_event_before_epilogue,
|
|
const bool& async_with_compute_stream,
|
|
const bool& allocate_on_comm_stream,
|
|
const bool& do_handle_copy, const bool& do_cpu_sync,
|
|
const bool& do_expand, const bool& do_zero_padding,
|
|
const bool& use_tma_aligned_col_major_sf) const {
|
|
// Check SM count
|
|
EP_HOST_ASSERT(num_sms > 0);
|
|
|
|
// Zero padding only makes sense with expand mode
|
|
EP_HOST_ASSERT(not do_zero_padding or do_expand);
|
|
|
|
// Cached mode must have responding handles
|
|
const bool cached_mode = cached_num_recv_tokens.has_value();
|
|
if (cached_mode) {
|
|
EP_HOST_ASSERT(cached_num_recv_tokens.has_value());
|
|
EP_HOST_ASSERT(cached_num_recv_tokens_per_expert_list.has_value());
|
|
EP_HOST_ASSERT(cached_num_expanded_tokens.has_value());
|
|
EP_HOST_ASSERT(cached_psum_num_recv_tokens_per_scaleup_rank.has_value());
|
|
EP_HOST_ASSERT(cached_psum_num_recv_tokens_per_expert.has_value());
|
|
EP_HOST_ASSERT(cached_dst_buffer_slot_idx.has_value());
|
|
|
|
// Hybrid kernels require more
|
|
if (nccl_context->num_scaleout_ranks > 1) {
|
|
EP_HOST_ASSERT(cached_token_metadata_at_forward.has_value());
|
|
EP_HOST_ASSERT(cached_channel_linked_list.has_value());
|
|
}
|
|
}
|
|
|
|
// Check data tensor
|
|
const auto [num_tokens, hidden] = get_shape<2>(x);
|
|
const auto num_hidden_bytes = hidden * static_cast<int>(x.element_size());
|
|
const auto num_local_experts = num_experts / nccl_context->num_ranks;
|
|
EP_HOST_ASSERT(x.is_cuda() and x.is_contiguous());
|
|
EP_HOST_ASSERT((x.size(1) * x.element_size()) % sizeof(int4) == 0);
|
|
EP_HOST_ASSERT(num_tokens <= num_max_tokens_per_rank);
|
|
|
|
// Check SF stuffs
|
|
int num_sf_packs = 0;
|
|
void* sf_ptr = nullptr;
|
|
int sf_token_stride = 0, sf_hidden_stride = 0;
|
|
if (sf.has_value()) {
|
|
// SF must be FP32 or packed UE8M0x4
|
|
const auto [num_tokens_, num_sf_packs_] = get_shape<2>(sf.value());
|
|
EP_HOST_ASSERT(num_tokens == num_tokens_);
|
|
EP_HOST_ASSERT(sf->is_cuda());
|
|
EP_HOST_ASSERT(sf->element_size() == sizeof(sf_pack_t));
|
|
num_sf_packs = num_sf_packs_;
|
|
sf_ptr = sf->data_ptr();
|
|
sf_token_stride = sf->stride(0);
|
|
sf_hidden_stride = sf->stride(1);
|
|
}
|
|
|
|
// Check top-k stuffs
|
|
const auto [num_tokens_, num_topk] = get_shape<2>(topk_idx);
|
|
EP_HOST_ASSERT(num_tokens == num_tokens_);
|
|
EP_HOST_ASSERT(topk_idx.scalar_type() == c10::CppTypeToScalarType<topk_idx_t>::value);
|
|
EP_HOST_ASSERT(topk_idx.is_cuda() and topk_idx.is_contiguous());
|
|
|
|
// Weights are optional for training backward
|
|
float* topk_weights_ptr = nullptr;
|
|
if (topk_weights.has_value()) {
|
|
const auto [num_tokens__, num_topk_] = get_shape<2>(topk_weights.value());
|
|
EP_HOST_ASSERT(num_tokens == num_tokens__);
|
|
EP_HOST_ASSERT(topk_weights->is_cuda() and topk_weights->is_contiguous());
|
|
topk_weights_ptr = topk_weights->data_ptr<float>();
|
|
}
|
|
|
|
// Expert receiving counter
|
|
int* cumulative_local_expert_recv_stats_ptr = nullptr;
|
|
if (cumulative_local_expert_recv_stats.has_value()) {
|
|
const auto [num_local_experts_] = get_shape<1>(cumulative_local_expert_recv_stats.value());
|
|
EP_HOST_ASSERT(cumulative_local_expert_recv_stats->is_cuda() and
|
|
cumulative_local_expert_recv_stats->is_contiguous());
|
|
EP_HOST_ASSERT(num_local_experts == num_local_experts_);
|
|
cumulative_local_expert_recv_stats_ptr = cumulative_local_expert_recv_stats->data_ptr<int>();
|
|
}
|
|
|
|
// Stream control
|
|
// All new tensor allocations should happen after this
|
|
const auto compute_stream = stream_control_prologue(previous_event, allocate_on_comm_stream, async_with_compute_stream);
|
|
|
|
// The number of received tokens per expert
|
|
// This is useful for expanding mode
|
|
EP_HOST_ASSERT(num_experts % nccl_context->num_ranks == 0);
|
|
auto psum_num_recv_tokens_per_expert = cached_psum_num_recv_tokens_per_expert.value_or(torch::Tensor());
|
|
if (cached_mode) {
|
|
const auto& [num_local_experts_] = get_shape<1>(psum_num_recv_tokens_per_expert);
|
|
EP_HOST_ASSERT(num_local_experts == num_local_experts_);
|
|
EP_HOST_ASSERT(psum_num_recv_tokens_per_expert.is_cuda() and psum_num_recv_tokens_per_expert.is_contiguous());
|
|
EP_HOST_ASSERT(psum_num_recv_tokens_per_expert.scalar_type() == torch::kInt);
|
|
} else {
|
|
// NOTES: for expand mode, the input is exclusive prefix sum, while for non-expand, it is inclusive
|
|
psum_num_recv_tokens_per_expert = torch::empty(
|
|
{num_local_experts + 1}, at::TensorOptions(torch::kCUDA).dtype(torch::kInt));
|
|
}
|
|
|
|
// The unaligned (actual) number of received tokens per expert
|
|
// Written by the dispatch kernel's notify warps, used by the epilogue for zero padding
|
|
auto num_unaligned_recv_tokens_per_expert = cached_num_unaligned_recv_tokens_per_expert.value_or(torch::Tensor());
|
|
int* num_unaligned_recv_tokens_per_expert_ptr = nullptr;
|
|
if (cached_mode) {
|
|
const auto& [num_local_experts_] = get_shape<1>(num_unaligned_recv_tokens_per_expert);
|
|
EP_HOST_ASSERT(num_local_experts == num_local_experts_);
|
|
EP_HOST_ASSERT(num_unaligned_recv_tokens_per_expert.is_cuda() and num_unaligned_recv_tokens_per_expert.is_contiguous());
|
|
EP_HOST_ASSERT(num_unaligned_recv_tokens_per_expert.scalar_type() == torch::kInt);
|
|
} else {
|
|
num_unaligned_recv_tokens_per_expert = torch::empty(
|
|
{num_local_experts}, at::TensorOptions(torch::kCUDA).dtype(torch::kInt));
|
|
}
|
|
num_unaligned_recv_tokens_per_expert_ptr = num_unaligned_recv_tokens_per_expert.data_ptr<int>();
|
|
|
|
// The prefix sum tensor of number of received tokens from each rank
|
|
// Will also be used in combine as the dispatch handle
|
|
auto psum_num_recv_tokens_per_scaleup_rank = cached_psum_num_recv_tokens_per_scaleup_rank.value_or(torch::Tensor());
|
|
if (cached_mode) {
|
|
const auto [num_scaleup_ranks] = get_shape<1>(psum_num_recv_tokens_per_scaleup_rank);
|
|
EP_HOST_ASSERT(num_scaleup_ranks == nccl_context->num_scaleup_ranks);
|
|
EP_HOST_ASSERT(psum_num_recv_tokens_per_scaleup_rank.is_cuda() and psum_num_recv_tokens_per_scaleup_rank.is_contiguous());
|
|
EP_HOST_ASSERT(psum_num_recv_tokens_per_scaleup_rank.scalar_type() == torch::kInt);
|
|
} else {
|
|
psum_num_recv_tokens_per_scaleup_rank = torch::empty(
|
|
{nccl_context->num_scaleup_ranks}, at::TensorOptions(torch::kCUDA).dtype(torch::kInt));
|
|
}
|
|
|
|
// Decide number of channels by shared memory consumption
|
|
// Only for hybrid version
|
|
int num_channels_per_sm = 1, num_channels = 1;
|
|
const int num_smem_bytes = jit::device_runtime->get_num_smem_bytes();
|
|
if (nccl_context->num_scaleout_ranks > 1) {
|
|
const auto dispatch_token_layout = get_dispatch_token_layout(hidden, x.element_size(), num_sf_packs, num_topk);
|
|
const auto combine_token_layout = get_combine_token_layout(hidden, sizeof(nv_bfloat16), num_topk);
|
|
EP_HOST_ASSERT(num_sms <= kNumMaxSMs);
|
|
num_channels_per_sm = std::min<int>(
|
|
(num_smem_bytes - get_num_notify_smem_bytes(nccl_context->num_ranks, num_experts)) / dispatch_token_layout.get_num_bytes<true>(),
|
|
32 - kNumNotifyWarps);
|
|
num_channels_per_sm = std::min<int>(
|
|
num_smem_bytes / combine_token_layout.get_num_bytes<true>(),
|
|
num_channels_per_sm);
|
|
num_channels_per_sm = std::min<int>(
|
|
/* 2 kinds of warps */ num_channels_per_sm / 2, kNumMaxChannelsPerSM);
|
|
if (not prefer_overlap_with_compute)
|
|
num_channels_per_sm = std::min<int>(num_channels_per_sm, 4);
|
|
num_channels = num_sms * num_channels_per_sm;
|
|
if (get_env<int>("EP_BUFFER_DEBUG"))
|
|
printf("Elastic buffer uses %d channels per SM\n", num_channels_per_sm);
|
|
}
|
|
|
|
// Non-hybrid mode handles
|
|
auto dst_buffer_slot_idx = cached_dst_buffer_slot_idx.value_or(torch::Tensor());
|
|
if (nccl_context->num_scaleout_ranks == 1) {
|
|
if (cached_mode) {
|
|
const auto [num_tokens__, num_topk_] = get_shape<2>(dst_buffer_slot_idx);
|
|
EP_HOST_ASSERT(num_tokens == num_tokens__ and num_topk == num_topk_);
|
|
EP_HOST_ASSERT(dst_buffer_slot_idx.is_cuda() and dst_buffer_slot_idx.is_contiguous());
|
|
EP_HOST_ASSERT(dst_buffer_slot_idx.scalar_type() == torch::kInt);
|
|
} else {
|
|
// Allocate a new tensor
|
|
dst_buffer_slot_idx = torch::empty(
|
|
{num_tokens, num_topk}, torch::TensorOptions(torch::kCUDA).dtype(torch::kInt));
|
|
}
|
|
}
|
|
|
|
// Hybrid mode handles
|
|
std::optional<torch::Tensor> token_metadata_at_forward, channel_linked_list;
|
|
int *token_metadata_at_forward_ptr = nullptr, *channel_linked_list_ptr = nullptr;
|
|
if (nccl_context->num_scaleout_ranks > 1) {
|
|
// The token destination slot idx during forward
|
|
// `[i, j, k, l]` means: from channel i from scale-out peer k, the j-th token's index in the l-th rank buffer
|
|
// NOTES: Used primarily for cached mode
|
|
// TODO: May make it a linked list to remove the redundant info in `token_metadata_at_forward`
|
|
const auto num_max_tokens_per_channel = math::ceil_div(num_max_tokens_per_rank, num_channels);
|
|
if (cached_mode) {
|
|
const auto [num_channels_, num_scaleout_ranks_, num_max_tokens_per_channel_, num_topk_] =
|
|
get_shape<4>(dst_buffer_slot_idx);
|
|
EP_HOST_ASSERT(num_channels == num_channels_ and nccl_context->num_scaleout_ranks == num_scaleout_ranks_ and
|
|
num_max_tokens_per_channel == num_max_tokens_per_channel_ and num_topk == num_topk_);
|
|
EP_HOST_ASSERT(dst_buffer_slot_idx.is_cuda() and dst_buffer_slot_idx.is_contiguous());
|
|
EP_HOST_ASSERT(dst_buffer_slot_idx.scalar_type() == torch::kInt);
|
|
} else {
|
|
dst_buffer_slot_idx = torch::empty(
|
|
{num_channels, nccl_context->num_scaleout_ranks, num_max_tokens_per_channel, num_topk},
|
|
torch::TensorOptions().device(torch::kCUDA).dtype(torch::kInt)
|
|
);
|
|
}
|
|
|
|
// The token metadata during forward
|
|
// `[i, j]` means: in channel i, the j-th forwarded token's metadata
|
|
// Info contains:
|
|
// - Scaleout rank index and source token index in the original rank (0)
|
|
// - Whether the token is the last one in the chunk (1)
|
|
// - cached top-k scaleup peer indices (top-k)
|
|
// - each selections' destination slot indices (top-k)
|
|
const auto num_max_forwarded_tokens = nccl_context->num_scaleout_ranks * num_max_tokens_per_channel + 1;
|
|
const auto num_forward_metadata_dims = 2 + num_topk * 2;
|
|
if (cached_mode) {
|
|
token_metadata_at_forward = cached_token_metadata_at_forward;
|
|
const auto [num_channels_, num_max_forwarded_tokens_, num_forward_metadata_dims_] = get_shape<3>(token_metadata_at_forward.value());
|
|
EP_HOST_ASSERT(num_channels == num_channels_ and num_max_forwarded_tokens == num_max_forwarded_tokens_
|
|
and num_forward_metadata_dims == num_forward_metadata_dims_);
|
|
EP_HOST_ASSERT(token_metadata_at_forward->is_cuda() and token_metadata_at_forward->is_contiguous());
|
|
EP_HOST_ASSERT(token_metadata_at_forward->scalar_type() == torch::kInt);
|
|
} else {
|
|
token_metadata_at_forward = torch::empty(
|
|
{num_channels, num_max_forwarded_tokens, num_forward_metadata_dims},
|
|
torch::TensorOptions().device(torch::kCUDA).dtype(torch::kInt)
|
|
);
|
|
}
|
|
token_metadata_at_forward_ptr = token_metadata_at_forward->data_ptr<int>();
|
|
|
|
// Per-scaleup-peer-per-channel linked list
|
|
// `[i, j, k]` means: from channel i from scaleup peer k, the j-th token's index in the combine's input
|
|
if (cached_mode) {
|
|
channel_linked_list = cached_channel_linked_list;
|
|
const auto [num_channels__, d1_, d2_] = get_shape<3>(channel_linked_list.value());
|
|
channel_linked_list_ptr = channel_linked_list->data_ptr<int>();
|
|
EP_HOST_ASSERT(num_channels == num_channels__);
|
|
EP_HOST_ASSERT(d1_ == nccl_context->num_scaleout_ranks * num_max_tokens_per_channel + 1);
|
|
EP_HOST_ASSERT(d2_ == nccl_context->num_scaleup_ranks);
|
|
EP_HOST_ASSERT(channel_linked_list->is_cuda() and channel_linked_list->is_contiguous());
|
|
EP_HOST_ASSERT(channel_linked_list->scalar_type() == torch::kInt);
|
|
} else {
|
|
channel_linked_list = torch::empty(
|
|
// Index 0 of the list means the starting item
|
|
{num_channels,
|
|
nccl_context->num_scaleout_ranks * num_max_tokens_per_channel + 1,
|
|
nccl_context->num_scaleup_ranks},
|
|
torch::TensorOptions().device(torch::kCUDA).dtype(torch::kInt)
|
|
);
|
|
}
|
|
channel_linked_list_ptr = channel_linked_list->data_ptr<int>();
|
|
}
|
|
|
|
// Clone `topk_idx` for saving in the handle (to prevent users' modification)
|
|
auto copied_topk_idx = std::optional<torch::Tensor>();
|
|
topk_idx_t* copied_topk_idx_ptr = nullptr;
|
|
if (do_handle_copy and not cached_mode) {
|
|
copied_topk_idx = torch::empty_like(topk_idx);
|
|
copied_topk_idx_ptr = copied_topk_idx->data_ptr<topk_idx_t>();
|
|
}
|
|
|
|
// Check buffer size
|
|
EP_HOST_ASSERT(get_dispatch_buffer_size(
|
|
num_max_tokens_per_rank, hidden, num_sf_packs, num_topk, x.element_size(),
|
|
nccl_context->num_scaleout_ranks, nccl_context->num_scaleup_ranks,
|
|
nccl_context->is_scaleup_nvlink) <= num_buffer_bytes);
|
|
|
|
// Ready and clean host workspace for this round
|
|
const auto host_workspace_layout = layout::WorkspaceLayout(
|
|
host_workspace,
|
|
nccl_context->num_scaleout_ranks,
|
|
nccl_context->num_scaleup_ranks,
|
|
num_experts);
|
|
std::fill_n(host_workspace_layout.get_scaleup_rank_count_ptr<false>(), nccl_context->num_scaleup_ranks, 0);
|
|
std::fill_n(host_workspace_layout.get_scaleup_expert_count_ptr<false>(), num_local_experts, 0);
|
|
std::atomic_thread_fence(std::memory_order_seq_cst);
|
|
|
|
// Do dispatch into the buffers (with SM limitation)
|
|
EP_HOST_ASSERT(num_sms <= jit::device_runtime->get_num_sms());
|
|
launch_dispatch(x.data_ptr(), sf_ptr,
|
|
topk_idx.data_ptr<topk_idx_t>(), topk_weights_ptr,
|
|
copied_topk_idx_ptr,
|
|
cumulative_local_expert_recv_stats_ptr,
|
|
psum_num_recv_tokens_per_scaleup_rank.data_ptr<int>(),
|
|
psum_num_recv_tokens_per_expert.data_ptr<int>(),
|
|
num_unaligned_recv_tokens_per_expert_ptr,
|
|
dst_buffer_slot_idx.data_ptr<int>(),
|
|
token_metadata_at_forward_ptr,
|
|
num_tokens, num_max_tokens_per_rank,
|
|
hidden, x.element_size(),
|
|
num_sf_packs, sf_token_stride, sf_hidden_stride,
|
|
num_experts, num_topk, expert_alignment,
|
|
nccl_context->dev_comm, nccl_context->window,
|
|
buffer,
|
|
workspace, mapped_host_workspace,
|
|
nccl_context->scaleout_rank_idx, nccl_context->scaleup_rank_idx,
|
|
nccl_context->num_scaleout_ranks, nccl_context->num_scaleup_ranks,
|
|
nccl_context->is_scaleup_nvlink,
|
|
num_sms, num_channels_per_sm,
|
|
num_smem_bytes,
|
|
num_qps, num_gpu_timeout_cycles,
|
|
cached_mode, do_cpu_sync,
|
|
comm_stream);
|
|
|
|
// Received token counters
|
|
int num_recv_tokens = 0, num_expanded_tokens = 0;
|
|
int counter_scaleup_rank_idx = 0, counter_local_expert_idx = 0;
|
|
std::vector<int> num_recv_tokens_per_expert_list;
|
|
|
|
// Assign these values according to modes
|
|
if (cached_mode) {
|
|
// Cached mode
|
|
EP_HOST_ASSERT(not do_cpu_sync and "Cannot do CPU sync with cached mode");
|
|
num_recv_tokens = cached_num_recv_tokens.value();
|
|
num_recv_tokens_per_expert_list = cached_num_recv_tokens_per_expert_list.value();
|
|
num_expanded_tokens = cached_num_expanded_tokens.value();
|
|
} else if (do_cpu_sync) {
|
|
// Non-cached mode with sync
|
|
const auto start_cpu_time = std::chrono::high_resolution_clock::now();
|
|
while (true) {
|
|
bool ready = true;
|
|
|
|
// Read number of received tokens from each scaleup rank
|
|
while (counter_scaleup_rank_idx < nccl_context->num_scaleup_ranks and ready) {
|
|
const auto count = math::encode_decode_positive(
|
|
host_workspace_layout.get_scaleup_rank_count_ptr<false>()[counter_scaleup_rank_idx]);
|
|
if ((ready = math::is_decoded_positive_ready(count))) {
|
|
num_recv_tokens += count;
|
|
++ counter_scaleup_rank_idx;
|
|
}
|
|
}
|
|
|
|
// Read expert counts
|
|
while (counter_local_expert_idx < num_local_experts and ready) {
|
|
const auto count = math::encode_decode_positive(
|
|
host_workspace_layout.get_scaleup_expert_count_ptr<false>()[counter_local_expert_idx]);
|
|
if ((ready = math::is_decoded_positive_ready(count))) {
|
|
num_recv_tokens_per_expert_list.push_back(count);
|
|
num_expanded_tokens += count;
|
|
++ counter_local_expert_idx;
|
|
}
|
|
}
|
|
|
|
// Ready and do next steps
|
|
const auto get_buffer_info = [&]() {
|
|
std::stringstream ss;
|
|
ss << "CPU side received count (scaleup: " << nccl_context->scaleup_rank_idx << "): ";
|
|
for (int i = 0; i < nccl_context->num_scaleup_ranks + num_local_experts; ++ i) {
|
|
ss << host_workspace_layout.get_scaleup_rank_expert_count_ptr<false>()[i];
|
|
ss << (i == nccl_context->num_scaleup_ranks - 1 ? " # ": " ");
|
|
}
|
|
return ss.str();
|
|
};
|
|
if (ready) {
|
|
if (get_env<int>("EP_BUFFER_DEBUG"))
|
|
printf("%s\n", get_buffer_info().c_str());
|
|
break;
|
|
}
|
|
|
|
// Timeout checks
|
|
const auto now = std::chrono::high_resolution_clock::now();
|
|
if (std::chrono::duration_cast<std::chrono::seconds>(now - start_cpu_time).count() > num_cpu_timeout_secs)
|
|
throw EPExceptionWithLineInfo("Dispatch CPU wait", get_buffer_info());
|
|
}
|
|
} else {
|
|
// Non-cached mode without CPU sync, allocate with the worst case
|
|
num_recv_tokens = num_max_tokens_per_rank * nccl_context->num_ranks;
|
|
num_expanded_tokens = nccl_context->num_ranks * num_max_tokens_per_rank * std::min(num_topk, num_local_experts);
|
|
num_expanded_tokens += (expert_alignment - 1) * num_local_experts;
|
|
num_expanded_tokens = math::align(num_expanded_tokens, expert_alignment);
|
|
}
|
|
|
|
// Allocate received tensors
|
|
// `recv_src_metadata` includes source token indices and buffer slot indices
|
|
const auto num_allocated_tokens = do_expand ? num_expanded_tokens : num_recv_tokens;
|
|
auto recv_x = torch::empty({num_allocated_tokens, hidden}, x.options());
|
|
auto recv_sf = std::optional<torch::Tensor>();
|
|
auto recv_topk_idx = std::optional<torch::Tensor>();
|
|
auto recv_topk_weights = std::optional<torch::Tensor>();
|
|
auto recv_src_metadata = cached_mode ?
|
|
cached_recv_src_metadata.value() :
|
|
torch::empty({num_recv_tokens, num_topk + 2},
|
|
torch::TensorOptions(torch::kCUDA).dtype(torch::kInt));
|
|
|
|
// Optional tensors
|
|
void* recv_sf_ptr = nullptr;
|
|
topk_idx_t* recv_topk_idx_ptr = nullptr;
|
|
float* recv_topk_weights_ptr = nullptr;
|
|
int recv_sf_token_stride = 0, recv_sf_hidden_stride = 0;
|
|
if (sf.has_value()) {
|
|
if (not use_tma_aligned_col_major_sf) {
|
|
recv_sf_token_stride = num_sf_packs, recv_sf_hidden_stride = 1;
|
|
} else {
|
|
// TMA-aligned layout for the next GEMM input
|
|
recv_sf_token_stride = 1, recv_sf_hidden_stride = math::align(num_allocated_tokens, kNumAlignedSFPacks);
|
|
}
|
|
recv_sf = torch::empty_strided({num_allocated_tokens, num_sf_packs},
|
|
{recv_sf_token_stride, recv_sf_hidden_stride},
|
|
sf->options());
|
|
recv_sf_ptr = recv_sf->data_ptr();
|
|
}
|
|
if (not do_expand) {
|
|
recv_topk_idx = torch::empty({num_allocated_tokens, num_topk}, topk_idx.options());
|
|
recv_topk_idx_ptr = recv_topk_idx->data_ptr<topk_idx_t>();
|
|
}
|
|
if (topk_weights.has_value()) {
|
|
recv_topk_weights = do_expand ?
|
|
torch::empty({num_allocated_tokens}, topk_weights->options()) :
|
|
torch::empty({num_allocated_tokens, num_topk}, topk_weights->options());
|
|
recv_topk_weights_ptr = recv_topk_weights->data_ptr<float>();
|
|
}
|
|
|
|
// Process prefix sum, in expanding mode, it is also atomic counters
|
|
if (not cached_mode) {
|
|
if (do_expand) {
|
|
// Slice the exclusive part and do atomic additions into inclusive
|
|
psum_num_recv_tokens_per_expert = psum_num_recv_tokens_per_expert.slice(0, 0, num_local_experts);
|
|
} else {
|
|
// Slice the inclusive part (and will not be used in the epilogue)
|
|
psum_num_recv_tokens_per_expert = psum_num_recv_tokens_per_expert.slice(0, 1, num_local_experts + 1);
|
|
}
|
|
}
|
|
EP_HOST_ASSERT(psum_num_recv_tokens_per_expert.size(0) == num_local_experts);
|
|
|
|
// Launch copy kernels with full SMs
|
|
stream_control_before_epilogue(previous_event_before_epilogue);
|
|
launch_dispatch_copy_epilogue(buffer, workspace,
|
|
psum_num_recv_tokens_per_scaleup_rank.data_ptr<int>(),
|
|
psum_num_recv_tokens_per_expert.data_ptr<int>(),
|
|
recv_x.data_ptr(), recv_sf_ptr,
|
|
recv_topk_idx_ptr, recv_topk_weights_ptr,
|
|
recv_src_metadata.data_ptr<int>(),
|
|
channel_linked_list_ptr,
|
|
num_unaligned_recv_tokens_per_expert_ptr,
|
|
num_recv_tokens, num_max_tokens_per_rank,
|
|
num_hidden_bytes,
|
|
num_sf_packs, recv_sf_token_stride, recv_sf_hidden_stride,
|
|
num_experts, num_topk, expert_alignment,
|
|
nccl_context->scaleout_rank_idx, nccl_context->scaleup_rank_idx,
|
|
nccl_context->num_scaleout_ranks, nccl_context->num_scaleup_ranks,
|
|
jit::device_runtime->get_num_sms(),
|
|
jit::device_runtime->get_num_smem_bytes(),
|
|
num_channels,
|
|
do_expand, cached_mode,
|
|
do_zero_padding,
|
|
comm_stream);
|
|
|
|
// Stream control
|
|
const auto event = stream_control_epilogue(
|
|
{x, sf, topk_idx, topk_weights,
|
|
recv_x, recv_sf, recv_topk_idx, recv_topk_weights,
|
|
cumulative_local_expert_recv_stats,
|
|
copied_topk_idx,
|
|
psum_num_recv_tokens_per_scaleup_rank,
|
|
psum_num_recv_tokens_per_expert,
|
|
num_unaligned_recv_tokens_per_expert,
|
|
recv_src_metadata,
|
|
dst_buffer_slot_idx,
|
|
token_metadata_at_forward,
|
|
channel_linked_list},
|
|
compute_stream,
|
|
allocate_on_comm_stream, async_with_compute_stream);
|
|
|
|
return {recv_x, recv_sf,
|
|
recv_topk_idx, recv_topk_weights,
|
|
copied_topk_idx,
|
|
num_recv_tokens, num_expanded_tokens,
|
|
num_recv_tokens_per_expert_list,
|
|
psum_num_recv_tokens_per_scaleup_rank,
|
|
psum_num_recv_tokens_per_expert,
|
|
num_unaligned_recv_tokens_per_expert,
|
|
recv_src_metadata,
|
|
dst_buffer_slot_idx,
|
|
token_metadata_at_forward,
|
|
channel_linked_list,
|
|
event};
|
|
}
|
|
|
|
std::tuple<torch::Tensor, std::optional<torch::Tensor>, std::optional<EventHandle>>
|
|
combine(const torch::Tensor& x,
|
|
const std::optional<torch::Tensor>& topk_weights,
|
|
const std::optional<torch::Tensor>& bias_0,
|
|
const std::optional<torch::Tensor>& bias_1,
|
|
const torch::Tensor& src_metadata,
|
|
const torch::Tensor& combined_topk_idx,
|
|
const torch::Tensor& psum_num_recv_tokens_per_scaleup_rank,
|
|
const std::optional<torch::Tensor>& token_metadata_at_forward,
|
|
const std::optional<torch::Tensor>& channel_linked_list,
|
|
const int& num_experts,
|
|
const int& num_max_tokens_per_rank,
|
|
const int& num_sms, const int& num_qps,
|
|
const std::optional<EventHandle>& previous_event,
|
|
const std::optional<EventHandle>& previous_event_before_epilogue,
|
|
const bool& async_with_compute_stream,
|
|
const bool& allocate_on_comm_stream,
|
|
const bool& use_expanded_layout) const {
|
|
// Check SM count
|
|
EP_HOST_ASSERT(num_sms > 0);
|
|
|
|
// Check data
|
|
const auto [num_tokens, hidden] = get_shape<2>(x);
|
|
EP_HOST_ASSERT(x.is_cuda() and x.is_contiguous());
|
|
EP_HOST_ASSERT(x.scalar_type() == torch::kBFloat16);
|
|
EP_HOST_ASSERT((x.size(1) * x.element_size()) % sizeof(int4) == 0);
|
|
|
|
// Check tensors at dispatch
|
|
const auto [num_combined_tokens, num_topk] = get_shape<2>(combined_topk_idx);
|
|
const auto [num_scaleup_ranks] = get_shape<1>(psum_num_recv_tokens_per_scaleup_rank);
|
|
EP_HOST_ASSERT(combined_topk_idx.is_cuda() and combined_topk_idx.is_contiguous());
|
|
EP_HOST_ASSERT(combined_topk_idx.scalar_type() == c10::CppTypeToScalarType<topk_idx_t>::value);
|
|
EP_HOST_ASSERT(num_scaleup_ranks == nccl_context->num_scaleup_ranks);
|
|
EP_HOST_ASSERT(psum_num_recv_tokens_per_scaleup_rank.is_cuda() and psum_num_recv_tokens_per_scaleup_rank.is_contiguous());
|
|
EP_HOST_ASSERT(psum_num_recv_tokens_per_scaleup_rank.scalar_type() == torch::kInt);
|
|
EP_HOST_ASSERT(num_combined_tokens <= num_max_tokens_per_rank);
|
|
|
|
// Check metadata
|
|
// For reduction mode, `num_tokens_` means the number of unexpanded tokens
|
|
const auto [num_reduced_tokens, num_topk_p2] = get_shape<2>(src_metadata);
|
|
EP_HOST_ASSERT(num_reduced_tokens == (use_expanded_layout ? num_reduced_tokens : num_tokens));
|
|
EP_HOST_ASSERT(num_topk_p2 == num_topk + 2);
|
|
EP_HOST_ASSERT(src_metadata.is_cuda() and src_metadata.is_contiguous());
|
|
EP_HOST_ASSERT(src_metadata.scalar_type() == torch::kInt);
|
|
|
|
// Check optional tensors
|
|
if (topk_weights.has_value()) {
|
|
if (use_expanded_layout) {
|
|
const auto [num_tokens__] = get_shape<1>(topk_weights.value());
|
|
EP_HOST_ASSERT(num_tokens == num_tokens__);
|
|
} else {
|
|
const auto [num_tokens__, num_topk__] = get_shape<2>(topk_weights.value());
|
|
EP_HOST_ASSERT(num_tokens == num_tokens__ and num_topk == num_topk__);
|
|
}
|
|
EP_HOST_ASSERT(topk_weights->is_cuda() and topk_weights->is_contiguous());
|
|
EP_HOST_ASSERT(topk_weights->scalar_type() == torch::kFloat);
|
|
}
|
|
|
|
const auto bias_opts = std::vector({bias_0, bias_1});
|
|
void* bias_ptrs[2] = {nullptr, nullptr};
|
|
for (int i = 0; i < 2; ++ i) {
|
|
if (bias_opts[i].has_value()) {
|
|
auto bias = bias_opts[i].value();
|
|
EP_HOST_ASSERT(bias.dim() == 2 and bias.is_contiguous());
|
|
EP_HOST_ASSERT(bias.scalar_type() == x.scalar_type());
|
|
EP_HOST_ASSERT(bias.size(0) == num_combined_tokens and bias.size(1) == hidden);
|
|
bias_ptrs[i] = bias.data_ptr();
|
|
}
|
|
}
|
|
|
|
// Stream control
|
|
// All new tensor allocations should happen after this
|
|
const auto compute_stream = stream_control_prologue(previous_event, allocate_on_comm_stream, async_with_compute_stream);
|
|
|
|
// Check buffer size
|
|
EP_HOST_ASSERT(get_combine_buffer_size(num_max_tokens_per_rank, hidden, num_topk,
|
|
nccl_context->num_scaleout_ranks, nccl_context->num_scaleup_ranks,
|
|
nccl_context->is_scaleup_nvlink, allow_multiple_reduction) <= num_buffer_bytes);
|
|
|
|
// Optional configs and metadata for hybrid combine
|
|
int num_channels = 1;
|
|
int* token_metadata_at_forward_ptr = nullptr;
|
|
int* channel_linked_list_ptr = nullptr;
|
|
if (nccl_context->num_scaleout_ranks > 1) {
|
|
// The token metadata during forward
|
|
const auto [num_channels_, d1, d2] = get_shape<3>(token_metadata_at_forward.value());
|
|
const auto num_max_tokens_per_channel = math::ceil_div(num_max_tokens_per_rank, num_channels_);
|
|
num_channels = num_channels_;
|
|
token_metadata_at_forward_ptr = token_metadata_at_forward->data_ptr<int>();
|
|
EP_HOST_ASSERT(d1 == nccl_context->num_scaleout_ranks * num_max_tokens_per_channel + 1);
|
|
EP_HOST_ASSERT(d2 == 2 + num_topk * 2);
|
|
EP_HOST_ASSERT(token_metadata_at_forward->is_cuda() and token_metadata_at_forward->is_contiguous());
|
|
EP_HOST_ASSERT(token_metadata_at_forward->scalar_type() == torch::kInt);
|
|
|
|
// Per-scaleup-peer-per-channel linked list
|
|
const auto [num_channels__, d1_, d2_] = get_shape<3>(channel_linked_list.value());
|
|
channel_linked_list_ptr = channel_linked_list->data_ptr<int>();
|
|
EP_HOST_ASSERT(num_channels == num_channels__);
|
|
EP_HOST_ASSERT(d1_ == nccl_context->num_scaleout_ranks * num_max_tokens_per_channel + 1);
|
|
EP_HOST_ASSERT(d2_ == nccl_context->num_scaleup_ranks);
|
|
EP_HOST_ASSERT(channel_linked_list->is_cuda() and channel_linked_list->is_contiguous());
|
|
EP_HOST_ASSERT(channel_linked_list->scalar_type() == torch::kInt);
|
|
}
|
|
|
|
// Push data into remote buffers
|
|
// NOTES: we don't use `num_hidden_bytes` due to enable later quantization possibility
|
|
const auto reduce_buffer = launch_combine(
|
|
x.data_ptr(),
|
|
topk_weights.has_value() ? topk_weights->data_ptr() : nullptr,
|
|
src_metadata.data_ptr<int>(),
|
|
psum_num_recv_tokens_per_scaleup_rank.data_ptr<int>(),
|
|
token_metadata_at_forward_ptr,
|
|
channel_linked_list_ptr,
|
|
nccl_context->dev_comm, nccl_context->window,
|
|
buffer, workspace,
|
|
num_reduced_tokens, num_max_tokens_per_rank,
|
|
hidden, num_experts, num_topk,
|
|
num_qps, num_gpu_timeout_cycles,
|
|
nccl_context->num_scaleout_ranks, nccl_context->num_scaleup_ranks,
|
|
nccl_context->scaleout_rank_idx, nccl_context->scaleup_rank_idx,
|
|
nccl_context->is_scaleup_nvlink,
|
|
num_sms, jit::device_runtime->get_num_smem_bytes(),
|
|
num_channels,
|
|
use_expanded_layout, allow_multiple_reduction,
|
|
comm_stream);
|
|
|
|
// Allocate output tensors
|
|
auto combined_x = torch::empty({num_combined_tokens, hidden}, x.options());
|
|
auto combined_topk_weights = std::optional<torch::Tensor>();
|
|
float* combined_topk_weights_ptr = nullptr;
|
|
if (topk_weights.has_value()) {
|
|
combined_topk_weights = torch::empty({num_combined_tokens, num_topk}, topk_weights->options());
|
|
combined_topk_weights_ptr = combined_topk_weights->data_ptr<float>();
|
|
}
|
|
|
|
// Combine pushed data
|
|
stream_control_before_epilogue(previous_event_before_epilogue);
|
|
launch_combine_reduce_epilogue(combined_x.data_ptr(),
|
|
combined_topk_weights_ptr,
|
|
combined_topk_idx.data_ptr<topk_idx_t>(),
|
|
num_combined_tokens, num_max_tokens_per_rank,
|
|
hidden,
|
|
num_experts, num_topk,
|
|
reduce_buffer,
|
|
bias_ptrs[0], bias_ptrs[1],
|
|
nccl_context->num_scaleout_ranks, nccl_context->num_scaleup_ranks,
|
|
nccl_context->scaleout_rank_idx, nccl_context->scaleup_rank_idx,
|
|
jit::device_runtime->get_num_sms(),
|
|
jit::device_runtime->get_num_smem_bytes(),
|
|
use_expanded_layout, allow_multiple_reduction,
|
|
comm_stream);
|
|
|
|
// Stream control
|
|
const auto event = stream_control_epilogue(
|
|
{x, topk_weights, bias_0, bias_1,
|
|
src_metadata,
|
|
combined_topk_idx,
|
|
combined_x, combined_topk_weights,
|
|
psum_num_recv_tokens_per_scaleup_rank,
|
|
token_metadata_at_forward,
|
|
channel_linked_list},
|
|
compute_stream,
|
|
allocate_on_comm_stream, async_with_compute_stream);
|
|
return {combined_x, combined_topk_weights, event};
|
|
}
|
|
};
|
|
|
|
static void register_apis(pybind11::module_& m) {
|
|
pybind11::class_<ElasticBuffer>(m, "ElasticBuffer")
|
|
.def(pybind11::init<int, int, int64_t, symmetric::cpu_comm_t, int64_t, int64_t, bool, bool, bool, int, int, int, int, bool>())
|
|
.def("destroy", &ElasticBuffer::destroy)
|
|
.def("get_comm_stream", &ElasticBuffer::get_comm_stream)
|
|
.def("get_physical_domain_size", &ElasticBuffer::get_physical_domain_size)
|
|
.def("get_logical_domain_size", &ElasticBuffer::get_logical_domain_size)
|
|
.def("barrier", &ElasticBuffer::barrier)
|
|
.def("engram_write", &ElasticBuffer::engram_write)
|
|
.def("engram_fetch", &ElasticBuffer::engram_fetch)
|
|
.def("pp_set_config", &ElasticBuffer::pp_set_config)
|
|
.def("pp_send", &ElasticBuffer::pp_send)
|
|
.def("pp_recv", &ElasticBuffer::pp_recv)
|
|
.def("create_agrs_session", &ElasticBuffer::create_agrs_session)
|
|
.def("destroy_agrs_session", &ElasticBuffer::destroy_agrs_session)
|
|
.def("agrs_set_config", &ElasticBuffer::agrs_set_config)
|
|
.def("agrs_get_inplace_tensor", &ElasticBuffer::agrs_get_inplace_tensor)
|
|
.def("all_gather", &ElasticBuffer::all_gather)
|
|
.def("dispatch", &ElasticBuffer::dispatch)
|
|
.def("combine", &ElasticBuffer::combine);
|
|
m.def("create_cpu_handle", &ElasticBuffer::create_cpu_handle);
|
|
m.def("calculate_elastic_buffer_size", &ElasticBuffer::calculate_buffer_size);
|
|
m.def("get_elastic_buffer_alignment", [=]() {
|
|
return symmetric::kNumAlignmentBytes;
|
|
});
|
|
|
|
// NCCL communicator handle
|
|
m.def("get_local_nccl_unique_id", &nccl::get_local_unique_id);
|
|
m.def("create_nccl_comm", &nccl::create_nccl_comm);
|
|
m.def("destroy_nccl_comm", &nccl::destroy_nccl_comm);
|
|
|
|
// Communication domain utilities
|
|
m.def("get_physical_domain_size", &nccl::get_physical_domain_size);
|
|
m.def("get_logical_domain_size", &nccl::get_logical_domain_size);
|
|
}
|
|
|
|
} // namespace deep_ep
|