949 lines
29 KiB
C
949 lines
29 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
#include "coordinator.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdarg.h>
|
|
#include <time.h>
|
|
#include <pthread.h>
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
|
|
/* ============================================================= */
|
|
/* Internal Structures */
|
|
/* ============================================================= */
|
|
|
|
#define MAX_WORKERS 128
|
|
|
|
struct sw_coordinator {
|
|
sw_coordinator_config config;
|
|
|
|
/* Network */
|
|
sw_socket* server;
|
|
sw_socket_set* socket_set;
|
|
|
|
/* Model */
|
|
tm_tensor** weights;
|
|
int num_weights;
|
|
tm_tensor** grad_accum; /* Accumulated gradients */
|
|
int grad_count; /* Number of gradient contributions */
|
|
|
|
/* Optimizer */
|
|
fo_optimizer* optimizer;
|
|
|
|
/* Workers */
|
|
sw_worker_info workers[MAX_WORKERS];
|
|
int num_workers;
|
|
pthread_mutex_t workers_lock;
|
|
|
|
/* State */
|
|
int global_step;
|
|
float current_lr;
|
|
bool running;
|
|
bool training_started;
|
|
uint64_t start_time;
|
|
uint64_t total_samples;
|
|
float avg_loss;
|
|
|
|
/* Checkpointing */
|
|
char last_checkpoint[SW_MAX_PATH];
|
|
|
|
/* Callbacks */
|
|
sw_on_worker_join on_join;
|
|
void* on_join_ctx;
|
|
sw_on_worker_leave on_leave;
|
|
void* on_leave_ctx;
|
|
sw_on_step on_step;
|
|
void* on_step_ctx;
|
|
sw_on_checkpoint on_checkpoint;
|
|
void* on_checkpoint_ctx;
|
|
|
|
/* Logging */
|
|
sw_log_level log_level;
|
|
FILE* log_file;
|
|
|
|
/* Threading */
|
|
pthread_t thread;
|
|
bool thread_started;
|
|
};
|
|
|
|
/* ============================================================= */
|
|
/* Logging */
|
|
/* ============================================================= */
|
|
|
|
static void coord_log(sw_coordinator* c, sw_log_level level, const char* fmt, ...) {
|
|
if (!c || level < c->log_level) return;
|
|
|
|
FILE* f = c->log_file ? c->log_file : stdout;
|
|
|
|
time_t now = time(NULL);
|
|
struct tm* tm = localtime(&now);
|
|
char timebuf[32];
|
|
strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", tm);
|
|
|
|
const char* level_str = "???";
|
|
switch (level) {
|
|
case SW_LOG_DEBUG: level_str = "DEBUG"; break;
|
|
case SW_LOG_INFO: level_str = "INFO "; break;
|
|
case SW_LOG_WARN: level_str = "WARN "; break;
|
|
case SW_LOG_ERROR: level_str = "ERROR"; break;
|
|
}
|
|
|
|
fprintf(f, "[%s] [%s] ", timebuf, level_str);
|
|
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
vfprintf(f, fmt, args);
|
|
va_end(args);
|
|
|
|
fprintf(f, "\n");
|
|
fflush(f);
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Configuration */
|
|
/* ============================================================= */
|
|
|
|
sw_coordinator_config sw_coordinator_default_config(void) {
|
|
sw_coordinator_config cfg = {0};
|
|
|
|
cfg.port = SW_DEFAULT_PORT;
|
|
|
|
cfg.learning_rate = 1e-4f;
|
|
cfg.lr_min = 1e-6f;
|
|
cfg.lr_warmup_steps = 100;
|
|
cfg.total_steps = 10000;
|
|
|
|
cfg.beta1 = 0.9f;
|
|
cfg.beta2 = 0.999f;
|
|
cfg.weight_decay = 0.0f;
|
|
|
|
cfg.min_workers = 1;
|
|
cfg.sync_every_n_grads = 1;
|
|
|
|
strcpy(cfg.checkpoint_dir, "./checkpoints");
|
|
cfg.checkpoint_every_steps = 1000;
|
|
|
|
cfg.heartbeat_timeout_sec = 60;
|
|
cfg.allow_rejoin = true;
|
|
|
|
cfg.compression = SW_COMPRESS_TOPK;
|
|
cfg.topk_ratio = 0.1f;
|
|
|
|
return cfg;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Gradient Accumulation */
|
|
/* ============================================================= */
|
|
|
|
static void init_grad_accum(sw_coordinator* c) {
|
|
c->grad_accum = calloc(c->num_weights, sizeof(tm_tensor*));
|
|
for (int i = 0; i < c->num_weights; i++) {
|
|
c->grad_accum[i] = tm_zeros(c->weights[i]->shape, c->weights[i]->ndim, TM_F32);
|
|
}
|
|
c->grad_count = 0;
|
|
}
|
|
|
|
static void free_grad_accum(sw_coordinator* c) {
|
|
if (!c->grad_accum) return;
|
|
for (int i = 0; i < c->num_weights; i++) {
|
|
tm_release(c->grad_accum[i]);
|
|
}
|
|
free(c->grad_accum);
|
|
c->grad_accum = NULL;
|
|
}
|
|
|
|
static void zero_grad_accum(sw_coordinator* c) {
|
|
for (int i = 0; i < c->num_weights; i++) {
|
|
float* data = tm_data_f32(c->grad_accum[i]);
|
|
memset(data, 0, c->grad_accum[i]->numel * sizeof(float));
|
|
}
|
|
c->grad_count = 0;
|
|
}
|
|
|
|
static void accumulate_grads(sw_coordinator* c, tm_tensor** grads) {
|
|
for (int i = 0; i < c->num_weights; i++) {
|
|
float* acc = tm_data_f32(c->grad_accum[i]);
|
|
float* g = tm_data_f32(grads[i]);
|
|
for (int64_t j = 0; j < c->grad_accum[i]->numel; j++) {
|
|
acc[j] += g[j];
|
|
}
|
|
}
|
|
c->grad_count++;
|
|
}
|
|
|
|
static void average_grads(sw_coordinator* c) {
|
|
if (c->grad_count == 0) return;
|
|
float scale = 1.0f / c->grad_count;
|
|
for (int i = 0; i < c->num_weights; i++) {
|
|
float* acc = tm_data_f32(c->grad_accum[i]);
|
|
for (int64_t j = 0; j < c->grad_accum[i]->numel; j++) {
|
|
acc[j] *= scale;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void apply_grads_to_weights(sw_coordinator* c) {
|
|
/* Copy accumulated grads to weight grad tensors */
|
|
for (int i = 0; i < c->num_weights; i++) {
|
|
if (!c->weights[i]->grad) {
|
|
c->weights[i]->grad = tm_zeros(c->weights[i]->shape, c->weights[i]->ndim, TM_F32);
|
|
}
|
|
float* wg = tm_data_f32(c->weights[i]->grad);
|
|
float* ag = tm_data_f32(c->grad_accum[i]);
|
|
memcpy(wg, ag, c->grad_accum[i]->numel * sizeof(float));
|
|
}
|
|
|
|
/* Apply optimizer step */
|
|
fo_step(c->optimizer);
|
|
c->global_step++;
|
|
c->current_lr = fo_get_lr(c->optimizer);
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Worker Management */
|
|
/* ============================================================= */
|
|
|
|
static sw_worker_info* find_worker_by_socket(sw_coordinator* c, sw_socket* sock) {
|
|
for (int i = 0; i < c->num_workers; i++) {
|
|
if (c->workers[i].socket == sock) {
|
|
return &c->workers[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static sw_worker_info* find_worker_by_id(sw_coordinator* c, const char* id) {
|
|
for (int i = 0; i < c->num_workers; i++) {
|
|
if (strcmp(c->workers[i].id, id) == 0) {
|
|
return &c->workers[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static int count_active_workers(sw_coordinator* c) {
|
|
int count = 0;
|
|
for (int i = 0; i < c->num_workers; i++) {
|
|
if (c->workers[i].state == SW_WORKER_ACTIVE) {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static void remove_worker(sw_coordinator* c, sw_worker_info* worker) {
|
|
if (!worker) return;
|
|
|
|
coord_log(c, SW_LOG_INFO, "Worker %s disconnected", worker->id);
|
|
|
|
if (c->on_leave) {
|
|
c->on_leave(c, worker, c->on_leave_ctx);
|
|
}
|
|
|
|
/* Close socket */
|
|
if (worker->socket) {
|
|
sw_socket_set_remove(c->socket_set, worker->socket);
|
|
sw_socket_close(worker->socket);
|
|
}
|
|
|
|
/* Remove from array */
|
|
int idx = worker - c->workers;
|
|
memmove(&c->workers[idx], &c->workers[idx + 1],
|
|
(c->num_workers - idx - 1) * sizeof(sw_worker_info));
|
|
c->num_workers--;
|
|
}
|
|
|
|
static void check_worker_heartbeats(sw_coordinator* c) {
|
|
uint64_t now = (uint64_t)time(NULL);
|
|
uint64_t timeout = c->config.heartbeat_timeout_sec;
|
|
|
|
for (int i = c->num_workers - 1; i >= 0; i--) {
|
|
sw_worker_info* w = &c->workers[i];
|
|
if (w->state == SW_WORKER_ACTIVE || w->state == SW_WORKER_PENDING) {
|
|
if (now - w->last_heartbeat > timeout) {
|
|
coord_log(c, SW_LOG_WARN, "Worker %s timed out (no heartbeat for %lus)",
|
|
w->id, now - w->last_heartbeat);
|
|
w->state = SW_WORKER_DEAD;
|
|
remove_worker(c, w);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Message Handling */
|
|
/* ============================================================= */
|
|
|
|
static void handle_register(sw_coordinator* c, sw_socket* sock, uint8_t* msg) {
|
|
sw_register_payload* payload = sw_msg_get_payload(msg);
|
|
|
|
coord_log(c, SW_LOG_INFO, "Worker %s registering (profile: %s, cores: %d, mem: %lu MB)",
|
|
payload->worker_id, sw_profile_name(payload->profile),
|
|
payload->num_cores, (unsigned long)payload->memory_mb);
|
|
|
|
/* Check if already registered */
|
|
sw_worker_info* existing = find_worker_by_id(c, payload->worker_id);
|
|
if (existing) {
|
|
if (c->config.allow_rejoin) {
|
|
coord_log(c, SW_LOG_INFO, "Worker %s rejoining", payload->worker_id);
|
|
remove_worker(c, existing);
|
|
} else {
|
|
coord_log(c, SW_LOG_WARN, "Worker %s already registered, rejecting", payload->worker_id);
|
|
/* Send error */
|
|
uint8_t* err = sw_msg_alloc(SW_MSG_ERROR, sizeof(sw_error_payload));
|
|
sw_error_payload* ep = sw_msg_get_payload(err);
|
|
ep->error_code = 1;
|
|
strcpy(ep->message, "Already registered");
|
|
sw_send_msg(sock, err);
|
|
sw_msg_free(err);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Check capacity */
|
|
if (c->num_workers >= MAX_WORKERS) {
|
|
coord_log(c, SW_LOG_WARN, "Max workers reached, rejecting %s", payload->worker_id);
|
|
uint8_t* err = sw_msg_alloc(SW_MSG_ERROR, sizeof(sw_error_payload));
|
|
sw_error_payload* ep = sw_msg_get_payload(err);
|
|
ep->error_code = 2;
|
|
strcpy(ep->message, "Max workers reached");
|
|
sw_send_msg(sock, err);
|
|
sw_msg_free(err);
|
|
return;
|
|
}
|
|
|
|
/* Add worker */
|
|
pthread_mutex_lock(&c->workers_lock);
|
|
sw_worker_info* w = &c->workers[c->num_workers++];
|
|
memset(w, 0, sizeof(sw_worker_info));
|
|
strncpy(w->id, payload->worker_id, SW_MAX_WORKER_ID - 1);
|
|
w->socket = sock;
|
|
w->state = SW_WORKER_PENDING;
|
|
w->profile = payload->profile;
|
|
w->shard_index = c->num_workers - 1;
|
|
w->batch_size = payload->suggested_batch_size > 0 ?
|
|
payload->suggested_batch_size :
|
|
sw_profile_batch_size(payload->profile);
|
|
w->accumulation_steps = sw_profile_accumulation_steps(payload->profile);
|
|
w->last_heartbeat = (uint64_t)time(NULL);
|
|
w->connected_at = w->last_heartbeat;
|
|
pthread_mutex_unlock(&c->workers_lock);
|
|
|
|
/* Send ack */
|
|
uint8_t* ack = sw_msg_alloc(SW_MSG_REGISTER_ACK, sizeof(sw_register_ack_payload));
|
|
sw_register_ack_payload* ap = sw_msg_get_payload(ack);
|
|
ap->accepted = true;
|
|
ap->assigned_shard = w->shard_index;
|
|
ap->total_shards = c->num_workers;
|
|
ap->batch_size = w->batch_size;
|
|
ap->accumulation_steps = w->accumulation_steps;
|
|
ap->compression = c->config.compression;
|
|
ap->topk_ratio = c->config.topk_ratio;
|
|
ap->global_step = c->global_step;
|
|
|
|
sw_send_msg(sock, ack);
|
|
sw_msg_free(ack);
|
|
|
|
sw_socket_set_add(c->socket_set, sock);
|
|
|
|
if (c->on_join) {
|
|
c->on_join(c, w, c->on_join_ctx);
|
|
}
|
|
|
|
coord_log(c, SW_LOG_INFO, "Worker %s registered (shard %d, batch %d)",
|
|
w->id, w->shard_index, w->batch_size);
|
|
}
|
|
|
|
static void handle_heartbeat(sw_coordinator* c, sw_socket* sock, uint8_t* msg) {
|
|
sw_heartbeat_payload* payload = sw_msg_get_payload(msg);
|
|
sw_worker_info* w = find_worker_by_id(c, payload->worker_id);
|
|
|
|
if (!w) {
|
|
coord_log(c, SW_LOG_WARN, "Heartbeat from unknown worker %s", payload->worker_id);
|
|
return;
|
|
}
|
|
|
|
w->last_heartbeat = (uint64_t)time(NULL);
|
|
w->local_step = payload->local_step;
|
|
w->last_loss = payload->current_loss;
|
|
w->samples_per_sec = payload->samples_per_sec;
|
|
|
|
/* Send ack */
|
|
uint8_t* ack = sw_msg_alloc(SW_MSG_HEARTBEAT_ACK, 0);
|
|
sw_send_msg(sock, ack);
|
|
sw_msg_free(ack);
|
|
}
|
|
|
|
static void handle_gradients(sw_coordinator* c, sw_socket* sock, uint8_t* msg) {
|
|
sw_gradients_payload* header = sw_msg_get_payload(msg);
|
|
sw_worker_info* w = find_worker_by_id(c, header->worker_id);
|
|
|
|
if (!w) {
|
|
coord_log(c, SW_LOG_WARN, "Gradients from unknown worker %s", header->worker_id);
|
|
return;
|
|
}
|
|
|
|
coord_log(c, SW_LOG_DEBUG, "Received gradients from %s (step %d)",
|
|
w->id, header->local_step);
|
|
|
|
/* Deserialize gradients */
|
|
uint8_t* grad_data = (uint8_t*)header + sizeof(sw_gradients_payload);
|
|
tm_tensor** grads = calloc(c->num_weights, sizeof(tm_tensor*));
|
|
|
|
size_t offset = 0;
|
|
for (int i = 0; i < c->num_weights; i++) {
|
|
grads[i] = tm_zeros(c->weights[i]->shape, c->weights[i]->ndim, TM_F32);
|
|
float* data = tm_data_f32(grads[i]);
|
|
offset += sw_deserialize_tensor(grad_data + offset, data,
|
|
grads[i]->numel, header->compression);
|
|
}
|
|
|
|
/* Accumulate */
|
|
pthread_mutex_lock(&c->workers_lock);
|
|
accumulate_grads(c, grads);
|
|
w->grads_contributed++;
|
|
|
|
/* Check if we should update */
|
|
bool should_update = (c->grad_count >= c->config.sync_every_n_grads);
|
|
if (should_update) {
|
|
average_grads(c);
|
|
apply_grads_to_weights(c);
|
|
|
|
/* Update average loss */
|
|
float total_loss = 0;
|
|
int loss_count = 0;
|
|
for (int i = 0; i < c->num_workers; i++) {
|
|
if (c->workers[i].state == SW_WORKER_ACTIVE) {
|
|
total_loss += c->workers[i].last_loss;
|
|
loss_count++;
|
|
}
|
|
}
|
|
if (loss_count > 0) c->avg_loss = total_loss / loss_count;
|
|
|
|
coord_log(c, SW_LOG_INFO, "Step %d | LR: %.2e | Loss: %.4f | Workers: %d",
|
|
c->global_step, c->current_lr, c->avg_loss, count_active_workers(c));
|
|
|
|
if (c->on_step) {
|
|
c->on_step(c, c->global_step, c->avg_loss, c->on_step_ctx);
|
|
}
|
|
|
|
/* Check for checkpoint */
|
|
if (c->config.checkpoint_every_steps > 0 &&
|
|
c->global_step % c->config.checkpoint_every_steps == 0) {
|
|
char path[SW_MAX_PATH];
|
|
snprintf(path, sizeof(path), "%s/step_%d",
|
|
c->config.checkpoint_dir, c->global_step);
|
|
sw_coordinator_save_checkpoint(c, path);
|
|
}
|
|
|
|
/* Check for completion */
|
|
if (c->global_step >= c->config.total_steps) {
|
|
coord_log(c, SW_LOG_INFO, "Training complete!");
|
|
sw_coordinator_finish_training(c);
|
|
}
|
|
|
|
zero_grad_accum(c);
|
|
}
|
|
pthread_mutex_unlock(&c->workers_lock);
|
|
|
|
/* Free temporary gradients */
|
|
for (int i = 0; i < c->num_weights; i++) {
|
|
tm_release(grads[i]);
|
|
}
|
|
free(grads);
|
|
|
|
/* Send ack */
|
|
uint8_t* ack = sw_msg_alloc(SW_MSG_GRADIENTS_ACK, 0);
|
|
sw_send_msg(sock, ack);
|
|
sw_msg_free(ack);
|
|
}
|
|
|
|
static void handle_weights_req(sw_coordinator* c, sw_socket* sock) {
|
|
coord_log(c, SW_LOG_DEBUG, "Sending weights to worker");
|
|
|
|
/* Send metadata first */
|
|
uint8_t* meta = sw_msg_alloc(SW_MSG_WEIGHTS_META,
|
|
sizeof(sw_weights_meta_payload) + c->num_weights * sizeof(sw_tensor_meta));
|
|
sw_weights_meta_payload* mp = sw_msg_get_payload(meta);
|
|
mp->num_tensors = c->num_weights;
|
|
mp->global_step = c->global_step;
|
|
mp->learning_rate = c->current_lr;
|
|
|
|
sw_tensor_meta* tmeta = (sw_tensor_meta*)(mp + 1);
|
|
for (int i = 0; i < c->num_weights; i++) {
|
|
snprintf(tmeta[i].name, sizeof(tmeta[i].name), "weight_%d", i);
|
|
tmeta[i].ndim = c->weights[i]->ndim;
|
|
for (int d = 0; d < c->weights[i]->ndim; d++) {
|
|
tmeta[i].shape[d] = c->weights[i]->shape[d];
|
|
}
|
|
tmeta[i].numel = c->weights[i]->numel;
|
|
tmeta[i].dtype = c->weights[i]->dtype;
|
|
}
|
|
|
|
sw_send_msg(sock, meta);
|
|
sw_msg_free(meta);
|
|
|
|
/* Send weights */
|
|
for (int i = 0; i < c->num_weights; i++) {
|
|
size_t data_size = c->weights[i]->numel * sizeof(float);
|
|
uint8_t* wmsg = sw_msg_alloc(SW_MSG_WEIGHTS, data_size);
|
|
memcpy(sw_msg_get_payload(wmsg), c->weights[i]->data, data_size);
|
|
sw_send_msg(sock, wmsg);
|
|
sw_msg_free(wmsg);
|
|
}
|
|
}
|
|
|
|
static void handle_message(sw_coordinator* c, sw_socket* sock, uint8_t* msg) {
|
|
sw_msg_header* hdr = sw_msg_get_header(msg);
|
|
|
|
switch (hdr->type) {
|
|
case SW_MSG_REGISTER:
|
|
handle_register(c, sock, msg);
|
|
break;
|
|
case SW_MSG_HEARTBEAT:
|
|
handle_heartbeat(c, sock, msg);
|
|
break;
|
|
case SW_MSG_GRADIENTS:
|
|
handle_gradients(c, sock, msg);
|
|
break;
|
|
case SW_MSG_WEIGHTS_REQ:
|
|
handle_weights_req(c, sock);
|
|
break;
|
|
case SW_MSG_DEREGISTER: {
|
|
sw_worker_info* w = find_worker_by_socket(c, sock);
|
|
if (w) remove_worker(c, w);
|
|
break;
|
|
}
|
|
default:
|
|
coord_log(c, SW_LOG_WARN, "Unknown message type: %d", hdr->type);
|
|
}
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Main Loop */
|
|
/* ============================================================= */
|
|
|
|
static void* coordinator_thread(void* arg) {
|
|
sw_coordinator* c = (sw_coordinator*)arg;
|
|
sw_coordinator_run(c);
|
|
return NULL;
|
|
}
|
|
|
|
void sw_coordinator_run(sw_coordinator* c) {
|
|
if (!c) return;
|
|
|
|
c->running = true;
|
|
c->start_time = (uint64_t)time(NULL);
|
|
|
|
coord_log(c, SW_LOG_INFO, "Coordinator starting on port %d", c->config.port);
|
|
|
|
while (c->running) {
|
|
/* Accept new connections */
|
|
sw_socket* new_client = sw_server_accept(c->server, 1);
|
|
if (new_client) {
|
|
coord_log(c, SW_LOG_DEBUG, "New connection from %s",
|
|
sw_socket_get_info(new_client).remote_addr);
|
|
/* Don't add to set yet - wait for registration */
|
|
|
|
/* Read registration message */
|
|
uint8_t* msg = sw_recv_msg(new_client, 10);
|
|
if (msg && sw_msg_validate(msg, sw_msg_total_size(msg))) {
|
|
handle_message(c, new_client, msg);
|
|
sw_msg_free(msg);
|
|
} else {
|
|
coord_log(c, SW_LOG_WARN, "Invalid registration, closing connection");
|
|
sw_socket_close(new_client);
|
|
}
|
|
}
|
|
|
|
/* Check existing workers */
|
|
if (c->num_workers > 0) {
|
|
int ready = sw_socket_set_wait(c->socket_set, 100);
|
|
if (ready > 0) {
|
|
for (int i = 0; i < c->num_workers; i++) {
|
|
sw_socket* sock = c->workers[i].socket;
|
|
if (sw_socket_set_is_readable(c->socket_set, sock)) {
|
|
uint8_t* msg = sw_recv_msg(sock, 1);
|
|
if (msg) {
|
|
if (sw_msg_validate(msg, sw_msg_total_size(msg))) {
|
|
handle_message(c, sock, msg);
|
|
}
|
|
sw_msg_free(msg);
|
|
} else if (!sw_socket_is_connected(sock)) {
|
|
/* Worker disconnected */
|
|
remove_worker(c, &c->workers[i]);
|
|
i--; /* Adjust index after removal */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Periodic tasks */
|
|
check_worker_heartbeats(c);
|
|
}
|
|
|
|
coord_log(c, SW_LOG_INFO, "Coordinator stopped");
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Public API */
|
|
/* ============================================================= */
|
|
|
|
sw_coordinator* sw_coordinator_new(
|
|
tm_tensor** weights,
|
|
int num_weights,
|
|
sw_coordinator_config* config
|
|
) {
|
|
if (!weights || num_weights <= 0) return NULL;
|
|
|
|
sw_coordinator* c = calloc(1, sizeof(sw_coordinator));
|
|
if (!c) return NULL;
|
|
|
|
c->config = config ? *config : sw_coordinator_default_config();
|
|
c->weights = weights;
|
|
c->num_weights = num_weights;
|
|
c->log_level = SW_LOG_INFO;
|
|
c->current_lr = c->config.learning_rate;
|
|
|
|
pthread_mutex_init(&c->workers_lock, NULL);
|
|
|
|
/* Create server socket */
|
|
c->server = sw_server_create(c->config.port);
|
|
if (!c->server) {
|
|
coord_log(c, SW_LOG_ERROR, "Failed to create server: %s", sw_socket_error());
|
|
free(c);
|
|
return NULL;
|
|
}
|
|
|
|
c->socket_set = sw_socket_set_new();
|
|
|
|
/* Initialize gradient accumulator */
|
|
init_grad_accum(c);
|
|
|
|
/* Create optimizer */
|
|
fo_adam_config adam_cfg = fo_adam_default(c->config.learning_rate);
|
|
adam_cfg.beta1 = c->config.beta1;
|
|
adam_cfg.beta2 = c->config.beta2;
|
|
adam_cfg.weight_decay = c->config.weight_decay;
|
|
|
|
/* Enable gradients on weights */
|
|
for (int i = 0; i < num_weights; i++) {
|
|
tm_set_requires_grad(weights[i], true);
|
|
}
|
|
|
|
c->optimizer = fo_adamw_new(weights, num_weights, &adam_cfg);
|
|
|
|
/* Set up learning rate schedule */
|
|
if (c->config.lr_warmup_steps > 0) {
|
|
fo_scheduler_config sched = fo_scheduler_default();
|
|
sched.type = FO_SCHED_WARMUP_COSINE;
|
|
sched.warmup_steps = c->config.lr_warmup_steps;
|
|
sched.total_steps = c->config.total_steps;
|
|
sched.min_lr = c->config.lr_min;
|
|
fo_set_scheduler(c->optimizer, &sched);
|
|
}
|
|
|
|
/* Create checkpoint directory */
|
|
mkdir(c->config.checkpoint_dir, 0755);
|
|
|
|
return c;
|
|
}
|
|
|
|
void sw_coordinator_free(sw_coordinator* c) {
|
|
if (!c) return;
|
|
|
|
sw_coordinator_stop(c);
|
|
|
|
/* Close all worker connections */
|
|
for (int i = 0; i < c->num_workers; i++) {
|
|
if (c->workers[i].socket) {
|
|
sw_socket_close(c->workers[i].socket);
|
|
}
|
|
}
|
|
|
|
if (c->socket_set) sw_socket_set_free(c->socket_set);
|
|
if (c->server) sw_socket_close(c->server);
|
|
if (c->optimizer) fo_free(c->optimizer);
|
|
|
|
free_grad_accum(c);
|
|
pthread_mutex_destroy(&c->workers_lock);
|
|
|
|
if (c->log_file && c->log_file != stdout) {
|
|
fclose(c->log_file);
|
|
}
|
|
|
|
free(c);
|
|
}
|
|
|
|
bool sw_coordinator_start(sw_coordinator* c) {
|
|
if (!c || c->thread_started) return false;
|
|
|
|
if (pthread_create(&c->thread, NULL, coordinator_thread, c) != 0) {
|
|
return false;
|
|
}
|
|
|
|
c->thread_started = true;
|
|
return true;
|
|
}
|
|
|
|
void sw_coordinator_stop(sw_coordinator* c) {
|
|
if (!c) return;
|
|
c->running = false;
|
|
|
|
if (c->thread_started) {
|
|
pthread_join(c->thread, NULL);
|
|
c->thread_started = false;
|
|
}
|
|
}
|
|
|
|
bool sw_coordinator_is_running(sw_coordinator* c) {
|
|
return c && c->running;
|
|
}
|
|
|
|
bool sw_coordinator_wait_for_workers(sw_coordinator* c, int timeout_sec) {
|
|
if (!c) return false;
|
|
|
|
time_t start = time(NULL);
|
|
while (c->num_workers < c->config.min_workers) {
|
|
if (time(NULL) - start > timeout_sec) {
|
|
return false;
|
|
}
|
|
usleep(100000); /* 100ms */
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void sw_coordinator_start_training(sw_coordinator* c) {
|
|
if (!c) return;
|
|
|
|
c->training_started = true;
|
|
|
|
/* Notify all workers */
|
|
uint8_t* msg = sw_msg_alloc(SW_MSG_RESUME, 0);
|
|
for (int i = 0; i < c->num_workers; i++) {
|
|
if (c->workers[i].state == SW_WORKER_PENDING) {
|
|
c->workers[i].state = SW_WORKER_ACTIVE;
|
|
sw_send_msg(c->workers[i].socket, msg);
|
|
}
|
|
}
|
|
sw_msg_free(msg);
|
|
|
|
coord_log(c, SW_LOG_INFO, "Training started with %d workers", c->num_workers);
|
|
}
|
|
|
|
void sw_coordinator_pause_training(sw_coordinator* c) {
|
|
if (!c) return;
|
|
|
|
uint8_t* msg = sw_msg_alloc(SW_MSG_PAUSE, 0);
|
|
for (int i = 0; i < c->num_workers; i++) {
|
|
if (c->workers[i].state == SW_WORKER_ACTIVE) {
|
|
c->workers[i].state = SW_WORKER_PAUSED;
|
|
sw_send_msg(c->workers[i].socket, msg);
|
|
}
|
|
}
|
|
sw_msg_free(msg);
|
|
|
|
coord_log(c, SW_LOG_INFO, "Training paused");
|
|
}
|
|
|
|
void sw_coordinator_resume_training(sw_coordinator* c) {
|
|
if (!c) return;
|
|
|
|
uint8_t* msg = sw_msg_alloc(SW_MSG_RESUME, 0);
|
|
for (int i = 0; i < c->num_workers; i++) {
|
|
if (c->workers[i].state == SW_WORKER_PAUSED) {
|
|
c->workers[i].state = SW_WORKER_ACTIVE;
|
|
sw_send_msg(c->workers[i].socket, msg);
|
|
}
|
|
}
|
|
sw_msg_free(msg);
|
|
|
|
coord_log(c, SW_LOG_INFO, "Training resumed");
|
|
}
|
|
|
|
void sw_coordinator_finish_training(sw_coordinator* c) {
|
|
if (!c) return;
|
|
|
|
uint8_t* msg = sw_msg_alloc(SW_MSG_SHUTDOWN, 0);
|
|
for (int i = 0; i < c->num_workers; i++) {
|
|
sw_send_msg(c->workers[i].socket, msg);
|
|
}
|
|
sw_msg_free(msg);
|
|
|
|
/* Save final checkpoint */
|
|
char path[SW_MAX_PATH];
|
|
snprintf(path, sizeof(path), "%s/final", c->config.checkpoint_dir);
|
|
sw_coordinator_save_checkpoint(c, path);
|
|
|
|
c->running = false;
|
|
}
|
|
|
|
int sw_coordinator_get_step(sw_coordinator* c) {
|
|
return c ? c->global_step : 0;
|
|
}
|
|
|
|
float sw_coordinator_get_lr(sw_coordinator* c) {
|
|
return c ? c->current_lr : 0.0f;
|
|
}
|
|
|
|
int sw_coordinator_num_workers(sw_coordinator* c) {
|
|
return c ? c->num_workers : 0;
|
|
}
|
|
|
|
int sw_coordinator_num_active_workers(sw_coordinator* c) {
|
|
return c ? count_active_workers(c) : 0;
|
|
}
|
|
|
|
sw_worker_info* sw_coordinator_get_worker(sw_coordinator* c, int index) {
|
|
if (!c || index < 0 || index >= c->num_workers) return NULL;
|
|
return &c->workers[index];
|
|
}
|
|
|
|
sw_worker_info* sw_coordinator_get_worker_by_id(sw_coordinator* c, const char* id) {
|
|
return c ? find_worker_by_id(c, id) : NULL;
|
|
}
|
|
|
|
uint64_t sw_coordinator_total_samples(sw_coordinator* c) {
|
|
return c ? c->total_samples : 0;
|
|
}
|
|
|
|
float sw_coordinator_avg_loss(sw_coordinator* c) {
|
|
return c ? c->avg_loss : 0.0f;
|
|
}
|
|
|
|
float sw_coordinator_throughput(sw_coordinator* c) {
|
|
if (!c) return 0.0f;
|
|
float total = 0.0f;
|
|
for (int i = 0; i < c->num_workers; i++) {
|
|
if (c->workers[i].state == SW_WORKER_ACTIVE) {
|
|
total += c->workers[i].samples_per_sec;
|
|
}
|
|
}
|
|
return total;
|
|
}
|
|
|
|
bool sw_coordinator_save_checkpoint(sw_coordinator* c, const char* path) {
|
|
if (!c || !path) return false;
|
|
|
|
mkdir(path, 0755);
|
|
|
|
coord_log(c, SW_LOG_INFO, "Saving checkpoint to %s", path);
|
|
|
|
/* TODO: Use safetensors module to save weights */
|
|
/* For now, save as raw binary */
|
|
char weights_path[SW_MAX_PATH];
|
|
snprintf(weights_path, sizeof(weights_path), "%s/weights.bin", path);
|
|
FILE* f = fopen(weights_path, "wb");
|
|
if (!f) return false;
|
|
|
|
fwrite(&c->num_weights, sizeof(int), 1, f);
|
|
for (int i = 0; i < c->num_weights; i++) {
|
|
fwrite(&c->weights[i]->ndim, sizeof(int), 1, f);
|
|
fwrite(c->weights[i]->shape, sizeof(int64_t), c->weights[i]->ndim, f);
|
|
fwrite(&c->weights[i]->numel, sizeof(int64_t), 1, f);
|
|
fwrite(c->weights[i]->data, sizeof(float), c->weights[i]->numel, f);
|
|
}
|
|
fclose(f);
|
|
|
|
/* Save training state */
|
|
char state_path[SW_MAX_PATH];
|
|
snprintf(state_path, sizeof(state_path), "%s/state.json", path);
|
|
f = fopen(state_path, "w");
|
|
if (f) {
|
|
fprintf(f, "{\n");
|
|
fprintf(f, " \"global_step\": %d,\n", c->global_step);
|
|
fprintf(f, " \"learning_rate\": %e,\n", c->current_lr);
|
|
fprintf(f, " \"avg_loss\": %f,\n", c->avg_loss);
|
|
fprintf(f, " \"total_samples\": %llu\n", (unsigned long long)c->total_samples);
|
|
fprintf(f, "}\n");
|
|
fclose(f);
|
|
}
|
|
|
|
strncpy(c->last_checkpoint, path, SW_MAX_PATH - 1);
|
|
|
|
if (c->on_checkpoint) {
|
|
c->on_checkpoint(c, path, c->on_checkpoint_ctx);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool sw_coordinator_load_checkpoint(sw_coordinator* c, const char* path) {
|
|
if (!c || !path) return false;
|
|
|
|
char weights_path[SW_MAX_PATH];
|
|
snprintf(weights_path, sizeof(weights_path), "%s/weights.bin", path);
|
|
FILE* f = fopen(weights_path, "rb");
|
|
if (!f) return false;
|
|
|
|
int num_weights;
|
|
fread(&num_weights, sizeof(int), 1, f);
|
|
if (num_weights != c->num_weights) {
|
|
fclose(f);
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < c->num_weights; i++) {
|
|
int ndim;
|
|
int64_t shape[8], numel;
|
|
fread(&ndim, sizeof(int), 1, f);
|
|
fread(shape, sizeof(int64_t), ndim, f);
|
|
fread(&numel, sizeof(int64_t), 1, f);
|
|
|
|
if (numel != c->weights[i]->numel) {
|
|
fclose(f);
|
|
return false;
|
|
}
|
|
fread(c->weights[i]->data, sizeof(float), numel, f);
|
|
}
|
|
fclose(f);
|
|
|
|
coord_log(c, SW_LOG_INFO, "Loaded checkpoint from %s", path);
|
|
return true;
|
|
}
|
|
|
|
const char* sw_coordinator_last_checkpoint(sw_coordinator* c) {
|
|
return c ? c->last_checkpoint : NULL;
|
|
}
|
|
|
|
void sw_coordinator_on_worker_join(sw_coordinator* c, sw_on_worker_join cb, void* ctx) {
|
|
if (!c) return;
|
|
c->on_join = cb;
|
|
c->on_join_ctx = ctx;
|
|
}
|
|
|
|
void sw_coordinator_on_worker_leave(sw_coordinator* c, sw_on_worker_leave cb, void* ctx) {
|
|
if (!c) return;
|
|
c->on_leave = cb;
|
|
c->on_leave_ctx = ctx;
|
|
}
|
|
|
|
void sw_coordinator_on_step(sw_coordinator* c, sw_on_step cb, void* ctx) {
|
|
if (!c) return;
|
|
c->on_step = cb;
|
|
c->on_step_ctx = ctx;
|
|
}
|
|
|
|
void sw_coordinator_on_checkpoint(sw_coordinator* c, sw_on_checkpoint cb, void* ctx) {
|
|
if (!c) return;
|
|
c->on_checkpoint = cb;
|
|
c->on_checkpoint_ctx = ctx;
|
|
}
|
|
|
|
void sw_coordinator_set_log_level(sw_coordinator* c, sw_log_level level) {
|
|
if (c) c->log_level = level;
|
|
}
|
|
|
|
void sw_coordinator_set_log_file(sw_coordinator* c, const char* path) {
|
|
if (!c) return;
|
|
if (c->log_file && c->log_file != stdout) {
|
|
fclose(c->log_file);
|
|
}
|
|
c->log_file = path ? fopen(path, "a") : NULL;
|
|
}
|