mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 11:35:42 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dda962679f | ||
|
|
5e265c4845 |
@@ -1,150 +0,0 @@
|
||||
# Race Condition Analysis for Eagle0
|
||||
|
||||
## Summary
|
||||
This document provides an analysis of potential race conditions in the Eagle0 codebase after merging the `fix-race` branch and re-adding ThreadPool functionality from PRs 4340, 4342, and 4343.
|
||||
|
||||
## Fixed Issues
|
||||
|
||||
### ✅ ShardokEngine Shared State Issue (FIXED by fix-race branch)
|
||||
The `fix-race` branch successfully addressed a major race condition by changing `ShardokEngine` from being passed as `shared_ptr<ShardokEngine>` to being passed by value (copy).
|
||||
|
||||
**Key changes:**
|
||||
- `BasicLookaheadCalculator` now takes `const ShardokEngine innerEngine` (by value)
|
||||
- `CalcOne` creates a local copy: `auto innerEngine = ShardokEngine(guessedEngine, false)`
|
||||
- All engine method calls changed from `innerEngine->` to `innerEngine.`
|
||||
|
||||
This ensures each thread works with its own independent copy of the engine state, eliminating concurrent access to shared mutable state.
|
||||
|
||||
## Remaining Potential Race Conditions
|
||||
|
||||
### 1. Transposition Table Global Access (HIGH RISK)
|
||||
**Location:** `TranspositionTable.cpp`, global instance `g_transpositionTable`
|
||||
|
||||
**Issues:**
|
||||
- Global shared state accessed by multiple threads simultaneously
|
||||
- Hash collisions possible under high concurrency
|
||||
- Depth-based storage logic when threads work at different depths
|
||||
- Potential ABA problems in compare-and-swap operations
|
||||
|
||||
**Impact:** Most likely culprit for remaining crashes given it was added recently
|
||||
|
||||
### 2. Global Random Generator (MEDIUM RISK)
|
||||
**Location:** `AIScoreCalculator.cpp`
|
||||
```cpp
|
||||
static const auto _averageGenerator = std::make_shared<SequenceRandomGenerator>(_averageSequence);
|
||||
```
|
||||
|
||||
**Issues:**
|
||||
- Shared across all threads
|
||||
- If `SequenceRandomGenerator` isn't thread-safe internally, concurrent access could corrupt state
|
||||
- No synchronization around access to this shared generator
|
||||
|
||||
### 3. ActionPointDistances Cache (MEDIUM RISK)
|
||||
**Location:** `ActionPointDistancesCache.cpp`
|
||||
|
||||
**Issues:**
|
||||
- Thread-local caching mechanism may have synchronization issues
|
||||
- Cache invalidation across threads could be problematic
|
||||
- Global cache updates might race with thread-local cache access
|
||||
|
||||
### 4. Performance Logging Atomics (LOW RISK)
|
||||
**Location:** `AIScoreCalculator.cpp`, `AttackerScorePerformanceLogger`
|
||||
```cpp
|
||||
intervalTime.fetch_add(duration); // duration is a double
|
||||
```
|
||||
|
||||
**Issues:**
|
||||
- Atomic operations on doubles aren't guaranteed lock-free on all platforms
|
||||
- Could cause performance degradation or incorrect metrics
|
||||
|
||||
### 5. ThreadPool Task Ordering (MEDIUM RISK)
|
||||
**Location:** `ThreadPool.hpp`
|
||||
|
||||
**Issues:**
|
||||
- FIFO queue (deque) could have ordering dependencies
|
||||
- Deadline handling might create races if tasks timeout while processing
|
||||
- Session-based metrics collection adds new shared state
|
||||
- Task cancellation and cleanup could race with execution
|
||||
|
||||
### 6. Future Aggregation (LOW RISK)
|
||||
**Location:** `AIScoreCalculator.cpp`, `BestCommandIndex` function
|
||||
|
||||
**Issues:**
|
||||
- Futures collected and results aggregated
|
||||
- Unexpected completion order could cause issues
|
||||
- Error states might not be handled consistently
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Immediate Actions
|
||||
|
||||
1. **Make Random Generators Thread-Local**
|
||||
```cpp
|
||||
thread_local auto t_randomGenerator =
|
||||
std::make_shared<SequenceRandomGenerator>(_averageSequence);
|
||||
```
|
||||
|
||||
2. **Add Defensive Checks**
|
||||
- Validate game state after engine operations
|
||||
- Check for NaN/infinity before storing scores
|
||||
- Assert transposition table entry consistency
|
||||
|
||||
3. **Improve Transposition Table Locking**
|
||||
- Consider sharding with separate locks per shard
|
||||
- Investigate lock-free data structures
|
||||
- Add more granular locking around critical sections
|
||||
|
||||
### Investigation Steps
|
||||
|
||||
1. **Run Thread Sanitizer**
|
||||
```bash
|
||||
bazel test --config=tsan //src/test/cpp/net/eagle0/shardok/ai:all
|
||||
```
|
||||
|
||||
2. **Add Detailed Logging**
|
||||
- Log all transposition table store/probe operations with thread IDs
|
||||
- Track random generator access patterns
|
||||
- Monitor ThreadPool task lifecycle
|
||||
|
||||
3. **Verify Thread Safety**
|
||||
- Audit `SequenceRandomGenerator` for thread safety
|
||||
- Review `ShardokEngine` copy constructor for deep copy completeness
|
||||
- Check all global/static variables for proper synchronization
|
||||
|
||||
### Long-term Improvements
|
||||
|
||||
1. **Redesign Transposition Table**
|
||||
- Implement thread-local transposition tables with periodic merging
|
||||
- Use concurrent hash map implementation (e.g., Intel TBB concurrent_hash_map)
|
||||
- Add versioning to prevent ABA problems
|
||||
|
||||
2. **Eliminate Global State**
|
||||
- Pass random generators explicitly rather than using globals
|
||||
- Consider dependency injection for caches and tables
|
||||
- Make performance loggers thread-local
|
||||
|
||||
3. **Improve ThreadPool Robustness**
|
||||
- Add task dependency tracking
|
||||
- Implement proper cancellation tokens
|
||||
- Add timeout recovery mechanisms
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
1. **Stress Testing**
|
||||
- Run AI calculations with many threads simultaneously
|
||||
- Use different random seeds to expose timing-dependent bugs
|
||||
- Test with various game states and board configurations
|
||||
|
||||
2. **Reproducibility**
|
||||
- Add deterministic mode with fixed seeds
|
||||
- Log thread scheduling information
|
||||
- Create minimal test cases that reproduce crashes
|
||||
|
||||
3. **Monitoring**
|
||||
- Add metrics for lock contention
|
||||
- Track task completion times and timeout rates
|
||||
- Monitor memory usage patterns
|
||||
|
||||
## Conclusion
|
||||
|
||||
While the `fix-race` branch addressed the critical ShardokEngine shared state issue, several race condition risks remain. The transposition table and global random generator are the most likely culprits for any remaining crashes. Implementing the recommended immediate actions should significantly improve stability, while the long-term improvements will make the system more robust and maintainable.
|
||||
@@ -0,0 +1,177 @@
|
||||
# Transposition Table Pruning Optimization
|
||||
|
||||
## Overview
|
||||
This document describes an optimization to prune duplicate game states during AI search, preventing redundant exploration of positions we've already seen in the current search path.
|
||||
|
||||
## The Problem
|
||||
Currently, when the AI encounters the same game state through different move sequences (a transposition), it may explore the same subtree multiple times. This wastes computational resources.
|
||||
|
||||
## The Solution
|
||||
Implement **transposition pruning** - when we encounter a game state that's already in our current search path, we immediately return without further exploration.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. Add Path Tracking
|
||||
We need to track which game states are currently being explored in the search tree:
|
||||
|
||||
```cpp
|
||||
// Add to AIScoreCalculator.cpp
|
||||
thread_local std::unordered_set<uint64_t> t_currentSearchPath;
|
||||
|
||||
// RAII helper to manage path tracking
|
||||
class SearchPathGuard {
|
||||
uint64_t hash;
|
||||
bool added;
|
||||
public:
|
||||
SearchPathGuard(uint64_t h) : hash(h), added(false) {
|
||||
auto [_, inserted] = t_currentSearchPath.insert(hash);
|
||||
added = inserted;
|
||||
}
|
||||
~SearchPathGuard() {
|
||||
if (added) {
|
||||
t_currentSearchPath.erase(hash);
|
||||
}
|
||||
}
|
||||
bool wasAlreadyInPath() const { return !added; }
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Modify BasicLookaheadCalculator
|
||||
Add duplicate detection at the start of the function:
|
||||
|
||||
```cpp
|
||||
auto BasicLookaheadCalculator(...) {
|
||||
// Get hash of current state
|
||||
uint64_t stateHash = hashGameState(innerEngine->GetCurrentGameState());
|
||||
|
||||
// Check if we're already exploring this state
|
||||
SearchPathGuard pathGuard(stateHash);
|
||||
if (pathGuard.wasAlreadyInPath()) {
|
||||
// State is already being explored - return neutral value
|
||||
std::promise<ScoreValue> p;
|
||||
p.set_value(0.0); // Or return current evaluation
|
||||
return p.get_future();
|
||||
}
|
||||
|
||||
// Continue with existing transposition table check...
|
||||
auto cachedScore = g_transpositionTable.probe(...);
|
||||
// ... rest of function
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Modify CalcOne
|
||||
Similar check when creating new engine states:
|
||||
|
||||
```cpp
|
||||
auto CalcOne(...) {
|
||||
auto innerEngine = std::make_shared<ShardokEngine>(guessedEngine, false);
|
||||
innerEngine->PostCommand(pid, commandIndex, randomGenerator);
|
||||
|
||||
// Check for duplicate state after applying move
|
||||
uint64_t stateHash = hashGameState(innerEngine->GetCurrentGameState());
|
||||
if (t_currentSearchPath.count(stateHash) > 0) {
|
||||
// This move leads to a state we're already exploring
|
||||
std::promise<ScoreValue> p;
|
||||
p.set_value(AIScoreCalculator::GuessedStateScore(...)); // Return static eval
|
||||
returnValue.lookaheadScore = p.get_future();
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
// Continue with normal evaluation...
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Add Metrics
|
||||
Track how often pruning occurs:
|
||||
|
||||
```cpp
|
||||
struct PruningStats {
|
||||
std::atomic<uint64_t> duplicatesDetected{0};
|
||||
std::atomic<uint64_t> branchesPruned{0};
|
||||
std::atomic<uint64_t> nodesExplored{0};
|
||||
|
||||
void print() const {
|
||||
printf("Pruning Stats: %lu duplicates, %lu pruned, %.2f%% pruning rate\n",
|
||||
duplicatesDetected.load(), branchesPruned.load(),
|
||||
100.0 * branchesPruned.load() / nodesExplored.load());
|
||||
}
|
||||
};
|
||||
|
||||
static PruningStats g_pruningStats;
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Reduced Computation**: Avoid exploring identical positions multiple times
|
||||
2. **Better Depth**: Can search deeper with the same time budget
|
||||
3. **Cache Efficiency**: Better use of transposition table space
|
||||
4. **Deterministic Results**: More consistent evaluations
|
||||
|
||||
## Potential Issues
|
||||
|
||||
1. **Hash Collisions**: Need robust hashing to avoid false positives
|
||||
2. **Thread Safety**: Path tracking must be thread-local
|
||||
3. **Memory Usage**: Set of visited states grows with search depth
|
||||
4. **Evaluation Consistency**: Need to handle different depths appropriately
|
||||
|
||||
## Alternative Approaches
|
||||
|
||||
### Option 1: Store "In Progress" Flag in Transposition Table
|
||||
Instead of a separate set, mark entries in the transposition table as "currently being explored":
|
||||
|
||||
```cpp
|
||||
struct TTEntry {
|
||||
// ... existing fields ...
|
||||
std::atomic<bool> in_progress; // Flag for current exploration
|
||||
std::atomic<std::thread::id> exploring_thread; // Which thread is exploring
|
||||
};
|
||||
```
|
||||
|
||||
### Option 2: Depth-Limited Path Tracking
|
||||
Only track states from the last N moves to limit memory usage:
|
||||
|
||||
```cpp
|
||||
thread_local std::deque<uint64_t> t_recentStates;
|
||||
constexpr size_t MAX_PATH_HISTORY = 10;
|
||||
```
|
||||
|
||||
### Option 3: Bloom Filter for Approximate Detection
|
||||
Use a Bloom filter for memory-efficient approximate duplicate detection:
|
||||
|
||||
```cpp
|
||||
class BloomFilter {
|
||||
std::bitset<65536> filter;
|
||||
// Multiple hash functions for low false positive rate
|
||||
};
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **Correctness Tests**:
|
||||
- Verify same evaluation with and without pruning
|
||||
- Test with known transposition-heavy positions
|
||||
- Check thread safety with concurrent searches
|
||||
|
||||
2. **Performance Tests**:
|
||||
- Measure nodes explored with/without pruning
|
||||
- Time to depth comparisons
|
||||
- Memory usage monitoring
|
||||
|
||||
3. **Regression Tests**:
|
||||
- Ensure no degradation in playing strength
|
||||
- Verify deterministic behavior
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
1. **Phase 1**: Basic path tracking with thread-local set
|
||||
2. **Phase 2**: Add metrics and logging
|
||||
3. **Phase 3**: Optimize memory usage if needed
|
||||
4. **Phase 4**: Consider more sophisticated approaches if beneficial
|
||||
|
||||
## Expected Impact
|
||||
|
||||
Based on typical game tree structures, we expect:
|
||||
- 10-30% reduction in nodes explored
|
||||
- 15-25% increase in achievable search depth
|
||||
- Minimal memory overhead (< 1MB per thread)
|
||||
- More consistent move selection in transposition-heavy positions
|
||||
@@ -0,0 +1,90 @@
|
||||
# Testing the Transposition Pruning Optimization
|
||||
|
||||
## What We've Implemented
|
||||
|
||||
The transposition pruning optimization adds the following features to AIScoreCalculator:
|
||||
|
||||
1. **Thread-local path tracking** (`t_currentSearchPath`) - tracks game state hashes currently being explored
|
||||
2. **SearchPathGuard** - RAII class to automatically manage path insertion/removal
|
||||
3. **Duplicate detection** in both `BasicLookaheadCalculator` and `CalcOne`
|
||||
4. **Pruning statistics** - tracks how often duplicates are detected and branches are pruned
|
||||
5. **Public API functions**:
|
||||
- `resetSearchPath()` - clear path at start of search
|
||||
- `printPruningStats()` - display statistics
|
||||
- `resetPruningStats()` - reset counters
|
||||
|
||||
## How It Works
|
||||
|
||||
### In BasicLookaheadCalculator:
|
||||
```cpp
|
||||
// Check if we're already exploring this state
|
||||
SearchPathGuard pathGuard(stateHash);
|
||||
if (pathGuard.wasAlreadyInPath()) {
|
||||
// Return current utility, increment pruning stats
|
||||
return future_with_current_utility;
|
||||
}
|
||||
// Continue with normal transposition table check and search...
|
||||
```
|
||||
|
||||
### In CalcOne:
|
||||
```cpp
|
||||
// After applying a move, check if resulting state is already being explored
|
||||
if (t_currentSearchPath.count(newStateHash) > 0) {
|
||||
// Return static evaluation, don't search further
|
||||
return static_evaluation;
|
||||
}
|
||||
// Continue with normal lookahead search...
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Prevents infinite loops** - cycles in the game tree are detected and cut short
|
||||
2. **Reduces redundant computation** - same positions aren't explored multiple times
|
||||
3. **Enables deeper search** - saved time can be used for exploring new positions
|
||||
4. **Maintains correctness** - returns reasonable evaluations (current/static eval) for pruned branches
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
The optimization includes comprehensive statistics:
|
||||
- `duplicatesDetected` - how many times we found a duplicate state
|
||||
- `branchesPruned` - how many subtrees were cut short
|
||||
- `nodesExplored` - total nodes considered (for pruning rate calculation)
|
||||
|
||||
## Thread Safety
|
||||
|
||||
- Uses `thread_local` storage for path tracking, so each thread has its own search path
|
||||
- No synchronization needed between threads
|
||||
- Statistics use atomic counters for safe concurrent updates
|
||||
|
||||
## Usage
|
||||
|
||||
To use the optimization:
|
||||
|
||||
```cpp
|
||||
// At start of AI search
|
||||
AIScoreCalculator::resetSearchPath();
|
||||
AIScoreCalculator::resetPruningStats();
|
||||
|
||||
// Run normal AI calculations...
|
||||
auto score = AIScoreCalculator::CommandScore(...);
|
||||
|
||||
// At end of search
|
||||
AIScoreCalculator::printPruningStats();
|
||||
```
|
||||
|
||||
## Expected Results
|
||||
|
||||
In game positions with transpositions (same position reachable via different move sequences), we should see:
|
||||
- Non-zero `duplicatesDetected` count
|
||||
- Significant pruning rate (5-20% in complex positions)
|
||||
- No change in final move selection quality
|
||||
- Potentially faster search times or deeper achievable search depths
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **Correctness**: Run existing tests to ensure no regressions
|
||||
2. **Functionality**: Create positions known to have transpositions
|
||||
3. **Performance**: Measure search time and depth with/without optimization
|
||||
4. **Statistics**: Verify counters increment appropriately
|
||||
|
||||
The optimization is conservative - it only prunes when absolutely safe (duplicate state in current path) and returns reasonable fallback evaluations.
|
||||
@@ -1,39 +0,0 @@
|
||||
//
|
||||
// TaskResult.hpp - Result wrapper for task execution with status information
|
||||
//
|
||||
|
||||
#ifndef EAGLE0_TASK_RESULT_HPP
|
||||
#define EAGLE0_TASK_RESULT_HPP
|
||||
|
||||
namespace eagle0::common {
|
||||
|
||||
enum class TaskStatus { SUCCESS = 0, DEADLINE_EXCEEDED = 1, CANCELLED = 2 };
|
||||
|
||||
template<typename T>
|
||||
struct TaskResult {
|
||||
T value;
|
||||
TaskStatus status;
|
||||
|
||||
TaskResult() : value{}, status(TaskStatus::SUCCESS) {}
|
||||
TaskResult(T val) : value(std::move(val)), status(TaskStatus::SUCCESS) {}
|
||||
TaskResult(T val, TaskStatus stat) : value(std::move(val)), status(stat) {}
|
||||
|
||||
// Convenience methods for checking status
|
||||
T get() const { return value; }
|
||||
bool succeeded() const { return status == TaskStatus::SUCCESS; }
|
||||
bool deadlineExceeded() const { return status == TaskStatus::DEADLINE_EXCEEDED; }
|
||||
bool cancelled() const { return status == TaskStatus::CANCELLED; }
|
||||
|
||||
// Factory methods for cleaner construction
|
||||
static TaskResult Success(T val) { return TaskResult(std::move(val), TaskStatus::SUCCESS); }
|
||||
static TaskResult DeadlineExceeded(T val = T{}) {
|
||||
return TaskResult(std::move(val), TaskStatus::DEADLINE_EXCEEDED);
|
||||
}
|
||||
static TaskResult Cancelled(T val = T{}) {
|
||||
return TaskResult(std::move(val), TaskStatus::CANCELLED);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace eagle0::common
|
||||
|
||||
#endif // EAGLE0_TASK_RESULT_HPP
|
||||
@@ -8,11 +8,11 @@
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
@@ -45,18 +45,38 @@ public:
|
||||
private:
|
||||
struct Task {
|
||||
std::function<void()> function;
|
||||
int priority;
|
||||
TimePoint deadline;
|
||||
bool has_deadline;
|
||||
|
||||
Task(std::function<void()> f, TimePoint d, bool has_d)
|
||||
Task(std::function<void()> f, int p, TimePoint d, bool has_d)
|
||||
: function(std::move(f)),
|
||||
priority(p),
|
||||
deadline(d),
|
||||
has_deadline(has_d) {}
|
||||
|
||||
// Higher priority values and earlier deadlines have higher priority
|
||||
bool operator<(const Task& other) const {
|
||||
if (priority != other.priority) {
|
||||
return priority < other.priority; // Lower priority values have lower priority in
|
||||
// priority_queue
|
||||
}
|
||||
if (has_deadline && other.has_deadline) {
|
||||
return deadline > other.deadline; // Later deadlines have lower priority
|
||||
}
|
||||
if (has_deadline && !other.has_deadline) {
|
||||
return false; // Tasks with deadlines have higher priority
|
||||
}
|
||||
if (!has_deadline && other.has_deadline) {
|
||||
return true; // Tasks without deadlines have lower priority
|
||||
}
|
||||
return false; // Equal priority, no preference
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::thread> workers;
|
||||
std::deque<Task> tasks; // Simple FIFO queue instead of priority queue
|
||||
mutable std::mutex queue_mutex; // mutable for const methods like queue_size()
|
||||
std::priority_queue<Task> tasks;
|
||||
std::mutex queue_mutex;
|
||||
std::condition_variable condition;
|
||||
std::atomic<bool> stop{false};
|
||||
|
||||
@@ -65,7 +85,7 @@ public:
|
||||
for (size_t i = 0; i < num_threads; ++i) {
|
||||
workers.emplace_back([this] {
|
||||
while (true) {
|
||||
Task task{nullptr, TimePoint{}, false};
|
||||
Task task{nullptr, 0, TimePoint{}, false};
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(queue_mutex);
|
||||
condition.wait(lock, [this] { return stop.load() || !tasks.empty(); });
|
||||
@@ -73,8 +93,8 @@ public:
|
||||
if (stop.load() && tasks.empty()) { return; }
|
||||
|
||||
if (!tasks.empty()) {
|
||||
task = std::move(tasks.front());
|
||||
tasks.pop_front();
|
||||
task = std::move(const_cast<Task&>(tasks.top()));
|
||||
tasks.pop();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
@@ -87,9 +107,9 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// Enqueue a task without deadline
|
||||
// Enqueue a task with priority only
|
||||
template<class F, class... Args>
|
||||
auto enqueue(F&& f, Args&&... args)
|
||||
auto enqueue(F&& f, Args&&... args, int priority = 0)
|
||||
-> std::future<TaskResult<std::invoke_result_t<F, Args...>>> {
|
||||
using return_type = std::invoke_result_t<F, Args...>;
|
||||
using result_type = TaskResult<return_type>;
|
||||
@@ -106,21 +126,21 @@ public:
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(queue_mutex);
|
||||
if (stop.load()) { throw std::runtime_error("enqueue on stopped ThreadPool"); }
|
||||
tasks.emplace_back([task]() { (*task)(); }, TimePoint{}, false);
|
||||
tasks.emplace([task]() { (*task)(); }, priority, TimePoint{}, false);
|
||||
}
|
||||
|
||||
condition.notify_one();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Enqueue a task with deadline
|
||||
template<class F>
|
||||
auto enqueue_with_deadline(F&& f, TimePoint deadline)
|
||||
-> std::future<TaskResult<std::invoke_result_t<F>>> {
|
||||
using return_type = std::invoke_result_t<F>;
|
||||
// Enqueue a task with priority and deadline
|
||||
template<class F, class... Args>
|
||||
auto enqueue_with_deadline(F&& f, Args&&... args, int priority, TimePoint deadline)
|
||||
-> std::future<TaskResult<std::invoke_result_t<F, Args...>>> {
|
||||
using return_type = std::invoke_result_t<F, Args...>;
|
||||
using result_type = TaskResult<return_type>;
|
||||
|
||||
auto actualTask = std::forward<F>(f);
|
||||
auto actualTask = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
|
||||
|
||||
auto task = std::make_shared<std::packaged_task<result_type()>>(
|
||||
[actualTask = std::move(actualTask), deadline]() mutable -> result_type {
|
||||
@@ -135,7 +155,7 @@ public:
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(queue_mutex);
|
||||
if (stop.load()) { throw std::runtime_error("enqueue on stopped ThreadPool"); }
|
||||
tasks.emplace_back([task]() { (*task)(); }, deadline, true);
|
||||
tasks.emplace([task]() { (*task)(); }, priority, deadline, true);
|
||||
}
|
||||
|
||||
condition.notify_one();
|
||||
@@ -144,27 +164,25 @@ public:
|
||||
|
||||
// Get current queue size (approximate, for monitoring)
|
||||
size_t queue_size() const {
|
||||
std::unique_lock<std::mutex> lock(queue_mutex);
|
||||
std::unique_lock<std::mutex> lock(const_cast<std::mutex&>(queue_mutex));
|
||||
return tasks.size();
|
||||
}
|
||||
|
||||
// Get detailed queue information for debugging
|
||||
void debug_queue_state() const {
|
||||
std::unique_lock<std::mutex> lock(queue_mutex);
|
||||
std::unique_lock<std::mutex> lock(const_cast<std::mutex&>(queue_mutex));
|
||||
printf("ThreadPool: Queue size: %zu\n", tasks.size());
|
||||
if (!tasks.empty()) {
|
||||
int with_deadline = 0;
|
||||
int without_deadline = 0;
|
||||
for (const auto& task : tasks) {
|
||||
if (task.has_deadline) {
|
||||
with_deadline++;
|
||||
} else {
|
||||
without_deadline++;
|
||||
}
|
||||
// Create a copy to inspect priorities without modifying queue
|
||||
auto queue_copy = tasks;
|
||||
std::vector<int> priorities;
|
||||
while (!queue_copy.empty()) {
|
||||
priorities.push_back(queue_copy.top().priority);
|
||||
queue_copy.pop();
|
||||
}
|
||||
printf("ThreadPool: Tasks with deadline: %d, without deadline: %d\n",
|
||||
with_deadline,
|
||||
without_deadline);
|
||||
printf("ThreadPool: Priorities in queue: ");
|
||||
for (int p : priorities) { printf("%d ", p); }
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <future>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "TranspositionTable.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/SequenceRandomGenerator.hpp"
|
||||
@@ -45,7 +46,7 @@ struct ImmediateAndLookaheadScore {
|
||||
bool isDefender,
|
||||
int remainingLookahead,
|
||||
int maxRepeatCount,
|
||||
const ShardokEngine innerEngine,
|
||||
const shared_ptr<ShardokEngine> &innerEngine,
|
||||
ScoreValue currentUtility,
|
||||
const AIStrategy &attackerStrategy,
|
||||
const SettingsGetter &settingsGetter,
|
||||
@@ -251,6 +252,53 @@ using Unit = fb::Unit;
|
||||
static const std::vector _averageSequence = {0.5};
|
||||
static const auto _averageGenerator = std::make_shared<SequenceRandomGenerator>(_averageSequence);
|
||||
|
||||
// Thread-local tracking of game states currently being explored in the search tree
|
||||
// This prevents infinite loops and redundant exploration of transpositions
|
||||
thread_local std::unordered_set<uint64_t> t_currentSearchPath;
|
||||
|
||||
// RAII helper to manage search path tracking
|
||||
class SearchPathGuard {
|
||||
uint64_t hash;
|
||||
bool added;
|
||||
|
||||
public:
|
||||
SearchPathGuard(uint64_t h) : hash(h), added(false) {
|
||||
auto [_, inserted] = t_currentSearchPath.insert(hash);
|
||||
added = inserted;
|
||||
}
|
||||
~SearchPathGuard() {
|
||||
if (added) { t_currentSearchPath.erase(hash); }
|
||||
}
|
||||
bool wasAlreadyInPath() const { return !added; }
|
||||
};
|
||||
|
||||
// Statistics for transposition pruning optimization
|
||||
struct PruningStats {
|
||||
std::atomic<uint64_t> duplicatesDetected{0};
|
||||
std::atomic<uint64_t> branchesPruned{0};
|
||||
std::atomic<uint64_t> nodesExplored{0};
|
||||
|
||||
void print() const {
|
||||
uint64_t explored = nodesExplored.load();
|
||||
uint64_t pruned = branchesPruned.load();
|
||||
if (explored > 0) {
|
||||
printf("Transposition Pruning: %llu duplicates detected, %llu branches pruned (%.2f%% "
|
||||
"pruning rate)\n",
|
||||
(unsigned long long)duplicatesDetected.load(),
|
||||
(unsigned long long)pruned,
|
||||
100.0 * pruned / explored);
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
duplicatesDetected = 0;
|
||||
branchesPruned = 0;
|
||||
nodesExplored = 0;
|
||||
}
|
||||
};
|
||||
|
||||
static PruningStats g_pruningStats;
|
||||
|
||||
static auto CommandSorter(const IndexAndScore &l, const IndexAndScore &r) -> bool {
|
||||
if (l.lookaheadScore < r.lookaheadScore) return true;
|
||||
if (l.lookaheadScore > r.lookaheadScore) return false;
|
||||
@@ -913,7 +961,7 @@ auto BasicLookaheadCalculator(
|
||||
const bool isDefender,
|
||||
const int remainingLookahead,
|
||||
const int maxRepeatCount,
|
||||
const ShardokEngine innerEngine,
|
||||
const shared_ptr<ShardokEngine> &innerEngine,
|
||||
const ScoreValue currentUtility,
|
||||
const AIStrategy &attackerStrategy,
|
||||
const SettingsGetter &settingsGetter,
|
||||
@@ -921,9 +969,27 @@ auto BasicLookaheadCalculator(
|
||||
const APDCache &apdCache,
|
||||
const ALCache &alCache,
|
||||
std::chrono::steady_clock::time_point deadline) -> std::future<ScoreValue> {
|
||||
g_pruningStats.nodesExplored++;
|
||||
|
||||
// Get hash of current state for duplicate detection
|
||||
uint64_t stateHash = innerEngine->GetCurrentGameState().ComputeFNV1aHash();
|
||||
|
||||
// Check if we're already exploring this state in the current search path
|
||||
SearchPathGuard pathGuard(stateHash);
|
||||
if (pathGuard.wasAlreadyInPath()) {
|
||||
// This state is already being explored higher up in the search tree
|
||||
// Return the current utility to avoid infinite loops and redundant work
|
||||
g_pruningStats.duplicatesDetected++;
|
||||
g_pruningStats.branchesPruned++;
|
||||
|
||||
std::promise<ScoreValue> p;
|
||||
p.set_value(currentUtility); // Return current evaluation
|
||||
return p.get_future();
|
||||
}
|
||||
|
||||
// Check transposition table before expensive computation
|
||||
auto cachedScore =
|
||||
g_transpositionTable.probe(innerEngine.GetCurrentGameState(), remainingLookahead, pid);
|
||||
g_transpositionTable.probe(innerEngine->GetCurrentGameState(), remainingLookahead, pid);
|
||||
|
||||
if (cachedScore.has_value()) {
|
||||
// Return cached result immediately
|
||||
@@ -938,14 +1004,14 @@ auto BasicLookaheadCalculator(
|
||||
// Store the current utility in the transposition table and return it
|
||||
// Note: Store with depth 1 since depth 0 indicates an empty entry in the transposition
|
||||
// table
|
||||
g_transpositionTable.store(innerEngine.GetCurrentGameState(), 1, pid, nextUtility);
|
||||
g_transpositionTable.store(innerEngine->GetCurrentGameState(), 1, pid, nextUtility);
|
||||
|
||||
std::promise<ScoreValue> p;
|
||||
p.set_value(nextUtility);
|
||||
return p.get_future();
|
||||
}
|
||||
|
||||
if (const CommandListSPtr nextCommands = innerEngine.GetAvailableCommandsForAIPlayer(pid);
|
||||
if (const CommandListSPtr nextCommands = innerEngine->GetAvailableCommandsForAIPlayer(pid);
|
||||
nextCommands && !nextCommands->empty()) {
|
||||
// Get the future from BestCommandIndex without calling .get()
|
||||
auto bestCommandFuture = BestCommandIndex(
|
||||
@@ -953,7 +1019,7 @@ auto BasicLookaheadCalculator(
|
||||
isDefender,
|
||||
remainingLookahead - 1,
|
||||
maxRepeatCount,
|
||||
innerEngine,
|
||||
*innerEngine,
|
||||
attackerStrategy,
|
||||
nextUtility,
|
||||
settingsGetter,
|
||||
@@ -975,7 +1041,7 @@ auto BasicLookaheadCalculator(
|
||||
|
||||
ScoreValue resultScore;
|
||||
if (auto &nextCommand =
|
||||
innerEngine.GetAvailableCommandsForAIPlayer(pid)->at(index);
|
||||
innerEngine->GetAvailableCommandsForAIPlayer(pid)->at(index);
|
||||
nextCommand->GetCommandType() !=
|
||||
net::eagle0::shardok::common::END_TURN_COMMAND) {
|
||||
resultScore = immediateScore;
|
||||
@@ -985,7 +1051,7 @@ auto BasicLookaheadCalculator(
|
||||
|
||||
// Store in transposition table before returning
|
||||
g_transpositionTable.store(
|
||||
innerEngine.GetCurrentGameState(),
|
||||
innerEngine->GetCurrentGameState(),
|
||||
remainingLookahead,
|
||||
pid,
|
||||
resultScore);
|
||||
@@ -996,7 +1062,7 @@ auto BasicLookaheadCalculator(
|
||||
|
||||
// No commands available, store and return the current utility as a future
|
||||
g_transpositionTable
|
||||
.store(innerEngine.GetCurrentGameState(), remainingLookahead, pid, nextUtility);
|
||||
.store(innerEngine->GetCurrentGameState(), remainingLookahead, pid, nextUtility);
|
||||
|
||||
std::promise<ScoreValue> p;
|
||||
p.set_value(nextUtility);
|
||||
@@ -1029,12 +1095,35 @@ auto CalcOne(
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
auto innerEngine = ShardokEngine(guessedEngine, false);
|
||||
innerEngine.PostCommand(pid, commandIndex, randomGenerator);
|
||||
auto innerEngine = std::make_shared<ShardokEngine>(guessedEngine, false);
|
||||
innerEngine->PostCommand(pid, commandIndex, randomGenerator);
|
||||
|
||||
// Check if this move leads to a state we're already exploring
|
||||
uint64_t newStateHash = innerEngine->GetCurrentGameState().ComputeFNV1aHash();
|
||||
if (t_currentSearchPath.count(newStateHash) > 0) {
|
||||
// This move creates a transposition to a state already in our search path
|
||||
// Return the static evaluation to avoid redundant exploration
|
||||
g_pruningStats.duplicatesDetected++;
|
||||
|
||||
auto staticEval = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
innerEngine->GetCurrentGameState(),
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
settingsGetter,
|
||||
apdCache,
|
||||
alCache);
|
||||
|
||||
returnValue.immediateScore = staticEval;
|
||||
std::promise<ScoreValue> p;
|
||||
p.set_value(staticEval);
|
||||
returnValue.lookaheadScore = p.get_future();
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
auto innerUtility = AIScoreCalculator::GuessedStateScore(
|
||||
isDefender,
|
||||
innerEngine.GetCurrentGameState(),
|
||||
innerEngine->GetCurrentGameState(),
|
||||
attackerStrategy,
|
||||
allCastleCoords,
|
||||
settingsGetter,
|
||||
@@ -1475,4 +1564,10 @@ auto EvaluateCommand(
|
||||
return std::move(result.lookaheadScore);
|
||||
}
|
||||
|
||||
void AIScoreCalculator::resetSearchPath() { t_currentSearchPath.clear(); }
|
||||
|
||||
void AIScoreCalculator::printPruningStats() { g_pruningStats.print(); }
|
||||
|
||||
void AIScoreCalculator::resetPruningStats() { g_pruningStats.reset(); }
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -57,6 +57,15 @@ public:
|
||||
const ALCache &alCache,
|
||||
size_t commandIndex,
|
||||
std::chrono::steady_clock::time_point deadline) -> std::future<ScoreValue>;
|
||||
|
||||
// Reset search path for a new search (call at start of each AI search)
|
||||
static void resetSearchPath();
|
||||
|
||||
// Print transposition pruning statistics
|
||||
static void printPruningStats();
|
||||
|
||||
// Reset pruning statistics
|
||||
static void resetPruningStats();
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
+10
-7
@@ -180,12 +180,15 @@ case class SuppressBeastsSucceededPromptGenerator(
|
||||
val provinceName =
|
||||
gameState.provinces(suppressBeastsSucceededMessage.provinceId).name
|
||||
|
||||
val battalionDescription =
|
||||
suppressBeastsSucceededMessage.battalionId
|
||||
.map(gameState.battalions)
|
||||
.map(
|
||||
BattalionDescriptions.descriptionForBattalion
|
||||
)
|
||||
val maybeDestroyedBattalion = suppressBeastsSucceededMessage.battalionId
|
||||
.map(bid =>
|
||||
gameState.battalions
|
||||
.getOrElse(bid, gameState.destroyedBattalions(bid))
|
||||
)
|
||||
|
||||
val battalionDescription = maybeDestroyedBattalion.map(
|
||||
BattalionDescriptions.descriptionForBattalion
|
||||
)
|
||||
|
||||
val actorDescription = battalionDescription
|
||||
.map { battD =>
|
||||
@@ -201,7 +204,7 @@ case class SuppressBeastsSucceededPromptGenerator(
|
||||
|
||||
val casualtyText =
|
||||
if (suppressBeastsSucceededMessage.casualtyCount > 0)
|
||||
s" ${suppressBeastsSucceededMessage.battalionId.map(gameState.battalions).get.name} took ${suppressBeastsSucceededMessage.casualtyCount} casualties."
|
||||
s" ${maybeDestroyedBattalion.get.name} took ${suppressBeastsSucceededMessage.casualtyCount} casualties."
|
||||
else ""
|
||||
|
||||
val command =
|
||||
|
||||
Reference in New Issue
Block a user