Drop7 Research
approaches/heuristic-search/exact-search/exact-depth4.cppC++527 lines · 19.4 KB
#include "../../../src/core/native/public-behavior.hpp"

#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <fstream>
#include <future>
#include <iomanip>
#include <iostream>
#include <limits>
#include <mutex>
#include <numeric>
#include <stdexcept>
#include <string>
#include <string_view>
#include <sys/resource.h>
#include <vector>

namespace drop7::exact_depth4 {

constexpr std::uint32_t kScreenSeedStart = 0x3d70'e000u;
constexpr std::uint32_t kConfirmationSeedStart = 0x3d70'e100u;
constexpr int kScreenGames = 4;
constexpr int kConfirmationGames = 8;
constexpr int kMaximumMoves = 500;
constexpr int kParallelism = 4;
constexpr int kChanceSamples = 5;
constexpr int kBaselineDepth = 3;
constexpr int kCandidateDepth = 4;
constexpr std::uint64_t kMaximumWork = 3'200'000;
constexpr std::size_t kMaximumCacheEntries = 60'000;

static_assert(kLevelBonus == 7'000);

constexpr std::uint64_t power(std::uint64_t base, int exponent) {
  std::uint64_t result = 1;
  for (int count = 0; count < exponent; ++count) result *= base;
  return result;
}

constexpr std::uint64_t worstCaseIterativeWork(int maximum_depth) {
  constexpr std::uint64_t branches = kBoardSize * kChanceSamples;
  std::uint64_t result = 0;
  for (int depth = 1; depth <= maximum_depth; ++depth) {
    for (int level = 1; level <= depth; ++level) {
      result += power(branches, level);
    }
    result += power(branches, depth);  // Leaf evaluations.
  }
  return result;
}

constexpr std::uint64_t worstCaseIterativeCacheEntries(int maximum_depth) {
  constexpr std::uint64_t branches = kBoardSize * kChanceSamples;
  std::uint64_t result = 0;
  for (int depth = 2; depth <= maximum_depth; ++depth) {
    for (int level = 1; level < depth; ++level) {
      result += power(branches, level);
    }
  }
  return result;
}

constexpr std::uint64_t kWorstCaseD4Work =
    worstCaseIterativeWork(kCandidateDepth);
constexpr std::uint64_t kWorstCaseD4CacheEntries =
    worstCaseIterativeCacheEntries(kCandidateDepth);
static_assert(kWorstCaseD4Work == 3'134'950);
static_assert(kWorstCaseD4CacheEntries == 45'430);
static_assert(kMaximumWork > kWorstCaseD4Work);
static_assert(kMaximumCacheEntries > kWorstCaseD4CacheEntries);

std::mutex progress_mutex;

std::uint64_t peakRssBytes() {
  rusage usage{};
  if (getrusage(RUSAGE_SELF, &usage) != 0) return 0;
#if defined(__APPLE__)
  return static_cast<std::uint64_t>(usage.ru_maxrss);
#else
  return static_cast<std::uint64_t>(usage.ru_maxrss) * 1024u;
#endif
}

cfpi::BehaviorOptions behaviorOptions(int depth) {
  cfpi::BehaviorOptions options;
  options.max_depth = depth;
  options.chance_samples = kChanceSamples;
  options.max_work = kMaximumWork;
  options.max_cache_entries = kMaximumCacheEntries;
  return options;
}

struct GameResult {
  std::int64_t score = 0;
  int moves = 0;
  bool censored = false;
  std::uint64_t work = 0;
  std::uint64_t nodes = 0;
  std::uint64_t cache_hits = 0;
  std::uint64_t depth_switches = 0;
  std::size_t peak_cache_entries = 0;
  std::uint64_t peak_rss_bytes = 0;
  double elapsed_seconds = 0.0;
};

GameResult runGame(std::uint32_t seed, int depth,
                   std::string_view label) {
  const auto started = std::chrono::steady_clock::now();
  const cfpi::BehaviorOptions options = behaviorOptions(depth);
  State state = initialHeadlessState(seed);
  GameResult result;
  int previous_action = -1;
  while (!state.game_over && state.moves_played < kMaximumMoves) {
    cfpi::BehaviorMetrics metrics;
    const int action = cfpi::chooseBehaviorAction(state, options, &metrics);
    if (metrics.completed_depth != depth || !metrics.complete) {
      throw std::runtime_error(
          "exact search failed its completed-depth assertion");
    }
    if (!isLegal(state.board, action)) {
      throw std::runtime_error("exact search selected an illegal action");
    }
    if (previous_action >= 0 && previous_action != action) {
      ++result.depth_switches;
    }
    previous_action = action;
    result.work += metrics.work;
    result.nodes += metrics.nodes;
    result.cache_hits += metrics.cache_hits;
    result.peak_cache_entries =
        std::max(result.peak_cache_entries, metrics.cache_entries);
    MoveResult move;
    if (!playHeadlessMove(state, seed, action, move)) {
      throw std::runtime_error("exact search transition failed");
    }
  }
  result.score = state.score;
  result.moves = state.moves_played;
  result.censored = !state.game_over;
  result.peak_rss_bytes = peakRssBytes();
  result.elapsed_seconds = std::chrono::duration<double>(
                               std::chrono::steady_clock::now() - started)
                               .count();
  {
    const std::lock_guard<std::mutex> lock(progress_mutex);
    std::cerr << label << " seed 0x" << std::hex << seed << std::dec << ' '
              << result.score << " (" << result.moves << " moves, work "
              << result.work << ", cache " << result.peak_cache_entries
              << ")\n";
  }
  return result;
}

struct Cohort {
  std::vector<GameResult> depth3;
  std::vector<GameResult> depth4;
};

Cohort runCohort(std::uint32_t seed_start, int games,
                 std::string_view phase) {
  Cohort cohort;
  cohort.depth3.resize(static_cast<std::size_t>(games));
  cohort.depth4.resize(static_cast<std::size_t>(games));
  std::atomic<int> next_game{0};
  std::vector<std::future<void>> workers;
  const int worker_count = std::min(kParallelism, games);
  for (int worker = 0; worker < worker_count; ++worker) {
    workers.push_back(std::async(std::launch::async, [&] {
      for (;;) {
        const int game = next_game.fetch_add(1);
        if (game >= games) return;
        const std::uint32_t seed =
            seed_start + static_cast<std::uint32_t>(game);
        cohort.depth3[static_cast<std::size_t>(game)] = runGame(
            seed, kBaselineDepth, std::string(phase) + "-exact-d3");
        cohort.depth4[static_cast<std::size_t>(game)] = runGame(
            seed, kCandidateDepth, std::string(phase) + "-exact-d4");
      }
    }));
  }
  for (auto& worker : workers) worker.get();
  return cohort;
}

struct Summary {
  int games = 0;
  int completed_depth = 0;
  double mean_score = 0.0;
  double mean_moves = 0.0;
  double work_per_move = 0.0;
  double nodes_per_move = 0.0;
  double cache_hits_per_move = 0.0;
  double moves_per_game_second = 0.0;
  double work_per_game_second = 0.0;
  double aggregate_game_seconds = 0.0;
  double switches_per_move = 0.0;
  int censored = 0;
  std::size_t peak_cache_entries = 0;
  std::uint64_t peak_rss_bytes = 0;
};

Summary summarize(const std::vector<GameResult>& games,
                  int completed_depth) {
  if (games.empty()) throw std::invalid_argument("empty exact cohort");
  Summary result;
  result.games = static_cast<int>(games.size());
  result.completed_depth = completed_depth;
  std::uint64_t moves = 0;
  std::uint64_t work = 0;
  std::uint64_t nodes = 0;
  std::uint64_t cache_hits = 0;
  std::uint64_t switches = 0;
  for (const GameResult& game : games) {
    result.mean_score += static_cast<double>(game.score) / games.size();
    result.mean_moves += static_cast<double>(game.moves) / games.size();
    moves += game.moves;
    work += game.work;
    nodes += game.nodes;
    cache_hits += game.cache_hits;
    switches += game.depth_switches;
    result.aggregate_game_seconds += game.elapsed_seconds;
    result.censored += game.censored;
    result.peak_cache_entries =
        std::max(result.peak_cache_entries, game.peak_cache_entries);
    result.peak_rss_bytes =
        std::max(result.peak_rss_bytes, game.peak_rss_bytes);
  }
  const double move_count =
      static_cast<double>(std::max<std::uint64_t>(1, moves));
  result.work_per_move = work / move_count;
  result.nodes_per_move = nodes / move_count;
  result.cache_hits_per_move = cache_hits / move_count;
  result.switches_per_move = switches / move_count;
  result.moves_per_game_second =
      move_count / std::max(1.0e-9, result.aggregate_game_seconds);
  result.work_per_game_second =
      work / std::max(1.0e-9, result.aggregate_game_seconds);
  return result;
}

struct PairedSummary {
  double mean_score_difference = 0.0;
  double mean_move_difference = 0.0;
  int wins = 0;
  int ties = 0;
  int losses = 0;
};

PairedSummary pairedSummary(const Cohort& cohort) {
  if (cohort.depth3.size() != cohort.depth4.size() ||
      cohort.depth3.empty()) {
    throw std::invalid_argument("exact cohort is not paired");
  }
  PairedSummary result;
  for (std::size_t game = 0; game < cohort.depth3.size(); ++game) {
    const GameResult& depth3 = cohort.depth3[game];
    const GameResult& depth4 = cohort.depth4[game];
    result.mean_score_difference +=
        static_cast<double>(depth4.score - depth3.score) /
        cohort.depth3.size();
    result.mean_move_difference +=
        static_cast<double>(depth4.moves - depth3.moves) /
        cohort.depth3.size();
    if (depth4.score > depth3.score) {
      ++result.wins;
    } else if (depth4.score < depth3.score) {
      ++result.losses;
    } else {
      ++result.ties;
    }
  }
  return result;
}

void writeSummary(std::ostream& output, const Summary& result) {
  output << "{\"games\":" << result.games
         << ",\"meanScore\":" << result.mean_score
         << ",\"meanMoves\":" << result.mean_moves
         << ",\"completedDepth\":" << result.completed_depth
         << ",\"workPerMove\":" << result.work_per_move
         << ",\"nodesPerMove\":" << result.nodes_per_move
         << ",\"cacheHitsPerMove\":" << result.cache_hits_per_move
         << ",\"movesPerGameSecond\":" << result.moves_per_game_second
         << ",\"workPerGameSecond\":" << result.work_per_game_second
         << ",\"aggregateGameSeconds\":"
         << result.aggregate_game_seconds
         << ",\"switchesPerMove\":" << result.switches_per_move
         << ",\"peakCacheEntries\":" << result.peak_cache_entries
         << ",\"peakRssBytes\":" << result.peak_rss_bytes
         << ",\"censored\":" << result.censored << '}';
}

void writePaired(std::ostream& output, const PairedSummary& result) {
  output << "{\"meanScoreDifference\":" << result.mean_score_difference
         << ",\"meanMoveDifference\":" << result.mean_move_difference
         << ",\"wins\":" << result.wins << ",\"ties\":"
         << result.ties << ",\"losses\":" << result.losses << '}';
}

void writeTrajectories(std::ostream& output,
                       const std::vector<GameResult>& games) {
  output << "{\"scores\":[";
  for (std::size_t game = 0; game < games.size(); ++game) {
    if (game > 0) output << ',';
    output << games[game].score;
  }
  output << "],\"moves\":[";
  for (std::size_t game = 0; game < games.size(); ++game) {
    if (game > 0) output << ',';
    output << games[game].moves;
  }
  output << "]}";
}

bool selfTest(std::ostream& output) {
  State state;
  state.board = initialBoard();
  state.board[indexOf(5, 0)] = 3;
  state.board[indexOf(5, 1)] = 5;
  state.board[indexOf(4, 1)] = 2;
  state.board[indexOf(5, 4)] = 4;
  state.next_disc = 6;
  state.moves_remaining = 3;
  const cfpi::BehaviorOptions options = behaviorOptions(kCandidateDepth);
  cfpi::BehaviorMetrics first_metrics;
  cfpi::BehaviorMetrics repeat_metrics;
  const int first =
      cfpi::chooseBehaviorAction(state, options, &first_metrics);
  const int repeat =
      cfpi::chooseBehaviorAction(state, options, &repeat_metrics);
  State mirrored = state;
  mirrored.board = cfpi::detail::mirrorBoard(state.board);
  cfpi::BehaviorMetrics reflected_metrics;
  const int reflected =
      cfpi::chooseBehaviorAction(mirrored, options, &reflected_metrics);
  State metadata = state;
  metadata.score = 999'999;
  metadata.level = 88;
  metadata.moves_played = 321;
  const int metadata_action = cfpi::chooseBehaviorAction(metadata, options);
  const bool completed = first_metrics.completed_depth == kCandidateDepth &&
                         first_metrics.complete;
  const bool deterministic = first == repeat &&
                             first_metrics.work == repeat_metrics.work;
  const bool reflection_safe =
      reflected == kBoardSize - 1 - first &&
      reflected_metrics.completed_depth == kCandidateDepth;
  const bool public_state_only = metadata_action == first;
  const bool bounded = first_metrics.work <= kMaximumWork &&
                       first_metrics.cache_entries <= kMaximumCacheEntries;
  const bool legal = isLegal(state.board, first);
  const bool theoretical_completion =
      kMaximumWork > kWorstCaseD4Work &&
      kMaximumCacheEntries > kWorstCaseD4CacheEntries;
  const bool passed = completed && deterministic && reflection_safe &&
                      public_state_only && bounded && legal &&
                      theoretical_completion;
  output << "EXACT_DEPTH4_SELF_TEST {\"passed\":"
         << (passed ? "true" : "false")
         << ",\"completedD4\":" << (completed ? "true" : "false")
         << ",\"deterministic\":"
         << (deterministic ? "true" : "false")
         << ",\"reflectionSafe\":"
         << (reflection_safe ? "true" : "false")
         << ",\"publicStateOnly\":"
         << (public_state_only ? "true" : "false")
         << ",\"bounded\":" << (bounded ? "true" : "false")
         << ",\"legal\":" << (legal ? "true" : "false")
         << ",\"canonicalLevelBonus\":" << kLevelBonus
         << ",\"predeclaredMaxWork\":" << kMaximumWork
         << ",\"worstCaseWork\":" << kWorstCaseD4Work
         << ",\"predeclaredMaxCache\":" << kMaximumCacheEntries
         << ",\"worstCaseCache\":" << kWorstCaseD4CacheEntries
         << "}\n";
  return passed;
}

struct ProgramOptions {
  std::string output = "/tmp/drop7-exact-depth4.json";
};

ProgramOptions parseOptions(int argc, char** argv, int first_argument) {
  ProgramOptions options;
  for (int index = first_argument; index < argc; ++index) {
    if (index + 1 >= argc) {
      throw std::invalid_argument("missing option value");
    }
    const std::string argument = argv[index++];
    if (argument == "--output") {
      options.output = argv[index];
    } else {
      throw std::invalid_argument("unknown option " + argument);
    }
  }
  return options;
}

int benchmark(const ProgramOptions& options, std::ostream& output) {
  const auto started = std::chrono::steady_clock::now();
  const Cohort screen = runCohort(kScreenSeedStart, kScreenGames, "screen");
  const Summary screen_depth3 = summarize(screen.depth3, kBaselineDepth);
  const Summary screen_depth4 = summarize(screen.depth4, kCandidateDepth);
  const PairedSummary screen_paired = pairedSummary(screen);
  const bool screen_passed = screen_paired.mean_score_difference > 0.0 &&
                             screen_paired.mean_move_difference > 0.0;

  Cohort confirmation;
  Summary confirmation_depth3;
  Summary confirmation_depth4;
  PairedSummary confirmation_paired;
  bool confirmed = false;
  if (screen_passed) {
    confirmation = runCohort(kConfirmationSeedStart, kConfirmationGames,
                             "confirmation");
    confirmation_depth3 =
        summarize(confirmation.depth3, kBaselineDepth);
    confirmation_depth4 =
        summarize(confirmation.depth4, kCandidateDepth);
    confirmation_paired = pairedSummary(confirmation);
    confirmed = confirmation_paired.mean_score_difference > 0.0 &&
                confirmation_paired.mean_move_difference > 0.0;
  }
  const double elapsed_seconds = std::chrono::duration<double>(
                                     std::chrono::steady_clock::now() -
                                     started)
                                     .count();
  std::ofstream artifact(options.output);
  if (!artifact) throw std::runtime_error("could not open d4 artifact");
  artifact << std::setprecision(10)
           << "{\n  \"format\": \"drop7-exact-depth4-v1\",\n"
           << "  \"trainingSeedOnly\": true,\n"
           << "  \"publicStateOnly\": true,\n"
           << "  \"levelBonus\": " << kLevelBonus << ",\n"
           << "  \"chanceSamples\": " << kChanceSamples << ",\n"
           << "  \"fullWidth\": true,\n"
           << "  \"phaseLeaf\": true,\n"
           << "  \"maximumWork\": " << kMaximumWork << ",\n"
           << "  \"worstCaseD4Work\": " << kWorstCaseD4Work << ",\n"
           << "  \"maximumCacheEntries\": " << kMaximumCacheEntries
           << ",\n  \"worstCaseD4CacheEntries\": "
           << kWorstCaseD4CacheEntries
           << ",\n  \"maximumMoves\": " << kMaximumMoves
           << ",\n  \"parallelism\": " << kParallelism
           << ",\n  \"screenSeedStart\": " << kScreenSeedStart
           << ",\n  \"screen\": {\"exactD3\":";
  writeSummary(artifact, screen_depth3);
  artifact << ",\"exactD4\":";
  writeSummary(artifact, screen_depth4);
  artifact << ",\"paired\":";
  writePaired(artifact, screen_paired);
  artifact << ",\"d3Trajectories\":";
  writeTrajectories(artifact, screen.depth3);
  artifact << ",\"d4Trajectories\":";
  writeTrajectories(artifact, screen.depth4);
  artifact << "},\n  \"screenPassed\": "
           << (screen_passed ? "true" : "false")
           << ",\n  \"confirmation\": ";
  if (!screen_passed) {
    artifact << "null";
  } else {
    artifact << "{\"seedStart\":" << kConfirmationSeedStart
             << ",\"exactD3\":";
    writeSummary(artifact, confirmation_depth3);
    artifact << ",\"exactD4\":";
    writeSummary(artifact, confirmation_depth4);
    artifact << ",\"paired\":";
    writePaired(artifact, confirmation_paired);
    artifact << ",\"d3Trajectories\":";
    writeTrajectories(artifact, confirmation.depth3);
    artifact << ",\"d4Trajectories\":";
    writeTrajectories(artifact, confirmation.depth4);
    artifact << '}';
  }
  artifact << ",\n  \"confirmed\": " << (confirmed ? "true" : "false")
           << ",\n  \"decision\": \""
           << (!screen_passed
                   ? "reject-screen"
                   : (confirmed ? "advance" : "reject-confirmation"))
           << "\",\n  \"elapsedSeconds\": " << elapsed_seconds << "\n}\n";

  output << std::fixed << std::setprecision(3)
         << "EXACT_DEPTH4_RESULT {\"screenD3Score\":"
         << screen_depth3.mean_score
         << ",\"screenD3Moves\":" << screen_depth3.mean_moves
         << ",\"screenD4Score\":" << screen_depth4.mean_score
         << ",\"screenD4Moves\":" << screen_depth4.mean_moves
         << ",\"screenScoreDifference\":"
         << screen_paired.mean_score_difference
         << ",\"screenMoveDifference\":"
         << screen_paired.mean_move_difference
         << ",\"screenPassed\":"
         << (screen_passed ? "true" : "false")
         << ",\"confirmationRan\":"
         << (screen_passed ? "true" : "false")
         << ",\"confirmed\":" << (confirmed ? "true" : "false")
         << ",\"decision\":\""
         << (!screen_passed
                 ? "reject-screen"
                 : (confirmed ? "advance" : "reject-confirmation"))
         << "\",\"elapsedSeconds\":" << elapsed_seconds
         << ",\"peakRssBytes\":" << peakRssBytes()
         << ",\"artifact\":\"" << options.output << "\"}\n";
  return 0;
}

}  // namespace drop7::exact_depth4

int main(int argc, char** argv) {
  try {
    if (argc >= 2 && std::string(argv[1]) == "--self-test") {
      return drop7::exact_depth4::selfTest(std::cout) ? 0 : 1;
    }
    if (argc >= 2 && std::string(argv[1]) == "--benchmark") {
      const auto options = drop7::exact_depth4::parseOptions(argc, argv, 2);
      return drop7::exact_depth4::benchmark(options, std::cout);
    }
    std::cerr << "usage: drop7_exact_depth4 --self-test | "
                 "--benchmark [--output PATH]\n";
    return 2;
  } catch (const std::exception& error) {
    std::cerr << "drop7_exact_depth4: " << error.what() << '\n';
    return EXIT_FAILURE;
  }
}