// Copyright (c) Microsoft Corporation. // SPDX-License-Identifier: Apache-2.0 // DeepSpeed Team #include #include #include "shm.h" // #define DO_PROFILE #ifdef DO_PROFILE #include #include #endif // Communication settings static int world_rank = -1; static int world_size = -1; static std::set _comm_ids; static std::set _colors; static std::vector _ccl_comms; static ccl::shared_ptr_class sub_kvs; static std::map, 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 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 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 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(main_addr.begin(), main_addr.end()); return ccl_kvs_addr; } else { ccl::kvs::address_type main_addr; auto ccl_kvs_addr = std::vector(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 s) { std::set::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 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(main_addr.begin(), main_addr.end()); return ccl_kvs_addr; } else { ccl::kvs::address_type main_addr; auto ccl_kvs_addr = std::vector(main_addr.begin(), main_addr.end()); return ccl_kvs_addr; } } void initialize_sub_comm(int size, int rank, torch::Tensor& kvs_data, std::vector 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 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 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 group, bool async_op) { ccl::allreduce_attr attr = ccl::default_allreduce_attr; auto match_str = ccl::v1::string(match_id); attr.template set(true); attr.template set(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(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 group, bool async_op) { CCLCHECK(ccl::barrier(_get_comm_from_group(group)).wait()); } std::vector get_available_coll() { std::vector 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"); }