// Copyright (c) Microsoft Corporation. // SPDX-License-Identifier: Apache-2.0 // DeepSpeed Team #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cpu_adam.h" #if defined(__linux__) #include #include #include #include #include #endif using namespace std::string_literals; static std::unordered_map> s_optimizers; // C++ interface template void Adam_Optimizer::Step_1(ds_params_precision_t* _params, ds_params_precision_t* grads, ds_state_precision_t* _exp_avg, ds_state_precision_t* _exp_avg_sq, size_t _param_size, bool parallel) { size_t rounded_size = 0; #if defined(__AVX512__) or defined(__AVX256__) Step_AVX<1>(&rounded_size, _params, grads, _exp_avg, _exp_avg_sq, _param_size, parallel); #endif if (_param_size > rounded_size) { float betta1_minus1 = 1 - _betta1; float betta2_minus1 = 1 - _betta2; float step_size = -1 * _alpha / _bias_correction1; float w_decay = -1 * _alpha * _weight_decay; for (size_t t = rounded_size; t < _param_size; t += TILE) { size_t copy_size = TILE; if ((t + TILE) > _param_size) copy_size = _param_size - t; size_t offset = copy_size + t; #pragma omp parallel for if (parallel) for (size_t k = t; k < offset; k++) { float grad = (float)grads[k]; float param = (float)_params[k]; float momentum = _exp_avg[k]; float variance = _exp_avg_sq[k]; if (_weight_decay > 0 && !_adamw_mode) { grad = param * _weight_decay + grad; } momentum = momentum * _betta1; momentum = grad * betta1_minus1 + momentum; variance = variance * _betta2; grad = grad * grad; variance = grad * betta2_minus1 + variance; grad = sqrt(variance); grad = grad * _bias_correction2 + _eps; grad = momentum / grad; if (_weight_decay > 0 && _adamw_mode) { param += w_decay * param; } param = grad * step_size + param; _params[k] = param; _exp_avg[k] = momentum; _exp_avg_sq[k] = variance; } } } } template void Adam_Optimizer::Step_4(ds_params_precision_t* _params, ds_params_precision_t* grads, ds_state_precision_t* _exp_avg, ds_state_precision_t* _exp_avg_sq, size_t _param_size, bool parallel) { size_t rounded_size = 0; #if defined(__AVX512__) or defined(__AVX256__) Step_AVX<4>(&rounded_size, _params, grads, _exp_avg, _exp_avg_sq, _param_size, parallel); #endif if (_param_size > rounded_size) Step_1((_params + rounded_size), (grads + rounded_size), (_exp_avg + rounded_size), (_exp_avg_sq + rounded_size), (_param_size - rounded_size), parallel); } int create_adam_optimizer(int optimizer_id, float alpha, float betta1, float betta2, float eps, float weight_decay, bool adamw_mode, bool should_log) { auto opt = std::make_shared(alpha, betta1, betta2, eps, weight_decay, adamw_mode); s_optimizers[optimizer_id] = opt; if (should_log) { std::string avx_type = ""; #if defined(__AVX512__) avx_type = "AVX512"; #else #if defined(__AVX256__) avx_type = "AVX2"; #else avx_type = "scalar"; #endif #endif printf("Adam Optimizer #%d is created with %s arithmetic capability.\n", optimizer_id, avx_type.c_str()); printf("Config: alpha=%f, betas=(%f, %f), weight_decay=%f, adam_w=%d\n", alpha, betta1, betta2, weight_decay, (int)adamw_mode); } return 0; } template void Adam_Optimizer::Step_8(ds_params_precision_t* _params, ds_params_precision_t* grads, ds_state_precision_t* _exp_avg, ds_state_precision_t* _exp_avg_sq, size_t _param_size, bool parallel) { size_t rounded_size = 0; #if defined(__AVX512__) or defined(__AVX256__) Step_AVX<8>(&rounded_size, _params, grads, _exp_avg, _exp_avg_sq, _param_size, parallel); #endif if (_param_size > rounded_size) Step_4((_params + rounded_size), (grads + rounded_size), (_exp_avg + rounded_size), (_exp_avg_sq + rounded_size), (_param_size - rounded_size), parallel); } template void step_invoker(std::shared_ptr opt, void* _params, void* grads, void* _exp_avg, void* _exp_avg_sq, size_t _param_size, bool parallel) { opt->Step_8((ds_params_precision_t*)(_params), (ds_params_precision_t*)(grads), (ds_state_precision_t*)(_exp_avg), (ds_state_precision_t*)(_exp_avg_sq), _param_size, parallel); } std::map< std::tuple, std::function, void*, void*, void*, void*, size_t, bool)>> invokers; // Fill map with template functions for each type template void create_invoker() { invokers[std::tuple(c10::CppTypeToScalarType(), c10::CppTypeToScalarType())] = step_invoker; } struct InvokerInitializer { InvokerInitializer() { create_invoker(); create_invoker(); create_invoker(); create_invoker(); create_invoker(); } } _invoker_initializer; void invoke(std::shared_ptr opt, torch::Tensor& params, torch::Tensor& grads, torch::Tensor& exp_avg, torch::Tensor& exp_avg_sq, size_t param_size, bool parallel = true) { c10::ScalarType params_type = at::typeMetaToScalarType(params.options().dtype()); c10::ScalarType state_type = at::typeMetaToScalarType(exp_avg.options().dtype()); auto it = invokers.find(std::tuple(params_type, state_type)); if (it == invokers.end()) { throw std::runtime_error("Adam optimizer with param type "s + c10::toString(params_type) + " and state type "s + c10::toString(state_type) + " is not supported on current hardware"s); } it->second(opt, params.data_ptr(), grads.data_ptr(), exp_avg.data_ptr(), exp_avg_sq.data_ptr(), param_size, parallel); } int ds_adam_step(int optimizer_id, size_t step, float lr, float beta1, float beta2, float epsilon, float weight_decay, bool bias_correction, torch::Tensor& params, torch::Tensor& grads, torch::Tensor& exp_avg, torch::Tensor& exp_avg_sq) { auto params_c = params.contiguous(); auto grads_c = grads.contiguous(); auto exp_avg_c = exp_avg.contiguous(); auto exp_avg_sq_c = exp_avg_sq.contiguous(); std::shared_ptr opt = std::static_pointer_cast(s_optimizers[optimizer_id]); opt->IncrementStep(step, beta1, beta2); opt->update_state(lr, epsilon, weight_decay, bias_correction); invoke(opt, params_c, grads_c, exp_avg_c, exp_avg_sq_c, params_c.numel()); return 0; } void adamw_rollback_inplace(float* params, const float* grads, float* momentum, float* variance, size_t param_size, float learning_rate, float beta1, float beta2, float eps, float weight_decay, int& step_count) { const float lr = learning_rate; const float lambda = weight_decay; const float beta1_pow = std::pow(beta1, step_count); const float beta2_pow = std::pow(beta2, step_count); const float one_minus_beta1 = 1.0f - beta1; const float one_minus_beta2 = 1.0f - beta2; const float lr_lambda = lr * lambda; const float one_minus_lr_lambda = 1.0f - lr_lambda; #pragma omp parallel for for (size_t i = 0; i < param_size; ++i) { const float bias_correction1 = 1.0f - beta1_pow; const float bias_correction2 = 1.0f - beta2_pow; const float m_hat = momentum[i] / bias_correction1; const float v_hat = variance[i] / bias_correction2; const float denominator = std::sqrt(v_hat) + eps; // Rollback parameter update const float update = lr * m_hat / denominator; float new_param = (params[i] + update) / one_minus_lr_lambda; // Handle numerical instability if (!std::isfinite(new_param)) { new_param = 0.0f; } params[i] = new_param; const float grad = grads[i]; momentum[i] = (momentum[i] - one_minus_beta1 * grad) / beta1; variance[i] = (variance[i] - one_minus_beta2 * grad * grad) / beta2; } --step_count; } int ds_adam_rollback(int optimizer_id, size_t step, float lr, float beta1, float beta2, float epsilon, float weight_decay, bool bias_correction, torch::Tensor& params, torch::Tensor& grads, torch::Tensor& exp_avg, torch::Tensor& exp_avg_sq) { try { // Validate tensor types - rollback currently only supports float32 if (params.scalar_type() != torch::kFloat32 || grads.scalar_type() != torch::kFloat32 || exp_avg.scalar_type() != torch::kFloat32 || exp_avg_sq.scalar_type() != torch::kFloat32) { printf("Error: Adam rollback currently only supports float32 tensors\n"); return -1; } float* params_ptr = params.data_ptr(); const float* grads_ptr = grads.data_ptr(); float* momentum_ptr = exp_avg.data_ptr(); float* variance_ptr = exp_avg_sq.data_ptr(); const size_t param_size = params.numel(); int step_count = static_cast(step); adamw_rollback_inplace(params_ptr, grads_ptr, momentum_ptr, variance_ptr, param_size, lr, beta1, beta2, epsilon, weight_decay, step_count); return 0; } catch (const std::exception& e) { printf("Error in Adam rollback for optimizer #%d: %s\n", optimizer_id, e.what()); return -1; } } int destroy_adam_optimizer(int optimizer_id) { s_optimizers.erase(optimizer_id); return 0; } // --------------------------------------------------------------------------- // ZenFlowAdam: the native CPU Adam that backs ZenFlow's overlapped optimizer step. // // The optimizer runs in a dedicated process (see zenflow_utils.start_optimizer_process): // run_worker() blocks on a shared-memory control block and, for each requested step, fans // the heavy per-element math out to a pool of worker threads pinned to ZenFlow's dedicated // cores, each running its element slice through the serial (parallel=false) kernel. The // Adam state lives in that process, NUMA-local to the pool. // --------------------------------------------------------------------------- // A persistent pool of threads pinned to a fixed core set. parallel_for() splits // [0, total) into one contiguous chunk per thread and blocks until all finish. class PinnedThreadPool { public: explicit PinnedThreadPool(const std::vector& affinity) { n_ = std::max(1, affinity.size()); for (size_t i = 0; i < n_; ++i) { int core = affinity.empty() ? -1 : affinity[i]; threads_.emplace_back([this, i, core] { worker(i, core); }); } } ~PinnedThreadPool() { { std::lock_guard lk(m_); stop_ = true; ++gen_; } cv_start_.notify_all(); for (auto& t : threads_) t.join(); } size_t size() const { return n_; } // Split [0, total) into one chunk per thread. Chunk boundaries are rounded up to a // multiple of `align` so each slice's AVX/scalar split lines up with the whole-tensor // kernel's split -- otherwise an element could be computed by AVX (FMA) in one layout // and the scalar tail (mul+add) in another, which differ in the last bit. void parallel_for(size_t total, size_t align, std::function fn) { { std::unique_lock lk(m_); fn_ = std::move(fn); total_ = total; align_ = std::max(1, align); done_count_ = 0; ++gen_; } cv_start_.notify_all(); std::unique_lock lk(m_); cv_done_.wait(lk, [this] { return done_count_ == n_; }); } private: void worker(size_t tid, int core) { #if defined(__linux__) if (core >= 0) { cpu_set_t set; CPU_ZERO(&set); CPU_SET(core, &set); pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &set); } #endif long seen = 0; while (true) { std::function fn; size_t total = 0; size_t align = 1; { std::unique_lock lk(m_); cv_start_.wait(lk, [this, seen] { return gen_ != seen; }); seen = gen_; if (stop_) return; fn = fn_; total = total_; align = align_; } size_t chunk = (total + n_ - 1) / n_; chunk = ((chunk + align - 1) / align) * align; // round up to SIMD-block alignment size_t begin = std::min(tid * chunk, total); size_t end = std::min(begin + chunk, total); if (end > begin) fn(begin, end); { std::lock_guard lk(m_); ++done_count_; if (done_count_ == n_) cv_done_.notify_one(); } } } size_t n_; std::vector threads_; std::mutex m_; std::condition_variable cv_start_, cv_done_; std::function fn_; size_t total_ = 0; size_t align_ = 1; size_t done_count_ = 0; long gen_ = 0; bool stop_ = false; }; // SIMD block the Adam AVX kernel rounds to (Step_8 => span 8). Slicing on multiples of // this keeps each slice's AVX/scalar boundary identical to the whole-tensor kernel. #if defined(__AVX512__) or defined(__AVX256__) static constexpr size_t kZenAdamAlign = SIMD_WIDTH * 8; #else static constexpr size_t kZenAdamAlign = 1; #endif struct ZenHP { float lr, beta1, beta2, eps, weight_decay; bool bias_correction; }; struct ZenGroup { torch::Tensor param; torch::Tensor grad[2]; torch::Tensor exp_avg[2]; torch::Tensor exp_avg_sq[2]; torch::Tensor stale; // may be undefined -> stale snapshot skipped }; #if defined(__linux__) // Control block placed in a shared-memory buffer (a shared torch tensor's storage) so the // main process and the optimizer process coordinate through two process-shared semaphores // instead of a pickling pipe. The main process writes a command + per-group hyperparameters // and posts cmd_ready; the worker runs the step and posts done. `done` is a counting // semaphore, so a skipped wait (the engine's post-warmup transition) is drained later. static constexpr int ZEN_MAX_GROUPS = 1024; // Hyperparameters packed per group in hp[]: lr, beta1, beta2, eps, weight_decay. static constexpr int ZEN_HP_PER_GROUP = 5; enum { ZEN_CMD_STEP = 0, ZEN_CMD_EXIT = 1 }; struct ZenControl { sem_t cmd_ready; sem_t done; int cmd; int now_state; int64_t step; int num_groups; float hp[ZEN_MAX_GROUPS * ZEN_HP_PER_GROUP]; uint8_t bias_correction[ZEN_MAX_GROUPS]; }; #endif class ZenFlowAdam { public: ZenFlowAdam(int optimizer_id, std::vector zf_affinity) : opt_id_(optimizer_id) { pool_ = std::make_unique(zf_affinity); } ~ZenFlowAdam() = default; void register_group(torch::Tensor param, torch::Tensor grad0, torch::Tensor grad1, torch::Tensor exp_avg0, torch::Tensor exp_avg1, torch::Tensor exp_avg_sq0, torch::Tensor exp_avg_sq1, torch::Tensor stale) { TORCH_CHECK(param.is_contiguous(), "ZenFlowAdam: param must be contiguous"); ZenGroup g; g.param = param; g.grad[0] = grad0; g.grad[1] = grad1; g.exp_avg[0] = exp_avg0; g.exp_avg[1] = exp_avg1; g.exp_avg_sq[0] = exp_avg_sq0; g.exp_avg_sq[1] = exp_avg_sq1; g.stale = stale; groups_.push_back(std::move(g)); } #if defined(__linux__) // Process-mode driver: run in the optimizer process, block on the shared-memory control // block, and run each requested step on the pinned pool. Returns on the exit command. void run_worker(void* control_ptr) { ZenControl* ctrl = reinterpret_cast(control_ptr); while (true) { while (sem_wait(&ctrl->cmd_ready) != 0) {} // retry on EINTR if (ctrl->cmd == ZEN_CMD_EXIT) break; const int ng = ctrl->num_groups; std::vector hps(ng); for (int g = 0; g < ng; ++g) { hps[g] = {ctrl->hp[g * ZEN_HP_PER_GROUP + 0], ctrl->hp[g * ZEN_HP_PER_GROUP + 1], ctrl->hp[g * ZEN_HP_PER_GROUP + 2], ctrl->hp[g * ZEN_HP_PER_GROUP + 3], ctrl->hp[g * ZEN_HP_PER_GROUP + 4], (bool)ctrl->bias_correction[g]}; } run_step(ctrl->now_state, ctrl->step, hps); sem_post(&ctrl->done); } } #endif private: void run_step(int now_state, int64_t step, const std::vector& hps) { auto opt = std::static_pointer_cast(s_optimizers[opt_id_]); for (size_t g = 0; g < groups_.size(); ++g) { const ZenHP& hp = hps[g]; // Groups share one Adam_Optimizer; advance its bias-correction state for // this group before the pool reads it (pool is idle here -> no race). opt->IncrementStep(step, hp.beta1, hp.beta2); opt->update_state(hp.lr, hp.eps, hp.weight_decay, hp.bias_correction); ZenGroup& grp = groups_[g]; torch::Tensor& P = grp.param; torch::Tensor& G = grp.grad[now_state]; torch::Tensor& M = grp.exp_avg[now_state]; torch::Tensor& V = grp.exp_avg_sq[now_state]; auto it = invokers.find(std::tuple(P.scalar_type(), M.scalar_type())); TORCH_CHECK(it != invokers.end(), "ZenFlowAdam: unsupported param/state dtype combination"); auto fn = it->second; char* pp = static_cast(P.data_ptr()); char* gp = static_cast(G.data_ptr()); char* mp = static_cast(M.data_ptr()); char* vp = static_cast(V.data_ptr()); char* sp = grp.stale.defined() ? static_cast(grp.stale.data_ptr()) : nullptr; const size_t pe = P.element_size(); const size_t se = M.element_size(); const size_t numel = P.numel(); pool_->parallel_for(numel, kZenAdamAlign, [=](size_t b, size_t e) { const size_t len = e - b; // parallel=false: each pinned thread runs its slice serially. fn(opt, pp + b * pe, gp + b * pe, mp + b * se, vp + b * se, len, false); if (sp) std::memcpy(sp + b * pe, pp + b * pe, len * pe); }); } } int opt_id_; std::vector groups_; std::unique_ptr pool_; }; // Handle-indexed registry, mirroring s_optimizers, so the Python side refers to a // ZenFlowAdam by an int handle and the class itself stays encapsulated here. static std::unordered_map> s_zenflow_adams; static int s_next_zenflow_id = 0; int zenflow_adam_create(int optimizer_id, std::vector zf_affinity) { int handle = s_next_zenflow_id++; s_zenflow_adams[handle] = std::make_unique(optimizer_id, std::move(zf_affinity)); return handle; } void zenflow_adam_register_group(int handle, torch::Tensor param, torch::Tensor grad0, torch::Tensor grad1, torch::Tensor exp_avg0, torch::Tensor exp_avg1, torch::Tensor exp_avg_sq0, torch::Tensor exp_avg_sq1, torch::Tensor stale) { s_zenflow_adams.at(handle)->register_group( param, grad0, grad1, exp_avg0, exp_avg1, exp_avg_sq0, exp_avg_sq1, stale); } void zenflow_adam_destroy(int handle) { // Erasing the unique_ptr runs ~ZenFlowAdam, which tears down the pinned pool. s_zenflow_adams.erase(handle); } #if defined(__linux__) // Size (bytes) the shared control tensor must hold. int64_t zenflow_adam_ctrl_size() { return (int64_t)sizeof(ZenControl); } // Called once by the main process before spawning the optimizer process. void zenflow_adam_ctrl_init(uintptr_t control_ptr, int num_groups) { TORCH_CHECK(num_groups <= ZEN_MAX_GROUPS, "ZenFlowAdam: too many param groups"); auto* ctrl = reinterpret_cast(control_ptr); ctrl->num_groups = num_groups; ctrl->cmd = ZEN_CMD_STEP; sem_init(&ctrl->cmd_ready, /*pshared=*/1, 0); sem_init(&ctrl->done, /*pshared=*/1, 0); } // Called in the optimizer process; blocks running steps until the exit command. void zenflow_adam_run_worker(int handle, uintptr_t control_ptr) { s_zenflow_adams.at(handle)->run_worker(reinterpret_cast(control_ptr)); } void zenflow_adam_submit(uintptr_t control_ptr, int now_state, int64_t step, std::vector lr, std::vector beta1, std::vector beta2, std::vector eps, std::vector weight_decay, std::vector bias_correction) { auto* ctrl = reinterpret_cast(control_ptr); const int ng = (int)lr.size(); for (int g = 0; g < ng; ++g) { ctrl->hp[g * ZEN_HP_PER_GROUP + 0] = lr[g]; ctrl->hp[g * ZEN_HP_PER_GROUP + 1] = beta1[g]; ctrl->hp[g * ZEN_HP_PER_GROUP + 2] = beta2[g]; ctrl->hp[g * ZEN_HP_PER_GROUP + 3] = eps[g]; ctrl->hp[g * ZEN_HP_PER_GROUP + 4] = weight_decay[g]; ctrl->bias_correction[g] = bias_correction[g]; } ctrl->now_state = now_state; ctrl->step = step; ctrl->cmd = ZEN_CMD_STEP; sem_post(&ctrl->cmd_ready); // release: hyperparameters above are visible to the worker } // Wait up to timeout_s for the optimizer process to post one completion. Returns true if a // completion was consumed, false on timeout -- so the training side can re-check that the // optimizer process is still alive and fail loudly instead of blocking forever if the process // died mid-step (e.g. an OOM or TORCH_CHECK in run_step after it signalled ready). bool zenflow_adam_wait(uintptr_t control_ptr, double timeout_s) { auto* ctrl = reinterpret_cast(control_ptr); struct timespec deadline; clock_gettime(CLOCK_REALTIME, &deadline); deadline.tv_sec += (time_t)timeout_s; deadline.tv_nsec += (long)((timeout_s - (double)(time_t)timeout_s) * 1e9); if (deadline.tv_nsec >= 1000000000L) { deadline.tv_sec += 1; deadline.tv_nsec -= 1000000000L; } while (sem_timedwait(&ctrl->done, &deadline) != 0) { if (errno == EINTR) continue; // retry on signal return false; // timed out (or error): caller re-checks process liveness } return true; } void zenflow_adam_ctrl_exit(uintptr_t control_ptr) { auto* ctrl = reinterpret_cast(control_ptr); ctrl->cmd = ZEN_CMD_EXIT; sem_post(&ctrl->cmd_ready); } #endif