Issues with futility pruning

Discussion of chess software programming and technical issues.

Moderator: Ras

Chan Rasjid
Posts: 588
Joined: Thu Mar 09, 2006 4:47 pm
Location: Singapore

Re: Issues with futility pruning

Post by Chan Rasjid »

algerbrex wrote: Sun Sep 05, 2021 8:51 pm
Hmm yeah thanks, I’m gonna try those suggestions, because last night I tried PVS again and it worked in the sense the number of nodes was reduced. But testing it against the version with out it, it was a -30 Elo gain.

So I’m still doing something wrong, and I think you’re onto what the problem is when you mentioned searched stability, because I forgot since I use a TT, I can run into search stabilities.

With regards to handling the instabilities, I just need to check if the score returned is greater than alpha AND less than beta, and then do a research with a full window? Or do I need to use a window of (score-1, beta)?
Yes. When instability found, just searched with full (alpha, beta) window.

Code: Select all

// w/o futility
info depth 17, score -384 cp, time 16665, nodes 23054519, nps 1383409, ebf  2.60,  pv h2f2 b3c2 f2e1 c2c5 e1d2 h8g6 d2d4 c5d4 e3d4 e6f6 f5f6 g7f6 d1e3 g6e7 g1f2 f7e6 e2f1  

//with futility/reverse futility/see-qs-prune in non-pv nodes
info depth 17, score -385 cp, time 9818, nodes 11737824, nps 1195541, ebf  2.51,  pv h2f2 b3c2 f2e1 c2c5 e1d2 e6e7 d2d4 c5d4 e3d4 e7b7 d1e3 f7e6 f5f1 b7b2 f1f2 b2b1 g1g2  
I just confirmed that my futility is working. The analysis with futility returned the same score and pv line, but with far fewer nodes searched and better time.

[edit]You can test if your futility pruning is working by just doing analysis of a position to the same depth; if futility is working, you'll get the same score and pv but with much better time as I found with my implementation. In such a case ELO cannot be going down!
Ferdy
Posts: 4851
Joined: Sun Aug 10, 2008 3:15 pm
Location: Philippines

Re: Issues with futility pruning

Post by Ferdy »

algerbrex wrote: Sun Sep 05, 2021 4:18 am I'm getting weird results when introducing futility pruning into Blunder. On the one hand, I'm getting a pretty sizeable reduction in nodes, and I'm searching a full ply deeper for some positions (e.g. kiwipete), but I'm getting no, or negative Elo gain when I'm testing.

Now, I understand this is because I'm likely pruning too many good lines, so the Elo gain from the increased search depth is being offset by missing winning tactical lines. Where I'm stuck is figuring out what I'm missing (or including) in my futility pruning code that's causing this "over-pruning". Over the past week or so, I've done quite a bit of research and have tried many different configurations and ideas, and none of them seem to be working for me.

I've also taken a look at the code of other engines, and it doesn't seem like I'm doing anything weird. Here are the relevant parts of my search code:

Code: Select all

// The primary negamax function, which only returns a score and no best move.
func (search *Search) negamax(depth, ply uint8, alpha, beta int16, doNull bool) int16 {
        ...

	//  Check if futility pruning can be done.
	canFutilityPrune := false
	futilityMargin := int16(200 * depth)
	if depth <= 4 &&
		!inCheck &&
		alpha < Checkmate {
		canFutilityPrune = staticScore+futilityMargin <= alpha
	}

	...

	// Set a flag to record if any pruning was done. If pruning was done, then
	// we can't declare a mate, since we didn't test every move.
	noPruning := true

	for index := 0; index < int(moves.Count); index++ {

                ...

		givesCheck := sqIsAttacked(
			&search.Pos,
			search.Pos.SideToMove,
			search.Pos.PieceBB[search.Pos.SideToMove][King].Msb())

		tactical := givesCheck || move.MoveType() == Attack || move.MoveType() == Promotion
		important := move.Equal(hashMove)

		if canFutilityPrune && legalMoves > 0 && !tactical && !important {
			search.Pos.UnmakeMove(move)
			noPruning = false
			continue
		}

		...
	}

	// If we don't have any legal moves, and we haven't pruned moves, it's either checkmate, or a stalemate.
	if legalMoves == 0 && noPruning {
		if inCheck {
			// If its checkmate, return a checkmate score of negative infinity,
			// with the current ply added to it. That way, the engine will be
			// rewarded for finding mate quicker, or avoiding mate longer.
			return -Inf + int16(ply)
		} else {
			// If it's a draw, return the draw value.
			return search.contempt()
		}
	}

	// Return the best score, which is alpha.
	return alpha
}
As I mentioned, I've fiddled in various ways with tuning the futility margin, what counts as a "tactical" move, and what counts as an "important" move. And nothing I've tried so far so shows any positive gain. And the other threads I've visited on TalkChess where futility pruning wasn't working didn't help much either.

I've also tried a debugging idea I saw in another thread, where at pre-horizon nodes (depth=1) if a move was pruned, I perform a normal full-search and make sure that the move failed-low and my futility margin was high enough and my pruning seemed to pass this test, so I must admit, at this point, I'm pretty stumped. Any ideas as to where I'm going wrong with my pruning would be appreciated.
There are two futility pruning schemes that you can use.
1. Position futility pruning
2. Move futility pruning

In (1) if current position is already bad then there is no need to search further, the best move of this position may no longer improve your root position, you can return alpha or qsearch() score in this case.

Code: Select all

	//  Check if futility pruning can be done.
	canFutilityPrune := false
	futilityMargin := int16(200 * depth)
	if depth <= 4 &&
		!inCheck &&
		alpha < Checkmate {
		canFutilityPrune = staticScore+futilityMargin <= alpha
	}
	if canFutilityPrune
	   return alpha or qsearch()
For example from startpos and into the position below which is already bad, you can now prune this branch.

[fen]rnbqkbnr/p1pp1ppp/p7/4p3/4P3/8/PPPP1PPP/RNBQK1NR w KQkq - 0 3[/fen]

In (2) when you are looping through your move list finding bad moves.

Code: Select all

if not tactical_move and other conditions
   continue checking other moves
This one is effective if the position is not yet bad. If position is not bad, there might be bad and good moves. You try to find the bad ones here and prune it. Example the position below is not bad for white but there are good and bad moves. Bxf7, Ba6 and Ng5 moves are bad here for example. These are the moves that you need to prune without searching or searching at shallow depth.

[fen]r1bqk1nr/pppp1ppp/2n5/2b1p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4[/fen]
In your current method, you prune moves from the already bad positions, so this is not effective and might lose strength.
Chan Rasjid
Posts: 588
Joined: Thu Mar 09, 2006 4:47 pm
Location: Singapore

Re: Issues with futility pruning

Post by Chan Rasjid »

Ferdy wrote: Mon Sep 06, 2021 3:09 pm There are two futility pruning schemes that you can use.
1. Position futility pruning
2. Move futility pruning

In (1) if current position is already bad then there is no need to search further, the best move of this position may no longer improve your root position, you can return alpha or qsearch() score in this case.

Code: Select all

	//  Check if futility pruning can be done.
	canFutilityPrune := false
	futilityMargin := int16(200 * depth)
	if depth <= 4 &&
		!inCheck &&
		alpha < Checkmate {
		canFutilityPrune = staticScore+futilityMargin <= alpha
	}
	if canFutilityPrune
	   return alpha or qsearch()
For example from startpos and into the position below which is already bad, you can now prune this branch.

[fen]rnbqkbnr/p1pp1ppp/p7/4p3/4P3/8/PPPP1PPP/RNBQK1NR w KQkq - 0 3[/fen]
I am not a chess player; can't see 1 move ahead so cannot see why the position is bad. But my program gives a score of-300cp! I don't know why my evaluation/search can see it.

Can it happen that our evaluation function may fail badly?
User avatar
Ras
Posts: 2703
Joined: Tue Aug 30, 2016 8:19 pm
Full name: Rasmus Althoff

Re: Issues with futility pruning

Post by Ras »

Chan Rasjid wrote: Mon Sep 06, 2021 3:35 pmI am not a chess player; can't see 1 move ahead so cannot see why the position is bad. But my program gives a score of-300cp! I don't know why my evaluation/search can see it.
Because your eval can count material and finds that White is down by one bishop.
Rasmus Althoff
https://www.ct800.net
Ferdy
Posts: 4851
Joined: Sun Aug 10, 2008 3:15 pm
Location: Philippines

Re: Issues with futility pruning

Post by Ferdy »

Chan Rasjid wrote: Mon Sep 06, 2021 3:35 pm
Ferdy wrote: Mon Sep 06, 2021 3:09 pm There are two futility pruning schemes that you can use.
1. Position futility pruning
2. Move futility pruning

In (1) if current position is already bad then there is no need to search further, the best move of this position may no longer improve your root position, you can return alpha or qsearch() score in this case.

Code: Select all

	//  Check if futility pruning can be done.
	canFutilityPrune := false
	futilityMargin := int16(200 * depth)
	if depth <= 4 &&
		!inCheck &&
		alpha < Checkmate {
		canFutilityPrune = staticScore+futilityMargin <= alpha
	}
	if canFutilityPrune
	   return alpha or qsearch()
For example from startpos and into the position below which is already bad, you can now prune this branch.

[fen]rnbqkbnr/p1pp1ppp/p7/4p3/4P3/8/PPPP1PPP/RNBQK1NR w KQkq - 0 3[/fen]
I am not a chess player; can't see 1 move ahead so cannot see why the position is bad. But my program gives a score of-300cp! I don't know why my evaluation/search can see it.

Can it happen that our evaluation function may fail badly?
Black is a bishop ahead.

There are positions that static eval may see the position as good for side to move, but when you apply a search that position is no longer good. Happens when king safety and passed pawn for example are involved.
User avatar
algerbrex
Posts: 608
Joined: Sun May 30, 2021 5:03 am
Location: United States
Full name: Christian Dean

Re: Issues with futility pruning

Post by algerbrex »

Ferdy wrote: Mon Sep 06, 2021 3:09 pm
algerbrex wrote: Sun Sep 05, 2021 4:18 am I'm getting weird results when introducing futility pruning into Blunder. On the one hand, I'm getting a pretty sizeable reduction in nodes, and I'm searching a full ply deeper for some positions (e.g. kiwipete), but I'm getting no, or negative Elo gain when I'm testing.

Now, I understand this is because I'm likely pruning too many good lines, so the Elo gain from the increased search depth is being offset by missing winning tactical lines. Where I'm stuck is figuring out what I'm missing (or including) in my futility pruning code that's causing this "over-pruning". Over the past week or so, I've done quite a bit of research and have tried many different configurations and ideas, and none of them seem to be working for me.

I've also taken a look at the code of other engines, and it doesn't seem like I'm doing anything weird. Here are the relevant parts of my search code:

Code: Select all

// The primary negamax function, which only returns a score and no best move.
func (search *Search) negamax(depth, ply uint8, alpha, beta int16, doNull bool) int16 {
        ...

	//  Check if futility pruning can be done.
	canFutilityPrune := false
	futilityMargin := int16(200 * depth)
	if depth <= 4 &&
		!inCheck &&
		alpha < Checkmate {
		canFutilityPrune = staticScore+futilityMargin <= alpha
	}

	...

	// Set a flag to record if any pruning was done. If pruning was done, then
	// we can't declare a mate, since we didn't test every move.
	noPruning := true

	for index := 0; index < int(moves.Count); index++ {

                ...

		givesCheck := sqIsAttacked(
			&search.Pos,
			search.Pos.SideToMove,
			search.Pos.PieceBB[search.Pos.SideToMove][King].Msb())

		tactical := givesCheck || move.MoveType() == Attack || move.MoveType() == Promotion
		important := move.Equal(hashMove)

		if canFutilityPrune && legalMoves > 0 && !tactical && !important {
			search.Pos.UnmakeMove(move)
			noPruning = false
			continue
		}

		...
	}

	// If we don't have any legal moves, and we haven't pruned moves, it's either checkmate, or a stalemate.
	if legalMoves == 0 && noPruning {
		if inCheck {
			// If its checkmate, return a checkmate score of negative infinity,
			// with the current ply added to it. That way, the engine will be
			// rewarded for finding mate quicker, or avoiding mate longer.
			return -Inf + int16(ply)
		} else {
			// If it's a draw, return the draw value.
			return search.contempt()
		}
	}

	// Return the best score, which is alpha.
	return alpha
}
As I mentioned, I've fiddled in various ways with tuning the futility margin, what counts as a "tactical" move, and what counts as an "important" move. And nothing I've tried so far so shows any positive gain. And the other threads I've visited on TalkChess where futility pruning wasn't working didn't help much either.

I've also tried a debugging idea I saw in another thread, where at pre-horizon nodes (depth=1) if a move was pruned, I perform a normal full-search and make sure that the move failed-low and my futility margin was high enough and my pruning seemed to pass this test, so I must admit, at this point, I'm pretty stumped. Any ideas as to where I'm going wrong with my pruning would be appreciated.
There are two futility pruning schemes that you can use.
1. Position futility pruning
2. Move futility pruning

In (1) if current position is already bad then there is no need to search further, the best move of this position may no longer improve your root position, you can return alpha or qsearch() score in this case.

Code: Select all

	//  Check if futility pruning can be done.
	canFutilityPrune := false
	futilityMargin := int16(200 * depth)
	if depth <= 4 &&
		!inCheck &&
		alpha < Checkmate {
		canFutilityPrune = staticScore+futilityMargin <= alpha
	}
	if canFutilityPrune
	   return alpha or qsearch()
For example from startpos and into the position below which is already bad, you can now prune this branch.

[fen]rnbqkbnr/p1pp1ppp/p7/4p3/4P3/8/PPPP1PPP/RNBQK1NR w KQkq - 0 3[/fen]

In (2) when you are looping through your move list finding bad moves.

Code: Select all

if not tactical_move and other conditions
   continue checking other moves
This one is effective if the position is not yet bad. If position is not bad, there might be bad and good moves. You try to find the bad ones here and prune it. Example the position below is not bad for white but there are good and bad moves. Bxf7, Ba6 and Ng5 moves are bad here for example. These are the moves that you need to prune without searching or searching at shallow depth.

[fen]r1bqk1nr/pppp1ppp/2n5/2b1p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4[/fen]
In your current method, you prune moves from the already bad positions, so this is not effective and might lose strength.
Hmm, ah. I think I see what you mean. That makes sense. I'll modify the code and re-run the tests to see if I get any improvement.
User avatar
algerbrex
Posts: 608
Joined: Sun May 30, 2021 5:03 am
Location: United States
Full name: Christian Dean

Re: Issues with futility pruning

Post by algerbrex »

Cardoso wrote: Mon Sep 06, 2021 1:01 am Assuming you don't have bugs it's always a question of finding the right equilibrium.
Too much futility pruning and the engine starts to get blind.
Please check if your staticScore describes your current position with some precision.
Another thing you can do is to insert code to test your futility mechanism but without ever applying futility.
Instead of continuing/skipping to the next move in case canFutilityPrune is true, just continue as normal
as if no futility pruning is implemented.
If you fail high where canFutilityPrune is true then you should dump this position to a log file, along with alpha, static score, depth, ply, etc.
Also use 2 counters: one for how many futility cases would occur (the number of times canFutilityPrune is set
to true), another for how many FHs occur in cases where futility should kick in.
Dump these counters when you dump the position with false futility cases.
This way you can catch bugs of incorrect futility cases.
Of course there always should be false/incorrect futility cases, but usually these should happen because there is a tactical shot.
Thanks Cardoso, I'll try that debugging idea to try to track down the problem.