493 lines
13 KiB
C++
493 lines
13 KiB
C++
/**
|
|
* @file work.h
|
|
*
|
|
* Work-queues, load balancing, and the like.
|
|
*/
|
|
|
|
#ifndef THOR_WORK_H
|
|
#define THOR_WORK_H
|
|
|
|
#include "rpc.h"
|
|
#include "cache.h"
|
|
#include "cachearray.h"
|
|
#include "thortree.h"
|
|
|
|
#include "col/heap.h"
|
|
#include "col/arraylist.h"
|
|
#include "la/uselapack.h"
|
|
#include "tree/bounds.h"
|
|
|
|
//------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Generic work-queue interface.
|
|
*
|
|
* Consider renaming this to "Scheduler" or "Work Scheduler" because it's
|
|
* not exactly a queue -- it can be any kind of scheduler.
|
|
*/
|
|
class WorkQueueInterface {
|
|
FORBID_COPY(WorkQueueInterface);
|
|
|
|
public:
|
|
typedef TreeGrain Grain;
|
|
|
|
public:
|
|
WorkQueueInterface() {}
|
|
virtual ~WorkQueueInterface() {}
|
|
|
|
/**
|
|
* Gets work items to do -- may get one or multiple items.
|
|
*
|
|
* Work items are labelled by an index. For dual-tree algorithms,
|
|
* this is probably the index in the array of tree nodes of the
|
|
* node to operate on.
|
|
*/
|
|
virtual void GetWork(int process, ArrayList<Grain> *work) = 0;
|
|
|
|
/**
|
|
* Report any relevant statistics to fastexec.
|
|
*/
|
|
virtual void Report(struct datanode *module);
|
|
};
|
|
|
|
//------------------------------------------------------------------------
|
|
|
|
/**
|
|
* A wrapper for a work-queue that ensures that multiple
|
|
* threads aren't going to trample on each other.
|
|
*/
|
|
class LockedWorkQueue : public WorkQueueInterface {
|
|
FORBID_COPY(LockedWorkQueue);
|
|
|
|
private:
|
|
WorkQueueInterface *inner_;
|
|
Mutex mutex_;
|
|
|
|
public:
|
|
/**
|
|
* Wraps another work-queue.
|
|
*
|
|
* Will delete the other work-queue when done.
|
|
*/
|
|
LockedWorkQueue(WorkQueueInterface *inner) : inner_(inner) {}
|
|
virtual ~LockedWorkQueue() { delete inner_; }
|
|
|
|
virtual void GetWork(int process, ArrayList<Grain> *work) {
|
|
mutex_.Lock();
|
|
inner_->GetWork(process, work);
|
|
mutex_.Unlock();
|
|
}
|
|
|
|
virtual void Report(struct datanode *module) {
|
|
inner_->Report(module);
|
|
}
|
|
};
|
|
|
|
//------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Work queue that randomly assigns tasks to processors, starting with the
|
|
* largest query node first. This works fine for small-scale parallelism,
|
|
* but when multiple computers are involved, something that is
|
|
* spatial-locality-aware like CentroidWorkQueue would be desirable.
|
|
*/
|
|
template<typename Node>
|
|
class SimpleWorkQueue
|
|
: public WorkQueueInterface {
|
|
FORBID_COPY(SimpleWorkQueue);
|
|
|
|
private:
|
|
MinHeap<double, Grain> tasks_;
|
|
|
|
public:
|
|
SimpleWorkQueue() {}
|
|
virtual ~SimpleWorkQueue() {}
|
|
|
|
/**
|
|
* Creates a work-queue with the specified minimum
|
|
* number of work items (k-hat).
|
|
*
|
|
* This does not keep a reference to array.
|
|
*/
|
|
void Init(CacheArray<Node> *array, index_t n_grains);
|
|
|
|
index_t n_grains() const {
|
|
return tasks_.size();
|
|
}
|
|
|
|
virtual void GetWork(int process, ArrayList<Grain> *work);
|
|
|
|
void Report(struct datanode *module);
|
|
|
|
private:
|
|
void AddWork_(CacheArray<Node> *array, index_t grain_size, index_t node_i,
|
|
index_t node_end_i);
|
|
};
|
|
|
|
template<typename Node>
|
|
void SimpleWorkQueue<Node>::Report(struct datanode *module) {
|
|
fx_format_result(module, "n_grains", "%"LI"d", n_grains());
|
|
}
|
|
|
|
template<typename Node>
|
|
void SimpleWorkQueue<Node>::Init(CacheArray<Node> *array, index_t n_grains) {
|
|
index_t root_index = 0;
|
|
const Node *root = array->StartRead(root_index);
|
|
tasks_.Init();
|
|
AddWork_(array, (root->count() + n_grains - 1) / n_grains,
|
|
root_index, array->end_index());
|
|
array->StopRead(root_index);
|
|
}
|
|
|
|
template<typename Node>
|
|
void SimpleWorkQueue<Node>::GetWork(int process, ArrayList<Grain> *work) {
|
|
if (tasks_.size() == 0) {
|
|
work->Init(0);
|
|
} else {
|
|
work->Init(1);
|
|
(*work)[0] = tasks_.Pop();
|
|
}
|
|
}
|
|
|
|
template<typename Node>
|
|
void SimpleWorkQueue<Node>::AddWork_(
|
|
CacheArray<Node> *array, index_t grain_size,
|
|
index_t node_i, index_t node_end_i) {
|
|
const Node *node = array->StartRead(node_i);
|
|
|
|
if (node->count() <= grain_size || node->is_leaf()) {
|
|
Grain grain;
|
|
grain.node_index = node_i;
|
|
grain.node_end_index = node_end_i;
|
|
grain.point_begin_index = node->begin();
|
|
grain.point_end_index = node->end();
|
|
tasks_.Put(-1.0 * node->count(), grain);
|
|
} else {
|
|
index_t end = node_end_i;
|
|
for (index_t k = 2; k--;) {
|
|
AddWork_(array, grain_size, node->child(k), end);
|
|
end = node->child(k);
|
|
}
|
|
}
|
|
|
|
array->StopRead(node_i);
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
|
|
|
|
template<typename Node>
|
|
class CentroidWorkQueue
|
|
: public WorkQueueInterface {
|
|
FORBID_COPY(CentroidWorkQueue);
|
|
|
|
private:
|
|
typedef typename ThorTreeDecomposition<Node>::DecompNode DecompNode;
|
|
enum Status { NONE, SOME, ALL };
|
|
typedef ThorSkeletonNode<Node, Status> InternalNode;
|
|
|
|
struct ProcessWorkQueue {
|
|
index_t n_centers;
|
|
index_t max_grain_size;
|
|
Vector sum_centers;
|
|
MinHeap<double, InternalNode*> work_items;
|
|
};
|
|
|
|
private:
|
|
CacheArray<Node> *tree_;
|
|
ArrayList<ProcessWorkQueue> processes_;
|
|
InternalNode *root_;
|
|
int n_threads_;
|
|
double granularity_;
|
|
index_t n_grains_;
|
|
index_t n_overflows_;
|
|
index_t n_preferred_;
|
|
index_t n_overflow_points_;
|
|
index_t n_assigned_points_;
|
|
bool no_overflow_;
|
|
|
|
public:
|
|
CentroidWorkQueue() {}
|
|
virtual ~CentroidWorkQueue() {
|
|
delete root_;
|
|
}
|
|
|
|
/**
|
|
* Creates a work-queue with the specified minimum number of
|
|
* grains (k-hat).
|
|
*
|
|
* This will NOT keep a permanent reference to the tree.
|
|
*/
|
|
void Init(CacheArray<Node> *tree_in, const DecompNode *decomp_root,
|
|
int n_threads, datanode *module);
|
|
|
|
index_t n_grains() const {
|
|
return n_grains_;
|
|
}
|
|
|
|
virtual void GetWork(int process_num, ArrayList<Grain> *work);
|
|
|
|
index_t n_overflows() const {
|
|
return n_overflows_;
|
|
}
|
|
index_t n_preferred() const {
|
|
return n_preferred_;
|
|
}
|
|
|
|
virtual void Report(struct datanode *datanode);
|
|
|
|
private:
|
|
void AddWork_(CacheArray<Node> *array, index_t grain_size, index_t node_i);
|
|
|
|
/** Creates the domain decomposition */
|
|
void DistributeInitialWork_(const DecompNode *decomp_node,
|
|
InternalNode *node);
|
|
};
|
|
|
|
template<typename Node>
|
|
void CentroidWorkQueue<Node>::Init(CacheArray<Node> *tree,
|
|
const DecompNode *decomp_root, int n_threads, datanode *module) {
|
|
granularity_ = fx_param_double(module, "granularity", 12);
|
|
no_overflow_ = fx_param_bool(module, "no_overflow", false);
|
|
tree_ = tree;
|
|
n_threads_ = n_threads;
|
|
root_ = new InternalNode(NONE, tree,
|
|
decomp_root->index(), decomp_root->end_index());
|
|
processes_.Init(rpc::n_peers());
|
|
DEBUG_ASSERT_MSG(decomp_root->info().begin_rank == 0
|
|
&& decomp_root->info().end_rank == rpc::n_peers(),
|
|
"Can't handle incomplete decompositions (yet)");
|
|
n_grains_ = 0;
|
|
n_overflows_ = 0;
|
|
n_overflow_points_ = 0;
|
|
n_assigned_points_ = 0;
|
|
n_preferred_ = 0;
|
|
|
|
DistributeInitialWork_(decomp_root, root_);
|
|
|
|
// Ensure we don't access the tree after we start the algorithm.
|
|
tree_ = NULL;
|
|
}
|
|
|
|
template<typename Node>
|
|
void CentroidWorkQueue<Node>::DistributeInitialWork_(
|
|
const DecompNode *decomp_node, InternalNode *node) {
|
|
int begin_rank = decomp_node->info().begin_rank;
|
|
int end_rank = decomp_node->info().end_rank;
|
|
int n_processs = end_rank - begin_rank;
|
|
|
|
if (n_processs == 1 || node->is_leaf()) {
|
|
// Prime each processor's centroid with the centroid of this block.
|
|
// Note there will probably only be 1 processor unless the pathological
|
|
// case where we're trying to subdivide a leaf between processors.
|
|
// If we're subdividing a leaf between processors, then some processors
|
|
// won't have any work to do...
|
|
Vector center;
|
|
node->node().bound().CalculateMidpoint(¢er);
|
|
index_t max_grain_size = int(nearbyint(
|
|
node->count() / granularity_ / n_threads_));
|
|
|
|
for (int i = begin_rank; i < end_rank; i++) {
|
|
ProcessWorkQueue *queue = &processes_[i];
|
|
queue->max_grain_size = max_grain_size;
|
|
queue->n_centers = 1;
|
|
queue->sum_centers.Copy(center);
|
|
queue->work_items.Init();
|
|
}
|
|
|
|
ProcessWorkQueue *queue = &processes_[begin_rank];
|
|
ArrayList<InternalNode*> node_stack;
|
|
node_stack.Init();
|
|
*node_stack.AddBack() = node;
|
|
|
|
// Subdivide the node further if possible.
|
|
while (node_stack.size() != 0) {
|
|
InternalNode *cur = *node_stack.PopBackPtr();
|
|
double distance = cur->node().bound().MinDistanceSq(center);
|
|
|
|
if (cur->is_leaf() || cur->count() <= max_grain_size) {
|
|
queue->work_items.Put(distance, cur);
|
|
n_grains_++;
|
|
} else {
|
|
for (index_t k = 0; k < Node::CARDINALITY; k++) {
|
|
*node_stack.AddBack() = cur->GetChild(tree_, k);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
for (index_t k = 0; k < Node::CARDINALITY; k++) {
|
|
DistributeInitialWork_(decomp_node->child(k), node->GetChild(tree_, k));
|
|
}
|
|
}
|
|
}
|
|
|
|
template<typename Node>
|
|
void CentroidWorkQueue<Node>::GetWork(int process_num, ArrayList<Grain> *work) {
|
|
InternalNode *found_node;
|
|
ProcessWorkQueue *queue = &processes_[process_num];
|
|
|
|
found_node = NULL;
|
|
|
|
while (found_node == NULL && !queue->work_items.is_empty()) {
|
|
InternalNode *node = queue->work_items.Pop();
|
|
if (node->info() == NONE) {
|
|
found_node = node;
|
|
}
|
|
}
|
|
|
|
if (!found_node) {
|
|
if (!no_overflow_) {
|
|
Vector center;
|
|
center.Copy(queue->sum_centers);
|
|
la::Scale(1.0 / queue->n_centers, ¢er);
|
|
|
|
MinHeap<double, InternalNode*> prio;
|
|
|
|
prio.Init();
|
|
prio.Put(0, root_);
|
|
|
|
// Single-tree nearest-node search
|
|
while (!prio.is_empty()) {
|
|
InternalNode *node = prio.Pop();
|
|
if (node->info() != ALL) {
|
|
if (!node->is_complete()) {
|
|
// We can't explore a node that is missing children or whose
|
|
// count is too large.
|
|
if (node->info() == NONE) {
|
|
found_node = node;
|
|
n_overflow_points_ += found_node->count();
|
|
n_overflows_++;
|
|
break;
|
|
}
|
|
} else {
|
|
DEBUG_ASSERT(node->is_complete());
|
|
for (int i = 0; i < Node::CARDINALITY; i++) {
|
|
InternalNode *child = node->child(i);
|
|
prio.Put(child->node().bound().MinDistanceSq(center), child);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
n_preferred_++;
|
|
}
|
|
|
|
if (found_node == NULL) {
|
|
work->Init();
|
|
} else {
|
|
// Show user-friendly status messages every 5% increment
|
|
index_t count = found_node->count();
|
|
n_assigned_points_ += count;
|
|
int interval = 20;
|
|
if ((n_assigned_points_ - count) * interval / root_->count()
|
|
!= n_assigned_points_ * interval / root_->count()) {
|
|
percent_indicator("scheduled", n_assigned_points_, root_->count());
|
|
}
|
|
|
|
// Mark all children as complete (non-recursive version)
|
|
ArrayList<InternalNode*> stack;
|
|
stack.Init();
|
|
*stack.AddBack() = found_node;
|
|
while (stack.size() != 0) {
|
|
InternalNode *c = *stack.PopBackPtr();
|
|
c->info() = ALL;
|
|
for (index_t k = 0; k < Node::CARDINALITY; k++) {
|
|
InternalNode *c_child = c->child(k);
|
|
if (c_child) {
|
|
*stack.AddBack() = c_child;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mark my parents as partially or fully complete
|
|
for (InternalNode *p = found_node->parent(); p != NULL; p = p->parent()) {
|
|
DEBUG_ASSERT(p->is_complete());
|
|
p->info() = ALL;
|
|
for (int k = 0; k < Node::CARDINALITY; k++) {
|
|
if (p->child(k)->info() != ALL) {
|
|
p->info() = SOME;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
work->Init(1);
|
|
Grain *grain = &(*work)[0];
|
|
grain->node_index = found_node->index();
|
|
grain->node_end_index = found_node->end_index();
|
|
grain->point_begin_index = found_node->node().begin();
|
|
grain->point_end_index = found_node->node().end();
|
|
|
|
Vector midpoint;
|
|
found_node->node().bound().CalculateMidpoint(&midpoint);
|
|
la::AddTo(midpoint, &queue->sum_centers);
|
|
queue->n_centers++;
|
|
}
|
|
}
|
|
|
|
template<typename Node>
|
|
void CentroidWorkQueue<Node>::Report(struct datanode *module) {
|
|
fx_format_result(module, "n_grains", "%"LI"d",
|
|
n_preferred_ + n_overflows_);
|
|
fx_format_result(module, "overflow_grain_ratio", "%.4f",
|
|
1.0 * n_overflows_ / (n_preferred_ + n_overflows_));
|
|
fx_format_result(module, "n_overflows", "%"LI"d",
|
|
n_overflows_);
|
|
fx_format_result(module, "overflow_point_ratio", "%.4f",
|
|
1.0 * n_overflow_points_ / root_->count());
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
|
|
struct WorkRequest {
|
|
enum Operation { GIVE_ME_WORK } operation;
|
|
int process;
|
|
|
|
bool requires_response() const { return true; }
|
|
|
|
OT_DEF_BASIC(WorkRequest) {
|
|
OT_MY_OBJECT(operation);
|
|
OT_MY_OBJECT(process);
|
|
}
|
|
};
|
|
|
|
struct WorkResponse {
|
|
ArrayList<WorkQueueInterface::Grain> work_items;
|
|
|
|
OT_DEF_BASIC(WorkResponse) {
|
|
OT_MY_OBJECT(work_items);
|
|
}
|
|
};
|
|
|
|
class RemoteWorkQueueBackend
|
|
: public RemoteObjectBackend<WorkRequest, WorkResponse> {
|
|
private:
|
|
WorkQueueInterface *inner_;
|
|
|
|
public:
|
|
void Init(WorkQueueInterface *inner_work_queue);
|
|
|
|
virtual void HandleRequest(
|
|
const WorkRequest& request, WorkResponse *response);
|
|
};
|
|
|
|
class RemoteWorkQueue
|
|
: public WorkQueueInterface {
|
|
FORBID_COPY(RemoteWorkQueue);
|
|
|
|
private:
|
|
int channel_;
|
|
int destination_;
|
|
|
|
public:
|
|
RemoteWorkQueue() {}
|
|
virtual ~RemoteWorkQueue() {}
|
|
|
|
void Init(int channel, int destination);
|
|
|
|
void GetWork(int process, ArrayList<Grain> *work_items);
|
|
};
|
|
|
|
#endif
|