Hash Depth Extension

Discussion of chess software programming and technical issues.

Moderator: Ras

QED
Posts: 60
Joined: Thu Nov 05, 2009 9:53 pm

Hash Depth Extension

Post by QED »

Finally, I have been lucky enough to code something that looks like improving stockfish. That raises many questions, so I am going to write them all here.

First, testing questions. I have run 1000 games match between original version and my new version, at 1minute/game, 32M hash, ponder off, 1 thread, with stockfish book, repeating, book depth 40 plies. The result was +202 =633 -165 in favor of my version, bayeselo says that means 96% likelihood of superiority. That could just mean it is 1 of 25 tests of equal strength versions.

Is the draw ratio normal? Or should I use lower "bookdepth" setting (risking duplicate games?)?

Umm, well, the opponent was not exactly the original stockfish. I went under impression that cutechess-cli does not clear hash properly enough between games, so for both versions, I have edited handle_command() in uci.cpp to

Code: Select all

else if (token == "ucinewgame")
    {
        push_button("New Game");
        push_button("Clear Hash");
        Position::init_piece_square_tables();
        RootPosition.from_fen(StartPosition);
    }
and considered it done without testing what it actually does. Or without trying to figure out how to set option with space in its name from cutechess-cli command line.

Test was using 1 of 2 cores, the other core was doing browsing, compiling, testposition analysis, etc. But when I was testing my previous idea, I noticed the result depended dramatically on usage of the other core. Previous idea was heavy on TT store/retrieve and it started to lose badly (from previously quite balanced match) when the other core was also fiddling with memory. I tried to test the final version with other core idle, but since the final version uses TT slightly less than original stockfish, it might be not so good on other hardware as is on my old cheap Core Duo laptop.

So, about the idea. The short name is "Hash Depth Extension", full name is "Transposition Table Replacement Scheme And PV Search Extension Based On Previous Higher Depth Transposition Table Entry". Stockfish does not use TT values for cutoff in pv search, but if there is a deep TT entry that is not compatible with what pv search sees, why not to extend pv search to find the promising truth? And even if TT entry is compatible, we probably do not want to overwrite it with shallower one.

Implementation is far from clean, I was in the "code first, edit later, think even later" mode, and I am still not sure why the final version works and previous versions not. Also, HDE uses a loop in which depth varies, so it should be possible to do Internal Iterative Deepening in this loop too, but it does not test well yet.

For the future, would it be a good idea if I try to write one template function for both search_pv() and search()? You know, less lines of code, doing things the same way (unless explicitly stated otherwise) and so on?

And at last, the code. Only search_pv() is changed, but on several places, so I paste it all:

Code: Select all

  // search_pv() is the main search function for PV nodes.

  Value search_pv(Position& pos, SearchStack ss[], Value oldAlpha, Value beta,
                  Depth oldDepth, int ply, int threadID) {

    assert(alpha >= -VALUE_INFINITE && alpha <= VALUE_INFINITE);
    assert(beta > alpha && beta <= VALUE_INFINITE);
    assert(ply >= 0 && ply < PLY_MAX);
    assert(threadID >= 0 && threadID < TM.active_threads());

    Move movesSearched[256];
    EvalInfo ei;
    StateInfo st;
    const TTEntry* tte;
    Move ttMove, move;
    Depth ttDepth, ext, newDepth, depth = oldDepth;
    Value ttValue, bestValue, value, alpha = oldAlpha;
    ValueType ttType;
    bool isCheck, singleEvasion, moveIsCheck, captureOrPromotion, dangerous;
    bool mateThreat = false;

    if (depth < OnePly)
        return qsearch(pos, ss, alpha, beta, Depth(0), ply, threadID);

    // Step 1. Initialize node and poll
    // Polling can abort search.
    init_node(ss, ply, threadID);

    // Step 2. Check for aborted search and immediate draw
    if (AbortSearch || TM.thread_should_stop(threadID))
        return Value(0);

    if (pos.is_draw() || ply >= PLY_MAX - 1)
        return VALUE_DRAW;

    // Step 3. Mate distance pruning
    alpha = Max(value_mated_in(ply), alpha);
    beta = Min(value_mate_in(ply+1), beta);
    if (alpha >= beta)
        return alpha;

    // Step 4. Transposition table lookup
    // At PV nodes, we don't use the TT for pruning, but only for move ordering.
    // This is to avoid problems in the following areas:
    //
    // * Repetition draw detection
    // * Fifty move rule detection
    // * Searching for a mate
    // * Printing of full PV line
    tte     = TT.retrieve(pos.get_key());
    ttMove  = (tte ? tte->move() : MOVE_NONE);
    ttDepth = (tte ? tte->depth() : Depth(-2));
    ttValue = (tte ? value_from_tt(tte->value(), ply) : Value(0));
    ttType  = (tte ? tte->type() : VALUE_TYPE_EV_LO); // hack, not to replace TT entry

    // Step 5. Evaluate the position statically
    // At PV nodes we do this only to update gain statistics
    isCheck = pos.is_check();
    if (!isCheck)
    {
        ss[ply].eval = evaluate(pos, ei, threadID);
        update_gains(pos, ss[ply - 1].currentMove, ss[ply - 1].eval, ss[ply].eval);
    }

    // Step 6. Razoring (is omitted in PV nodes)
    // Step 7. Static null move pruning (is omitted in PV nodes)
    // Step 8. Null move search with verification search (is omitted in PV nodes)

    // Step 9. Internal iterative deepening
    if (   depth >= IIDDepthAtPVNodes
        && ttMove == MOVE_NONE)
    {
        search_pv(pos, ss, alpha, beta, depth-2*OnePly, ply, threadID);
        ttMove = ss[ply].pv[ply];
        tte = TT.retrieve(pos.get_key());
        ttDepth = (tte ? tte->depth() : Depth(-2));
        ttValue = (tte ? value_from_tt(tte->value(), ply) : Value(0));
        ttType  = (tte ? tte->type() : VALUE_TYPE_EV_LO); // hack, not to replace TT entry
    }

    // Initialize a MovePicker object for the current position
    mateThreat = pos.has_mate_threat(opposite_color(pos.side_to_move()));
    CheckInfo ci(pos);

    // Step extra. HDE loop.
    while(1)
    { // Also, the scope for mp.

        // Check for abort.
        if (AbortSearch || TM.thread_should_stop(threadID))
            return bestValue;

        int moveCount = 0;
        MovePicker mp = MovePicker(pos, ttMove, depth, H, &ss[ply]);
        alpha = oldAlpha;
        bestValue = -VALUE_INFINITE;

        // Step 10. Loop through moves
        // Loop through all legal moves until no moves remain or a beta cutoff occurs
        while (   alpha < beta
               && (move = mp.get_next_move()) != MOVE_NONE
               && !TM.thread_should_stop(threadID))
        {
            assert(move_is_ok(move));

            singleEvasion = (isCheck && mp.number_of_evasions() == 1);
            moveIsCheck = pos.move_is_check(move, ci);
            captureOrPromotion = pos.move_is_capture_or_promotion(move);

            // Step 11. Decide the new search depth
            ext = extension(pos, move, true, captureOrPromotion, moveIsCheck, singleEvasion, mateThreat, &dangerous);

            // Singular extension search. We extend the TT move if its value is much better than
            // its siblings. To verify this we do a reduced search on all the other moves but the
            // ttMove, if result is lower then ttValue minus a margin then we extend ttMove.
            if (   depth >= SingularExtensionDepthAtPVNodes
                && tte
                && move == ttMove
                && ext < OnePly
                && is_lower_bound(ttType)
                && ttDepth >= depth - 3 * OnePly)
            {
                // Value ttValue = value_from_tt(tte->value(), ply);

                if (abs(ttValue) < VALUE_KNOWN_WIN)
                {
                    Value excValue = search(pos, ss, ttValue - SingularExtensionMargin, depth / 2, ply, false, threadID, move);

                    if (excValue < ttValue - SingularExtensionMargin)
                        ext = OnePly;
                }
            }

            newDepth = depth - OnePly + ext;

            // Update current move (this must be done after singular extension search)
            movesSearched[moveCount++] = ss[ply].currentMove = move;

            // Step 12. Futility pruning (is omitted in PV nodes)

            // Step 13. Make the move
            pos.do_move(move, st, ci, moveIsCheck);

            // Step extra. pv search (only in PV nodes)
            // The first move in list is the expected PV
            if (moveCount == 1)
            {
                value = -search_pv(pos, ss, -beta, -alpha, newDepth, ply+1, threadID);
            }
            else
            {
                // Step 14. Reduced search
                // if the move fails high will be re-searched at full depth.
                bool doFullDepthSearch = true;

                if (    depth >= 3 * OnePly
                    && !dangerous
                    && !captureOrPromotion
                    && !move_is_castle(move)
                    && !move_is_killer(move, ss[ply]))
                {
                    ss[ply].reduction = pv_reduction(depth, moveCount);
                    if (ss[ply].reduction)
                    {
                        value = -search(pos, ss, -alpha, newDepth-ss[ply].reduction, ply+1, true, threadID);
                        doFullDepthSearch = (value > alpha);
                    }
                }

                // Step 15. Full depth search
                if (doFullDepthSearch)
                {
                    ss[ply].reduction = Depth(0);
                    value = -search(pos, ss, -alpha, newDepth, ply+1, true, threadID);

                    // Step extra. pv search (only in PV nodes)
                    if (value > alpha && value < beta) // (value > bestValue)
                        value = -search_pv(pos, ss, -beta, -alpha, newDepth, ply+1, threadID);
                }
            }

            // Step 16. Undo move
            pos.undo_move(move);

            assert(value > -VALUE_INFINITE && value < VALUE_INFINITE);

            // Step 17. Check for new best move
            if (value > bestValue)
            {
                bestValue = value;
                update_pv(ss, ply);
                if (value > alpha)
                {
                    alpha = value;
                    if (value == value_mate_in(ply + 1))
                        ss[ply].mateKiller = move;
                }
            }

            // Step 18. Check for split
            if (   TM.active_threads() > 1
                && bestValue < beta
                && depth >= MinimumSplitDepth
                && Iteration <= 99
                && TM.available_thread_exists(threadID)
                && !AbortSearch
                && !TM.thread_should_stop(threadID)
                && TM.split(pos, ss, ply, &alpha, beta, &bestValue,
                            depth, mateThreat, &moveCount, &mp, threadID, true))
                break;

        } // Move list.

        // Step 19. Check for mate and stalemate
        // All legal moves have been searched and if there were
        // no legal moves, it must be mate or stalemate.
        if (moveCount == 0)
            return (isCheck ? value_mated_in(ply) : VALUE_DRAW);

        // Step 20. Update tables
        // If the search is not aborted, update the transposition table,
        // history counters, and killer moves.
        if (AbortSearch || TM.thread_should_stop(threadID))
            return bestValue;

        if (true) // HDE
        {
            if (bestValue <= oldAlpha)
            { // failing low
                // Hash depth extension.
                if (ttDepth > depth && is_lower_bound(ttType) && ttValue > oldAlpha)
                { // Incompatible deeper TT entry.
                    depth = Min(ttDepth, depth+2*OnePly);
                    continue; // The HDE loop with higher depth.
                } // HDE low.

                if (   depth > ttDepth || !is_upper_bound(ttType)
                    || (depth >= ttDepth && ttType != VALUE_TYPE_EXACT))
                { // Storing new entry.
                    TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_UPPER, depth, MOVE_NONE);
                }
            }
            else if (bestValue >= beta)
            { // failing high

                // Hash depth extension.
                if (ttDepth > depth && is_upper_bound(ttType) && ttValue < beta)
                { // Incompatible deeper TT entry.
                    depth = Min(ttDepth, depth+2*OnePly);
                    continue; // The HDE loop with higher depth.
                } // HDE high.

                // Update statistics after HDE check.
                TM.incrementBetaCounter(pos.side_to_move(), depth, threadID);
                move = ss[ply].pv[ply];
                if (!pos.move_is_capture_or_promotion(move))
                {
                    update_history(pos, move, depth, movesSearched, moveCount);
                    update_killers(move, ss[ply]);
                }

                if (   depth > ttDepth || !is_lower_bound(ttType)
                    || (depth >= ttDepth && ttType != VALUE_TYPE_EXACT))
                { // Storing new entry.
                    TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_LOWER, depth, move);
                }
            }
            else
            { // exact score
                // Hash depth extension.
                if (   (ttDepth > depth && is_lower_bound(ttType) && ttValue >= beta)
                    || (ttDepth > depth && is_upper_bound(ttType) && ttValue <= oldAlpha)
                    || (ttDepth > depth && ttType == VALUE_TYPE_EXACT && ttMove != ss[ply].pv[ply]))
                { // Incompatible deeper TT entry.
                    depth = Min(ttDepth, depth+2*OnePly);
                    continue; // The HDE loop with higher depth.
                } // HDE exact.

                if (depth >= ttDepth || ttType != VALUE_TYPE_EXACT)
                { // Storing new entry.
                    TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_EXACT, depth, ss[ply].pv[ply]);
                }
            }
        }
        else // No HDE.
        {
            if (bestValue <= oldAlpha)
                TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_UPPER, depth, MOVE_NONE);

            else if (bestValue >= beta)
            {
                TM.incrementBetaCounter(pos.side_to_move(), depth, threadID);
                move = ss[ply].pv[ply];
                if (!pos.move_is_capture_or_promotion(move))
                {
                    update_history(pos, move, depth, movesSearched, moveCount);
                    update_killers(move, ss[ply]);
                }
                TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_LOWER, depth, move);
            }
            else
                TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_EXACT, depth, ss[ply].pv[ply]);
        }

        break; // Exit from HDE loop.
    } // HDE loop.

    return bestValue;
  }
zamar
Posts: 613
Joined: Sun Jan 18, 2009 7:03 am

Re: Hash Depth Extension

Post by zamar »

QED wrote: Is the draw ratio normal? Or should I use lower "bookdepth" setting (risking duplicate games?)?
Draw ratio is normal
Umm, well, the opponent was not exactly the original stockfish. I went under impression that cutechess-cli does not clear hash properly enough between games, so for both versions, I have edited handle_command() in uci.cpp to

Code: Select all

else if (token == "ucinewgame")
    {
        push_button("New Game");
        push_button("Clear Hash");
        Position::init_piece_square_tables();
        RootPosition.from_fen(StartPosition);
    }
and considered it done without testing what it actually does. Or without trying to figure out how to set option with space in its name from cutechess-cli command line.
Looks alright.
Test was using 1 of 2 cores, the other core was doing browsing, compiling, testposition analysis, etc.
AARGGH! Sorry to say, but in my experience this makes the result useless to conclude anything. Now please repeat the 1000 game test and make sure your computer does nothing else CPU-intensive but runs the match.

If you can repeat the result, then I'm starting to believe in your patch, otherwise I'm very sceptical.
Joona Kiiski
mcostalba
Posts: 2684
Joined: Sat Jun 14, 2008 9:17 pm

Re: Hash Depth Extension

Post by mcostalba »

QED wrote: Is the draw ratio normal? Or should I use lower "bookdepth" setting (risking duplicate games?)?
The important thing is that engines go out of the book in more or less equally balanced positions.
QED wrote: For the future, would it be a good idea if I try to write one template function for both search_pv() and search()? You know, less lines of code, doing things the same way (unless explicitly stated otherwise) and so on?
We have already done it ;-) it is in current development branch. I tell you this so just to avoid you an useless work.
QED wrote: And at last, the code. Only search_pv() is changed, but on several places, so I paste it all:
It is very difficult to spot the actual change. Could you please post only the diff in the typical patch format ? You know the one with + and - lines.


Anyhow thanks a lot for the idea and for testing. Tomorrow I will try to better understand what have you done exactly....now I am very tired and I'll go to bed ! Good night :-)


BTW your test result, if it holds, it is _very_ good. Congrats.
QED
Posts: 60
Joined: Thu Nov 05, 2009 9:53 pm

Re: Hash Depth Extension

Post by QED »

Marco Costalba wrote:It is very difficult to spot the actual change. Could you please post only the diff in the typical patch format ? You know the one with + and - lines.
Interesting things are in step 20, beside this there are only changes due to the fact that I have used variables ttMove, ttValue, ttType and ttDepth instead tte->...() methods. And some details that should have been cleaned away. And I have reordered things to reset MovePicker everytime new depth is searched. Oh, and now I see that I should moved also movesSearched[] inside the loop. After I close browser I am going to test slightly different version.

Anyway, I was not able to explain my diff how to produce + or -, only < and > are there (the first working HDE vesion):

Code: Select all

vrato@notas:~/Prg/stockfish-171/src-HdeCh$ diff -wd search.cpp ../src-Ch/search.cpp 
1027,1028c1027,1028
<   Value search_pv(Position& pos, SearchStack ss[], Value oldAlpha, Value beta,
<                   Depth oldDepth, int ply, int threadID) {
---
>   Value search_pv(Position& pos, SearchStack ss[], Value alpha, Value beta,
>                   Depth depth, int ply, int threadID) {
1040,1042c1040,1041
<     Depth ttDepth, ext, newDepth, depth = oldDepth;
<     Value ttValue, bestValue, value, alpha = oldAlpha;
<     ValueType ttType;
---
>     Depth ext, newDepth;
>     Value bestValue, value, oldAlpha;
1044a1044,1045
>     int moveCount = 0;
>     bestValue = value = -VALUE_INFINITE;
1060a1062
>     oldAlpha = alpha;
1076,1078d1077
<     ttDepth = (tte ? tte->depth() : Depth(-2));
<     ttValue = (tte ? value_from_tt(tte->value(), ply) : Value(0));
<     ttType  = (tte ? tte->type() : VALUE_TYPE_EV_LO); // hack, not to replace TT entry
1100,1102d1098
<         ttDepth = (tte ? tte->depth() : Depth(-2));
<         ttValue = (tte ? value_from_tt(tte->value(), ply) : Value(0));
<         ttType  = (tte ? tte->type() : VALUE_TYPE_EV_LO); // hack, not to replace TT entry
1107,1117d1102
<     CheckInfo ci(pos);
< 
<     // Step extra. HDE loop.
<     while(1)
<     { // Also, the scope for mp.
< 
<         // Check for abort.
<         if (AbortSearch || TM.thread_should_stop(threadID))
<             return bestValue;
< 
<         int moveCount = 0;
1119,1120c1104
<         alpha = oldAlpha;
<         bestValue = -VALUE_INFINITE;
---
>     CheckInfo ci(pos);
1142c1126
<                 && move == ttMove
---
>           && move == tte->move()
1144,1145c1128,1129
<                 && is_lower_bound(ttType)
<                 && ttDepth >= depth - 3 * OnePly)
---
>           && is_lower_bound(tte->type())
>           && tte->depth() >= depth - 3 * OnePly)
1147c1131
<                 // Value ttValue = value_from_tt(tte->value(), ply);
---
>           Value ttValue = value_from_tt(tte->value(), ply);
1171d1154
<             {
1173d1155
<             }
1201c1183
<                     if (value > alpha && value < beta) // (value > bestValue)
---
>             if (value > alpha && value < beta)
1215d1196
<                 update_pv(ss, ply);
1218a1200
>               update_pv(ss, ply);
1235,1236c1217
< 
<         } // Move list.
---
>     }
1250,1310d1230
<         if (true) // HDE
<         {
<             if (bestValue <= oldAlpha)
<             { // failing low
<                 // Hash depth extension.
<                 if (ttDepth > depth && is_lower_bound(ttType) && ttValue > oldAlpha)
<                 { // Incompatible deeper TT entry.
<                     depth = Min(ttDepth, depth+2*OnePly);
<                     continue; // The HDE loop with higher depth.
<                 } // HDE low.
< 
<                 if (   depth > ttDepth || !is_upper_bound(ttType)
<                     || (depth >= ttDepth && ttType != VALUE_TYPE_EXACT))
<                 { // Storing new entry.
<                     TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_UPPER, depth, MOVE_NONE);
<                 }
<             }
<             else if (bestValue >= beta)
<             { // failing high
< 
<                 // Hash depth extension.
<                 if (ttDepth > depth && is_upper_bound(ttType) && ttValue < beta)
<                 { // Incompatible deeper TT entry.
<                     depth = Min(ttDepth, depth+2*OnePly);
<                     continue; // The HDE loop with higher depth.
<                 } // HDE high.
< 
<                 // Update statistics after HDE check.
<                 TM.incrementBetaCounter(pos.side_to_move(), depth, threadID);
<                 move = ss[ply].pv[ply];
<                 if (!pos.move_is_capture_or_promotion(move))
<                 {
<                     update_history(pos, move, depth, movesSearched, moveCount);
<                     update_killers(move, ss[ply]);
<                 }
< 
<                 if (   depth > ttDepth || !is_lower_bound(ttType)
<                     || (depth >= ttDepth && ttType != VALUE_TYPE_EXACT))
<                 { // Storing new entry.
<                     TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_LOWER, depth, move);
<                 }
<             }
<             else
<             { // exact score
<                 // Hash depth extension.
<                 if (   (ttDepth > depth && is_lower_bound(ttType) && ttValue >= beta)
<                     || (ttDepth > depth && is_upper_bound(ttType) && ttValue <= oldAlpha)
<                     || (ttDepth > depth && ttType == VALUE_TYPE_EXACT && ttMove != ss[ply].pv[ply]))
<                 { // Incompatible deeper TT entry.
<                     depth = Min(ttDepth, depth+2*OnePly);
<                     continue; // The HDE loop with higher depth.
<                 } // HDE exact.
< 
<                 if (depth >= ttDepth || ttType != VALUE_TYPE_EXACT)
<                 { // Storing new entry.
<                     TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_EXACT, depth, ss[ply].pv[ply]);
<                 }
<             }
<         }
<         else // No HDE.
<         {
1327,1330d1246
<         }
< 
<         break; // Exit from HDE loop.
<     } // HDE loop.
UncombedCoconut
Posts: 319
Joined: Fri Dec 18, 2009 11:40 am
Location: Naperville, IL

Re: Hash Depth Extension

Post by UncombedCoconut »

QED wrote:Anyway, I was not able to explain my diff how to produce + or -, only < and > are there (the first working HDE vesion):
In hope it's useful: ask the program for "unified" diff format. If you use a command-line diff program (esp. in a unix-like environment) just try inserting "-u".
kongsian
Posts: 46
Joined: Thu Jun 15, 2006 11:21 am

Re: Hash Depth Extension

Post by kongsian »

Better yet, use git to manage the changes. Then just "git diff search.cpp" will show the differences. After all the Stockfish guys use git. If it is a positive change, it will be easy for them to merge in the new code.
mcostalba
Posts: 2684
Joined: Sat Jun 14, 2008 9:17 pm

Re: Hash Depth Extension

Post by mcostalba »

Here we go. It is a very complicated patch, I will need time to understand, by now just the diff version:

Code: Select all

 src/search.cpp |  342 +++++++++++++++++++++++++++++++++++---------------------
 1 files changed, 213 insertions(+), 129 deletions(-)

diff --git a/src/search.cpp b/src/search.cpp
index 0fa7824..fe3dd1c 100644
--- a/src/search.cpp
+++ b/src/search.cpp
@@ -1024,8 +1024,8 @@ namespace {
 
   // search_pv() is the main search function for PV nodes.
 
-  Value search_pv(Position& pos, SearchStack ss[], Value alpha, Value beta,
-                  Depth depth, int ply, int threadID) {
+  Value search_pv(Position& pos, SearchStack ss[], Value oldAlpha, Value beta,
+                  Depth oldDepth, int ply, int threadID) {
 
     assert(alpha >= -VALUE_INFINITE && alpha <= VALUE_INFINITE);
     assert(beta > alpha && beta <= VALUE_INFINITE);
@@ -1037,12 +1037,11 @@ namespace {
     StateInfo st;
     const TTEntry* tte;
     Move ttMove, move;
-    Depth ext, newDepth;
-    Value bestValue, value, oldAlpha;
+    Depth ttDepth, ext, newDepth, depth = oldDepth;
+    Value ttValue, bestValue, value, alpha = oldAlpha;
+    ValueType ttType;
     bool isCheck, singleEvasion, moveIsCheck, captureOrPromotion, dangerous;
     bool mateThreat = false;
-    int moveCount = 0;
-    bestValue = value = -VALUE_INFINITE;
 
     if (depth < OnePly)
         return qsearch(pos, ss, alpha, beta, Depth(0), ply, threadID);
@@ -1059,7 +1058,6 @@ namespace {
         return VALUE_DRAW;
 
     // Step 3. Mate distance pruning
-    oldAlpha = alpha;
     alpha = Max(value_mated_in(ply), alpha);
     beta = Min(value_mate_in(ply+1), beta);
     if (alpha >= beta)
@@ -1073,8 +1071,11 @@ namespace {
     // * Fifty move rule detection
     // * Searching for a mate
     // * Printing of full PV line
-    tte = TT.retrieve(pos.get_key());
-    ttMove = (tte ? tte->move() : MOVE_NONE);
+    tte     = TT.retrieve(pos.get_key());
+    ttMove  = (tte ? tte->move() : MOVE_NONE);
+    ttDepth = (tte ? tte->depth() : Depth(-2));
+    ttValue = (tte ? value_from_tt(tte->value(), ply) : Value(0));
+    ttType  = (tte ? tte->type() : VALUE_TYPE_EV_LO); // hack, not to replace TT entry
 
     // Step 5. Evaluate the position statically
     // At PV nodes we do this only to update gain statistics
@@ -1096,154 +1097,237 @@ namespace {
         search_pv(pos, ss, alpha, beta, depth-2*OnePly, ply, threadID);
         ttMove = ss[ply].pv[ply];
         tte = TT.retrieve(pos.get_key());
+        ttDepth = (tte ? tte->depth() : Depth(-2));
+        ttValue = (tte ? value_from_tt(tte->value(), ply) : Value(0));
+        ttType  = (tte ? tte->type() : VALUE_TYPE_EV_LO); // hack, not to replace TT entry
     }
 
     // Initialize a MovePicker object for the current position
     mateThreat = pos.has_mate_threat(opposite_color(pos.side_to_move()));
-    MovePicker mp = MovePicker(pos, ttMove, depth, H, &ss[ply]);
     CheckInfo ci(pos);
 
-    // Step 10. Loop through moves
-    // Loop through all legal moves until no moves remain or a beta cutoff occurs
-    while (   alpha < beta
-           && (move = mp.get_next_move()) != MOVE_NONE
-           && !TM.thread_should_stop(threadID))
-    {
-      assert(move_is_ok(move));
+    // Step extra. HDE loop.
+    while(1)
+    { // Also, the scope for mp.
 
-      singleEvasion = (isCheck && mp.number_of_evasions() == 1);
-      moveIsCheck = pos.move_is_check(move, ci);
-      captureOrPromotion = pos.move_is_capture_or_promotion(move);
+        // Check for abort.
+        if (AbortSearch || TM.thread_should_stop(threadID))
+            return bestValue;
 
-      // Step 11. Decide the new search depth
-      ext = extension(pos, move, true, captureOrPromotion, moveIsCheck, singleEvasion, mateThreat, &dangerous);
+        int moveCount = 0;
+        MovePicker mp = MovePicker(pos, ttMove, depth, H, &ss[ply]);
+        alpha = oldAlpha;
+        bestValue = -VALUE_INFINITE;
 
-      // Singular extension search. We extend the TT move if its value is much better than
-      // its siblings. To verify this we do a reduced search on all the other moves but the
-      // ttMove, if result is lower then ttValue minus a margin then we extend ttMove.
-      if (   depth >= SingularExtensionDepthAtPVNodes
-          && tte
-          && move == tte->move()
-          && ext < OnePly
-          && is_lower_bound(tte->type())
-          && tte->depth() >= depth - 3 * OnePly)
-      {
-          Value ttValue = value_from_tt(tte->value(), ply);
+        // Step 10. Loop through moves
+        // Loop through all legal moves until no moves remain or a beta cutoff occurs
+        while (   alpha < beta
+               && (move = mp.get_next_move()) != MOVE_NONE
+               && !TM.thread_should_stop(threadID))
+        {
+            assert(move_is_ok(move));
 
-          if (abs(ttValue) < VALUE_KNOWN_WIN)
-          {
-              Value excValue = search(pos, ss, ttValue - SingularExtensionMargin, depth / 2, ply, false, threadID, move);
+            singleEvasion = (isCheck && mp.number_of_evasions() == 1);
+            moveIsCheck = pos.move_is_check(move, ci);
+            captureOrPromotion = pos.move_is_capture_or_promotion(move);
 
-              if (excValue < ttValue - SingularExtensionMargin)
-                  ext = OnePly;
-          }
-      }
+            // Step 11. Decide the new search depth
+            ext = extension(pos, move, true, captureOrPromotion, moveIsCheck, singleEvasion, mateThreat, &dangerous);
+
+            // Singular extension search. We extend the TT move if its value is much better than
+            // its siblings. To verify this we do a reduced search on all the other moves but the
+            // ttMove, if result is lower then ttValue minus a margin then we extend ttMove.
+            if (   depth >= SingularExtensionDepthAtPVNodes
+                && tte
+                && move == ttMove
+                && ext < OnePly
+                && is_lower_bound(ttType)
+                && ttDepth >= depth - 3 * OnePly)
+            {
+                // Value ttValue = value_from_tt(tte->value(), ply);
 
-      newDepth = depth - OnePly + ext;
+                if (abs(ttValue) < VALUE_KNOWN_WIN)
+                {
+                    Value excValue = search(pos, ss, ttValue - SingularExtensionMargin, depth / 2, ply, false, threadID, move);
 
-      // Update current move (this must be done after singular extension search)
-      movesSearched[moveCount++] = ss[ply].currentMove = move;
+                    if (excValue < ttValue - SingularExtensionMargin)
+                        ext = OnePly;
+                }
+            }
 
-      // Step 12. Futility pruning (is omitted in PV nodes)
+            newDepth = depth - OnePly + ext;
 
-      // Step 13. Make the move
-      pos.do_move(move, st, ci, moveIsCheck);
+            // Update current move (this must be done after singular extension search)
+            movesSearched[moveCount++] = ss[ply].currentMove = move;
 
-      // Step extra. pv search (only in PV nodes)
-      // The first move in list is the expected PV
-      if (moveCount == 1)
-          value = -search_pv(pos, ss, -beta, -alpha, newDepth, ply+1, threadID);
-      else
-      {
-        // Step 14. Reduced search
-        // if the move fails high will be re-searched at full depth.
-        bool doFullDepthSearch = true;
-
-        if (    depth >= 3 * OnePly
-            && !dangerous
-            && !captureOrPromotion
-            && !move_is_castle(move)
-            && !move_is_killer(move, ss[ply]))
-        {
-            ss[ply].reduction = pv_reduction(depth, moveCount);
-            if (ss[ply].reduction)
-            {
-                value = -search(pos, ss, -alpha, newDepth-ss[ply].reduction, ply+1, true, threadID);
-                doFullDepthSearch = (value > alpha);
-            }
-        }
+            // Step 12. Futility pruning (is omitted in PV nodes)
 
-        // Step 15. Full depth search
-        if (doFullDepthSearch)
-        {
-            ss[ply].reduction = Depth(0);
-            value = -search(pos, ss, -alpha, newDepth, ply+1, true, threadID);
+            // Step 13. Make the move
+            pos.do_move(move, st, ci, moveIsCheck);
 
             // Step extra. pv search (only in PV nodes)
-            if (value > alpha && value < beta)
+            // The first move in list is the expected PV
+            if (moveCount == 1)
+            {
                 value = -search_pv(pos, ss, -beta, -alpha, newDepth, ply+1, threadID);
-        }
-      }
+            }
+            else
+            {
+                // Step 14. Reduced search
+                // if the move fails high will be re-searched at full depth.
+                bool doFullDepthSearch = true;
+
+                if (    depth >= 3 * OnePly
+                    && !dangerous
+                    && !captureOrPromotion
+                    && !move_is_castle(move)
+                    && !move_is_killer(move, ss[ply]))
+                {
+                    ss[ply].reduction = pv_reduction(depth, moveCount);
+                    if (ss[ply].reduction)
+                    {
+                        value = -search(pos, ss, -alpha, newDepth-ss[ply].reduction, ply+1, true, threadID);
+                        doFullDepthSearch = (value > alpha);
+                    }
+                }
 
-      // Step 16. Undo move
-      pos.undo_move(move);
+                // Step 15. Full depth search
+                if (doFullDepthSearch)
+                {
+                    ss[ply].reduction = Depth(0);
+                    value = -search(pos, ss, -alpha, newDepth, ply+1, true, threadID);
 
-      assert(value > -VALUE_INFINITE && value < VALUE_INFINITE);
+                    // Step extra. pv search (only in PV nodes)
+                    if (value > alpha && value < beta) // (value > bestValue)
+                        value = -search_pv(pos, ss, -beta, -alpha, newDepth, ply+1, threadID);
+                }
+            }
 
-      // Step 17. Check for new best move
-      if (value > bestValue)
-      {
-          bestValue = value;
-          if (value > alpha)
-          {
-              alpha = value;
-              update_pv(ss, ply);
-              if (value == value_mate_in(ply + 1))
-                  ss[ply].mateKiller = move;
-          }
-      }
+            // Step 16. Undo move
+            pos.undo_move(move);
 
-      // Step 18. Check for split
-      if (   TM.active_threads() > 1
-          && bestValue < beta
-          && depth >= MinimumSplitDepth
-          && Iteration <= 99
-          && TM.available_thread_exists(threadID)
-          && !AbortSearch
-          && !TM.thread_should_stop(threadID)
-          && TM.split(pos, ss, ply, &alpha, beta, &bestValue,
-                      depth, mateThreat, &moveCount, &mp, threadID, true))
-          break;
-    }
+            assert(value > -VALUE_INFINITE && value < VALUE_INFINITE);
 
-    // Step 19. Check for mate and stalemate
-    // All legal moves have been searched and if there were
-    // no legal moves, it must be mate or stalemate.
-    if (moveCount == 0)
-        return (isCheck ? value_mated_in(ply) : VALUE_DRAW);
+            // Step 17. Check for new best move
+            if (value > bestValue)
+            {
+                bestValue = value;
+                update_pv(ss, ply);
+                if (value > alpha)
+                {
+                    alpha = value;
+                    if (value == value_mate_in(ply + 1))
+                        ss[ply].mateKiller = move;
+                }
+            }
 
-    // Step 20. Update tables
-    // If the search is not aborted, update the transposition table,
-    // history counters, and killer moves.
-    if (AbortSearch || TM.thread_should_stop(threadID))
-        return bestValue;
+            // Step 18. Check for split
+            if (   TM.active_threads() > 1
+                && bestValue < beta
+                && depth >= MinimumSplitDepth
+                && Iteration <= 99
+                && TM.available_thread_exists(threadID)
+                && !AbortSearch
+                && !TM.thread_should_stop(threadID)
+                && TM.split(pos, ss, ply, &alpha, beta, &bestValue,
+                            depth, mateThreat, &moveCount, &mp, threadID, true))
+                break;
 
-    if (bestValue <= oldAlpha)
-        TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_UPPER, depth, MOVE_NONE);
+        } // Move list.
 
-    else if (bestValue >= beta)
-    {
-        TM.incrementBetaCounter(pos.side_to_move(), depth, threadID);
-        move = ss[ply].pv[ply];
-        if (!pos.move_is_capture_or_promotion(move))
+        // Step 19. Check for mate and stalemate
+        // All legal moves have been searched and if there were
+        // no legal moves, it must be mate or stalemate.
+        if (moveCount == 0)
+            return (isCheck ? value_mated_in(ply) : VALUE_DRAW);
+
+        // Step 20. Update tables
+        // If the search is not aborted, update the transposition table,
+        // history counters, and killer moves.
+        if (AbortSearch || TM.thread_should_stop(threadID))
+            return bestValue;
+
+        if (true) // HDE
         {
-            update_history(pos, move, depth, movesSearched, moveCount);
-            update_killers(move, ss[ply]);
+            if (bestValue <= oldAlpha)
+            { // failing low
+                // Hash depth extension.
+                if (ttDepth > depth && is_lower_bound(ttType) && ttValue > oldAlpha)
+                { // Incompatible deeper TT entry.
+                    depth = Min(ttDepth, depth+2*OnePly);
+                    continue; // The HDE loop with higher depth.
+                } // HDE low.
+
+                if (   depth > ttDepth || !is_upper_bound(ttType)
+                    || (depth >= ttDepth && ttType != VALUE_TYPE_EXACT))
+                { // Storing new entry.
+                    TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_UPPER, depth, MOVE_NONE);
+                }
+            }
+            else if (bestValue >= beta)
+            { // failing high
+
+                // Hash depth extension.
+                if (ttDepth > depth && is_upper_bound(ttType) && ttValue < beta)
+                { // Incompatible deeper TT entry.
+                    depth = Min(ttDepth, depth+2*OnePly);
+                    continue; // The HDE loop with higher depth.
+                } // HDE high.
+
+                // Update statistics after HDE check.
+                TM.incrementBetaCounter(pos.side_to_move(), depth, threadID);
+                move = ss[ply].pv[ply];
+                if (!pos.move_is_capture_or_promotion(move))
+                {
+                    update_history(pos, move, depth, movesSearched, moveCount);
+                    update_killers(move, ss[ply]);
+                }
+
+                if (   depth > ttDepth || !is_lower_bound(ttType)
+                    || (depth >= ttDepth && ttType != VALUE_TYPE_EXACT))
+                { // Storing new entry.
+                    TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_LOWER, depth, move);
+                }
+            }
+            else
+            { // exact score
+                // Hash depth extension.
+                if (   (ttDepth > depth && is_lower_bound(ttType) && ttValue >= beta)
+                    || (ttDepth > depth && is_upper_bound(ttType) && ttValue <= oldAlpha)
+                    || (ttDepth > depth && ttType == VALUE_TYPE_EXACT && ttMove != ss[ply].pv[ply]))
+                { // Incompatible deeper TT entry.
+                    depth = Min(ttDepth, depth+2*OnePly);
+                    continue; // The HDE loop with higher depth.
+                } // HDE exact.
+
+                if (depth >= ttDepth || ttType != VALUE_TYPE_EXACT)
+                { // Storing new entry.
+                    TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_EXACT, depth, ss[ply].pv[ply]);
+                }
+            }
         }
-        TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_LOWER, depth, move);
-    }
-    else
-        TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_EXACT, depth, ss[ply].pv[ply]);
+        else // No HDE.
+        {
+            if (bestValue <= oldAlpha)
+                TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_UPPER, depth, MOVE_NONE);
+
+            else if (bestValue >= beta)
+            {
+                TM.incrementBetaCounter(pos.side_to_move(), depth, threadID);
+                move = ss[ply].pv[ply];
+                if (!pos.move_is_capture_or_promotion(move))
+                {
+                    update_history(pos, move, depth, movesSearched, moveCount);
+                    update_killers(move, ss[ply]);
+                }
+                TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_LOWER, depth, move);
+            }
+            else
+                TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_EXACT, depth, ss[ply].pv[ply]);
+        }
+
+        break; // Exit from HDE loop.
+    } // HDE loop.
 
     return bestValue;
   }
Just a couple of quick notes:


1 ) You have also changed indentation. This is better to do in a separate patch to avoid making the diff unreadable and separate actual changes by identation fix.

2) It does not compile in debug mode:

1>.\src\search.cpp(1030) : error C2065: 'alpha': identifier not declared
QED
Posts: 60
Joined: Thu Nov 05, 2009 9:53 pm

Re: Hash Depth Extension

Post by QED »

First of all, I have to apologize for giving you wrong numbers in my first post in this thread. The +202 was not the number of wins by HDE version, but the nuber of wins by white player. The actual nuber of wins by HDE version was 212.

Second of all, I have made another HDE version, this time movesSearched are declared inside the loop (hopefully getting cleared everytime research with extended depth happens), and the statistics are now updated before extension (not sure if this is better). This time there was no activity beside the running test, and I this time the HDE version has won 232 games (606 draws, 162 wins by original stockfish). Bayeselo says it means HDE is better with 98% probability.

Maybe more interesting statistics would be a table of all 2-game minimatch results, so I edited Bayeselo sources to print it. Here, =-42 means that 42 times (out of 500 minimatches) the original version has drawn with white and lost with black (with repeated opening). Statistics of first version test was ++12 +=46 +-29 =+39 ==206 =-71 -+17 -=65 --15 and statistics for second version test are ++12 +=40 +-32 =+35 ==207 =-47 -+31 -=70 --26 (unless I have another bug somewhere).

I was not trying to change indentation, but I was adding and removing loops and this is a result. In future versions, I will try to preserve original indentation. The debug mode compilation error is because I had oldAlpha as an argument of search_pv() instead of alpha (no idea how this drifted to hde version).

Here is the new patch (the version that played the second test match, but with alpha back as an argument).

Code: Select all

vrato@notas:~/Prg/stockfish-171/src-Ch$ diff -wdu search.cpp ../src-HdeCh/search.cpp 
--- search.cpp  2010-04-20 00:45:49.000000000 +0200
+++ ../src-HdeCh/search.cpp     2010-05-20 07:26:50.000000000 +0200
@@ -1025,24 +1025,22 @@
   // search_pv() is the main search function for PV nodes.
 
   Value search_pv(Position& pos, SearchStack ss[], Value alpha, Value beta,
-                  Depth depth, int ply, int threadID) {
+                  Depth oldDepth, int ply, int threadID) {
 
     assert(alpha >= -VALUE_INFINITE && alpha <= VALUE_INFINITE);
     assert(beta > alpha && beta <= VALUE_INFINITE);
     assert(ply >= 0 && ply < PLY_MAX);
     assert(threadID >= 0 && threadID < TM.active_threads());
 
-    Move movesSearched[256];
     EvalInfo ei;
     StateInfo st;
     const TTEntry* tte;
     Move ttMove, move;
-    Depth ext, newDepth;
-    Value bestValue, value, oldAlpha;
+    Depth ttDepth, ext, newDepth, depth = oldDepth;
+    Value ttValue, bestValue, value, oldAlpha = alpha;
+    ValueType ttType;
     bool isCheck, singleEvasion, moveIsCheck, captureOrPromotion, dangerous;
     bool mateThreat = false;
-    int moveCount = 0;
-    bestValue = value = -VALUE_INFINITE;
 
     if (depth < OnePly)
         return qsearch(pos, ss, alpha, beta, Depth(0), ply, threadID);
@@ -1059,7 +1057,6 @@
         return VALUE_DRAW;
 
     // Step 3. Mate distance pruning
-    oldAlpha = alpha;
     alpha = Max(value_mated_in(ply), alpha);
     beta = Min(value_mate_in(ply+1), beta);
     if (alpha >= beta)
@@ -1075,6 +1072,9 @@
     // * Printing of full PV line
     tte = TT.retrieve(pos.get_key());
     ttMove = (tte ? tte->move() : MOVE_NONE);
+    ttDepth = (tte ? tte->depth() : Depth(-2));
+    ttValue = (tte ? value_from_tt(tte->value(), ply) : Value(0));
+    ttType  = (tte ? tte->type() : VALUE_TYPE_EV_LO); // hack, not to replace TT entry
 
     // Step 5. Evaluate the position statically
     // At PV nodes we do this only to update gain statistics
@@ -1096,13 +1096,29 @@
         search_pv(pos, ss, alpha, beta, depth-2*OnePly, ply, threadID);
         ttMove = ss[ply].pv[ply];
         tte = TT.retrieve(pos.get_key());
+        ttDepth = (tte ? tte->depth() : Depth(-2));
+        ttValue = (tte ? value_from_tt(tte->value(), ply) : Value(0));
+        ttType  = (tte ? tte->type() : VALUE_TYPE_EV_LO); // hack, not to replace TT entry
     }
 
     // Initialize a MovePicker object for the current position
     mateThreat = pos.has_mate_threat(opposite_color(pos.side_to_move()));
-    MovePicker mp = MovePicker(pos, ttMove, depth, H, &ss[ply]);
     CheckInfo ci(pos);
 
+    // Step extra. HDE loop.
+    while(1)
+    { // Also, the scope for mp, movesSearched, etc.
+
+        // Check for abort.
+        if (AbortSearch || TM.thread_should_stop(threadID))
+            return bestValue;
+
+        Move movesSearched[256];
+        int moveCount = 0;
+        MovePicker mp = MovePicker(pos, ttMove, depth, H, &ss[ply]);
+        alpha = oldAlpha;
+        bestValue = -VALUE_INFINITE;
+
     // Step 10. Loop through moves
     // Loop through all legal moves until no moves remain or a beta cutoff occurs
     while (   alpha < beta
@@ -1123,12 +1139,12 @@
       // ttMove, if result is lower then ttValue minus a margin then we extend ttMove.
       if (   depth >= SingularExtensionDepthAtPVNodes
           && tte
-          && move == tte->move()
+                && move == ttMove
           && ext < OnePly
-          && is_lower_bound(tte->type())
-          && tte->depth() >= depth - 3 * OnePly)
+                && is_lower_bound(ttType)
+                && ttDepth >= depth - 3 * OnePly)
       {
-          Value ttValue = value_from_tt(tte->value(), ply);
+                // Value ttValue = value_from_tt(tte->value(), ply);
 
           if (abs(ttValue) < VALUE_KNOWN_WIN)
           {
@@ -1152,7 +1168,9 @@
       // Step extra. pv search (only in PV nodes)
       // The first move in list is the expected PV
       if (moveCount == 1)
+            {
           value = -search_pv(pos, ss, -beta, -alpha, newDepth, ply+1, threadID);
+            }
       else
       {
         // Step 14. Reduced search
@@ -1180,7 +1198,7 @@
             value = -search(pos, ss, -alpha, newDepth, ply+1, true, threadID);
 
             // Step extra. pv search (only in PV nodes)
-            if (value > alpha && value < beta)
+                    if (value > alpha && value < beta) // (value > bestValue)
                 value = -search_pv(pos, ss, -beta, -alpha, newDepth, ply+1, threadID);
         }
       }
@@ -1194,10 +1212,10 @@
       if (value > bestValue)
       {
           bestValue = value;
+                update_pv(ss, ply);
           if (value > alpha)
           {
               alpha = value;
-              update_pv(ss, ply);
               if (value == value_mate_in(ply + 1))
                   ss[ply].mateKiller = move;
           }
@@ -1214,7 +1232,8 @@
           && TM.split(pos, ss, ply, &alpha, beta, &bestValue,
                       depth, mateThreat, &moveCount, &mp, threadID, true))
           break;
-    }
+
+        } // Move list.
 
     // Step 19. Check for mate and stalemate
     // All legal moves have been searched and if there were
@@ -1228,6 +1247,67 @@
     if (AbortSearch || TM.thread_should_stop(threadID))
         return bestValue;
 
+        if (true) // HDE
+        {
+            if (bestValue <= oldAlpha)
+            { // failing low
+                // Hash depth extension.
+                if (ttDepth > depth && is_lower_bound(ttType) && ttValue > oldAlpha)
+                { // Incompatible deeper TT entry.
+                    depth = Min(ttDepth, depth+2*OnePly);
+                    continue; // The HDE loop with higher depth.
+                } // HDE low.
+
+                if (   depth > ttDepth || !is_upper_bound(ttType)
+                    || (depth >= ttDepth && ttType != VALUE_TYPE_EXACT))
+                { // Storing new entry.
+                    TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_UPPER, depth, MOVE_NONE);
+                }
+            }
+            else if (bestValue >= beta)
+            { // failing high
+
+                // Update statistics before HDE check.
+                TM.incrementBetaCounter(pos.side_to_move(), depth, threadID);
+                move = ss[ply].pv[ply];
+                if (!pos.move_is_capture_or_promotion(move))
+                {
+                    update_history(pos, move, depth, movesSearched, moveCount);
+                    update_killers(move, ss[ply]);
+                }
+
+                // Hash depth extension.
+                if (ttDepth > depth && is_upper_bound(ttType) && ttValue < beta)
+                { // Incompatible deeper TT entry.
+                    depth = Min(ttDepth, depth+2*OnePly);
+                    continue; // The HDE loop with higher depth.
+                } // HDE high.
+
+                if (   depth > ttDepth || !is_lower_bound(ttType)
+                    || (depth >= ttDepth && ttType != VALUE_TYPE_EXACT))
+                { // Storing new entry.
+                    TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_LOWER, depth, move);
+                }
+            }
+            else
+            { // exact score
+                // Hash depth extension.
+                if (   (ttDepth > depth && is_lower_bound(ttType) && ttValue >= beta)
+                    || (ttDepth > depth && is_upper_bound(ttType) && ttValue <= oldAlpha)
+                    || (ttDepth > depth && ttType == VALUE_TYPE_EXACT && ttMove != ss[ply].pv[ply]))
+                { // Incompatible deeper TT entry.
+                    depth = Min(ttDepth, depth+2*OnePly);
+                    continue; // The HDE loop with higher depth.
+                } // HDE exact.
+
+                if (depth >= ttDepth || ttType != VALUE_TYPE_EXACT)
+                { // Storing new entry.
+                    TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_EXACT, depth, ss[ply].pv[ply]);
+                }
+            }
+        }
+        else // No HDE.
+        {
     if (bestValue <= oldAlpha)
         TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_UPPER, depth, MOVE_NONE);
 
@@ -1244,6 +1324,10 @@
     }
     else
         TT.store(pos.get_key(), value_to_tt(bestValue, ply), VALUE_TYPE_EXACT, depth, ss[ply].pv[ply]);
+        }
+
+        break; // Exit from HDE loop.
+    } // HDE loop.
 
     return bestValue;
   }

mcostalba
Posts: 2684
Joined: Sat Jun 14, 2008 9:17 pm

Re: Hash Depth Extension

Post by mcostalba »

QED wrote: Here is the new patch (the version that played the second test match, but with alpha back as an argument).
Hi Vratko,

this seems to be a very interesting idea. Unfortunatly I didn't had the time to test it, but it is on my test queue. I have missed your test conditions: time control used and book used. Could you post them pelase ?

If you don't mind I have written a much simpler and less invasive patch (still to test) that somehow catches your underline idea but instead of a loop uses the recursivity of the search() call to iterate up to the TT entry depth.

Another difference is that I always extend in advance instead of doing it _after_ the search did not return an expected value.

Here is my attempt but, as I said still to be tested:

Code: Select all

--- a/src/search.cpp
+++ b/src/search.cpp
@@ -1243,6 +1243,17 @@ namespace {
       // Step 11. Decide the new search depth
       ext = extension<PvNode>(pos, move, captureOrPromotion, moveIsCheck, singleEvasion, mateThreat, &dangerous);
 
+
+     // Hash extension. We extend the PV if we have a valid TT entry with depth bigger then
+     // current one.
+     if (    PvNode
+          && tte
+          && move == tte->move()
+          && ext < OnePly
+          && is_lower_bound(tte->type())
+          && tte->depth() > depth)
+          ext = Min(tte->depth() - depth, OnePly);
+
       // Singular extension search. We extend the TT move if its value is much better than
       // its siblings. To verify this we do a reduced search on all the other moves but the
       // ttMove, if result is lower then ttValue minus a margin then we extend ttMove.

QED
Posts: 60
Joined: Thu Nov 05, 2009 9:53 pm

Re: Hash Depth Extension

Post by QED »

Marco Costalba wrote:I have missed your test conditions: time control used and book used. Could you post them pelase ?
The second test match had the same condition as the first one:
QED wrote:I have run 1000 games match between original version and my new version, at 1minute/game, 32M hash, ponder off, 1 thread, with stockfish book, repeating, book depth 40 plies.
On a notebook that makes around 400knps (startposition, 1 thread).
Marco Costalba wrote:If you don't mind I have written a much simpler and less invasive patch (still to test) that somehow catches your underline idea but instead of a loop uses the recursivity of the search() call to iterate up to the TT entry depth.

Another difference is that I always extend in advance instead of doing it _after_ the search did not return an expected value.

Here is my attempt but, as I said still to be tested:

Code: Select all

--- a/src/search.cpp
+++ b/src/search.cpp
@@ -1243,6 +1243,17 @@ namespace {
       // Step 11. Decide the new search depth
       ext = extension<PvNode>(pos, move, captureOrPromotion, moveIsCheck, singleEvasion, mateThreat, &dangerous);
 
+
+     // Hash extension. We extend the PV if we have a valid TT entry with depth bigger then
+     // current one.
+     if (    PvNode
+          && tte
+          && move == tte->move()
+          && ext < OnePly
+          && is_lower_bound(tte->type())
+          && tte->depth() > depth)
+          ext = Min(tte->depth() - depth, OnePly);
+
       // Singular extension search. We extend the TT move if its value is much better than
       // its siblings. To verify this we do a reduced search on all the other moves but the
       // ttMove, if result is lower then ttValue minus a margin then we extend ttMove.

Very nice patch. Short and clean. Here are my comments and questions.

To name your patch, I will call it Hash Move Extension (because of move == tte->move() condition). Now we can compare HME to HDE.

How come, "iterate up to the TT entry depth"? IID can not produce a cutoff, and unless there is some additional deepening loop I do not see, it looks more like "jump to the TT entry depth".

Why "ext < OnePly"? If a move gets ext = OnePly at step 11, is the move really not allowed to get (perhaps huge) ext from HME?

The version of HDE that I posted extends only when a shallow search is incompatible with TT entry (in the sense the principal variation would be different) and even then it extends iteratively. My previous attempts did not do it, and I have not posted them because they were (probably) worse than the original stockfish. That is because extending when TT entry is compatible does not change bestmove and it is generally a complete waste of time. I have witnessed (quite rare but still) situations when even after 20 seconds (in 2'+15'' match) the search was still only at depth 3, and a clearly inferior move was played, because bugs (or TT replacement scheme) caused the move to be the only one not to trigger depth==20 refutation. My latest HDE is perhaps still vulnerable to this behavior, but your HME will be probably vulnerable more than HDE, maybe to the point the test will show it actually hurts.

But from the development point of view, it is probably a good idea to test small steps in the right direction (and not a huge complicated patch that even its author claims not to undestand).