Magic SEE

Discussion of chess software programming and technical issues.

Moderators: hgm, Dann Corbit, Harvey Williamson

D Sceviour
Posts: 570
Joined: Mon Jul 20, 2015 5:06 pm

Re: Magic SEE

Post by D Sceviour »

hgm wrote:But how would you know the captures are good? MVV/LVA by itself is a pretty poor sort. Using SEE for the high x low captures works much better. And SEE might benefit from x-rays. You might be tempted to capture a Knight that is attacked by R and Q and only protected by a Rook, if you wouldn't realized that the Rook actually is a battery. While the move deserves to be pruned in QS...
Yes, I agree. However, the issue is whether the attacks information should be calculated during the move generation process for each move. The attacks should be calculated after the last make move, and perhaps that is what you mean. Is that the mis-understanding?

I also have doubts about MVV/LVA. In Quiescence, I use a simple HeavySort() that captures the heaviest piece first. It is faster to calculate and produces the same or better nodes-per-second on average.
User avatar
hgm
Posts: 27703
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Magic SEE

Post by hgm »

I am not sure what you mean by 'the last MakeMove'. In Joker I associate an MVV/LVA sort key to each capture when it is generated. That allows extraction of the best capture candidate from the move list by the time you start searching moves. But if it is a L x H capture it might actually be a bad capture, so I have to verify it by SEE. The SEE score will then replace the MVV/LVA sort key, and I will have to extract another move candidate if it is disappointing (e.g. if capture of a Rook only gained me a Pawn).

That can all happen before any moves were made at all, if there are no L x H or equal captures. Keep in mind that it is always the poor captures that remain, as you will never make them, so that they remain on the board.

To do the SEE on the selected move candidate it has to generate all moves for both sides to the to-square, including x-ray attacks. That is a pretty slow process, and profiling showed that the SEE takes about 30% of the execution time. It would have been much faster to get that same info as a side effect of move generation, when you have to subject many squares to SEE.
User avatar
hgm
Posts: 27703
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Magic SEE

Post by hgm »

mvk wrote:I don't recognise the 80% statistic, it sounds poor.
Well, I just made it up as an example. But the accuracy of SEE is typically poor. If it tells you the piece is unprotected then you can of course trust it. But you would not have needed a SEE for that. The point I want to make is that it doesn't bring any benefits to make the calculation of SEE more precise than the inherent error in limiting yourself to only a single square. There are so many cases that would cause the one-square assumption to break down, and exposing your King on another square to check is just a very minor part of those.
It was my strategy not to compromise on qsearch tree shape because the program will spend most of its time there. This part was done early in the design and of course I had a secondary requirement based on cycles per node as well. Everything can be solved by a generic search, but I want my program to be competitive as well.
Definitely QS deserves to be optimized as much as you can. So in fact I would like to use a move ordering that is better then what MVV/LVA+SEE can provide. Even in the absence of counter threats, plain MVV/LVA + SEE does pretty stupid things sometimes. E.g. when the same Knight can make a SEE=+3 capture on a Bishop, and a SEE=+2 capture on a Rook, you would try NxR first, because R > B. But that just lets the Bishop walk, and you remain at +2. (Of course when it would have been a different Knight that would have done the NxR it would have given you +5 doing that one first.)

Another such stupidity that seems easy to prevent occurs if the Bishop that was up for grabs would have been able to do the recapture for the NxR. This would also make the NxB threat evaporate. This also would not be too hard to detect.

In the face of counter threats things get even more complex. I really would like to have a QS that understands that if his own Queen is attacked by a Pawn playing Q x protected R should be tried before N x protected Q (rather than pruned!), provided that N x Q was not a check, and that his Queen does not attack anything as valuable as a Rook (or the Q x R delivers check). That doesn't seem asking for too much, and I would expect it to have a much larger impact than improving accuracy of a very deep SEE.
D Sceviour
Posts: 570
Joined: Mon Jul 20, 2015 5:06 pm

Re: Magic SEE

Post by D Sceviour »

hgm wrote:I am not sure what you mean by 'the last MakeMove'.
If you think about it, all the information for the attacks is already there without any extra calculation when generating the move list. Simply look up the information from the last makemove() in the previous ply. This would save 30 magic calculations for a move list with 30 moves.
hgm wrote:That is a pretty slow process, and profiling showed that the SEE takes about 30% of the execution time.
Ugh! Make life easier for yourself.
User avatar
hgm
Posts: 27703
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Magic SEE

Post by hgm »

D Sceviour wrote:
hgm wrote:I am not sure what you mean by 'the last MakeMove'.
If you think about it, all the information for the attacks is already there without any extra calculation when generating the move list. Simply look up the information from the last makemove() in the previous ply. This would save 30 magic calculations for a move list with 30 moves.
Well, the move list doesn't stay entirely the same after the move is made. The moved piece will have other moves from its new position, pieces can be blocked or unblocked. But if you searched null move before generating your own, you would have moves for both sides from the position. That doesn't help you for QS, however.

Move generation doesn't give you protectors or x-ray attacks, however. But it would be a lot cheaper to obtain those while you were generating moves, than doing it again from scratch. And with bitboards, pieces might be involved in attacks on more than one square, so redoing it for each attacked square would be wasteful too.

I really have a feeling a lot can be achieved here. I have been browsig a bit through a tree of a 5-ply search that took a rather large number of nodes, to see how well QS move sorting works, and I stumble on unfortunate wrong move choices all the time. E.g. in
[d]r1B1k2r/4bppp/1p2p3/6B1/N3n3/P7/1P3PPP/R3K2R b KQkq - 0 7
black can choose between 4 captures, and it goes for Nxg5 (14 nodes) and Bxg5 (20 nodes) first, because it has a lower attacker. Eventually the cut-move is Rxa4 (8 nodes), because it prevents Nxb6, by which the others are refuted.

If it had gone for Rxa4 immediately the QS of this position would have taken 9 nodes, instead of 43. And if it would have been threat-aware, Rxa4 would have been an obvious choice, as capturing the attacker of the Nxb6 threat could assume it solves a SEE=+1 threat, and up the sort key by 1 compared to the other captures. If Nxa4 was the previous white move, this could be used as a poor-man's method to go for Na4, as statisticall the moved piece has a high chance of creating new threats. This engine ('Simple') does add half a Pawn to the MVV/LVA key if it was the mover, but unfortunately this is offset here by the fact that it also is a HxL capture, and that in this version these were unconditionally postponed until after all LxH and equal captures. Future versions should only do that when the lower victim is protected, which would not have been the case here. The LVA part of the search also seems a bit pointless if there is no protector.
D Sceviour
Posts: 570
Joined: Mon Jul 20, 2015 5:06 pm

Re: Magic SEE

Post by D Sceviour »

hgm wrote:I really have a feeling a lot can be achieved here. I have been browsing a bit through a tree of a 5-ply search that took a rather large number of nodes, to see how well QS move sorting works, and I stumble on unfortunate wrong move choices all the time.
[d]r1B1k2r/4bppp/1p2p3/6B1/N3n3/P7/1P3PPP/R3K2R b KQkq - 0 7
I obtain similar results. From the initial move list:

Code: Select all

Black(1): list
#Move Value Capture Flag Nodes:
 1 e4-g5  0  3 0 0              2 e7-g5  0  3 0 0
 3 a8-a4  0  2 0 0              4 a8-c8  0  3 0 0
 5 e7-a3  0  1 0 0              6 e4-f2  0  1 0 0
 7 a8-a6  0  0 0 0              8 a8-a5  0  0 0 0
 9 h8-g8  0  0 0 0             10 a8-b8  0  0 0 0
11 h8-f8  0  0 0 0             12 h7-h6  0  0 0 0
13 h7-h5  0  0 0 0             14 g7-g6  0  0 0 0
15 f7-f6  0  0 0 0             16 f7-f5  0  0 0 0
17 e7-f8  0  0 0 0             18 e7-d6  0  0 0 0
19 e7-c5  0  0 0 0             20 e7-b4  0  0 0 0
21 e8-f8  0  0 0 0             22 e7-d8  0  0 0 0
23 e7-f6  0  0 0 0             24 e8-d8  0  0 0 0
25 e6-e5  0  0 0 0             26 b6-b5  0  0 0 0
27 e4-f6  0  0 0 0             28 e4-d6  0  0 0 0
29  o-o   0  0 CASTLE_KS 0     30 e4-c5  0  0 0 0
31 e4-g3  0  0 0 0             32 e4-c3  0  0 0 0
33 a8-a7  0  0 0 0             34 e4-d2  0  0 0 0

Search Iteration =  1
PV [1] -111  e4-g5 a4-b6
PV [1]  -70  e7-g5 a4-b6
PV [1]   31  a8-a4 g5-e7
Hash: Hits =0 Misses =0 Installs =0
Extensions= 0 Researches= 2

#Move Value Capture Flag Nodes:
 1 a8-a4  31   2 0 8            2 e7-g5 -70   3 0 14
 3 e4-g5 -111  3 0 10           4 a8-c8 -34   3 0 5
 5 e7-a3 -289  1 0 1            6 e4-f2 -281  1 0 1
 7 a8-a6 -368  0 0 1            8 a8-a5 -360  0 0 1
 9 h8-g8 -359  0 0 1           10 a8-b8 -364  0 0 1
11 h8-f8 -357  0 0 1           12 h7-h6 -324  0 0 1
13 h7-h5 -367  0 0 1           14 g7-g6 -378  0 0 1
15 f7-f6 -338  0 0 1           16 f7-f5 -378  0 0 1
17 e7-f8 -392  0 0 1           18 e7-d6 -358  0 0 1
19 e7-c5 -341  0 0 1           20 e7-b4 -701  0 0 6
21 e8-f8 -451  0 0 1           22 e7-d8 -381  0 0 1
23 e7-f6 -349  0 0 1           24 e8-d8 -385  0 0 1
25 e6-e5 -392  0 0 1           26 b6-b5 -322  0 0 1
27 e4-f6 -395  0 0 1           28 e4-d6 -400  0 0 1
29  o-o  -381  0 CASTLE_KS 1   30 e4-c5 -394  0 0 1
31 e4-g3 -410  0 0 1           32 e4-c3 -394  0 0 1
33 a8-a7 -363  0 0 1           34 e4-d2 -378  0 0 1
Nodes =             72
Total Nodes =       120
I agree that MVV/LVA is a waste of time here, and often in other positions. How is it that you propose to select first move as Rxa4 over Nxg5? How would magic SEE help here?

SEE routines are supposed to swap off all the material and help determine something, but frequently they miss critical interpolations, forks, promotions and back rank mates, etc. The main issue for me is to find a way to increase nodes-per-second and SEE does not help much. The use of magic SEE would only make the program slower, unless I am missing something.
User avatar
hgm
Posts: 27703
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Magic SEE

Post by hgm »

D Sceviour wrote:I agree that MVV/LVA is a waste of time here, and often in other positions. How is it that you propose to select first move as Rxa4 over Nxg5? How would magic SEE help here?
Magic SEE would not help directly; it would only help in the sense that a simple table lookup based on already available information on attackers sets wouldnot make it prohibitively expensive to subject every friendly and enemy piece to SEE, and take decisions based on that.

And if the tables for this only need to be small, it would also be possible to tabulate 'parallel SEE', based not on exchanges on a single square, but on two squares with initially enemy occupants. Such a DSEE(attackers1, protectors1, attackers2, protectors2) could be used to sort moves in the face of a counter attack. E.g. if anything can capture my unprotected Rook, SEE of a P x protected B capture would be +2, but DSEE would be -2, and making the capture would lose me the exchange.
The main issue for me is to find a way to increase nodes-per-second and SEE does not help much. The use of magic SEE would only make the program slower, unless I am missing something.
This is a wrong goal. Decreasing the number of nodes is far more important than increasing nodes/sec. If you can reduce nodes by a factor 1000 by slowing down nps a factor 10, you gain about 400 Elo.

Increasing nodes/sec is really a last resort, a dead-end street that never will get you very far. Of course eventually you should do that too, as everything helps. This is why I would like to use incrementally updated capture maps and such. But I don't think it could ever be more than 2-3 times faster than magic bitboard, for 8x8 boards, nps-wise. And the danger is that you paint yourself into a corner in using a very specific speed-tailored engine design, that turns out to be incompatible with providing information that could be used to reduce the node count by another factor 10.
User avatar
hgm
Posts: 27703
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Magic SEE

Post by hgm »

D Sceviour wrote:How is it that you propose to select first move as Rxa4 over Nxg5?
So to explicitly answer this question:

Calculation of SEE on all the black pieces would show that Pb6 is hanging,and threatened by Na4. This would reduce the 'double-SEE' of capturing all other captures of a minor from +3 to +2. Even with ordinary SEE this could be approximated by applying the rule that any tempo-losing (odd-SEE) capture should be reduced by the value of the threat against you, unless it solves the threat by capturing the perpetrator of that threat or capturing with the target. And by replacing LVA by SEE as secondary sort key to decide between equal-valued victims. (Which usually would imply LVA, except in cases where there is no protector, or where you would use multiple attackers that will always be recaptured, like P+R attack B protected by N+Q, where it does not matter very much if you start with P or R, and side effects (like the R being threatened by another P, so that the LVA-dictated PxB would be refuted by PxR) are more important.)
User avatar
hgm
Posts: 27703
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Magic SEE

Post by hgm »

mvk wrote:The hit rate for the imperfect hash of attacks^defenders is good enough, where attackers and defenders are just the linear combination of the piece counts, each of 12 bits if I recall correctly.
This means you store a signature in the hash table to verify a hit,and rehash on a miss?

I like the idea of hashing on some difference measure of the attacker and protector set, as often equal pieces would cancel. In the difference Pawn and Rook counts run from -2 to +2, though, so you would need a larger stride for the other pieces. But that can be arranged.

The problem is that they don't always cancel. E.g. a NR attack on a square defended by PN loses a minor (which is still attractive if the occupant would have been a Rook), while an R attack on a square defended by P loses a Rook. So the N does not cancel. In particular minors might not cancel if there is a P imbalance, etc.

In the engine design I have in mind attacker sets would be available for free as bits scattered in strides of 4 across an int64, in the captures map. The 8 bits in this representing Pawns will be sparse, as a square can be attacked by at most 2 Pawns,and we are interested in the number only. So collecting the 16 attackers bit representing the pieces by the usual multiply magic would give you a wastefully large index. Instead you could use a mask 0x88888888LL (say) to isolate the Pawn bits, and then multiply with 0x1111111100000000LL and shift right 60 to get the Pawn count. To get a piece index you could then mask with 0x8888888800000000LL, multiply by 0x204081 and shift right 56. You could use the resulting bit set to index a table that would give you a number uniquely representing the 3 or 4 lowest attackers, which could be added to the Pawn count.

Code: Select all

int attP = (attackers & pawnMask)*PAWNMAGIC>>60;
int att = attP + set2code[(attackers & pieceMask)*PIECEMAGIC>>56];
int protP = (protectors & pawnMask)*PAWNMAGIC>>60;
int prot = protP + set2code[(protectors & pieceMask)*PIECEMAGIC>>56];
see = seeTable[att][prot];
When the att and prot key only provide info on the lowest few attackers and perhaps the total number, they would not run up very high, and the seeTable could be very small. Possible combinations of up to 3 non-Pawn attackers are -, m, R, Q, K, mm, mR, mQ, mK, RR, RQ, RK, QK, mmm, mmR, mmQ, mmK, mRR, mRQ, mRK, mQK, RRQ, RRK. That is only 23, times 3 Pawn states (0, 1 or 2) = 69 combinations. So the see Table would only be 69x69 = 5 KB.

Trying to get SEEs with more than 3 stages right hardly serves any purpose. Note that the table would only be used for the recapture, so that the exchanges you calculate are really one ply longer. In practice I would already be happy if the SEE told me:

*) whether the piece I want to capture is hanging (no protectors at all).
*) if not, whether the first two ply are an investment that lose me material.
*) whether the exchange ends bad already after 3 ply, because the piece I capture and its lowest protector are worth less than the capturer.

The latter case I could prune without remorse. Captures that are no investment I would want to try immediately; captures that are an investment but where I could conceivably get out ahead I would want to try those (just to check that, and test for side effects), but only later. This mainly because I run the risk there that they are tempo-losing, so I cannot 'chain' them with other gains, and they hardly ever are very profitable by themselves due to the initial investment.

Having the first two attackers and protectors, plus the information on who has the most, might actually be more helpful than having the first 3. The Pawn count could also be used as part of the set2code index, to really get the 4 lowest attackers including Pawns, rather than 3-5 attackers depending on how many Pawns are involved.
D Sceviour
Posts: 570
Joined: Mon Jul 20, 2015 5:06 pm

Re: Magic SEE

Post by D Sceviour »

hgm wrote:This is a wrong goal. Decreasing the number of nodes is far more important than increasing nodes/sec. If you can reduce nodes by a factor 1000 by slowing down nps a factor 10, you gain about 400 Elo.

Increasing nodes/sec is really a last resort, a dead-end street that never will get you very far.
Your numbers simply do not add up. The fact is the top programs exhibit incredible speed. Every doubling of speed results in an extra ply depth of search (assuming a branching factor of 2). And every extra ply is worth 100-150 elo. If what you said were true, then threading would be a waste of time. Maybe it is for other reasons, but not for improvement with speed. There is no substitution for depth of search.

Node counts are important, but if you look at Schooner, you will find the node counts low. Much lower than Crafty for example, but Crafty exhibits speeds of 200 million moves per second (i7 - 3.4 GHz).
Crafty v23.4 PS (1 cpus)

White(1): perf
generated 80000000 moves, time=0.41 seconds
generated 197530864 moves per second
generated/made/unmade 80000000 moves, time=1.61 seconds
generated/made/unmade 49689440 moves per second
Crafty could not possibly do that with strenuous SEE calculations for move ordering. Your dead-end advice seems more suitable for an engine playing 1500 strength. Maybe it would be better increasing the number of nodes searched slightly to reduce errors and improve play.