PBeFS
Parallel Best-First Search
Loading...
Searching...
No Matches
SafePBNF.h
Go to the documentation of this file.
1/*--------------------------------------------------------------------------*/
2/*---------------------------- File SafePBNF.h -----------------------------*/
3/*--------------------------------------------------------------------------*/
107/*--------------------------------------------------------------------------*/
108/*------------------------------ DEFINITIONS -------------------------------*/
109/*--------------------------------------------------------------------------*/
110
111#ifndef __PBeFS_SafePBNF
112 #define __PBeFS_SafePBNF
113 /* self-identification: #endif at the end of the file */
114
115/*--------------------------------------------------------------------------*/
116/*-------------------------------- INCLUDES --------------------------------*/
117/*--------------------------------------------------------------------------*/
118
119#include <algorithm>
120
121#include <atomic>
122
123#include <condition_variable>
124
125#include <memory>
126
127#include <mutex>
128
129#include <type_traits>
130
131#include <utility>
132
133#include <vector>
134
135#include "PBeFS/astar/HDAStar.h"
136
137#include "ADS/tree/BTreeMap.h"
138
139/*--------------------------------------------------------------------------*/
140/*------------------------------- NAMESPACE --------------------------------*/
141/*--------------------------------------------------------------------------*/
142
144namespace PBeFS_di_unipi_it
145{
146
147/*--------------------------------------------------------------------------*/
148/*--------------------------- CLASS PBNFOptions ----------------------------*/
149/*--------------------------------------------------------------------------*/
151
153
155 unsigned min_expansions = 32;
156
157 }; // end( struct( PBNFOptions ) )
158
159/*--------------------------------------------------------------------------*/
160/*----------------------------- CLASS SafePBNF -----------------------------*/
161/*--------------------------------------------------------------------------*/
163
194
195template< NBlockDomain D , ADS::WorkerExecutor Executor = ADS::ThreadPool ,
196 class Open = std::conditional_t<
197 std::is_integral_v< cost_t< D > > ,
200class SafePBNF {
201
202/*------------------------ PUBLIC PART OF THE CLASS ------------------------*/
203
204 public:
205
206/*------------------------------ PUBLIC TYPES ------------------------------*/
207
212
213/*------------------------------ CONSTRUCTOR -------------------------------*/
214
216
217 explicit SafePBNF( const D & domain , const PBNFOptions & options = {} )
218 : f_domain( domain ) , f_options( options ) {
219 f_options.normalize();
220 if( f_options.min_expansions == 0 )
221 f_options.min_expansions = 1;
222 }
223
224/*------------------------- OTHER INITIALIZATIONS --------------------------*/
228
229 void set_executor( Executor & ex ) noexcept { f_ex = &ex; }
230
231/*-------------------------------- SOLVING ---------------------------------*/
233
234 [[nodiscard]] SolutionT solve( void ) const
235 {
236 const unsigned p = f_options.threads;
237 Stopwatch clock;
238 Graph g( f_domain );
239 std::vector< std::unique_ptr< Worker > > w( p );
240 for( unsigned i = 0 ; i < p ; ++i )
241 w[ i ] = std::make_unique< Worker >( *this , g );
242
243 { // the root, in its nblock, which is the first free one
244 const State s0 = f_domain.initial_state();
245 const std::size_t b = f_domain.nblock( s0 );
246 w[ 0 ]->insert( g.block( b ) , s0 , Cost( 0 ) ,
247 Cost( f_domain.heuristic( s0 ) ) , nullptr , no_op ,
248 f_domain.hash( s0 ) );
249 g.make_free( b );
250 }
251
252 run_workers( f_ex , p , [ & ]( unsigned i ) { w[ i ]->run(); } );
253
254 SolutionT sol;
255 SearchStats & st = sol.stats;
256 for( auto & x : w ) {
257 st.expanded += x->f_st.expanded;
258 st.reexpanded += x->f_st.reexpanded;
259 st.generated += x->f_st.generated;
260 st.duplicates += x->f_st.duplicates;
261 st.stored += x->f_pool.size();
262 st.expanded_by.push_back( x->f_st.expanded );
263 st.stored_by.push_back( x->f_pool.size() );
264 }
265 if( g.limit_hit.load() )
266 sol.status = Status::node_limit;
267 else
268 if( g.goal ) {
269 sol.status = Status::solved;
270 sol.cost = g.goal->g;
271 sol.path = trace_path( g.goal );
272 }
273 st.seconds = clock.seconds();
274 return( sol );
275 }
276
277/*----------------------- PRIVATE PART OF THE CLASS ------------------------*/
278
279 private:
280
281 using Lock = typename Executor::lock_type;
282
283/*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/
284 // an nblock: its OPEN and CLOSED lists and its place in the graph
285
286 struct NBlock {
287 Open open; // its OPEN list
288 NodeTable< NodeT > table{ 16 }; // its CLOSED list
289 std::vector< std::size_t > interferes; // its interference scope
290 std::vector< std::size_t > scope; // its successors (and itself)
291 unsigned sigma = 0; // nblocks in use interfering
292 unsigned sigma_h = 0; // hot nblocks interfering
293 bool hot = false; // marked hot
294 bool inuse = false; // acquired by a worker
295 bool in_free = false; // in the free list
296 Cost free_key{}; // its key in the free list
297 std::atomic< Cost > best{ infinite_cost< Cost >() }; // published best f
298 };
299
300/*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/
301 // the nblock graph, shared by the workers
302
303 struct Graph {
304
305 explicit Graph( const D & d )
306 : f_d( d ) , v_b( new std::atomic< NBlock * >[ d.num_nblocks() ] ) {
307 for( std::size_t i = 0 ; i < d.num_nblocks() ; ++i )
308 v_b[ i ].store( nullptr , std::memory_order_relaxed );
309 }
310
311 // the nblock b if it has been built, nullptr otherwise
312 NBlock * find( std::size_t b ) const {
313 return( v_b[ b ].load( std::memory_order_acquire ) );
314 }
315
316 // the nblock b, built at its first use with its scopes
317 NBlock & block( std::size_t b ) {
318 if( NBlock * x = find( b ) )
319 return( *x );
320 std::lock_guard< std::mutex > guard( f_build );
321 if( NBlock * x = v_b[ b ].load( std::memory_order_relaxed ) )
322 return( *x );
323 auto n = std::make_unique< NBlock >();
324 std::vector< std::size_t > succ , iset;
325 f_d.for_each_nblock_successor( b , [ & ]( std::size_t s ) {
326 succ.push_back( s );
327 } );
328 // undirected abstract graphs: the predecessors are the successors
329 for( std::size_t s : succ ) {
330 iset.push_back( s );
331 f_d.for_each_nblock_successor( s , [ & ]( std::size_t t ) {
332 iset.push_back( t );
333 } );
334 }
335 std::sort( iset.begin() , iset.end() );
336 iset.erase( std::unique( iset.begin() , iset.end() ) , iset.end() );
337 iset.erase( std::remove( iset.begin() , iset.end() , b ) , iset.end() );
338 n->interferes = std::move( iset );
339 n->scope = std::move( succ );
340 n->scope.push_back( b );
341 v_b[ b ].store( n.get() , std::memory_order_release );
342 NBlock & ref = *n;
343 v_own.push_back( std::move( n ) );
344 return( ref );
345 }
346
347 // the best f of the OPEN list of b, the stale entries dropped; only for
348 // an nblock that no worker can touch, or that the caller holds
349 Cost best_f( NBlock & n ) {
350 while( ! n.open.empty() && n.open.top().stale() )
351 n.open.pop();
352 return( n.open.empty() ? infinite_cost< Cost >() : n.open.top_f() );
353 }
354
355 // free: not in use, no interference, nodes below the incumbent
356 bool is_free( NBlock & n ) {
357 return( ! n.inuse && ( n.sigma == 0 ) && ( n.sigma_h == 0 ) &&
358 ( best_f( n ) < incumbent.load() ) );
359 }
360
361 // puts b in the free list if it is free (lock held)
362 void make_free( std::size_t b ) {
363 NBlock & n = block( b );
364 if( n.in_free || ! is_free( n ) )
365 return;
366 n.free_key = best_f( n );
367 n.best.store( n.free_key , std::memory_order_relaxed );
368 free.insert( { n.free_key , b } , true );
369 n.in_free = true;
370 wake.notify_all();
371 }
372
373 // takes b out of the free list (lock held)
374 void make_busy( std::size_t b ) {
375 NBlock & n = block( b );
376 if( n.in_free ) {
377 free.remove( { n.free_key , b } );
378 n.in_free = false;
379 }
380 }
381
382 // the f of the best free nblock (lock held)
383 Cost best_free( void ) const {
384 return( free.empty() ? infinite_cost< Cost >()
385 : ( *free.begin() ).first.first );
386 }
387
388 // the nblock of best published f among the interferers of n, or none
389 std::pair< Cost , std::size_t > best_scope( const NBlock & n ) {
390 std::pair< Cost , std::size_t > best{ infinite_cost< Cost >() ,
391 std::size_t( -1 ) };
392 for( std::size_t i : n.interferes )
393 if( const NBlock * x = find( i ) ) {
394 const Cost f = x->best.load( std::memory_order_relaxed );
395 if( f < best.first )
396 best = { f , i };
397 }
398 return( best );
399 }
400
401 // SetHot of appendix A (lock held)
402 void set_hot( std::size_t b ) {
403 NBlock & n = block( b );
404 if( n.hot || ( n.sigma == 0 ) )
405 return;
406 for( std::size_t i : n.interferes )
407 if( const NBlock * x = find( i ) ; x && x->hot &&
408 ( x->best.load( std::memory_order_relaxed ) <
409 n.best.load( std::memory_order_relaxed ) ) )
410 return; // a better hot nblock interferes
411 n.hot = true;
412 for( std::size_t i : n.interferes ) {
413 NBlock & m = block( i );
414 if( m.hot )
415 set_cold( i );
416 if( m.in_free )
417 make_busy( i );
418 ++m.sigma_h;
419 }
420 }
421
422 // SetCold of appendix A (lock held)
423 void set_cold( std::size_t b ) {
424 NBlock & n = block( b );
425 n.hot = false;
426 for( std::size_t i : n.interferes ) {
427 NBlock & m = block( i );
428 --m.sigma_h;
429 if( ( m.sigma == 0 ) && ( m.sigma_h == 0 ) ) {
430 if( m.hot )
431 set_cold( i );
432 make_free( i );
433 }
434 }
435 }
436
437 // Release of appendix A (lock held)
438 void release( std::size_t b ) {
439 NBlock & n = block( b );
440 n.inuse = false;
441 --inuse;
442 for( std::size_t i : n.interferes ) {
443 NBlock & m = block( i );
444 if( --m.sigma == 0 ) {
445 if( ( m.sigma_h == 0 ) && m.hot )
446 set_cold( i );
447 make_free( i );
448 }
449 }
450 make_free( b );
451 }
452
453 // acquires b, taken from the free list (lock held)
454 void acquire( std::size_t b ) {
455 make_busy( b );
456 NBlock & n = block( b );
457 n.inuse = true;
458 ++inuse;
459 for( std::size_t i : n.interferes ) {
460 NBlock & m = block( i );
461 if( m.in_free )
462 make_busy( i );
463 ++m.sigma;
464 }
465 }
466
467 // drops from the free list the nblocks with no node below the incumbent
468 void prune_free( void ) {
469 const Cost c = incumbent.load();
470 for( auto it = free.lowerBound( { c , 0 } ) ; it != free.end() ; ) {
471 block( ( *it ).first.second ).in_free = false;
472 it = free.erase( it );
473 }
474 }
475
476 const D & f_d; // the Domain
477 std::unique_ptr< std::atomic< NBlock * >[] > v_b; // the nblocks
478 std::vector< std::unique_ptr< NBlock > > v_own; // their ownership
479 std::mutex f_build; // serializes block()
480 Lock lock; // protects the graph
481 std::condition_variable_any wake; // a free nblock, or the end
482 ADS::BTreeMap< std::pair< Cost , std::size_t > , bool > free; // free list
483 unsigned inuse = 0; // the nblocks in use
484 bool done = false; // the search is over
485 std::atomic< Cost > incumbent{ infinite_cost< Cost >() };
486 std::atomic< bool > limit_hit{ false };
487 std::mutex goal_mutex; // protects goal
488 NodeT * goal = nullptr; // the best goal found
489 };
490
491/*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/
492 // a worker: its pool of nodes and its counters
493
494 struct alignas( cache_line ) Worker {
495
496 Worker( const SafePBNF & a , Graph & g ) : f_a( a ) , f_g( g ) {}
497
498 // ThreadSearch of appendix A
499 void run( void ) {
500 std::size_t b = none;
501 bool just_hot = false;
502 for( ;; ) {
503 b = next_nblock( b , just_hot );
504 just_hot = false;
505 if( b == none )
506 return;
507 NBlock & n = f_g.block( b );
508 unsigned exp = 0;
509 while( ! should_switch( b , n , exp , just_hot ) ) {
510 const OpenEntry< NodeT > e = n.open.pop();
511 if( e.stale() )
512 continue;
513 NodeT * m = e.node;
514 if( m->g + m->h >= f_g.incumbent.load() ) {
515 while( ! n.open.empty() ) // all the others are not better
516 n.open.pop();
517 break;
518 }
519 expand( n , m );
520 ++exp;
521 n.best.store( n.open.empty() ? infinite_cost< Cost >()
522 : n.open.top_f() ,
523 std::memory_order_relaxed );
524 if( f_a.f_options.node_limit &&
525 ( f_pool.size() >= f_a.f_options.node_limit ) ) {
526 std::lock_guard< Lock > guard( f_g.lock );
527 f_g.limit_hit.store( true );
528 f_g.done = true;
529 f_g.wake.notify_all();
530 return;
531 }
532 }
533 }
534 }
535
536 // ShouldSwitch of appendix A
537 bool should_switch( std::size_t b , NBlock & n , unsigned & exp ,
538 bool & just_hot ) {
539 while( ! n.open.empty() && n.open.top().stale() )
540 n.open.pop();
541 if( n.open.empty() )
542 return( true );
543 if( exp < f_a.f_options.min_expansions )
544 return( false );
545 exp = 0;
546 const Cost mine = n.open.top_f();
547 std::lock_guard< Lock > guard( f_g.lock );
548 if( f_g.done )
549 return( true );
550 const auto [ fs , bs ] = f_g.best_scope( n );
551 const Cost ff = f_g.best_free();
552 if( ( ff < mine ) || ( fs < mine ) ) {
553 if( ( bs != none ) && ( fs < ff ) ) {
554 f_g.set_hot( bs );
555 just_hot = true;
556 }
557 return( true );
558 }
559 for( std::size_t i : n.interferes )
560 if( const NBlock * x = f_g.find( i ) ; x && x->hot )
561 f_g.set_cold( i );
562 (void) b;
563 return( false );
564 }
565
566 // NextNBlock of appendix A
567 std::size_t next_nblock( std::size_t b , bool just_hot ) {
568 std::unique_lock< Lock > guard( f_g.lock , std::defer_lock );
569 NBlock * n = b == none ? nullptr : & f_g.block( b );
570 if( ! n || n->open.empty() || just_hot )
571 guard.lock();
572 else
573 if( ! guard.try_lock() )
574 return( b );
575 if( n ) {
576 const Cost mine = f_g.best_f( *n );
577 if( ! f_g.done && ( mine < f_g.incumbent.load() ) &&
578 ( mine <= f_g.best_scope( *n ).first ) &&
579 ( mine <= f_g.best_free() ) && ! just_hot )
580 return( b );
581 n->best.store( mine , std::memory_order_relaxed );
582 f_g.release( b );
583 }
584 f_g.prune_free();
585 for( ;; ) {
586 if( f_g.done )
587 return( none );
588 if( ! f_g.free.empty() )
589 break;
590 if( f_g.inuse == 0 ) {
591 f_g.done = true;
592 f_g.wake.notify_all();
593 return( none );
594 }
595 f_g.wake.wait( guard );
596 f_g.prune_free();
597 }
598 const std::size_t m = ( *f_g.free.begin() ).first.second;
599 f_g.acquire( m );
600 return( m );
601 }
602
603 // expands m, which is in n, or records it as the incumbent
604 void expand( NBlock & , NodeT * m ) {
605 m->closed = true;
606 ++f_st.expanded;
607 if( m->expanded )
608 ++f_st.reexpanded;
609 m->expanded = true;
610 if( f_a.f_domain.is_goal( m->state ) ) {
611 std::lock_guard< std::mutex > guard( f_g.goal_mutex );
612 if( m->g < f_g.incumbent.load() ) {
613 f_g.incumbent.store( m->g );
614 f_g.goal = m;
615 }
616 return;
617 }
618 // first the successors but the parent, with their slots prefetched,
619 // then the lookups
620 v_kids.clear();
621 for_each_child( f_a.f_domain , m->state , m->h , m->op ,
622 [ & ]( const State & t , Cost c , Cost ht ,
623 std::uint8_t ot ) {
624 ++f_st.generated;
625 const Cost g = m->g + c;
626 if( g + ht >= f_g.incumbent.load( std::memory_order_relaxed ) )
627 return;
628 NBlock & cb = f_g.block( f_a.f_domain.nblock( t ) );
629 const std::uint64_t hash = f_a.f_domain.hash( t );
630 cb.table.prefetch( hash );
631 v_kids.push_back( Kid{ t , g , ht , hash , & cb , ot } );
632 } );
633 for( const Kid & k : v_kids )
634 if( insert( *k.cb , k.s , k.g , k.h , m , k.op , k.hash ) )
635 k.cb->best.store( std::min( k.cb->best.load( std::memory_order_relaxed ) ,
636 k.g + k.h ) , std::memory_order_relaxed );
637 }
638
639 // records the path to t, whose hash is hash, of cost g in the nblock cb
640 // (which the worker holds in its scope); true if t goes into OPEN
641 bool insert( NBlock & cb , const State & t , Cost g , Cost h ,
642 NodeT * parent , std::uint8_t op , std::uint64_t hash ) {
643 auto [ x , fresh ] = cb.table.find_or_insert( t , hash , [ & ] {
644 NodeT * y = f_pool.allocate();
645 *y = NodeT{ t , g , h , parent , false , false , op };
646 return( y );
647 } );
648 if( ! fresh ) {
649 if( g >= x->g ) {
650 ++f_st.duplicates;
651 return( false );
652 }
653 x->g = g;
654 x->parent = parent;
655 x->op = op;
656 x->closed = false;
657 }
658 cb.open.push( x );
659 return( true );
660 }
661
662 static constexpr std::size_t none = std::size_t( -1 );
663
664 // a successor: its state, g, h and hash, its nblock and its operator
665 struct Kid {
669 std::uint64_t hash;
670 NBlock * cb;
671 std::uint8_t op;
672 };
673
674 std::vector< Kid > v_kids; // the successors of a node
675
676 const SafePBNF & f_a; // the search
677 Graph & f_g; // the nblock graph
678 NodePool< NodeT > f_pool; // the nodes it allocated
679 SearchStats f_st; // its counters
680 };
681
682/*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/
683
684 const D & f_domain;
685 PBNFOptions f_options;
686 Executor * f_ex = nullptr;
687
688 }; // end( class( SafePBNF ) )
689
690/*--------------------------------------------------------------------------*/
691
692} // end( namespace PBeFS_di_unipi_it )
693
694/*--------------------------------------------------------------------------*/
695
696#endif /* __PBeFS_SafePBNF */
697
698/*--------------------------------------------------------------------------*/
699/*-------------------------- End File SafePBNF.h ---------------------------*/
700/*--------------------------------------------------------------------------*/
Header file for the class template HDAStar, Hash Distributed A* on a shared-memory machine,...
the OPEN list on the BucketQueue of ADS, for integer costs
Definition OpenList.h:122
the OPEN list on the DaryHeap of ADS
Definition OpenList.h:52
the arena of the nodes, on the Arena of ADS, on huge pages
Definition NodePool.h:48
the closed list, on the PointerTable of ADS, on huge pages
Definition NodeTable.h:49
SafePBNF(const D &domain, const PBNFOptions &options={})
constructor, taking the Domain (which must outlive the SafePBNF)
Definition SafePBNF.h:217
state_t< D > State
the state type
Definition SafePBNF.h:208
Solution< State, Cost > SolutionT
the solution type
Definition SafePBNF.h:211
Node< State, Cost > NodeT
the node type
Definition SafePBNF.h:210
cost_t< D > Cost
the cost type
Definition SafePBNF.h:209
SolutionT solve(void) const
searches the Domain, returning the Solution
Definition SafePBNF.h:234
void set_executor(Executor &ex) noexcept
sets the Executor that runs the workers, which must outlive the searches; without it,...
Definition SafePBNF.h:229
a wall-clock stopwatch, started at construction
Definition SearchUtils.h:175
double seconds(void) const
the seconds elapsed since the construction
Definition SearchUtils.h:182
a Domain whose state space is partitioned into nblocks
Definition Domain.h:182
namespace for the Parallel Best-First Search library
Definition AStar.h:83
void for_each_child(const D &d, const state_t< D > &s, cost_t< D > h, std::uint8_t op, F &&f)
the successors of s but its parent, when D knows the operators
Definition Domain.h:276
typename D::cost_type cost_t
the cost type of a Domain
Definition Domain.h:245
@ solved
an optimal path has been found
Definition Solution.h:47
@ node_limit
the limit on the stored nodes was reached first
Definition Solution.h:49
constexpr Cost infinite_cost(void) noexcept
the largest value of a cost type, used as "no incumbent yet"
Definition Node.h:100
typename D::state_type state_t
the state type of a Domain
Definition Domain.h:241
void run_workers(Executor *ex, unsigned p, F &&f)
runs the workers f( 0 ) , ... , f( p - 1 ) at once on an Executor
Definition SearchUtils.h:121
constexpr std::uint8_t no_op
the operator of the root, which no state was generated by
Definition Domain.h:205
auto trace_path(const NodeT *goal)
the states from the root to goal, following the parent pointers
Definition Node.h:126
a node of the search graph
Definition Node.h:74
Cost g
the cost of the best path found from the root
Definition Node.h:77
the options of SafePBNF
Definition SafePBNF.h:152
unsigned min_expansions
expansions in an nblock before a worker may switch to a better one
Definition SafePBNF.h:155
the options of the parallel searches: those of the sequential search, and the parameters of the paral...
Definition HDAStar.h:114
void normalize(void) noexcept
sets to 1 any parameter that is 0
Definition SearchUtils.h:102
State s
Definition SafePBNF.h:666
NBlock * cb
Definition SafePBNF.h:670
std::uint8_t op
Definition SafePBNF.h:671
Cost g
Definition SafePBNF.h:667
Cost h
Definition SafePBNF.h:668
std::uint64_t hash
Definition SafePBNF.h:669
the counters of a search
Definition Solution.h:64
std::vector< std::uint64_t > expanded_by
expansions of each worker
Definition Solution.h:75
std::uint64_t generated
successors generated
Definition Solution.h:68
std::uint64_t reexpanded
expansions of a node already expanded
Definition Solution.h:67
std::uint64_t stored
distinct nodes stored at the end
Definition Solution.h:73
double seconds
wall-clock time of the search
Definition Solution.h:74
std::uint64_t duplicates
generated states already known with a not larger
Definition Solution.h:69
std::vector< std::uint64_t > stored_by
nodes stored by each worker
Definition Solution.h:76
std::uint64_t expanded
expansions, re-expansions included
Definition Solution.h:66
a path from the initial state to a goal, and its cost
Definition Solution.h:100