Files
2026-07-13 13:18:33 +08:00

345 lines
12 KiB
C++

// Copyright (c) Microsoft Corporation.
// SPDX-License-Identifier: Apache-2.0
// DeepSpeed Team
#include <torch/extension.h>
#include <oneapi/ccl.hpp>
#include "shm.h"
// #define DO_PROFILE
#ifdef DO_PROFILE
#include <cfloat>
#include <chrono>
#endif
// Communication settings
static int world_rank = -1;
static int world_size = -1;
static std::set<int> _comm_ids;
static std::set<int> _colors;
static std::vector<ccl::communicator> _ccl_comms;
static ccl::shared_ptr_class<ccl::kvs> sub_kvs;
static std::map<std::vector<int>, int> group_to_comm_id;
ccl::communicator& _get_comm_from_group() { return _ccl_comms[0]; }
ccl::communicator& _get_comm_from_group(py::object group) { return _ccl_comms[0]; }
ccl::communicator& _get_comm_from_group(std::vector<int> ranks)
{
if (group_to_comm_id.find(ranks) != group_to_comm_id.end()) {
auto id = group_to_comm_id.find(ranks);
return _ccl_comms[id->second];
}
return _ccl_comms[0];
}
#define CCLCHECK(cmd) \
do { \
cmd; \
} while (0)
#define KVS_CREATE_SUCCESS 0
#define KVS_CREATE_FAILURE -1
static bool is_initialized = 0;
static ccl::shared_ptr_class<ccl::kvs> kvs;
static bool all_ranks_local_p = false;
void initialize(int size, int rank, torch::Tensor& kvs_data)
{
if (is_initialized) return;
// Check whether all ranks is on the same physical machine.
// If true, we will use an SHM based low latency allreduce
auto ls_string = std::getenv("LOCAL_SIZE");
int ls = 0;
if (ls_string != NULL) { ls = std::stoi(std::getenv("LOCAL_SIZE")); }
if (size >= 1 && size == ls) { all_ranks_local_p = true; }
world_size = size;
world_rank = rank;
is_initialized = 1;
ccl::kvs::address_type main_addr;
if (rank != 0) {
memcpy(main_addr.data(), kvs_data.data_ptr(), main_addr.size());
kvs = ccl::create_kvs(main_addr);
}
_ccl_comms.emplace_back(ccl::create_communicator(size, rank, kvs));
auto addr_string = std::getenv("MASTER_ADDR");
if (addr_string == NULL) { addr_string = ""; }
auto port_string = std::getenv("MASTER_PORT");
if (port_string == NULL) { port_string = ""; }
if (all_ranks_local_p) { shm_initialize(size, rank, addr_string, port_string); }
}
/*
rank == 0: create main kvs and return its address
rank == else: return an empty address
*/
std::vector<uint8_t> get_kvs_addr(int rank)
{
if (rank == 0) {
kvs = ccl::create_main_kvs();
ccl::kvs::address_type main_addr = kvs->get_address();
auto ccl_kvs_addr = std::vector<uint8_t>(main_addr.begin(), main_addr.end());
return ccl_kvs_addr;
} else {
ccl::kvs::address_type main_addr;
auto ccl_kvs_addr = std::vector<uint8_t>(main_addr.begin(), main_addr.end());
return ccl_kvs_addr;
}
}
int get_rank(int group = 0) { return world_rank; }
int get_world_size(int group = 0) { return world_size; }
// Find the next ordered, unique value to a set. E.g. <0,1,2,7> --> 3
int next_unique_val(std::set<int> s)
{
std::set<int>::iterator itr;
// Base case. Add 0 to start of set.
if (s.empty() || *s.begin() != 0) {
return 0;
// second base case where s = {0} (the case of s = {n != 0} is caught above)
} else if (s.size() == 1) {
return 1;
} else {
int prev_val = *s.begin();
for (itr = std::next(s.begin()); itr != s.end(); itr++) {
if (*itr != prev_val + 1) { return prev_val + 1; }
prev_val = *itr;
}
return *(s.end()) + 1;
}
}
std::vector<uint8_t> get_sub_kvs_addr(bool first)
{
if (first) {
sub_kvs = ccl::create_main_kvs();
ccl::kvs::address_type main_addr = sub_kvs->get_address();
auto ccl_kvs_addr = std::vector<uint8_t>(main_addr.begin(), main_addr.end());
return ccl_kvs_addr;
} else {
ccl::kvs::address_type main_addr;
auto ccl_kvs_addr = std::vector<uint8_t>(main_addr.begin(), main_addr.end());
return ccl_kvs_addr;
}
}
void initialize_sub_comm(int size, int rank, torch::Tensor& kvs_data, std::vector<int> ranks)
{
ccl::kvs::address_type main_addr;
if (rank != 0) {
memcpy(main_addr.data(), kvs_data.data_ptr(), main_addr.size());
sub_kvs = ccl::create_kvs(main_addr);
}
_ccl_comms.push_back(ccl::create_communicator(size, rank, sub_kvs));
group_to_comm_id[ranks] = _ccl_comms.size() - 1;
}
ccl::datatype get_ccl_datatype(c10::ScalarType type)
{
ccl::datatype ccl_type;
switch (type) {
case c10::ScalarType::Int: ccl_type = ccl::datatype::int32; break;
case c10::ScalarType::Long: ccl_type = ccl::datatype::int64; break;
case c10::ScalarType::Float: ccl_type = ccl::datatype::float32; break;
case c10::ScalarType::Double: ccl_type = ccl::datatype::float64; break;
case c10::ScalarType::BFloat16: ccl_type = ccl::datatype::bfloat16; break;
case c10::ScalarType::Half: ccl_type = ccl::datatype::float16; break;
default: ccl_type = ccl::datatype::int8;
}
return ccl_type;
}
ccl::reduction get_ccl_reduce_op(py::object op, at::Tensor& input)
{
py::object ReduceOp = py::module_::import("deepspeed.comm").attr("ReduceOp");
if (!py::isinstance(op, ReduceOp)) {
throw std::runtime_error("Error: Op must be of type ReduceOp");
}
int op_val = py::int_(op.attr("value"));
ccl::reduction ccl_op;
if (input.scalar_type() == at::kBool) {
if (op_val == (int)py::int_(ReduceOp.attr("SUM").attr("value"))) {
// For bool tensors, map sum to max, which both represent a bitwise or.
// This is to prevent overflow issues with sum, since we use uint8 to
// represent a bool (see cclDataType mapping).
ccl_op = ccl::reduction::max;
} else if (op_val == (int)py::int_(ReduceOp.attr("AVG").attr("value"))) {
throw std::runtime_error("Error: For bool tensors, op must be of type ReduceOp");
}
}
if (op_val == (int)py::int_(ReduceOp.attr("SUM").attr("value"))) {
ccl_op = ccl::reduction::sum;
} else if (op_val == (int)py::int_(ReduceOp.attr("MIN").attr("value"))) {
ccl_op = ccl::reduction::min;
} else if (op_val == (int)py::int_(ReduceOp.attr("MAX").attr("value"))) {
ccl_op = ccl::reduction::max;
} else if (op_val == (int)py::int_(ReduceOp.attr("PRODUCT").attr("value"))) {
ccl_op = ccl::reduction::prod;
} else {
throw std::runtime_error("Error: Unrecognized ReduceOp type");
}
return ccl_op;
}
void broadcast(torch::Tensor& data, int src, std::vector<int> group, bool async_op)
{
CCLCHECK(ccl::broadcast(data.data_ptr(),
data.numel(),
get_ccl_datatype(data.scalar_type()),
src,
_get_comm_from_group(group))
.wait());
}
// TODO: implement torch's async_op behavior, document it.
void all_reduce(torch::Tensor& data, py::object op, std::vector<int> group, bool async_op)
{
CCLCHECK(ccl::allreduce(data.data_ptr(),
data.data_ptr(),
data.numel(),
get_ccl_datatype(data.scalar_type()),
get_ccl_reduce_op(op, data),
_get_comm_from_group(group))
.wait());
}
void all_reduce_caching(torch::Tensor& data,
py::object op,
std::string match_id,
std::vector<int> group,
bool async_op)
{
ccl::allreduce_attr attr = ccl::default_allreduce_attr;
auto match_str = ccl::v1::string(match_id);
attr.template set<ccl::operation_attr_id::to_cache>(true);
attr.template set<ccl::operation_attr_id::match_id>(match_str);
// To control this, use operation attribute and set true value for to_cache field and unique
// string (for example, tensor name) for match_id field. Note that:
// match_id should be the same for a specific communication operation across all ranks.
// If the same tensor is a part of different communication operations, match_id should have
// different values for each of these operations.
CCLCHECK(ccl::allreduce(data.data_ptr(),
data.data_ptr(),
data.numel(),
get_ccl_datatype(data.scalar_type()),
get_ccl_reduce_op(op, data),
_get_comm_from_group(group),
attr)
.wait());
}
void inference_all_reduce(torch::Tensor& data, py::object op)
{
#ifdef DO_PROFILE
static double total_time = 0.0;
static double total_time_sq = 0.0;
static int count = -16; // warmup
static double max_time = 0.0;
static double min_time = DBL_MAX;
// make sure all rank reach this point before measuring time
// turn on this if you suspect each rank didn't reach here at the same time (stragger)
// if (all_ranks_local_p) {
// barrier_wait(0, world_size);
//}
auto start = std::chrono::system_clock::now();
#endif
static py::object ReduceOp = py::module_::import("deepspeed.comm").attr("ReduceOp");
static auto ReduceOpSum = (int)py::int_(ReduceOp.attr("SUM").attr("value"));
assert(py::int_(op.attr("value")) == ReduceOpSum);
auto numel = data.numel();
int data_size = 0;
bool data_type_fallback = false;
switch (data.scalar_type()) {
case c10::ScalarType::BFloat16: data_size = numel * 2; break;
case c10::ScalarType::Float: data_size = numel * 4; break;
default: data_type_fallback = true;
}
if (data_type_fallback || !all_ranks_local_p) {
// fallback to oneccl allreduce
CCLCHECK(ccl::allreduce(data.data_ptr(),
data.data_ptr(),
data.numel(),
get_ccl_datatype(data.scalar_type()),
get_ccl_reduce_op(op, data),
_get_comm_from_group())
.wait());
} else {
all_reduce_outer_loop(data, numel, data_size);
}
#ifdef DO_PROFILE
auto end = std::chrono::system_clock::now();
count++;
if (count > 0) {
double elapsed = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
if (elapsed > max_time) { max_time = elapsed; }
if (elapsed < min_time) { min_time = elapsed; }
total_time += elapsed;
total_time_sq += elapsed * elapsed;
if (world_rank == 0 && count == 1000) {
auto avg = total_time / count;
auto sd =
sqrt(total_time_sq / count - total_time * total_time / (count * count)) / avg * 100;
printf(" C++ kernel\t\t %.2f\t %.2f\t%.2f\t %.2f\n",
min_time,
max_time,
total_time / count,
sd);
}
}
#endif
}
void barrier(std::vector<int> group, bool async_op)
{
CCLCHECK(ccl::barrier(_get_comm_from_group(group)).wait());
}
std::vector<std::string> get_available_coll()
{
std::vector<std::string> colls{
"broadcast", "all_reduce", "inference_all_reduce", "all_reduce_caching", "barrier"};
return colls;
}
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m)
{
m.def("get_kvs_addr", &get_kvs_addr, "create and get main kvs addr");
m.def("initialize", &initialize, "ccl initialize");
m.def("get_rank", &get_rank, "get rank");
m.def("get_world_size", &get_world_size, "get world size");
m.def("broadcast", &broadcast, "ccl broadcast");
m.def("all_reduce", &all_reduce, "ccl all_reduce");
m.def("inference_all_reduce", &inference_all_reduce, "low latency all_reduce implementation");
m.def("all_reduce_caching", &all_reduce_caching, "ccl all_reduce with caching");
m.def("barrier", &barrier, "barrier");
m.def("initialize_sub_comm", &initialize_sub_comm, "initialize_sub_comm");
m.def("get_sub_kvs_addr", &get_sub_kvs_addr, "get_sub_kvs_addr");
m.def("get_available_coll", &get_available_coll, "get_available_coll");
}