diff options
| author | Aargh Rai <aargh.rai+git@gmail.com> | 2026-08-23 16:37:21 +0530 |
|---|---|---|
| committer | Aargh Rai <aargh.rai+git@gmail.com> | 2026-08-23 16:37:21 +0530 |
| commit | 0e107345a54bf987aab0495dbbcf0dcfead131a0 (patch) | |
| tree | 3b425006d17f4655ab2497f7d1c0476db4ba5f24 | |
| parent | 99de764d1d1b936594643e34ae4d2c4a239f69db (diff) | |
phase F
| -rw-r--r-- | PLAN.md | 8 | ||||
| -rw-r--r-- | include/engine/threads.h | 8 | ||||
| -rw-r--r-- | src/engine/thread.c | 280 | ||||
| -rw-r--r-- | src/engine/threads.c | 21 | ||||
| -rw-r--r-- | tests/generated.c | 10 |
5 files changed, 312 insertions, 15 deletions
@@ -307,16 +307,16 @@ build (`just test`) green before the next. per-thread args). Build + existing tests still pass. ### Phase B — Sequential search + eval (no threads yet) -- [ ] **B1.** Create `include/engine/evaluate.h` + `src/engine/evaluate.c` +- [x] **B1.** Create `include/engine/evaluate.h` + `src/engine/evaluate.c` with `i32 evaluate(position_t)` (material + simple PST). `#include` in `engine.c`. Add `test_eval_startpos` under `TEST_MOD`. -- [ ] **B2.** Create `include/engine/search.h` + `src/engine/search.c` with +- [x] **B2.** Create `include/engine/search.h` + `src/engine/search.c` with `search_result_t` and `search(...)`. Probe/emit TT. Add `TEST_MOD` tests: `test_search_findmate`, `test_search_respects_depth`. -- [ ] **B3.** `just test` — all 17 original tests + new search/eval tests green. +- [x] **B3.** `just test` — all 17 original tests + new search/eval tests green. ### Phase C — Root thread driver (still effectively single-worker) -- [ ] **C0.** Fix required. src/engine/ttable.c — zobrist_hash scanned all 64 +- [x] **C0.** Fix required. src/engine/ttable.c — zobrist_hash scanned all 64 squares via find_piece_on_square, which asserts on empty squares. The per-node TT probe made search hit this on any non-full-board position. Rewrote the piece loop to iterate occupied bitboards diff --git a/include/engine/threads.h b/include/engine/threads.h index 43c4459..1e63a5d 100644 --- a/include/engine/threads.h +++ b/include/engine/threads.h @@ -56,6 +56,13 @@ struct root_thread_args { go_args_t *go_args; engine_message_handler_t *message; ttable_t *ttable; + + split_point_t *split_points; // shared DTS pool (§2.4) + size_t sp_pool_capacity; + + // the root stops its workers when the search completes + atomic_int *workers_stop; + int workers_count; }; typedef union { @@ -87,6 +94,7 @@ void __threads_args_init( // DTS primitives (§2.3–2.8): split point pool + lock-free move claiming split_point_t *split_point_alloc(struct threads *handler); +split_point_t *__pool_alloc(split_point_t *pool, size_t capacity); void split_point_free(struct threads *handler, split_point_t *split_point); void split_point_publish(split_point_t *split_point); bool split_point_claim_move(split_point_t *split_point, move_t *move); diff --git a/src/engine/thread.c b/src/engine/thread.c index df4dec8..5891558 100644 --- a/src/engine/thread.c +++ b/src/engine/thread.c @@ -4,6 +4,7 @@ #include <stdio.h> #include <stdlib.h> #include <limits.h> +#include <sched.h> #define ROOT_MAX_DEPTH 100 #define MATE_WINDOW 1000 @@ -20,8 +21,14 @@ static int __score_to_mate(i32 score) { void threads_go(struct threads *handler) { int running_count = handler->running_count; + + // initialise every arg struct before spawning: the root hands out + // pointers into this array (workers_stop), so all of them must be valid + // and settled before any thread starts running. for (int i = 0; i < running_count; i++) { __threads_args_init(handler->args + i, handler, i); + } + for (int i = 0; i < running_count; i++) { pthread_create( handler->threads + i, NULL, @@ -60,10 +67,16 @@ void *child_thread(void *_args) { args->sp_pool_capacity ); - // nothing active anywhere: the pool is drained, so we are done helping - // (the root re-publishes per iteration once root splitting lands) - if (sp == NULL) break; + // nothing active right now: idle until the root publishes the next + // iteration's block or the search is stopped + if (sp == NULL) { + static _Thread_local int spins = 0; + if (++spins == 5000000) fprintf(stderr, "CHILD %d STUCK stop=%d\n", args->id, atomic_load(&args->stop)); + sched_yield(); + continue; + } + fprintf(stderr,"CHILD %d found sp\n",args->id); // register as a worker at this block so Merge knows the headcount atomic_fetch_add(&sp->workers, 1); @@ -136,7 +149,85 @@ void *root_thread(void *_args) { // iterative deepening: search at increasing depths, publishing an info // message per completed depth so the UCI thread can emit `info ... pv`. for (int depth = 1; depth <= max_depth; depth++) { - result = search(*args->position, depth, -SEARCH_INF, SEARCH_INF, &ctx); + if (atomic_load(&stop)) break; + + // root move list (DTS §3.4: splitting at the root is always safe/PV) + moves_t moves; + moves_init(&moves); + get_legal_moves(&moves, *args->position); + + bool can_split = moves.length > 1 && args->split_points != NULL; + split_point_t *sp = can_split + ? __pool_alloc(args->split_points, args->sp_pool_capacity) + : NULL; + + fprintf(stderr,"ROOT iter %d sp=%p\n",depth,(void*)sp); + if (sp != NULL) { + // Split (§2.4): snapshot the root node and hand the whole move list to + // the pool; the root works the list alongside its workers. + sp->depth = depth; + sp->ply = 0; + sp->node_type = NODE_PV; + sp->position_snapshot = *args->position; + atomic_store(&sp->alpha, -SEARCH_INF); + atomic_store(&sp->beta, SEARCH_INF); + atomic_store(&sp->best_score, -SEARCH_INF); + sp->best_move = (move_t) { 0 }; + moves_deinit(sp->moves); + sp->moves = moves; + split_point_publish(sp); + atomic_fetch_add(&sp->workers, 1); // the root is a worker too + + i32 best_score = -SEARCH_INF; + move_t best_move = (move_t) { 0 }; + move_t move; + while (split_point_claim_move(sp, &move)) { + position_t child = sp->position_snapshot; + position_make_move(&child, move); + + int alpha = atomic_load(&sp->alpha); + search_result_t res = search( + child, + depth - 1, + -SEARCH_INF, + -alpha, + &ctx + ); + + i32 score = -res.score; + split_point_share(sp, score, move); + if (score > best_score) { + best_score = score; + best_move = move; + atomic_store(&sp->alpha, best_score); // Share narrowed bounds + } + } + + // Merge (§2.6): leave the block; the last worker out Unsplits it. + // The root then waits until nobody can write to the block anymore. + if (split_point_merge(sp)) { + atomic_store(&sp->state, SPLITPOINT_STATE_FREE); + } + fprintf(stderr,"ROOT iter %d waiting w=%d\n",depth,atomic_load(&sp->workers)); + int spins=0; + while (atomic_load(&sp->state) == SPLITPOINT_STATE_ACTIVE) { + sched_yield(); + if (++spins==1000000) fprintf(stderr,"ROOT STUCK iter %d w=%d idx=%d\n",depth,atomic_load(&sp->workers),atomic_load(&sp->next_move_idx)); + } + while (atomic_load(&sp->state) == SPLITPOINT_STATE_ACTIVE) { + sched_yield(); + } + fprintf(stderr,"ROOT iter %d done\n",depth); + + result.score = atomic_load(&sp->best_score); + result.best_move = sp->best_move; + // the move list now belongs to the pool block (released on its next + // reuse), so there is nothing to free here + } else { + // single reply or no pool available: search sequentially + result = search(*args->position, depth, -SEARCH_INF, SEARCH_INF, &ctx); + moves_deinit(moves); + } if (atomic_load(&stop)) break; @@ -165,7 +256,10 @@ void *root_thread(void *_args) { message->next = NULL; engine_message_mark_ready(message); - // signal children to stop their loops and mark this thread as finished. + // signal workers to stop their loops, then mark this thread as finished. + for (int i = 0; i < args->workers_count; i++) { + atomic_store(&args->workers_stop[i], 1); + } atomic_store(&args->stop, 1); atomic_store(&args->finished, 1); @@ -377,6 +471,17 @@ static void __child_test_setup( } } +static void __child_test_stop_all(__child_test_env_t *env, int worker_count) { + // workers idle when the pool is drained, so they must be signalled + for (int i = 0; i < worker_count; i++) { + atomic_store(&env->args[i].stop, 1); + } +} + +static void __wait_split_point_done(split_point_t *sp) { + while (atomic_load(&sp->state) == SPLITPOINT_STATE_ACTIVE) sched_yield(); +} + static void __child_test_teardown(__child_test_env_t *env, int worker_count) { ttable_deinit(env->handler->ttable); for (int i = 0; i < worker_count; i++) { @@ -410,6 +515,10 @@ bool test_child_workers_claim_and_share() { for (int i = 0; i < WORKERS; i++) { pthread_create(env.threads + i, NULL, child_thread, env.args + i); } + + // workers idle once the block is drained — signal them, then collect + __wait_split_point_done(sp); + __child_test_stop_all(&env, WORKERS); for (int i = 0; i < WORKERS; i++) { pthread_join(env.threads[i], NULL); } @@ -494,6 +603,9 @@ bool test_child_workers_find_mate_score() { for (int i = 0; i < WORKERS; i++) { pthread_create(env.threads + i, NULL, child_thread, env.args + i); } + + __wait_split_point_done(sp); + __child_test_stop_all(&env, WORKERS); for (int i = 0; i < WORKERS; i++) { pthread_join(env.threads[i], NULL); } @@ -507,5 +619,163 @@ bool test_child_workers_find_mate_score() { __child_test_teardown(&env, WORKERS); return ok; } + +// ---- Phase F tests: root move-list splitting ---- + +enum { ROOT_SPLIT_MAX = 5 }; + +typedef struct { + struct threads handler; + go_args_t go_args; + position_t position; + engine_message_handler_t message_handlers[ROOT_SPLIT_MAX]; + engine_message_handler_t *message_ptrs[ROOT_SPLIT_MAX]; + engine_messages_t messages; + pthread_t threads[ROOT_SPLIT_MAX]; +} __root_split_env_t; + +static void __root_split_env_init( + __root_split_env_t *env, + int thread_count, + go_args_t go_args, + position_t position +) { + threads_init(&env->handler, (size_t)thread_count); + ttable_init(&env->handler.ttable, 2); + + env->go_args = go_args; + env->position = position; + env->messages.count = thread_count; + env->messages.capacity = thread_count; + for (int i = 0; i < thread_count; i++) { + env->message_handlers[i] = (engine_message_handler_t) { 0 }; + env->message_ptrs[i] = &env->message_handlers[i]; + engine_message_handler_init(env->message_ptrs[i]); + } + env->messages.data = env->message_ptrs; + + env->handler.engine_messages = &env->messages; + env->handler.go_args = &env->go_args; + env->handler.position = &env->position; + env->handler.running_count = thread_count; + + // args live in handler.args so that the root's workers_stop pointers + // target the very structs the worker threads receive. + for (int i = 0; i < thread_count; i++) { + __threads_args_init(env->handler.args + i, &env->handler, i); + } +} + +static void __root_split_env_run(__root_split_env_t *env, int thread_count) { + for (int i = 1; i < thread_count; i++) { + pthread_create( + env->threads + i, + NULL, + child_thread, + env->handler.args + i + ); + } + pthread_create(env->threads, NULL, root_thread, env->handler.args); + + pthread_join(env->threads[0], NULL); // root stops its workers on exit + for (int i = 1; i < thread_count; i++) { + pthread_join(env->threads[i], NULL); + } +} + +static void __root_split_env_deinit( + __root_split_env_t *env, + int thread_count +) { + for (int i = 0; i < thread_count; i++) { + while (engine_message_handler_can_pop(&env->message_handlers[i])) { + engine_message_deinit(engine_message_handler_pop(&env->message_handlers[i])); + } + engine_message_handler_deinit(&env->message_handlers[i]); + } + ttable_deinit(env->handler.ttable); + threads_deinit(&env->handler); +} + +bool test_root_split_all_moves_claimed() { + enum { THREADS = 4 }; + __root_split_env_t env; + __root_split_env_init( + &env, + THREADS, + (go_args_t) { .depth = 3 }, + position_starting() + ); + + __root_split_env_run(&env, THREADS); + + bool ok = true; + + // root + workers all finished cleanly + for (int i = 0; i < THREADS; i++) { + ok &= atomic_load( + &((thread_args_t*)env.handler.args)[i].root_thread_args.finished + ) == 1; + } + + // every pool block was Unsplitten back to FREE + split_point_t *pool = env.handler.split_point_pool; + bool any_used = false; + for (size_t b = 0; b < env.handler.capacity; b++) { + ok &= atomic_load(&pool[b].state) == SPLITPOINT_STATE_FREE; + if (atomic_load(&pool[b].next_move_idx) > 0) any_used = true; + } + ok &= any_used; // splitting actually happened + + // the root published info messages for each completed depth + int count = 0; + engine_message_t *messages = + __root_thread_pop_all(&env.message_handlers[0], &count); + ok &= count == 3; + ok &= messages->depth == 1 && messages->next->depth == 2; + ok &= messages->score_cp > -SEARCH_INF; + + __root_thread_free_messages(messages); + __root_split_env_deinit(&env, THREADS); + return ok; +} + +bool test_root_with_workers_reports_mate() { + struct fen_load r = load_fen("k7/8/KQ6/8/8/8/8/8 w - - 0 1"); + if (r.failed) return false; + + enum { THREADS = 3 }; + __root_split_env_t env; + __root_split_env_init( + &env, + THREADS, + (go_args_t) { .depth = 3 }, + r.position + ); + + __root_split_env_run(&env, THREADS); + + bool ok = true; + for (int i = 0; i < THREADS; i++) { + ok &= atomic_load( + &((thread_args_t*)env.handler.args)[i].root_thread_args.finished + ) == 1; + } + + // the merged root result must report the mate in 1 + int count = 0; + engine_message_t *messages = + __root_thread_pop_all(&env.message_handlers[0], &count); + engine_message_t *tail = messages; + while (tail->next != NULL) tail = tail->next; + ok &= tail->mate == 1; + ok &= tail->score_cp == SEARCH_MATE; + ok &= tail->best_move.from >= 0 && tail->best_move.from < 64; + ok &= tail->best_move.to >= 0 && tail->best_move.to < 64; + + __root_thread_free_messages(messages); + __root_split_env_deinit(&env, THREADS); + return ok; +} #endif diff --git a/src/engine/threads.c b/src/engine/threads.c index c0d1da7..fe38013 100644 --- a/src/engine/threads.c +++ b/src/engine/threads.c @@ -109,11 +109,17 @@ void __threads_args_init( args->go_args = handler->go_args; args->message = handler->engine_messages->data[i]; args->ttable = &handler->ttable; + args->split_points = handler->split_point_pool; + args->sp_pool_capacity = handler->capacity; + args->workers_stop = handler->running_count > 1 + ? &handler->args[1].child_thread_args.stop + : NULL; + args->workers_count = handler->running_count - 1; } -split_point_t *split_point_alloc(struct threads *handler) { - for (size_t i = 0; i < handler->capacity; i++) { - split_point_t *sp = (split_point_t*)handler->split_point_pool + i; +split_point_t *__pool_alloc(split_point_t *pool, size_t capacity) { + for (size_t i = 0; i < capacity; i++) { + split_point_t *sp = pool + i; int expected = SPLITPOINT_STATE_FREE; if (!atomic_compare_exchange_weak(&sp->state, &expected, SPLITPOINT_STATE_ACTIVE)) continue; @@ -124,11 +130,18 @@ split_point_t *split_point_alloc(struct threads *handler) { // workers counts only registered helpers; the creator registers itself atomic_store(&sp->workers, 0); sp->best_move = (move_t) { 0 }; + // recycled block: release the previous occupant's move list + moves_deinit(sp->moves); + sp->moves = moves_empty(); return sp; } return NULL; } +split_point_t *split_point_alloc(struct threads *handler) { + return __pool_alloc(handler->split_point_pool, handler->capacity); +} + void split_point_free(struct threads *handler, split_point_t *split_point) { // Unsplit (§2.7): drop the block back into the pool atomic_store(&split_point->state, SPLITPOINT_STATE_FREE); @@ -141,6 +154,8 @@ void split_point_publish(split_point_t *split_point) { // Select (§2.5): lock-free claim of the next move via the shared counter bool split_point_claim_move(split_point_t *split_point, move_t *move) { + // a block that was already Unsplitten must not be claimed from anymore + if (atomic_load(&split_point->state) != SPLITPOINT_STATE_ACTIVE) return false; int idx = atomic_fetch_add(&split_point->next_move_idx, 1); if (idx >= (int)split_point->moves.length) return false; *move = split_point->moves.moves[idx]; diff --git a/tests/generated.c b/tests/generated.c index 408a8b2..608d623 100644 --- a/tests/generated.c +++ b/tests/generated.c @@ -8,8 +8,8 @@ #include "../src/engine/moves/king.c" #include "../src/uci/command.c" -int total_test_count = 37; -bool (*tests[37])(void) = { +int total_test_count = 39; +bool (*tests[39])(void) = { test_fen_no_passant, test_fen_passant, test_starting_position, @@ -26,6 +26,8 @@ bool (*tests[37])(void) = { test_child_workers_claim_and_share, test_child_thread_stops_on_signal, test_child_workers_find_mate_score, + test_root_split_all_moves_claimed, + test_root_with_workers_reports_mate, test_ttable_empty, test_ttable_insert_and_find, test_ttable_wrong_key, @@ -50,7 +52,7 @@ bool (*tests[37])(void) = { }; int max_test_name_size = 35; -char test_names[37][100] = { +char test_names[39][100] = { "fen_no_passant", "fen_passant", "starting_position", @@ -67,6 +69,8 @@ char test_names[37][100] = { "child_workers_claim_and_share", "child_thread_stops_on_signal", "child_workers_find_mate_score", + "root_split_all_moves_claimed", + "root_with_workers_reports_mate", "ttable_empty", "ttable_insert_and_find", "ttable_wrong_key", |
