895 lines
26 KiB
C
895 lines
26 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/*
|
|
* Swarm Worker - Training worker client implementation.
|
|
*
|
|
* Connects to coordinator, receives weights, computes gradients,
|
|
* and sends them back for aggregation.
|
|
*/
|
|
|
|
#include "worker.h"
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <stdarg.h>
|
|
#include <time.h>
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#define sleep_ms(ms) Sleep(ms)
|
|
#else
|
|
#include <unistd.h>
|
|
#define sleep_ms(ms) usleep((ms) * 1000)
|
|
#endif
|
|
|
|
/* ============================================================= */
|
|
/* Internal Structures */
|
|
/* ============================================================= */
|
|
|
|
struct sw_worker {
|
|
sw_worker_config config;
|
|
sw_socket* socket;
|
|
sw_wstate state;
|
|
|
|
/* Model */
|
|
tm_tensor** weights;
|
|
int num_weights;
|
|
tm_tensor** gradients; /* Accumulated gradients */
|
|
|
|
/* Training callbacks */
|
|
sw_forward_fn forward_fn;
|
|
void* forward_ctx;
|
|
sw_backward_fn backward_fn;
|
|
void* backward_ctx;
|
|
|
|
/* Data loader callbacks */
|
|
sw_next_batch_fn next_batch_fn;
|
|
sw_reset_data_fn reset_data_fn;
|
|
sw_free_batch_fn free_batch_fn;
|
|
void* data_ctx;
|
|
|
|
/* Event callbacks */
|
|
sw_on_state_change on_state_change;
|
|
void* on_state_change_ctx;
|
|
sw_on_weights_received on_weights_received;
|
|
void* on_weights_received_ctx;
|
|
sw_on_step_complete on_step_complete;
|
|
void* on_step_complete_ctx;
|
|
|
|
/* State */
|
|
int local_step;
|
|
int accumulation_count;
|
|
float current_loss;
|
|
float total_loss;
|
|
int loss_count;
|
|
|
|
/* Assigned by coordinator */
|
|
int assigned_shard;
|
|
int assigned_batch_size;
|
|
int assigned_accumulation_steps;
|
|
|
|
/* Timing */
|
|
uint64_t last_heartbeat_sent;
|
|
uint64_t step_start_time;
|
|
float samples_per_sec;
|
|
|
|
/* Logging */
|
|
sw_log_level log_level;
|
|
FILE* log_file;
|
|
|
|
/* Control */
|
|
bool should_stop;
|
|
bool running;
|
|
};
|
|
|
|
/* ============================================================= */
|
|
/* Logging Helpers */
|
|
/* ============================================================= */
|
|
|
|
static uint64_t get_time_ms(void) {
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
|
}
|
|
|
|
static void worker_log(sw_worker* w, sw_log_level level, const char* fmt, ...) {
|
|
if (level < w->log_level) return;
|
|
|
|
FILE* out = w->log_file ? w->log_file : stdout;
|
|
|
|
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;
|
|
}
|
|
|
|
time_t now = time(NULL);
|
|
struct tm* tm_info = localtime(&now);
|
|
fprintf(out, "[%02d:%02d:%02d] [%s] [%s] ",
|
|
tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec,
|
|
level_str, w->config.worker_id);
|
|
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
vfprintf(out, fmt, args);
|
|
va_end(args);
|
|
|
|
fprintf(out, "\n");
|
|
fflush(out);
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* State Management */
|
|
/* ============================================================= */
|
|
|
|
static void set_state(sw_worker* w, sw_wstate new_state) {
|
|
sw_wstate old_state = w->state;
|
|
if (old_state == new_state) return;
|
|
|
|
w->state = new_state;
|
|
|
|
const char* state_names[] = {
|
|
"DISCONNECTED", "CONNECTING", "REGISTERED",
|
|
"WAITING", "TRAINING", "PAUSED", "FINISHED", "ERROR"
|
|
};
|
|
worker_log(w, SW_LOG_DEBUG, "State: %s -> %s",
|
|
state_names[old_state], state_names[new_state]);
|
|
|
|
if (w->on_state_change) {
|
|
w->on_state_change(w, old_state, new_state, w->on_state_change_ctx);
|
|
}
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Configuration */
|
|
/* ============================================================= */
|
|
|
|
sw_worker_config sw_worker_default_config(void) {
|
|
sw_worker_config config;
|
|
memset(&config, 0, sizeof(config));
|
|
|
|
strcpy(config.coordinator_host, "localhost");
|
|
config.coordinator_port = SW_DEFAULT_PORT;
|
|
config.reconnect_attempts = 10;
|
|
config.reconnect_delay_sec = 5;
|
|
|
|
config.num_threads = 0; /* Auto-detect */
|
|
config.max_memory_mb = 0; /* Auto-detect */
|
|
|
|
config.batch_size = 0; /* Auto from profile */
|
|
config.accumulation_steps = 0;
|
|
|
|
config.shard_index = -1; /* Auto-assign */
|
|
config.compression = SW_COMPRESS_TOPK;
|
|
config.topk_ratio = 0.1f;
|
|
|
|
return config;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Worker Creation */
|
|
/* ============================================================= */
|
|
|
|
static void generate_worker_id(char* id, size_t max_len) {
|
|
/* Generate simple unique ID: hostname-pid-random */
|
|
char hostname[64] = "unknown";
|
|
gethostname(hostname, sizeof(hostname));
|
|
|
|
uint32_t random_val = (uint32_t)time(NULL) ^ (uint32_t)getpid();
|
|
snprintf(id, max_len, "%s-%d-%04x", hostname, getpid(), random_val & 0xFFFF);
|
|
}
|
|
|
|
sw_worker* sw_worker_new(sw_worker_config* config) {
|
|
sw_worker* w = calloc(1, sizeof(sw_worker));
|
|
if (!w) return NULL;
|
|
|
|
w->config = *config;
|
|
w->state = SW_WSTATE_DISCONNECTED;
|
|
w->log_level = SW_LOG_INFO;
|
|
w->assigned_shard = -1;
|
|
|
|
/* Generate worker ID if not provided */
|
|
if (w->config.worker_id[0] == '\0') {
|
|
generate_worker_id(w->config.worker_id, SW_MAX_WORKER_ID);
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
void sw_worker_free(sw_worker* w) {
|
|
if (!w) return;
|
|
|
|
sw_worker_disconnect(w);
|
|
|
|
/* Free accumulated gradients */
|
|
if (w->gradients) {
|
|
for (int i = 0; i < w->num_weights; i++) {
|
|
if (w->gradients[i]) {
|
|
tm_free(w->gradients[i]);
|
|
}
|
|
}
|
|
free(w->gradients);
|
|
}
|
|
|
|
if (w->log_file && w->log_file != stdout) {
|
|
fclose(w->log_file);
|
|
}
|
|
|
|
free(w);
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Connection */
|
|
/* ============================================================= */
|
|
|
|
static bool send_register(sw_worker* w) {
|
|
sw_hardware_profile profile = sw_detect_profile();
|
|
uint32_t batch_size = w->config.batch_size > 0 ?
|
|
(uint32_t)w->config.batch_size :
|
|
sw_profile_batch_size(profile);
|
|
|
|
worker_log(w, SW_LOG_INFO, "Registering with coordinator (profile=%s, batch=%d)",
|
|
sw_profile_name(profile), batch_size);
|
|
|
|
uint8_t* msg = sw_msg_alloc(SW_MSG_REGISTER, sizeof(sw_register_payload));
|
|
if (!msg) return false;
|
|
|
|
sw_register_payload* payload = sw_msg_get_payload(msg);
|
|
memset(payload, 0, sizeof(*payload));
|
|
strncpy(payload->worker_id, w->config.worker_id, SW_MAX_WORKER_ID - 1);
|
|
payload->protocol_version = SW_PROTOCOL_VERSION;
|
|
payload->profile = profile;
|
|
payload->suggested_batch_size = batch_size;
|
|
|
|
bool ok = sw_send_msg(w->socket, msg);
|
|
sw_msg_free(msg);
|
|
|
|
return ok;
|
|
}
|
|
|
|
static bool handle_register_ack(sw_worker* w, uint8_t* msg) {
|
|
sw_register_ack_payload* ack = sw_msg_get_payload(msg);
|
|
|
|
if (!ack->accepted) {
|
|
worker_log(w, SW_LOG_ERROR, "Registration rejected by coordinator");
|
|
return false;
|
|
}
|
|
|
|
w->assigned_shard = ack->assigned_shard;
|
|
w->assigned_batch_size = ack->batch_size;
|
|
w->assigned_accumulation_steps = ack->accumulation_steps;
|
|
|
|
worker_log(w, SW_LOG_INFO, "Registered! shard=%d, batch=%d, accum=%d",
|
|
ack->assigned_shard, ack->batch_size, ack->accumulation_steps);
|
|
|
|
set_state(w, SW_WSTATE_REGISTERED);
|
|
return true;
|
|
}
|
|
|
|
bool sw_worker_connect(sw_worker* w) {
|
|
if (w->socket) {
|
|
sw_socket_close(w->socket);
|
|
w->socket = NULL;
|
|
}
|
|
|
|
set_state(w, SW_WSTATE_CONNECTING);
|
|
|
|
for (int attempt = 0; attempt < w->config.reconnect_attempts; attempt++) {
|
|
if (attempt > 0) {
|
|
worker_log(w, SW_LOG_INFO, "Reconnect attempt %d/%d in %d seconds...",
|
|
attempt + 1, w->config.reconnect_attempts,
|
|
w->config.reconnect_delay_sec);
|
|
sleep_ms(w->config.reconnect_delay_sec * 1000);
|
|
}
|
|
|
|
worker_log(w, SW_LOG_INFO, "Connecting to %s:%d...",
|
|
w->config.coordinator_host, w->config.coordinator_port);
|
|
|
|
w->socket = sw_client_connect(w->config.coordinator_host,
|
|
w->config.coordinator_port,
|
|
5); /* 5 second timeout */
|
|
|
|
if (!w->socket) {
|
|
worker_log(w, SW_LOG_WARN, "Connection failed: %s", sw_socket_error());
|
|
continue;
|
|
}
|
|
|
|
worker_log(w, SW_LOG_INFO, "Connected!");
|
|
|
|
/* Send registration */
|
|
if (!send_register(w)) {
|
|
worker_log(w, SW_LOG_ERROR, "Failed to send registration");
|
|
sw_socket_close(w->socket);
|
|
w->socket = NULL;
|
|
continue;
|
|
}
|
|
|
|
/* Wait for ack */
|
|
uint8_t* msg = sw_recv_msg(w->socket, 10); /* 10 sec timeout */
|
|
if (!msg) {
|
|
worker_log(w, SW_LOG_ERROR, "No registration response");
|
|
sw_socket_close(w->socket);
|
|
w->socket = NULL;
|
|
continue;
|
|
}
|
|
|
|
sw_msg_header* hdr = sw_msg_get_header(msg);
|
|
if (hdr->type != SW_MSG_REGISTER_ACK) {
|
|
worker_log(w, SW_LOG_ERROR, "Unexpected message type: 0x%02x", hdr->type);
|
|
sw_msg_free(msg);
|
|
sw_socket_close(w->socket);
|
|
w->socket = NULL;
|
|
continue;
|
|
}
|
|
|
|
bool ok = handle_register_ack(w, msg);
|
|
sw_msg_free(msg);
|
|
|
|
if (ok) {
|
|
return true;
|
|
}
|
|
|
|
sw_socket_close(w->socket);
|
|
w->socket = NULL;
|
|
}
|
|
|
|
set_state(w, SW_WSTATE_ERROR);
|
|
return false;
|
|
}
|
|
|
|
void sw_worker_disconnect(sw_worker* w) {
|
|
if (w->socket) {
|
|
sw_socket_close(w->socket);
|
|
w->socket = NULL;
|
|
}
|
|
set_state(w, SW_WSTATE_DISCONNECTED);
|
|
}
|
|
|
|
bool sw_worker_is_connected(sw_worker* w) {
|
|
return w->socket != NULL && sw_socket_is_connected(w->socket);
|
|
}
|
|
|
|
sw_wstate sw_worker_get_state(sw_worker* w) {
|
|
return w->state;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Weight Management */
|
|
/* ============================================================= */
|
|
|
|
void sw_worker_set_weights(sw_worker* w, tm_tensor** weights, int num_weights) {
|
|
w->weights = weights;
|
|
w->num_weights = num_weights;
|
|
|
|
/* Allocate gradient accumulators */
|
|
if (w->gradients) {
|
|
for (int i = 0; i < w->num_weights; i++) {
|
|
if (w->gradients[i]) {
|
|
tm_free(w->gradients[i]);
|
|
}
|
|
}
|
|
free(w->gradients);
|
|
}
|
|
|
|
w->gradients = calloc(num_weights, sizeof(tm_tensor*));
|
|
for (int i = 0; i < num_weights; i++) {
|
|
/* Create zero-initialized gradient tensor matching weight shape */
|
|
w->gradients[i] = tm_zeros_like(weights[i]);
|
|
}
|
|
}
|
|
|
|
bool sw_worker_fetch_weights(sw_worker* w) {
|
|
if (!sw_worker_is_connected(w)) {
|
|
worker_log(w, SW_LOG_ERROR, "Not connected");
|
|
return false;
|
|
}
|
|
|
|
worker_log(w, SW_LOG_INFO, "Requesting initial weights...");
|
|
|
|
uint8_t* req = sw_msg_alloc(SW_MSG_WEIGHTS_REQ, 0);
|
|
if (!req) return false;
|
|
|
|
bool ok = sw_send_msg(w->socket, req);
|
|
sw_msg_free(req);
|
|
|
|
if (!ok) {
|
|
worker_log(w, SW_LOG_ERROR, "Failed to send weights request");
|
|
return false;
|
|
}
|
|
|
|
/* Wait for weights metadata */
|
|
uint8_t* meta_msg = sw_recv_msg(w->socket, 60); /* 60 sec for large models */
|
|
if (!meta_msg) {
|
|
worker_log(w, SW_LOG_ERROR, "No weights metadata response");
|
|
return false;
|
|
}
|
|
|
|
sw_msg_header* hdr = sw_msg_get_header(meta_msg);
|
|
if (hdr->type != SW_MSG_WEIGHTS_META) {
|
|
worker_log(w, SW_LOG_ERROR, "Unexpected message type: 0x%02x", hdr->type);
|
|
sw_msg_free(meta_msg);
|
|
return false;
|
|
}
|
|
|
|
sw_weights_meta_payload* meta = sw_msg_get_payload(meta_msg);
|
|
if ((int)meta->num_tensors != w->num_weights) {
|
|
worker_log(w, SW_LOG_ERROR, "Weight count mismatch: got %d, expected %d",
|
|
meta->num_tensors, w->num_weights);
|
|
sw_msg_free(meta_msg);
|
|
return false;
|
|
}
|
|
|
|
worker_log(w, SW_LOG_INFO, "Receiving %d weight tensors (step=%d)",
|
|
meta->num_tensors, meta->global_step);
|
|
sw_msg_free(meta_msg);
|
|
|
|
/* Receive each weight tensor */
|
|
for (int i = 0; i < w->num_weights; i++) {
|
|
uint8_t* weight_msg = sw_recv_msg(w->socket, 60);
|
|
if (!weight_msg) {
|
|
worker_log(w, SW_LOG_ERROR, "Failed to receive weight tensor %d", i);
|
|
return false;
|
|
}
|
|
|
|
hdr = sw_msg_get_header(weight_msg);
|
|
if (hdr->type != SW_MSG_WEIGHTS) {
|
|
worker_log(w, SW_LOG_ERROR, "Unexpected message type: 0x%02x", hdr->type);
|
|
sw_msg_free(weight_msg);
|
|
return false;
|
|
}
|
|
|
|
/* Copy weight data */
|
|
void* data = sw_msg_get_payload(weight_msg);
|
|
size_t size = tm_numel(w->weights[i]) * sizeof(float);
|
|
memcpy(w->weights[i]->data, data, size);
|
|
|
|
sw_msg_free(weight_msg);
|
|
}
|
|
|
|
worker_log(w, SW_LOG_INFO, "Received all weights");
|
|
|
|
if (w->on_weights_received) {
|
|
w->on_weights_received(w, w->on_weights_received_ctx);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Training Callbacks */
|
|
/* ============================================================= */
|
|
|
|
void sw_worker_set_forward(sw_worker* w, sw_forward_fn fn, void* ctx) {
|
|
w->forward_fn = fn;
|
|
w->forward_ctx = ctx;
|
|
}
|
|
|
|
void sw_worker_set_backward(sw_worker* w, sw_backward_fn fn, void* ctx) {
|
|
w->backward_fn = fn;
|
|
w->backward_ctx = ctx;
|
|
}
|
|
|
|
void sw_worker_set_data_loader(sw_worker* w, sw_next_batch_fn next,
|
|
sw_reset_data_fn reset, sw_free_batch_fn free_batch,
|
|
void* ctx) {
|
|
w->next_batch_fn = next;
|
|
w->reset_data_fn = reset;
|
|
w->free_batch_fn = free_batch;
|
|
w->data_ctx = ctx;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Gradient Handling */
|
|
/* ============================================================= */
|
|
|
|
static void zero_gradients(sw_worker* w) {
|
|
for (int i = 0; i < w->num_weights; i++) {
|
|
tm_fill_(w->gradients[i], 0.0f);
|
|
}
|
|
}
|
|
|
|
static void accumulate_gradients(sw_worker* w) {
|
|
/* After backward pass, accumulate gradients from weights->grad */
|
|
for (int i = 0; i < w->num_weights; i++) {
|
|
tm_tensor* grad = w->weights[i]->grad;
|
|
if (grad) {
|
|
tm_add_(w->gradients[i], grad);
|
|
}
|
|
}
|
|
w->accumulation_count++;
|
|
}
|
|
|
|
static bool send_gradients(sw_worker* w) {
|
|
if (w->accumulation_count == 0) return true;
|
|
|
|
/* Average accumulated gradients */
|
|
float scale = 1.0f / w->accumulation_count;
|
|
for (int i = 0; i < w->num_weights; i++) {
|
|
tm_scale_(w->gradients[i], scale);
|
|
}
|
|
|
|
/* Calculate total gradient size */
|
|
size_t total_elements = 0;
|
|
for (int i = 0; i < w->num_weights; i++) {
|
|
total_elements += tm_numel(w->gradients[i]);
|
|
}
|
|
|
|
/* Determine compressed size */
|
|
sw_compress_type compression = w->config.compression;
|
|
size_t data_size;
|
|
|
|
if (compression == SW_COMPRESS_TOPK) {
|
|
size_t k = (size_t)(total_elements * w->config.topk_ratio);
|
|
data_size = k * (sizeof(uint32_t) + sizeof(float));
|
|
} else if (compression == SW_COMPRESS_FP16) {
|
|
data_size = total_elements * sizeof(uint16_t);
|
|
} else {
|
|
data_size = total_elements * sizeof(float);
|
|
}
|
|
|
|
size_t payload_size = sizeof(sw_gradients_payload) + data_size;
|
|
uint8_t* msg = sw_msg_alloc(SW_MSG_GRADIENTS, payload_size);
|
|
if (!msg) return false;
|
|
|
|
sw_gradients_payload* gp = sw_msg_get_payload(msg);
|
|
memset(gp, 0, sizeof(*gp));
|
|
strncpy(gp->worker_id, w->config.worker_id, SW_MAX_WORKER_ID - 1);
|
|
gp->local_step = w->local_step;
|
|
gp->num_tensors = w->num_weights;
|
|
gp->compression = compression;
|
|
gp->uncompressed_size = total_elements * sizeof(float);
|
|
|
|
/* Serialize gradients (after the header struct) */
|
|
uint8_t* data = (uint8_t*)(gp + 1);
|
|
|
|
if (compression == SW_COMPRESS_TOPK) {
|
|
size_t k = (size_t)(total_elements * w->config.topk_ratio);
|
|
|
|
/* Flatten all gradients */
|
|
float* all_grads = malloc(total_elements * sizeof(float));
|
|
size_t offset = 0;
|
|
for (int i = 0; i < w->num_weights; i++) {
|
|
size_t n = tm_numel(w->gradients[i]);
|
|
memcpy(all_grads + offset, tm_data_f32(w->gradients[i]), n * sizeof(float));
|
|
offset += n;
|
|
}
|
|
|
|
/* Get TopK */
|
|
uint32_t* indices = (uint32_t*)data;
|
|
float* values = (float*)(data + k * sizeof(uint32_t));
|
|
sw_topk_compress(all_grads, total_elements, k, indices, values);
|
|
|
|
free(all_grads);
|
|
} else if (compression == SW_COMPRESS_FP16) {
|
|
for (int i = 0; i < w->num_weights; i++) {
|
|
size_t n = tm_numel(w->gradients[i]);
|
|
sw_fp32_to_fp16(tm_data_f32(w->gradients[i]), (uint16_t*)data, n);
|
|
data += n * sizeof(uint16_t);
|
|
}
|
|
} else {
|
|
for (int i = 0; i < w->num_weights; i++) {
|
|
size_t n = tm_numel(w->gradients[i]) * sizeof(float);
|
|
memcpy(data, tm_data_f32(w->gradients[i]), n);
|
|
data += n;
|
|
}
|
|
}
|
|
|
|
worker_log(w, SW_LOG_DEBUG, "Sending gradients (step=%d, samples=%d)",
|
|
w->local_step, w->accumulation_count * w->assigned_batch_size);
|
|
|
|
bool ok = sw_send_msg(w->socket, msg);
|
|
sw_msg_free(msg);
|
|
|
|
/* Reset accumulators */
|
|
zero_gradients(w);
|
|
w->accumulation_count = 0;
|
|
w->total_loss = 0;
|
|
w->loss_count = 0;
|
|
|
|
return ok;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Message Handling */
|
|
/* ============================================================= */
|
|
|
|
static bool send_heartbeat(sw_worker* w) {
|
|
uint8_t* msg = sw_msg_alloc(SW_MSG_HEARTBEAT, sizeof(sw_heartbeat_payload));
|
|
if (!msg) return false;
|
|
|
|
sw_heartbeat_payload* hb = sw_msg_get_payload(msg);
|
|
memset(hb, 0, sizeof(*hb));
|
|
strncpy(hb->worker_id, w->config.worker_id, SW_MAX_WORKER_ID - 1);
|
|
hb->local_step = w->local_step;
|
|
hb->current_loss = w->current_loss;
|
|
hb->samples_per_sec = w->samples_per_sec;
|
|
|
|
bool ok = sw_send_msg(w->socket, msg);
|
|
sw_msg_free(msg);
|
|
|
|
if (ok) {
|
|
w->last_heartbeat_sent = get_time_ms();
|
|
}
|
|
|
|
return ok;
|
|
}
|
|
|
|
static bool handle_message(sw_worker* w, uint8_t* msg) {
|
|
sw_msg_header* hdr = sw_msg_get_header(msg);
|
|
|
|
switch (hdr->type) {
|
|
case SW_MSG_HEARTBEAT_ACK:
|
|
/* Just confirms connection is alive */
|
|
return true;
|
|
|
|
case SW_MSG_START:
|
|
worker_log(w, SW_LOG_INFO, "Received START command");
|
|
set_state(w, SW_WSTATE_TRAINING);
|
|
return true;
|
|
|
|
case SW_MSG_PAUSE:
|
|
worker_log(w, SW_LOG_INFO, "Received PAUSE command");
|
|
set_state(w, SW_WSTATE_PAUSED);
|
|
return true;
|
|
|
|
case SW_MSG_RESUME:
|
|
worker_log(w, SW_LOG_INFO, "Received RESUME command");
|
|
set_state(w, SW_WSTATE_TRAINING);
|
|
return true;
|
|
|
|
case SW_MSG_SHUTDOWN:
|
|
worker_log(w, SW_LOG_INFO, "Received SHUTDOWN command");
|
|
set_state(w, SW_WSTATE_FINISHED);
|
|
w->should_stop = true;
|
|
return true;
|
|
|
|
case SW_MSG_GRADIENTS_ACK:
|
|
worker_log(w, SW_LOG_DEBUG, "Gradients acknowledged");
|
|
return true;
|
|
|
|
default:
|
|
worker_log(w, SW_LOG_WARN, "Unknown message type: 0x%02x", hdr->type);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
static void process_pending_messages(sw_worker* w) {
|
|
/* Process any pending messages (non-blocking) */
|
|
while (sw_data_available(w->socket)) {
|
|
uint8_t* msg = sw_recv_msg(w->socket, 0);
|
|
if (msg) {
|
|
handle_message(w, msg);
|
|
sw_msg_free(msg);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Training Loop */
|
|
/* ============================================================= */
|
|
|
|
void sw_worker_train(sw_worker* w) {
|
|
if (!w->forward_fn || !w->backward_fn) {
|
|
worker_log(w, SW_LOG_ERROR, "Forward/backward callbacks not set");
|
|
set_state(w, SW_WSTATE_ERROR);
|
|
return;
|
|
}
|
|
|
|
if (!w->next_batch_fn) {
|
|
worker_log(w, SW_LOG_ERROR, "Data loader not set");
|
|
set_state(w, SW_WSTATE_ERROR);
|
|
return;
|
|
}
|
|
|
|
if (!sw_worker_is_connected(w)) {
|
|
worker_log(w, SW_LOG_ERROR, "Not connected to coordinator");
|
|
set_state(w, SW_WSTATE_ERROR);
|
|
return;
|
|
}
|
|
|
|
w->running = true;
|
|
w->should_stop = false;
|
|
w->local_step = 0;
|
|
w->accumulation_count = 0;
|
|
zero_gradients(w);
|
|
|
|
int accum_steps = w->assigned_accumulation_steps > 0 ?
|
|
w->assigned_accumulation_steps : 50;
|
|
|
|
worker_log(w, SW_LOG_INFO, "Starting training loop (accum_steps=%d)", accum_steps);
|
|
|
|
/* Wait for START signal */
|
|
set_state(w, SW_WSTATE_WAITING);
|
|
while (w->state == SW_WSTATE_WAITING && !w->should_stop) {
|
|
process_pending_messages(w);
|
|
|
|
/* Send heartbeat periodically */
|
|
uint64_t now = get_time_ms();
|
|
if (now - w->last_heartbeat_sent > 5000) { /* Every 5 seconds */
|
|
send_heartbeat(w);
|
|
}
|
|
|
|
sleep_ms(100);
|
|
}
|
|
|
|
if (w->should_stop) {
|
|
w->running = false;
|
|
return;
|
|
}
|
|
|
|
/* Reset data loader */
|
|
if (w->reset_data_fn) {
|
|
w->reset_data_fn(w->data_ctx);
|
|
}
|
|
|
|
/* Main training loop */
|
|
w->step_start_time = get_time_ms();
|
|
|
|
while (!w->should_stop) {
|
|
/* Handle pause state */
|
|
while (w->state == SW_WSTATE_PAUSED && !w->should_stop) {
|
|
process_pending_messages(w);
|
|
|
|
uint64_t now = get_time_ms();
|
|
if (now - w->last_heartbeat_sent > 5000) {
|
|
send_heartbeat(w);
|
|
}
|
|
|
|
sleep_ms(100);
|
|
}
|
|
|
|
if (w->should_stop) break;
|
|
|
|
/* Get next batch */
|
|
void* batch = w->next_batch_fn(w->data_ctx);
|
|
if (!batch) {
|
|
/* Epoch complete - reset and continue */
|
|
worker_log(w, SW_LOG_INFO, "Epoch complete, resetting data loader");
|
|
if (w->reset_data_fn) {
|
|
w->reset_data_fn(w->data_ctx);
|
|
}
|
|
batch = w->next_batch_fn(w->data_ctx);
|
|
if (!batch) {
|
|
worker_log(w, SW_LOG_ERROR, "No data available");
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Forward pass */
|
|
float loss = w->forward_fn(w->weights, w->num_weights, batch, w->forward_ctx);
|
|
w->current_loss = loss;
|
|
w->total_loss += loss;
|
|
w->loss_count++;
|
|
|
|
/* Backward pass */
|
|
w->backward_fn(w->weights, w->num_weights, batch, w->backward_ctx);
|
|
|
|
/* Accumulate gradients */
|
|
accumulate_gradients(w);
|
|
|
|
/* Free batch */
|
|
if (w->free_batch_fn) {
|
|
w->free_batch_fn(batch, w->data_ctx);
|
|
}
|
|
|
|
w->local_step++;
|
|
|
|
/* Calculate throughput */
|
|
uint64_t now = get_time_ms();
|
|
float elapsed_sec = (now - w->step_start_time) / 1000.0f;
|
|
if (elapsed_sec > 0) {
|
|
w->samples_per_sec = (w->local_step * w->assigned_batch_size) / elapsed_sec;
|
|
}
|
|
|
|
/* Step complete callback */
|
|
if (w->on_step_complete) {
|
|
w->on_step_complete(w, w->local_step, loss, w->on_step_complete_ctx);
|
|
}
|
|
|
|
/* Send gradients after accumulation steps */
|
|
if (w->accumulation_count >= accum_steps) {
|
|
if (!send_gradients(w)) {
|
|
worker_log(w, SW_LOG_ERROR, "Failed to send gradients");
|
|
/* Try to reconnect */
|
|
if (!sw_worker_connect(w)) {
|
|
set_state(w, SW_WSTATE_ERROR);
|
|
break;
|
|
}
|
|
/* Request fresh weights after reconnect */
|
|
sw_worker_fetch_weights(w);
|
|
}
|
|
}
|
|
|
|
/* Process incoming messages */
|
|
process_pending_messages(w);
|
|
|
|
/* Send heartbeat periodically */
|
|
if (now - w->last_heartbeat_sent > 5000) {
|
|
send_heartbeat(w);
|
|
}
|
|
}
|
|
|
|
/* Send any remaining gradients */
|
|
if (w->accumulation_count > 0) {
|
|
send_gradients(w);
|
|
}
|
|
|
|
w->running = false;
|
|
worker_log(w, SW_LOG_INFO, "Training loop ended (steps=%d)", w->local_step);
|
|
}
|
|
|
|
void sw_worker_stop(sw_worker* w) {
|
|
w->should_stop = true;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* State Accessors */
|
|
/* ============================================================= */
|
|
|
|
const char* sw_worker_get_id(sw_worker* w) {
|
|
return w->config.worker_id;
|
|
}
|
|
|
|
int sw_worker_get_step(sw_worker* w) {
|
|
return w->local_step;
|
|
}
|
|
|
|
float sw_worker_get_loss(sw_worker* w) {
|
|
return w->current_loss;
|
|
}
|
|
|
|
float sw_worker_get_throughput(sw_worker* w) {
|
|
return w->samples_per_sec;
|
|
}
|
|
|
|
int sw_worker_get_shard(sw_worker* w) {
|
|
return w->assigned_shard;
|
|
}
|
|
|
|
int sw_worker_get_batch_size(sw_worker* w) {
|
|
return w->assigned_batch_size;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Callbacks */
|
|
/* ============================================================= */
|
|
|
|
void sw_worker_on_state_change(sw_worker* w, sw_on_state_change cb, void* ctx) {
|
|
w->on_state_change = cb;
|
|
w->on_state_change_ctx = ctx;
|
|
}
|
|
|
|
void sw_worker_on_weights_received(sw_worker* w, sw_on_weights_received cb, void* ctx) {
|
|
w->on_weights_received = cb;
|
|
w->on_weights_received_ctx = ctx;
|
|
}
|
|
|
|
void sw_worker_on_step_complete(sw_worker* w, sw_on_step_complete cb, void* ctx) {
|
|
w->on_step_complete = cb;
|
|
w->on_step_complete_ctx = ctx;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Logging */
|
|
/* ============================================================= */
|
|
|
|
void sw_worker_set_log_level(sw_worker* w, sw_log_level level) {
|
|
w->log_level = level;
|
|
}
|
|
|
|
void sw_worker_set_log_file(sw_worker* w, const char* path) {
|
|
if (w->log_file && w->log_file != stdout) {
|
|
fclose(w->log_file);
|
|
}
|
|
|
|
if (path) {
|
|
w->log_file = fopen(path, "a");
|
|
} else {
|
|
w->log_file = NULL;
|
|
}
|
|
}
|