LMR revisited.

Discussion of chess software programming and technical issues.

Moderators: hgm, Rebel, chrisw

bob
Posts: 20943
Joined: Mon Feb 27, 2006 7:30 pm
Location: Birmingham, AL

Re: LMR revisited.

Post by bob »

hgm wrote:OK, I have a suggestion:

The main disadvantage of LMR seems to be that it tends to make the engine blind against threats in side branches. This is particularly bad in combination with null moves: if the opponent can mount a dangerous attack with a sequence of late moves, while I am null-moving all the time, there is a reduction of 2 or 3 for each of my moves, and a reduction of 1 (or 2) for each of his moves, and you need excessive nominal depth before you start to see the trouble coming.

The deminishing returns of combining null-move pruning and LMR might not only be from that they partly do the same, but from that together they actually overdo it.

So my suggestion is: do not apply the LMR reduction to the null-move search of the daughter. E.g. if you use null move with R=2, when a node is reached through a late move, you would normally reduce the null move by R=3, and when it fails low, reduce the other moves by 1, and only if all these fail low too, you would search them unreduced (because the late move below you now fails high, and you take the reduction away.)

What I propose is to keep the reduction of the null move at R=2, but when it fails low, reduce all the normal moves still by 1.

This should make you much less blind. And my suspicion is that it would be comparatively cheap, because I already did try the opposite: only reduce the null move one extra after a late move, and switch to normal depth immediately when it fails low. This hardly produced any reduction of the tree size for the same nominal depth. Apparently the branches with null moves are already so much reduced, that they hrdly contribute to the node count of the tree, and reducing them further has no measurable effect on tree size, but a very detrimental effect on playing strength! So you might as well extend them, in the hope that the opposite will hold.
I'll try to "digest" that and run a test. Right now I am running some long games just to confirm that the last batch of LMR changes are no good regardless of the time control.
bob
Posts: 20943
Joined: Mon Feb 27, 2006 7:30 pm
Location: Birmingham, AL

Re: LMR revisited.

Post by bob »

Don wrote:
bob wrote:
Kempelen wrote:More ideas: The tree searched is like a triangle. What about if we based reductions deph based on how far are we from the left side (that is, the pv). More far more reduction....
That is the L in LMR in fact, although I have tried this as well. So far, nothing significant has happened with the idea of reducing "later" moves more.
It works for me. As I said before, I reduce by 1 until I'm later in the list then I reduce by 2. So each move gets 0, 1 or 2 reductions.
I've tested this every which way. Including calling static eval and double-reduce when the static eval is _way_ below alpha. Made no difference at all in the actual performance, short or long time controls...
bob
Posts: 20943
Joined: Mon Feb 27, 2006 7:30 pm
Location: Birmingham, AL

Re: LMR revisited.

Post by bob »

bob wrote:
hgm wrote:OK, I have a suggestion:

The main disadvantage of LMR seems to be that it tends to make the engine blind against threats in side branches. This is particularly bad in combination with null moves: if the opponent can mount a dangerous attack with a sequence of late moves, while I am null-moving all the time, there is a reduction of 2 or 3 for each of my moves, and a reduction of 1 (or 2) for each of his moves, and you need excessive nominal depth before you start to see the trouble coming.

The deminishing returns of combining null-move pruning and LMR might not only be from that they partly do the same, but from that together they actually overdo it.

So my suggestion is: do not apply the LMR reduction to the null-move search of the daughter. E.g. if you use null move with R=2, when a node is reached through a late move, you would normally reduce the null move by R=3, and when it fails low, reduce the other moves by 1, and only if all these fail low too, you would search them unreduced (because the late move below you now fails high, and you take the reduction away.)

What I propose is to keep the reduction of the null move at R=2, but when it fails low, reduce all the normal moves still by 1.

This should make you much less blind. And my suspicion is that it would be comparatively cheap, because I already did try the opposite: only reduce the null move one extra after a late move, and switch to normal depth immediately when it fails low. This hardly produced any reduction of the tree size for the same nominal depth. Apparently the branches with null moves are already so much reduced, that they hrdly contribute to the node count of the tree, and reducing them further has no measurable effect on tree size, but a very detrimental effect on playing strength! So you might as well extend them, in the hope that the opposite will hold.
I'll try to "digest" that and run a test. Right now I am running some long games just to confirm that the last batch of LMR changes are no good regardless of the time control.
I am also toying with an LMR search using an offset window. Lower the window and reduce the depth. The lowered window has the opportunity to find that a reduced move still wants to fail high, then the normal re-search with normal depth will see if it is really better. Seems interesting, has some plusses, has to pass the "match test" first...
diep
Posts: 1822
Joined: Thu Mar 09, 2006 11:54 pm
Location: The Netherlands

Re: LMR revisited.

Post by diep »

bob wrote:
bob wrote:
hgm wrote:OK, I have a suggestion:

The main disadvantage of LMR seems to be that it tends to make the engine blind against threats in side branches. This is particularly bad in combination with null moves: if the opponent can mount a dangerous attack with a sequence of late moves, while I am null-moving all the time, there is a reduction of 2 or 3 for each of my moves, and a reduction of 1 (or 2) for each of his moves, and you need excessive nominal depth before you start to see the trouble coming.

The deminishing returns of combining null-move pruning and LMR might not only be from that they partly do the same, but from that together they actually overdo it.

So my suggestion is: do not apply the LMR reduction to the null-move search of the daughter. E.g. if you use null move with R=2, when a node is reached through a late move, you would normally reduce the null move by R=3, and when it fails low, reduce the other moves by 1, and only if all these fail low too, you would search them unreduced (because the late move below you now fails high, and you take the reduction away.)

What I propose is to keep the reduction of the null move at R=2, but when it fails low, reduce all the normal moves still by 1.

This should make you much less blind. And my suspicion is that it would be comparatively cheap, because I already did try the opposite: only reduce the null move one extra after a late move, and switch to normal depth immediately when it fails low. This hardly produced any reduction of the tree size for the same nominal depth. Apparently the branches with null moves are already so much reduced, that they hrdly contribute to the node count of the tree, and reducing them further has no measurable effect on tree size, but a very detrimental effect on playing strength! So you might as well extend them, in the hope that the opposite will hold.
I'll try to "digest" that and run a test. Right now I am running some long games just to confirm that the last batch of LMR changes are no good regardless of the time control.
I am also toying with an LMR search using an offset window. Lower the window and reduce the depth. The lowered window has the opportunity to find that a reduced move still wants to fail high, then the normal re-search with normal depth will see if it is really better. Seems interesting, has some plusses, has to pass the "match test" first...
I tried this extensively in 2004/2005 for like 6 months in all kind of ways and different ideas. Whatever i tried there, never could get it to work.

Using different windows each time is very complicated and expensive for the search. The overhead is too much.
bob
Posts: 20943
Joined: Mon Feb 27, 2006 7:30 pm
Location: Birmingham, AL

Re: LMR revisited.

Post by bob »

diep wrote:
bob wrote:
bob wrote:
hgm wrote:OK, I have a suggestion:

The main disadvantage of LMR seems to be that it tends to make the engine blind against threats in side branches. This is particularly bad in combination with null moves: if the opponent can mount a dangerous attack with a sequence of late moves, while I am null-moving all the time, there is a reduction of 2 or 3 for each of my moves, and a reduction of 1 (or 2) for each of his moves, and you need excessive nominal depth before you start to see the trouble coming.

The deminishing returns of combining null-move pruning and LMR might not only be from that they partly do the same, but from that together they actually overdo it.

So my suggestion is: do not apply the LMR reduction to the null-move search of the daughter. E.g. if you use null move with R=2, when a node is reached through a late move, you would normally reduce the null move by R=3, and when it fails low, reduce the other moves by 1, and only if all these fail low too, you would search them unreduced (because the late move below you now fails high, and you take the reduction away.)

What I propose is to keep the reduction of the null move at R=2, but when it fails low, reduce all the normal moves still by 1.

This should make you much less blind. And my suspicion is that it would be comparatively cheap, because I already did try the opposite: only reduce the null move one extra after a late move, and switch to normal depth immediately when it fails low. This hardly produced any reduction of the tree size for the same nominal depth. Apparently the branches with null moves are already so much reduced, that they hrdly contribute to the node count of the tree, and reducing them further has no measurable effect on tree size, but a very detrimental effect on playing strength! So you might as well extend them, in the hope that the opposite will hold.
I'll try to "digest" that and run a test. Right now I am running some long games just to confirm that the last batch of LMR changes are no good regardless of the time control.
I am also toying with an LMR search using an offset window. Lower the window and reduce the depth. The lowered window has the opportunity to find that a reduced move still wants to fail high, then the normal re-search with normal depth will see if it is really better. Seems interesting, has some plusses, has to pass the "match test" first...
I tried this extensively in 2004/2005 for like 6 months in all kind of ways and different ideas. Whatever i tried there, never could get it to work.

Using different windows each time is very complicated and expensive for the search. The overhead is too much.
I'm not seeing any overhead, other than as the window is offset downward, you naturally get more fail-highs and have to re-search the reduced move, but to normal depth. Whether this will help or not is a guess. My hope is to find a reasonable offset so that potentially good moves that would normally fail low on the reduced search now fail high, causing a normal-depth search. More after a few million games are done. :)

I'm seeing some promise here however, in that this offset can be adjusted to significantly speed up tactical positions. Real game testing is next on the list, after the current longish-game test is done.
jarkkop
Posts: 198
Joined: Thu Mar 09, 2006 2:44 am
Location: Helsinki, Finland

Re: LMR revisited.

Post by jarkkop »

Try and test the code below inspired by strelka . Quick test gave about 30 ELO. All pure Crafty code.

Code: Select all

     if (first_move) {
        if (depth - 1 + extensions > 0)
			if(Check(wtm))
				value =  -SearchCheck(tree, -beta, -alpha, Flip(wtm), depth - 1 + extensions, 2, DO_NULL);
			else
				value =  -Search(tree, -beta, -alpha, Flip(wtm), depth - 1 + extensions, 2, DO_NULL);
        else
          value = -QuiesceChecks(tree, -beta, -alpha, Flip(wtm), 2);
        first_move = 0;
      } else {
        if (depth - 1 + extensions > 0) {
			if(Check(wtm))
				value =  -SearchCheck(tree, -alpha - 1, -alpha, Flip(wtm), depth - 1 + extensions, 2, DO_NULL);
			else
          value = -Search(tree, -alpha - 1, -alpha, Flip(wtm), depth - 1 + extensions, 2, DO_NULL);
          if &#40;value > alpha && extensions < 0&#41;
            value =  -Search&#40;tree, -alpha - 1, -alpha, Flip&#40;wtm&#41;, depth - 1, 2, DO_NULL&#41;;
        &#125; else
          value = -QuiesceChecks&#40;tree, -alpha - 1, -alpha, Flip&#40;wtm&#41;, 2&#41;;
        if &#40;abort_search&#41;
          break;
        if (&#40;value > alpha&#41; && &#40;value < beta&#41;) &#123;
          extensions = Max&#40;extensions, 0&#41;;
          if &#40;depth - 1 + extensions > 0&#41;
            value =  -Search&#40;tree, -beta, -alpha, Flip&#40;wtm&#41;, depth - 1 + extensions, 2, DO_NULL&#41;;
          else
            value = -QuiesceChecks&#40;tree, -beta, -alpha, Flip&#40;wtm&#41;, 2&#41;;
        &#125;
      &#125;

and

Code: Select all

int SearchCheck&#40;TREE * RESTRICT tree, int alpha, int beta, int wtm, int depth, int ply, int do_null&#41; &#123;
  register BITBOARD start_nodes = tree->nodes_searched;
  register int moves_searched = 0;
  register int o_alpha, value = 0, t_beta = beta;
  register int extensions, extended, pieces;
  register int fprune;

/*
 ************************************************************
 *                                                          *
 *   Check to see if we have searched enough nodes that it  *
 *   is time to peek at how much time has been used, or if  *
 *   is time to check for operator keyboard input.  This is *
 *   usually enough nodes to force a time/input check about *
 *   once per second, except when the target time per move  *
 *   is very small, in which case we try to check the time  *
 *   at least 10 times during the search.                   *
 *                                                          *
 *   Note that we check for timeout in all active threads,  *
 *   but we only do I/O in thread 0 to avoid read race      *
 *   conditions that are problematic.                       *
 *                                                          *
 ************************************************************
 */
  tree->nodes_searched++;
#if defined&#40;NODES&#41;
  temp_search_nodes--;
  if &#40;temp_search_nodes <= 0&#41; &#123;
    time_abort++;
    abort_search = 1;
    return &#40;0&#41;;
  &#125;
#endif
  if (--next_time_check <= 0&#41; &#123;
    next_time_check = nodes_between_time_checks;
    if &#40;TimeCheck&#40;tree, 0&#41;) &#123;
      time_abort++;
      abort_search = 1;
      return &#40;0&#41;;
    &#125;
    if &#40;tree->thread_id == 0&#41; &#123;
      if &#40;CheckInput&#40;))
        Interrupt&#40;ply&#41;;
    &#125;
  &#125;
  if &#40;ply >= MAXPLY - 1&#41;
    return &#40;beta&#41;;
/*
 ************************************************************
 *                                                          *
 *   Check for draw by repetition.                          *
 *                                                          *
 ************************************************************
 */
  if &#40;RepetitionCheck&#40;tree, ply, wtm&#41;) &#123;
    value = DrawScore&#40;wtm&#41;;
    if &#40;value < beta&#41;
      SavePV&#40;tree, ply, 0&#41;;
#if defined&#40;TRACE&#41;
    if &#40;ply <= trace_level&#41;
      printf&#40;"draw by repetition detected, ply=%d.\n", ply&#41;;
#endif
    return &#40;value&#41;;
  &#125;
/*
 ************************************************************
 *                                                          *
 *   Now call HashProbe&#40;) to see if this position has been  *
 *   searched before.  If so, we may get a real score,      *
 *   produce a cutoff, or get nothing more than a good move *
 *   to try first.  There are four cases to handle&#58;         *
 *                                                          *
 *   1. HashProbe&#40;) returns "EXACT" if this score is        *
 *   greater than beta, return beta.  Otherwise, return the *
 *   score.  In either case, no further searching is needed *
 *   from this position.  Note that lookup verified that    *
 *   the table position has sufficient "draft" to meet the  *
 *   requirements of the current search depth remaining.    *
 *                                                          *
 *   2. HashProbe&#40;) returns "UPPER" which means that when   *
 *   this position was searched previously, every move was  *
 *   "refuted" by one of its descendents.  As a result,     *
 *   when the search was completed, we returned alpha at    *
 *   that point.  We simply return alpha here as well.      *
 *                                                          *
 *   3. HashProbe&#40;) returns "LOWER" which means that when   *
 *   we encountered this position before, we searched one   *
 *   branch &#40;probably&#41; which promptly refuted the move at   *
 *   the previous ply.                                      *
 *                                                          *
 *   4. HashProbe&#40;) returns "AVOID_NULL_MOVE" which means   *
 *   the hashed score/bound was no good, but it indicated   *
 *   that trying a null-move in this position would be a    *
 *   waste of time since it will likely fail low, not high. *
 *                                                          *
 ************************************************************
 */
  switch &#40;HashProbe&#40;tree, ply, depth, wtm, &alpha, beta&#41;) &#123;
    case EXACT&#58;
      if &#40;alpha < beta&#41;
        SavePV&#40;tree, ply, 1&#41;;
      return &#40;alpha&#41;;
    case EXACTEGTB&#58;
      if &#40;alpha < beta&#41;
        SavePV&#40;tree, ply, 2&#41;;
      return &#40;alpha&#41;;
    case LOWER&#58;
      return &#40;beta&#41;;
    case UPPER&#58;
      return &#40;alpha&#41;;
    case AVOID_NULL_MOVE&#58;
      do_null = 0;
  &#125;
/*
 ************************************************************
 *                                                          *
 *   Now it's time to try a probe into the endgame table-   *
 *   base files.  This is done if we notice that there are  *
 *   6 or fewer pieces left on the board.  EGTB_use tells   *
 *   us how many pieces to probe on.  Note that this can be *
 *   zero when trying to swindle the opponent, so that no   *
 *   probes are done since we know it is a draw.            *
 *                                                          *
 ************************************************************
 */
#if !defined&#40;NOEGTB&#41;
  if &#40;ply <= iteration_depth && TotalAllPieces <= EGTB_use &&
      Castle&#40;ply, white&#41; + Castle&#40;ply, black&#41; == 0 &&
      &#40;CaptureOrPromote&#40;tree->curmv&#91;ply - 1&#93;) || ply < 3&#41;) &#123;
    int egtb_value;

    tree->egtb_probes++;
    if &#40;EGTBProbe&#40;tree, ply, wtm, &egtb_value&#41;) &#123;
      tree->egtb_probes_successful++;
      alpha = egtb_value;
      if &#40;abs&#40;alpha&#41; > MATE - 300&#41;
        alpha += &#40;alpha > 0&#41; ? -ply + 1 &#58; ply;
      else if &#40;alpha == 0&#41; &#123;
        alpha = DrawScore&#40;wtm&#41;;
        if &#40;Material > 0&#41;
          alpha += &#40;wtm&#41; ? 1 &#58; -1;
        else if &#40;Material < 0&#41;
          alpha -= &#40;wtm&#41; ? 1 &#58; -1;
      &#125;
      if &#40;alpha < beta&#41;
        SavePV&#40;tree, ply, 2&#41;;
      tree->pv&#91;ply&#93;.pathl = 0;
      HashStore&#40;tree, ply, MAX_DRAFT, wtm, EXACT, alpha,
          tree->pv&#91;ply&#93;.path&#91;ply&#93;);
      return &#40;alpha&#41;;
    &#125;
  &#125;
#endif

/*
 ************************************************************
 *                                                          *
 *   Now iterate through the move list and search the       *
 *   resulting positions.  Note that Search&#40;) culls any     *
 *   move that is not legal by using Check&#40;).  The special  *
 *   case is that we must find one legal move to search to  *
 *   confirm that it's not a mate or draw.                  *
 *                                                          *
 *   First step is to see if we need to extend this move    *
 *   for some tactical reason.  If not, we check to see if  *
 *   we can reduce the depth &#40;LMR&#41; to save time.  A final   *
 *   case is to determine if we can use AEL pruning         *
 *   &#40;Heinz 2000&#41; near the search frontier.  Note for those *
 *   that have read Heinz's paper.  Frontier nodes in       *
 *   crafty have a depth = 2.  Pre-frontier nodes  have a   *
 *   depth = 3.  And finally, pre-pre-frontier nodes have a *
 *   depth = 4.                                             *
 *                                                          *
 ************************************************************
 */
  tree->next_status&#91;ply&#93;.phase = HASH_MOVE;
 // if&#40;tree->inchk&#91;ply&#93;)
//	  tree->phase&#91;ply&#93; = NextEvasion&#40;tree, ply, wtm&#41; 

  while &#40;tree->phase&#91;ply&#93; = NextEvasion&#40;tree, ply, wtm&#41;) &#123;
#if defined&#40;TRACE&#41;
    if &#40;ply <= trace_level&#41;
      Trace&#40;tree, ply, depth, wtm, alpha, beta, "Search2", tree->phase&#91;ply&#93;);
#endif
    MakeMove&#40;tree, ply, tree->curmv&#91;ply&#93;, wtm&#41;;
    if &#40;tree->inchk&#91;ply&#93; || !Check&#40;wtm&#41;)
      do &#123;
        extended = SearchExtensions&#40;tree, wtm, ply, depth&#41;;
        extensions = extended - 1;
        fprune = 0;


		
		if &#40;depth + extensions > 0&#41;&#123;				  
			if &#40;Check&#40;wtm&#41;)
			      value = -SearchCheck&#40;tree, -beta, -alpha, Flip&#40;wtm&#41;, depth + extensions, ply + 1, DO_NULL&#41;;
			else   
				  value = -Search&#40;tree, -beta, -alpha, Flip&#40;wtm&#41;, depth + extensions, ply + 1, DO_NULL&#41;;        
		&#125;else 			
			value = -QuiesceChecks&#40;tree, -beta, -alpha, Flip&#40;wtm&#41;, ply + 1&#41;;

        if &#40;abort_search || tree->stop&#41;
            break;
        
        if &#40;value > alpha&#41; &#123;
          if &#40;value >= beta&#41; &#123;
            Killer&#40;tree, ply, tree->curmv&#91;ply&#93;);
            UnmakeMove&#40;tree, ply, tree->curmv&#91;ply&#93;, wtm&#41;;
            HashStore&#40;tree, ply, depth, wtm, LOWER, value, tree->curmv&#91;ply&#93;);
            tree->fail_high++;
            if (!moves_searched&#41;
              tree->fail_high_first++;
            return &#40;value&#41;;
          &#125;
          alpha = value;
        &#125;
        t_beta = alpha + 1;
        moves_searched++;
      &#125; while &#40;0&#41;;
    else
      tree->nodes_searched++;
    UnmakeMove&#40;tree, ply, tree->curmv&#91;ply&#93;, wtm&#41;;
    if &#40;abort_search || tree->stop&#41;
      return &#40;0&#41;;
/*
 ************************************************************
 *                                                          *
 *   If this is an SMP search, and we have idle processors, *
 *   now is the time to get them involved.  We have now     *
 *   satisfied the "young brothers wait" condition since we *
 *   have searched one move.  All that is left is to check  *
 *   the size of the tree we have searched so far, so that  *
 *   we do not split too near the tips and drive up the     *
 *   overhead unacceptably.  This has the additional effect *
 *   that we might split after 2-3 moves have been searched *
 *   which might sound like an issue, but the overhead is   *
 *   not so critical if we are more certain that we need to *
 *   actually search every move.  The more moves we have    *
 *   searched, the greater the probability that we are      *
 *   going to search them all.                              *
 *                                                          *
 ************************************************************
 */
#if &#40;CPUS > 1&#41;
    if &#40;smp_idle && moves_searched &&
        tree->nodes_searched - start_nodes > smp_split_nodes&#41; &#123;
      tree->alpha = alpha;
      tree->beta = beta;
      tree->value = alpha;
      tree->wtm = wtm;
      tree->ply = ply;
      tree->depth = depth;
      if &#40;Thread&#40;tree&#41;) &#123;
        if &#40;abort_search || tree->stop&#41;
          return &#40;0&#41;;
        if &#40;tree->thread_id == 0 && CheckInput&#40;))
          Interrupt&#40;ply&#41;;
        value = tree->search_value;
        if &#40;value > alpha&#41; &#123;
          if &#40;value >= beta&#41; &#123;
            Killer&#40;tree, ply, tree->cutmove&#41;;
            HashStore&#40;tree, ply, depth, wtm, LOWER, value, tree->cutmove&#41;;
            tree->fail_high++;
            return &#40;value&#41;;
          &#125;
          alpha = value;
          break;
        &#125;
      &#125;
    &#125;
#endif
  &#125;
/*
 ************************************************************
 *                                                          *
 *   All moves have been searched.  If none were legal,     *
 *   return either MATE or DRAW depending on whether the    *
 *   side to move is in check or not.                       *
 *                                                          *
 ************************************************************
 */
  if &#40;moves_searched == 0&#41; &#123;
    value = &#40;Check&#40;wtm&#41;) ? -&#40;MATE - ply&#41; &#58; DrawScore&#40;wtm&#41;;
    if &#40;value >= alpha && value < beta&#41; &#123;
      SavePV&#40;tree, ply, 0&#41;;
#if defined&#40;TRACE&#41;
      if &#40;ply <= trace_level&#41;
        printf&#40;"Search&#40;) no moves!  ply=%d\n", ply&#41;;
#endif
    &#125;
    return &#40;value&#41;;
  &#125; else &#123;
    int bestmove = &#40;alpha == o_alpha&#41; ? 0 &#58; tree->pv&#91;ply&#93;.path&#91;ply&#93;;
    int type = &#40;alpha == o_alpha&#41; ? UPPER &#58; EXACT;

    if &#40;alpha != o_alpha&#41; &#123;
      memcpy&#40;&tree->pv&#91;ply - 1&#93;.path&#91;ply&#93;, &tree->pv&#91;ply&#93;.path&#91;ply&#93;,
          &#40;tree->pv&#91;ply&#93;.pathl - ply + 1&#41; * sizeof&#40;int&#41;);
      memcpy&#40;&tree->pv&#91;ply - 1&#93;.pathh, &tree->pv&#91;ply&#93;.pathh, 3&#41;;
      tree->pv&#91;ply - 1&#93;.path&#91;ply - 1&#93; = tree->curmv&#91;ply - 1&#93;;
      Killer&#40;tree, ply, tree->pv&#91;ply&#93;.path&#91;ply&#93;);
    &#125;
    HashStore&#40;tree, ply, depth, wtm, type, alpha, bestmove&#41;;
    return &#40;alpha&#41;;
  &#125;
&#125;