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

724 lines
26 KiB
C++

// Copyright (c) Microsoft Corporation.
// SPDX-License-Identifier: Apache-2.0
// DeepSpeed Team
#include <torch/extension.h>
#include <algorithm>
#include <cassert>
#include <condition_variable>
#include <cstdint>
#include <cstring>
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <mutex>
#include <thread>
#include <type_traits>
#include <unordered_map>
#include <vector>
#include "cpu_adam.h"
#if defined(__linux__)
#include <pthread.h>
#include <sched.h>
#include <semaphore.h>
#include <cerrno>
#include <ctime>
#endif
using namespace std::string_literals;
static std::unordered_map<int, std::shared_ptr<void>> s_optimizers;
// C++ interface
template <typename ds_params_precision_t, typename ds_state_precision_t>
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 <typename ds_params_precision_t, typename ds_state_precision_t>
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<Adam_Optimizer>(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 <typename ds_params_precision_t, typename ds_state_precision_t>
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 <typename ds_params_precision_t, typename ds_state_precision_t>
void step_invoker(std::shared_ptr<Adam_Optimizer> 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<c10::ScalarType, c10::ScalarType>,
std::function<void(std::shared_ptr<Adam_Optimizer>, void*, void*, void*, void*, size_t, bool)>>
invokers;
// Fill map with template functions for each type
template <class ds_params_precision_t, class ds_state_precision_t>
void create_invoker()
{
invokers[std::tuple(c10::CppTypeToScalarType<ds_params_precision_t>(),
c10::CppTypeToScalarType<ds_state_precision_t>())] =
step_invoker<ds_params_precision_t, ds_state_precision_t>;
}
struct InvokerInitializer {
InvokerInitializer()
{
create_invoker<c10::Half, float>();
create_invoker<c10::Half, c10::Half>();
create_invoker<c10::BFloat16, float>();
create_invoker<c10::BFloat16, c10::BFloat16>();
create_invoker<float, float>();
}
} _invoker_initializer;
void invoke(std::shared_ptr<Adam_Optimizer> 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<Adam_Optimizer> opt =
std::static_pointer_cast<Adam_Optimizer>(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<float>();
const float* grads_ptr = grads.data_ptr<float>();
float* momentum_ptr = exp_avg.data_ptr<float>();
float* variance_ptr = exp_avg_sq.data_ptr<float>();
const size_t param_size = params.numel();
int step_count = static_cast<int>(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<int>& affinity)
{
n_ = std::max<size_t>(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<std::mutex> 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<void(size_t, size_t)> fn)
{
{
std::unique_lock<std::mutex> lk(m_);
fn_ = std::move(fn);
total_ = total;
align_ = std::max<size_t>(1, align);
done_count_ = 0;
++gen_;
}
cv_start_.notify_all();
std::unique_lock<std::mutex> 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<void(size_t, size_t)> fn;
size_t total = 0;
size_t align = 1;
{
std::unique_lock<std::mutex> 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<std::mutex> lk(m_);
++done_count_;
if (done_count_ == n_) cv_done_.notify_one();
}
}
}
size_t n_;
std::vector<std::thread> threads_;
std::mutex m_;
std::condition_variable cv_start_, cv_done_;
std::function<void(size_t, size_t)> 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<int> zf_affinity) : opt_id_(optimizer_id)
{
pool_ = std::make_unique<PinnedThreadPool>(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<ZenControl*>(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<ZenHP> 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<ZenHP>& hps)
{
auto opt = std::static_pointer_cast<Adam_Optimizer>(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<char*>(P.data_ptr());
char* gp = static_cast<char*>(G.data_ptr());
char* mp = static_cast<char*>(M.data_ptr());
char* vp = static_cast<char*>(V.data_ptr());
char* sp = grp.stale.defined() ? static_cast<char*>(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<ZenGroup> groups_;
std::unique_ptr<PinnedThreadPool> 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<int, std::unique_ptr<ZenFlowAdam>> s_zenflow_adams;
static int s_next_zenflow_id = 0;
int zenflow_adam_create(int optimizer_id, std::vector<int> zf_affinity)
{
int handle = s_next_zenflow_id++;
s_zenflow_adams[handle] = std::make_unique<ZenFlowAdam>(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<ZenControl*>(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<void*>(control_ptr));
}
void zenflow_adam_submit(uintptr_t control_ptr,
int now_state,
int64_t step,
std::vector<float> lr,
std::vector<float> beta1,
std::vector<float> beta2,
std::vector<float> eps,
std::vector<float> weight_decay,
std::vector<uint8_t> bias_correction)
{
auto* ctrl = reinterpret_cast<ZenControl*>(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<ZenControl*>(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<ZenControl*>(control_ptr);
ctrl->cmd = ZEN_CMD_EXIT;
sem_post(&ctrl->cmd_ready);
}
#endif