Page 2 of 4

Re: Making dumb dumber

Posted: Thu Mar 04, 2021 6:57 pm
by mvanthoor
lithander wrote: Thu Mar 04, 2021 6:32 pm It may be a stupid question but do you count every node evaluated in QSearch or stop counting as soon as you reach depth 0 and start QSearch?
In my case, as I said, I count every node that is being searched; thus, every node that generates a move list.

(I could add a node increment where the evaluation is called as well, because that is also work... but I haven't yet.) Obviously I don't know what is done in Dumber, because I didn't check it yet :P

Re: Making dumb dumber

Posted: Thu Mar 04, 2021 7:35 pm
by lithander
mvanthoor wrote: Thu Mar 04, 2021 6:57 pm In my case, as I said, I count every node that is being searched; thus, every node that generates a move list.

(I could add a node increment where the evaluation is called as well, because that is also work... but I haven't yet.) Obviously I don't know what is done in Dumber, because I didn't check it yet :P
I meant "you" in it's informal plural form. ("you all") What I'm really looking for is a consensus of what is usually counted and what isn't.

QSearch could be considered as a form of better evaluation and not part of the search. After all it's called only when you have reach the final search depth already and it replaces the static evaluation that I previously did with these (former) leaf nodes. So I count each transition of normal-search to q-search as *one* node at the moment. (Explains my abysmal NPS, now that I think about it, if other engines count differently.)

Re: Making dumb dumber

Posted: Thu Mar 04, 2021 7:43 pm
by hgm
One usually does count QS nodes. None of my engines actually does have a separate routine for QS. The most common way is to count every call to Search/QSearch. (So also those that return after very little work, such as a hash or stand-pat cutoff.) But if you have separate QS, that is called through an if(depth <= 0) early in Search, make sure you would not count it as two nodes.

Re: Making dumb dumber

Posted: Fri Mar 05, 2021 9:23 am
by mvanthoor
hgm wrote: Thu Mar 04, 2021 7:43 pm One usually does count QS nodes. None of my engines actually does have a separate routine for QS. The most common way is to count every call to Search/QSearch. (So also those that return after very little work, such as a hash or stand-pat cutoff.) But if you have separate QS, that is called through an if(depth <= 0) early in Search, make sure you would not count it as two nodes.
In that case I'll change the counting by puttin the node count increment at the top of Search and QSearch.

However, as you already state, any node where the engine goes into QSearch, will thus be counted twice; once at the top of each routine. That would mean that in "depth <= 0", the node count must be decremented first to undo the earlier count in search. At least, I asssume that this is what you mean with regard to being counted twice.

Re: Making dumb dumber

Posted: Fri Mar 05, 2021 2:00 pm
by hgm
Indeed. But I assume that you can simply increment at the top of QSearch(), and place the increment of the counter in Search() just after the call to QSearch(). I don't suppose there are reasons to return from Search() before you have made this test.

Re: Making dumb dumber

Posted: Fri Mar 05, 2021 2:08 pm
by mvanthoor
hgm wrote: Fri Mar 05, 2021 2:00 pm Indeed. But I assume that you can simply increment at the top of QSearch(), and place the increment of the counter in Search() just after the call to QSearch(). I don't suppose there are reasons to return from Search() before you have made this test.
There are some early exists before depth <= 0 (such as time is up, position is draw because of rules or material) which would make the node to be not counted, if I put the increment after depth <= 0.

Re: Making dumb dumber

Posted: Fri Mar 05, 2021 2:43 pm
by hgm
I would not worry about wether aborted nodes are counted in a 'time is up' situation. The purpose of the node count is mainly to judge the speed in a running search. I put that test always after the UnMake(), btw, because then it automatically prevents something is done with the returned (corrupted) score. Whether a few nodes more are searched after timeout by going deeper into the recursion is insignificant; I only test for timeout every thousand nodes or so anyway.

The repetition test is more of an issue. I usually do that in the parent, though. Even before MakeMove(). So for me they would never be counted. Draws by insufficient material and such would only occur in some end-games. I wouldn't worry much whether these were counted or not either.

Re: Making dumb dumber

Posted: Fri Mar 05, 2021 2:53 pm
by mvanthoor
hgm wrote: Fri Mar 05, 2021 2:43 pm I would not worry about wether aborted nodes are counted in a 'time is up' situation. The purpose of the node count is mainly to judge the speed in a running search. I put that test always after the UnMake(), btw, because then it automatically prevents something is done with the returned (corrupted) score. Whether a few nodes more are searched after timeout by going deeper into the recursion is insignificant; I only test for timeout every thousand nodes or so anyway.
Same; in my case, the score returned doesn't matter. When the time is up, the search_info will have "time up" as the reason for quitting search, and Iterative deepening will not use the result. If the run finishes normally for a depth, the flag will just be "finished". Iterative Deepening can see how a run has ended. (Finished, Aborted by Stop command, Time Up, that sort of thing.)
The repetition test is more of an issue. I usually do that in the parent, though. Even before MakeMove(). So for me they would never be counted. Draws by insufficient material and such would only occur in some end-games. I wouldn't worry much whether these were counted or not either.
I had it there before, but now it's an early exit because some engines do it like that. I have been testing if there's a difference, but it seems there isn't.

Re: Making dumb dumber

Posted: Fri Mar 05, 2021 3:04 pm
by hgm
Not that many positions will be repetitions, so it will definitely not make a significant difference. But doing it in the child definitely would make unneeded moves just to unmake them immediately afterwards, and that must always be slower. There is zero upside.

In Joker I even do the TT probe in the parent; I also don't need anything but the hash key for that, and if I get a TT cut-off, why waste time on make/unmake? This happens much more frequently than repetitions. Downside is that I need to pass the hash move to the child. Futility pruning of course does the stand-pat on behalf of the child (but based on an estimated evaluation, so it cannot do it when it is a close call, and the full evaluation would need the move to be made first).

Re: Making dumb dumber

Posted: Fri Mar 05, 2021 3:14 pm
by mvanthoor
hgm wrote: Fri Mar 05, 2021 3:04 pm Not that many positions will be repetitions, so it will definitely not make a significant difference. But doing it in the child definitely would make unneeded moves just to unmake them immediately afterwards, and that must always be slower. There is zero upside.

In Joker I even do the TT probe in the parent; I also don't need anything but the hash key for that, and if I get a TT cut-off, why waste time on make/unmake? This happens much more frequently than repetitions. Downside is that I need to pass the hash move to the child. Futility pruning of course does the stand-pat on behalf of the child (but based on an estimated evaluation, so it cannot do it when it is a close call, and the full evaluation would need the move to be made first).
Seems we're not talking about the same thing.

Previously, I did this:

Code: Select all

for m in move_loop {
	// do move loop stuff
	
	let evaluation = if !draw() {
		-alpha_beta(d -1, -b, -a)
	} else {
		DRAW
	}
}
So here, evaluation would either become what comes out of the recursive call, or DRAW immediately and then continue on to the next move. Now I do:

Code: Select all

if draw() {
	return DRAW;
}

for m in move_loop {
	// do move loop things
	let evaluation = -alpha_beta(d -1 , -b, -a);
}
In both cases, evaluation will be assigned DRAW. In the first case, through an if-statement, and in the second case, right after it re-enters alpha_beta and returns from there with DRAW. I was testing if the second case would gain anything over the first (as more engines are using the second), but it doesn't seem to.

With regard to hash probe: I do this immediately after the depth <= 0 QSearch check. If there is a TT cutoff by value, alpha-beta returns, and there are no moves generated and the move-loop is not started.