diff options
| author | tslil <tslil@posteo.de> | 2026-09-11 12:56:12 +0100 |
|---|---|---|
| committer | tslil <tslil@posteo.de> | 2026-09-11 17:00:18 +0100 |
| commit | 6e90f1145b2c5c0f2dcc82d21ec45432274b8cad (patch) | |
| tree | 18db1c03ea7c3c4724028fc5154d4bca3d7200a5 /src/selfplay.c | |
| parent | 220a4b79e4b6d742a9ca7c278a1962b870500fbc (diff) | |
move from single candidate td-leaf with gating to tournament of a population
The goal in this change was to overcome the rock-paper-scissors phenomenon that was happening in capability space by having on a single candidate learn to beat the most recent version of itself. In some sense this lead to rotating around in capability space instead of chasing monotonic improvements.
I hope that flipping to a system in which many (N=5) candidates play each other and learn from their own set of diverse games in this pool, combined with --replay-iters, will lead to a given network having to have a much more comprehensive understanding of the space of tactics. I haven't yet had time to train this thorougly, will run those experiments on my desktop computer.
Diffstat (limited to 'src/selfplay.c')
| -rw-r--r-- | src/selfplay.c | 125 |
1 files changed, 88 insertions, 37 deletions
diff --git a/src/selfplay.c b/src/selfplay.c index 42d82d2..f6736c0 100644 --- a/src/selfplay.c +++ b/src/selfplay.c @@ -30,25 +30,45 @@ #include "xorshift64.h" #ifndef RECORD_PV -#error "selfplay must be built with -DRECORD_PV (see the selfplay Makefile target)" +#error "selfplay must be built with -DRECORD_PV" #endif -#define MAX_PLY 255 -#define MAX_CELLS (6 * 6) +#ifndef CAND_A +#error "selfplay must be built with -DCAND_A=<id>" +#endif -static tak_state_p state; +#ifndef CAND_B +#error "selfplay must be built with -DCAND_B=<id>" +#endif + +#include CAND_A_WEIGHTS_5 +#include CAND_B_WEIGHTS_5 +#include CAND_A_WEIGHTS_6 +#include CAND_B_WEIGHTS_6 + +// sorry not sorry +#define STR2U(a, b) a##_##b +#define STR4U(a, b, c, d) a##_##b##_##c##_##d +#define STR4U_EXPAND(a, b, c, d) STR4U(a, b, c, d) +#define C(variant, size, suffix) STR4U_EXPAND(cand, STR2U(CAND, variant), size, suffix) +#define NETWORK(size, called) \ + static inline float STR2U(size, called)(tak_state_p state) { \ + static float cur_board[STR2U(size, INP_NUM)] NN_ALIGNED; \ + nn_prepare_input(cur_board, state, C(called, size, embedding)); \ + return eval_net(cur_board, STR2U(size, INP_NUM), &C(called, size, dense_weights)[0][0], \ + C(called, size, dense_biases), C(called, size, logit_weights), C(called, size, logit_bias), \ + STR2U(size, DENSE_NUM), state->ply & 1 ? 1.0f : -1.0f); \ + } -static float row_head[MAX_PLY][2]; -static uint8_t row_cells[MAX_PLY][MAX_CELLS]; -static uint8_t row_term[MAX_PLY]; -static float row_terminal_value[MAX_PLY]; -static float row_parity_sign[MAX_PLY]; -static uint32_t row_game_id[MAX_PLY]; -static uint32_t n_rows; +NETWORK(FIVE, A) +NETWORK(FIVE, B) +NETWORK(SIX, A) +NETWORK(SIX, B) -static uint32_t game_id_black; -static uint32_t game_id_white; +#define MAX_PLY 255 +#define MAX_CELLS (6 * 6) +static tak_state_p state; void negamax_display_progress(const uint8_t cur_depth, const uint8_t init_depth, @@ -58,30 +78,49 @@ void negamax_display_progress(const uint8_t cur_depth, (void)(length); } -static float win_value(const enum WIN_TYPE w) { +static int8_t win_value(const enum WIN_TYPE w) { switch (w) { case WIN_FLAT_BLACK: case WIN_ROAD_BLACK: - return 1.0f; + return 1; case WIN_FLAT_WHITE: case WIN_ROAD_WHITE: - return -1.0f; + return -1; default: - return 0.0f; + return 0; } } -static int play_game(const uint8_t min_ply) { +static int play_game(const uint8_t min_ply, const nn_function fn_1, + const nn_function fn_2, const uint32_t cand_id_1, + const uint32_t cand_id_2, + const uint32_t game_id, char record[][16], uint8_t replay) { + float row_head[MAX_PLY][2]; + uint8_t row_cells[MAX_PLY][MAX_CELLS]; + uint8_t row_term[MAX_PLY]; + float row_terminal_value[MAX_PLY]; + float row_parity_sign[MAX_PLY]; + uint32_t row_cand_id[MAX_PLY]; + int8_t row_mover_sign[MAX_PLY]; + uint32_t n_rows; + reset_state(state, state->board_size); n_rows = 0; char ptn[16]; for (uint32_t ply = 0; ply + negamax_search_depth < MAX_PLY; ply++) { if (ply < min_ply) { - uint32_t n_actions; - action_t *actions = actions_generate(state, &n_actions); - action_to_ptn(actions[rand32() % n_actions], ptn); + if (replay == 0) { + uint32_t n_actions; + action_t *actions = actions_generate(state, &n_actions); + action_to_ptn(actions[rand32() % n_actions], ptn); + strcpy(record[ply],ptn); + } else { + strcpy(ptn, record[ply]); + } } else { + const nn_function fn = (state->current_colour & 1) ? fn_1 : fn_2; + negamax_set_evaluator(fn); negamax_generate(state); strcpy(ptn, negamax_ptn); @@ -92,7 +131,8 @@ static int play_game(const uint8_t min_ply) { row_term[n_rows] = leaf->terminal; row_terminal_value[n_rows] = leaf->terminal_value; row_parity_sign[n_rows] = leaf->parity_sign; - row_game_id[n_rows] = state->current_colour == C_BLACK ? game_id_black : game_id_white; + row_cand_id[n_rows] = (state->current_colour&1) ? cand_id_1 : cand_id_2; + row_mover_sign[n_rows] = (state->current_colour&1) ? 1 : -1; n_rows++; } @@ -113,25 +153,25 @@ static int play_game(const uint8_t min_ply) { const uint8_t n_cells = state->board_size * state->board_size; // final outcome of the played game, from black's perspective - const float R = win_value(state->won); + const int8_t R = win_value(state->won); for (uint32_t k = 0; k < n_rows; k++) { - fprintf(stdout, "%.8f,%.8f,", row_head[k][0], row_head[k][1]); + fprintf(stdout, "%.1f,%.8f,%.8f,", row_parity_sign[k], row_head[k][0], row_head[k][1]); for (uint8_t c = 0; c < n_cells; c++) - fprintf(stdout, "%u,", (unsigned)row_cells[k][c]); - fprintf(stdout, "%u,%.0f,%u,%.0f,%.0f\n", row_game_id[k], R, - (unsigned)row_term[k], row_terminal_value[k], row_parity_sign[k]); + fprintf(stdout, "%u,", row_cells[k][c]); + fprintf(stdout, "%i,%u,%.0f,%u,%u,%i\n", R, row_term[k], row_terminal_value[k], game_id, row_cand_id[k], + row_mover_sign[k] * R); } return (int)n_rows; } const char *usage = - "selfplay, one game of asymmetric self-play training data.\n\n\ + "selfplay, one game of training data between candidates.\n\n\ Usage: selfplay <size> <depth> <min_ply>\n\n\ -Plays a single game of <size>x<size> self-play, with <min_ply> moves made randomly. Only the deterministic plies are written to stdout, one CSV row per recorded search, from ply <min_ply> onwards:\n\n\ -\tmover_flats,opponent_flats,<cell ids...>,game_id,R,terminal,leaf_R,parity_sign\n\n\ -nn_train.py runs many of these in parallel to generate training data.\n\n\ +Plays a single game of <size>x<size>, with <min_ply> moves made randomly. Only the deterministic plies are written to stdout, one CSV row per recorded search, from ply <min_ply> onwards:\n\n\ +\tparity_sign,mover_flats,opponent_flats,<cell ids...>,R,terminal,leaf_R,game_id,cand_id,cand_R\n\n\ +R is the outcome from black's perspective, cand_R the outcome from the recorded candidate's perspective. nn_train.py runs many of these in parallel to generate training data.\n\n\ Copyright (C) 2026, tslil clingman\n\n\ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version."; @@ -160,16 +200,27 @@ int main(int argc, char **argv) { // Processes launched in the same second must not share a seed xors = ((uint64_t)time(NULL) ^ ((uint64_t)getpid() << 32)) * - 0x9E3779B97F4A7C15ULL + - 0xC0FFEE; - - game_id_black = rand32(); - game_id_white = rand32(); + 0x9E3779B97F4A7C15ULL + + 0xC0FFEE; state = new_tak_state(size); negamax_init(size); - play_game(min_ply); + char record[min_ply][16]; + + play_game(min_ply, + size == 5 ? FIVE_A : SIX_A, size == 5 ? FIVE_B : SIX_B, + CAND_A, + CAND_B, + rand32(), + record, 0); + play_game(min_ply, + size == 5 ? FIVE_B : SIX_B, size == 5 ? FIVE_A : SIX_A, + CAND_B, + CAND_A, + rand32(), + record, 1); + negamax_free(); free_tak_state(state); |
