diff --git a/miniapp/miniapp.cpp b/miniapp/miniapp.cpp index 474c57f3af6be715c3979e9d0d89e52c231326ba..fe06203a694ec76978ed4f4b6d94d3397f452bbe 100644 --- a/miniapp/miniapp.cpp +++ b/miniapp/miniapp.cpp @@ -34,7 +34,6 @@ using file_export_type = io::exporter_spike_file<global_policy>; void banner(); std::unique_ptr<recipe> make_recipe(const io::cl_options&, const probe_distribution&); std::unique_ptr<sample_trace_type> make_trace(cell_member_type probe_id, probe_spec probe); -std::pair<cell_gid_type, cell_gid_type> distribute_cells(cell_size_type ncells); using communicator_type = communication::communicator<communication::global_policy>; void write_trace_json(const sample_trace_type& trace, const std::string& prefix = "trace_"); @@ -77,16 +76,6 @@ int main(int argc, char** argv) { pdist.all_segments = !options.probe_soma_only; auto recipe = make_recipe(options, pdist); - auto cell_range = distribute_cells(recipe->num_cells()); - - std::vector<cell_gid_type> group_divisions; - for (auto i = cell_range.first; i<cell_range.second; i+=options.group_size) { - group_divisions.push_back(i); - } - group_divisions.push_back(cell_range.second); - - EXPECTS(group_divisions.front() == cell_range.first); - EXPECTS(group_divisions.back() == cell_range.second); auto register_exporter = [] (const io::cl_options& options) { return @@ -95,9 +84,14 @@ int main(int argc, char** argv) { options.file_extension, options.over_write); }; - model m(*recipe, - util::partition_view(group_divisions), - config::has_cuda? backend_policy::prefer_gpu: backend_policy::use_multicore); + group_rules rules; + rules.policy = config::has_cuda? + backend_policy::prefer_gpu: backend_policy::use_multicore; + rules.target_group_size = options.group_size; + auto decomp = domain_decomposition(*recipe, rules); + + model m(*recipe, decomp); + if (options.report_compartments) { report_compartment_stats(*recipe); } @@ -111,8 +105,7 @@ int main(int argc, char** argv) { m.set_binning_policy(binning_policy, options.bin_dt); // Inject some artificial spikes, 1 per 20 neurons. - cell_gid_type first_spike_cell = 20*((cell_range.first+19)/20); - for (auto c=first_spike_cell; c<cell_range.second; c+=20) { + for (cell_gid_type c=0; c<recipe->num_cells(); c+=20) { m.add_artificial_spike({c, 0}); } @@ -179,20 +172,6 @@ int main(int argc, char** argv) { return 0; } -std::pair<cell_gid_type, cell_gid_type> distribute_cells(cell_size_type num_cells) { - // Crude load balancing: - // divide [0, num_cells) into num_domains non-overlapping, contiguous blocks - // of size as close to equal as possible. - - auto num_domains = communication::global_policy::size(); - auto domain_id = communication::global_policy::id(); - - cell_gid_type cell_from = (cell_gid_type)(num_cells*(domain_id/(double)num_domains)); - cell_gid_type cell_to = (cell_gid_type)(num_cells*((domain_id+1)/(double)num_domains)); - - return {cell_from, cell_to}; -} - void banner() { std::cout << "====================\n"; std::cout << " starting miniapp\n"; diff --git a/src/domain_decomposition.hpp b/src/domain_decomposition.hpp new file mode 100644 index 0000000000000000000000000000000000000000..a4d5d406f794d4dab85e4f249870de8c33c4f495 --- /dev/null +++ b/src/domain_decomposition.hpp @@ -0,0 +1,155 @@ +#pragma once + +#include <vector> + +#include <backends.hpp> +#include <common_types.hpp> +#include <communication/global_policy.hpp> +#include <recipe.hpp> +#include <util/optional.hpp> +#include <util/partition.hpp> +#include <util/transform.hpp> + +namespace nest { +namespace mc { + +// Meta data used to guide the domain_decomposition in distributing +// and grouping cells. +struct group_rules { + cell_size_type target_group_size; + backend_policy policy; +}; + +class domain_decomposition { + using gid_partition_type = + util::partition_range<std::vector<cell_gid_type>::const_iterator>; + +public: + /// Utility type for meta data for a local cell group. + struct group_range_type { + cell_gid_type begin; + cell_gid_type end; + cell_kind kind; + }; + + domain_decomposition(const recipe& rec, const group_rules& rules): + backend_policy_(rules.policy) + { + EXPECTS(rules.target_group_size>0); + + auto num_domains = communication::global_policy::size(); + auto domain_id = communication::global_policy::id(); + + // Partition the cells globally across the domains. + num_global_cells_ = rec.num_cells(); + cell_begin_ = (cell_gid_type)(num_global_cells_*(domain_id/(double)num_domains)); + cell_end_ = (cell_gid_type)(num_global_cells_*((domain_id+1)/(double)num_domains)); + + // Partition the local cells into cell groups that satisfy three + // criteria: + // 1. the cells in a group have contiguous gid + // 2. the size of a cell group does not exceed rules.target_group_size; + // 3. all cells in a cell group have the same cell_kind. + // This simple greedy algorithm appends contiguous cells to a cell + // group until either the target group size is reached, or a cell with a + // different kind is encountered. + // On completion, cell_starts_ partitions the local gid into cell + // groups, and group_kinds_ records the cell kind in each cell group. + if (num_local_cells()>0) { + cell_size_type group_size = 1; + + // 1st group starts at cell_begin_ + group_starts_.push_back(cell_begin_); + auto group_kind = rec.get_cell_kind(cell_begin_); + + // set kind for 1st group + group_kinds_.push_back(group_kind); + + cell_gid_type gid = cell_begin_+1; + while (gid<cell_end_) { + auto kind = rec.get_cell_kind(gid); + + // Test if gid belongs to a new cell group, i.e. whether it has + // a new cell_kind or if the target group size has been reached. + if (kind!=group_kind || group_size>=rules.target_group_size) { + group_starts_.push_back(gid); + group_kinds_.push_back(kind); + group_size = 0; + } + ++group_size; + ++gid; + } + group_starts_.push_back(cell_end_); + } + } + + /// Returns the local index of the cell_group that contains a cell with + /// with gid. + /// If the cell is not on the local domain, the optional return value is + /// not set. + util::optional<cell_size_type> + local_group_from_gid(cell_gid_type gid) const { + // check if gid is a local cell + if (!is_local_gid(gid)) { + return util::nothing; + } + return gid_group_partition().index(gid); + } + + /// Returns the gid of the first cell on the local domain. + cell_gid_type cell_begin() const { + return cell_begin_; + } + + /// Returns one past the gid of the last cell in the local domain. + cell_gid_type cell_end() const { + return cell_end_; + } + + /// Returns the total number of cells in the global model. + cell_size_type num_global_cells() const { + return num_global_cells_; + } + + /// Returns the number of cells on the local domain. + cell_size_type num_local_cells() const { + return cell_end()-cell_begin(); + } + + /// Returns the number of cell groups on the local domain. + cell_size_type num_local_groups() const { + return group_kinds_.size(); + } + + /// Returns meta data for a local cell group. + group_range_type get_group(cell_size_type i) const { + return {group_starts_[i], group_starts_[i+1], group_kinds_[i]}; + } + + /// Tests whether a gid is on the local domain. + bool is_local_gid(cell_gid_type i) const { + return i>=cell_begin_ && i<cell_end_; + } + + /// Return a partition of the cell gid over local cell groups. + gid_partition_type gid_group_partition() const { + return util::partition_view(group_starts_); + } + + /// Returns the backend policy. + backend_policy backend() const { + return backend_policy_; + } + +private: + + backend_policy backend_policy_; + cell_gid_type cell_begin_; + cell_gid_type cell_end_; + cell_size_type num_global_cells_; + std::vector<cell_size_type> group_starts_; + std::vector<cell_kind> group_kinds_; +}; + +} // namespace mc +} // namespace nest diff --git a/src/model.hpp b/src/model.hpp index 91d0cd4e150a42ebfa5a62a9777e48651074c5d4..cef1da4dcd5748f2df87bda9f77e2aa02e49aa7c 100644 --- a/src/model.hpp +++ b/src/model.hpp @@ -13,6 +13,7 @@ #include <cell_group.hpp> #include <communication/communicator.hpp> #include <communication/global_policy.hpp> +#include <domain_decomposition.hpp> #include <mc_cell_group.hpp> #include <profiling/profiler.hpp> #include <recipe.hpp> @@ -43,18 +44,15 @@ public: probe_spec probe; }; - template <typename Iter> - model( const recipe& rec, - const util::partition_range<Iter>& groups, - backend_policy policy): - cell_group_divisions_(groups.divisions().begin(), groups.divisions().end()), - backend_policy_(policy) + model(const recipe& rec, const domain_decomposition& decomp): + domain_(decomp) { // set up communicator based on partition - communicator_ = communicator_type(gid_partition()); + communicator_ = communicator_type(domain_.gid_group_partition()); // generate the cell groups in parallel, with one task per cell group - cell_groups_.resize(gid_partition().size()); + cell_groups_.resize(domain_.num_local_groups()); + // thread safe vector for constructing the list of probes in parallel threading::parallel_vector<probe_record> probe_tmp; @@ -62,11 +60,11 @@ public: [&](cell_gid_type i) { PE("setup", "cells"); - auto gids = gid_partition()[i]; - std::vector<cell> cells{gids.second-gids.first}; + auto gids = domain_.get_group(i); + std::vector<cell> cells(gids.end-gids.begin); - for (auto gid: util::make_span(gids)) { - auto i = gid-gids.first; + for (auto gid: util::make_span(gids.begin, gids.end)) { + auto i = gid-gids.begin; cells[i] = rec.get_cell(gid); cell_lid_type j = 0; @@ -76,11 +74,11 @@ public: } } - if (backend_policy_==backend_policy::use_multicore) { - cell_groups_[i] = make_cell_group<multicore_lowered_cell>(gids.first, cells); + if (domain_.backend()==backend_policy::use_multicore) { + cell_groups_[i] = make_cell_group<multicore_lowered_cell>(gids.begin, cells); } else { - cell_groups_[i] = make_cell_group<gpu_lowered_cell>(gids.first, cells); + cell_groups_[i] = make_cell_group<gpu_lowered_cell>(gids.begin, cells); } PL(2); }); @@ -89,7 +87,7 @@ public: probes_.assign(probe_tmp.begin(), probe_tmp.end()); // generate the network connections - for (cell_gid_type i: util::make_span(gid_partition().bounds())) { + for (cell_gid_type i: util::make_span(domain_.cell_begin(), domain_.cell_end())) { for (const auto& cc: rec.connections_on(i)) { connection conn{cc.source, cc.dest, cc.weight, cc.delay}; communicator_.add_connection(conn); @@ -104,11 +102,6 @@ public: future_events().resize(num_groups()); } - // one cell per group: - model(const recipe& rec, backend_policy policy): - model(rec, util::partition_view(util::make_span(0, rec.num_cells()+1)), policy) - {} - void reset() { t_ = 0.; for (auto& group: cell_groups_) { @@ -220,16 +213,18 @@ public: // only thread safe if called outside the run() method void add_artificial_spike(cell_member_type source, time_type tspike) { - current_spikes().get().push_back({source, tspike}); + if (domain_.is_local_gid(source.gid)) { + current_spikes().get().push_back({source, tspike}); + } } void attach_sampler(cell_member_type probe_id, sampler_function f, time_type tfrom = 0) { - if (!algorithms::in_interval(probe_id.gid, gid_partition().bounds())) { - return; - } + const auto idx = domain_.local_group_from_gid(probe_id.gid); - const auto idx = gid_partition().index(probe_id.gid); - cell_groups_[idx]->add_sampler(probe_id, f, tfrom); + // only attach samplers for local cells + if (idx) { + cell_groups_[*idx]->add_sampler(probe_id, f, tfrom); + } } const std::vector<probe_record>& probes() const { return probes_; } @@ -243,8 +238,7 @@ public: } std::size_t num_cells() const { - auto bounds = gid_partition().bounds(); - return bounds.second-bounds.first; + return domain_.num_local_cells(); } // Set event binning policy on all our groups. @@ -272,12 +266,7 @@ public: } private: - std::vector<cell_gid_type> cell_group_divisions_; - backend_policy backend_policy_; - - auto gid_partition() const -> decltype(util::partition_view(cell_group_divisions_)) { - return util::partition_view(cell_group_divisions_); - } + const domain_decomposition &domain_; time_type t_ = 0.; std::vector<std::unique_ptr<cell_group>> cell_groups_; diff --git a/src/recipe.hpp b/src/recipe.hpp index f64c7992e33d65e87fd18588dae49f86d8718211..683ac1d6f2efb73a77ac2b8b97c0412fd57df500 100644 --- a/src/recipe.hpp +++ b/src/recipe.hpp @@ -4,6 +4,8 @@ #include <memory> #include <stdexcept> +#include <cell.hpp> + namespace nest { namespace mc { diff --git a/src/util/meta.hpp b/src/util/meta.hpp index 52c676698844e8405897dbf240fc83382e10fbec..b1ce5f7540407844e60832b03714f93fd86b0b1b 100644 --- a/src/util/meta.hpp +++ b/src/util/meta.hpp @@ -187,10 +187,6 @@ struct common_random_access_iterator< template <typename I, typename E> using common_random_access_iterator_t = typename common_random_access_iterator<I, E>::type; -// -// TODO : now that we are using gcc 5+, replace these old skool SFINAE thingys -// - namespace impl { /// Helper for SFINAE tests that can "sink" any type template<typename T> diff --git a/tests/global_communication/CMakeLists.txt b/tests/global_communication/CMakeLists.txt index 8e66b9afc8035b5df6c96dae7cdb10f87f92c10b..6a653a415bae8d160259e6e5ee277a9482b8d23e 100644 --- a/tests/global_communication/CMakeLists.txt +++ b/tests/global_communication/CMakeLists.txt @@ -2,6 +2,7 @@ set(HEADERS ${PROJECT_SOURCE_DIR}/src/swcio.hpp ) set(COMMUNICATION_SOURCES + test_domain_decomposition.cpp test_exporter_spike_file.cpp test_communicator.cpp test_mpi_gather_all.cpp diff --git a/tests/global_communication/test_communicator.cpp b/tests/global_communication/test_communicator.cpp index b13c8e4f9a7412456289fa2c9dd02e31313dd86d..5cbceec6b08a2f538e063b73e350010b80923460 100644 --- a/tests/global_communication/test_communicator.cpp +++ b/tests/global_communication/test_communicator.cpp @@ -13,7 +13,7 @@ using namespace nest::mc; using communicator_type = communication::communicator<communication::global_policy>; -bool is_dry_run() { +static bool is_dry_run() { return communication::global_policy::kind() == communication::global_policy_kind::dryrun; } diff --git a/tests/global_communication/test_domain_decomposition.cpp b/tests/global_communication/test_domain_decomposition.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ad0048f0cffc529faa49d2d26cc60f66da43b36b --- /dev/null +++ b/tests/global_communication/test_domain_decomposition.cpp @@ -0,0 +1,30 @@ +#include "../gtest.h" + +#include <cstdio> +#include <fstream> +#include <iostream> +#include <string> +#include <vector> + +#include <communication/communicator.hpp> +#include <communication/global_policy.hpp> + +using namespace nest::mc; + +using communicator_type = communication::communicator<communication::global_policy>; + +static bool is_dry_run() { + return communication::global_policy::kind() == + communication::global_policy_kind::dryrun; +} + +TEST(domain_decomp, basic) { + using policy = communication::global_policy; + +/* + const auto num_domains = policy::size(); + const auto rank = policy::id(); +*/ + + +} diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 0bae169cdb41837b3f1263743acd37681cfd95a2..ecae57a183301a035d4d0b4d117d2594b0eaff31 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -37,6 +37,7 @@ set(TEST_SOURCES test_compartments.cpp test_counter.cpp test_cycle.cpp + test_domain_decomposition.cpp test_either.cpp test_event_queue.cpp test_event_binner.cpp diff --git a/tests/unit/test_domain_decomposition.cpp b/tests/unit/test_domain_decomposition.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ab5d3fe93c931031d1a1ead420cd301ea737fe48 --- /dev/null +++ b/tests/unit/test_domain_decomposition.cpp @@ -0,0 +1,118 @@ +#include "../gtest.h" + +#include <domain_decomposition.hpp> +#include <backends.hpp> + +using namespace nest::mc; + +namespace { + +// dummy recipe type for testing +class test_recipe: public recipe { +public: + test_recipe(cell_size_type s): size_(s) + {} + + cell_size_type num_cells() const override { + return size_; + } + + cell get_cell(cell_gid_type) const override { + return cell(); + } + cell_kind get_cell_kind(cell_gid_type) const override { + return cell_kind::cable1d_neuron; + } + + cell_count_info get_cell_count_info(cell_gid_type) const override { + return {0, 0, 0}; + } + std::vector<cell_connection> connections_on(cell_gid_type) const override { + return {}; + } + +private: + cell_size_type size_; +}; + +} + +TEST(domain_decomposition, one_cell_groups) +{ + group_rules rules{1, backend_policy::use_multicore}; + + unsigned num_cells = 10; + domain_decomposition decomp(test_recipe(num_cells), rules); + + EXPECT_EQ(0u, decomp.cell_begin()); + EXPECT_EQ(num_cells, decomp.cell_end()); + + EXPECT_EQ(num_cells, decomp.num_local_cells()); + EXPECT_EQ(num_cells, decomp.num_global_cells()); + EXPECT_EQ(num_cells, decomp.num_local_groups()); + + // cell group indexes are monotonically increasing + for (unsigned i=0u; i<num_cells; ++i) { + auto g = decomp.get_group(i); + EXPECT_LT(g.begin, g.end); + EXPECT_EQ(g.end-g.begin, 1u); + } + + // check that local gid are identified as local + for (auto i=0u; i<num_cells; ++i) { + EXPECT_TRUE(decomp.is_local_gid(i)); + } + EXPECT_FALSE(decomp.is_local_gid(num_cells)); +} + +TEST(domain_decomposition, multi_cell_groups) +{ + unsigned num_cells = 10; + + // test group sizes from 1 up to 1 more than the total number of cells + for (unsigned group_size=1u; group_size<=num_cells+1; ++group_size) { + group_rules rules{group_size, backend_policy::use_multicore}; + domain_decomposition decomp(test_recipe(num_cells), rules); + + EXPECT_EQ(0u, decomp.cell_begin()); + EXPECT_EQ(num_cells, decomp.cell_end()); + + EXPECT_EQ(num_cells, decomp.num_local_cells()); + EXPECT_EQ(num_cells, decomp.num_global_cells()); + + unsigned num_groups = decomp.num_local_groups(); + + // check that cell group indexes are monotonically increasing + unsigned total_cells = 0; + std::vector<cell_size_type> bounds{decomp.cell_begin()}; + for (auto i=0u; i<num_groups; ++i) { + auto g = decomp.get_group(i); + auto size = g.end-g.begin; + + // assert that size of the group: + // is nonzero + EXPECT_LT(g.begin, g.end); + // is no larger than group_size + EXPECT_TRUE(size<=group_size); + + // Check that the start of this group matches the end of + // the preceding group. + EXPECT_EQ(bounds.back(), g.begin); + bounds.push_back(g.end); + + total_cells += size; + } + // check that the sum of the group sizes euqals the total number of cells + EXPECT_EQ(total_cells, num_cells); + + // check that local gid are identified as local + for (auto i=0u; i<num_cells; ++i) { + EXPECT_TRUE(decomp.is_local_gid(i)); + auto group_id = decomp.local_group_from_gid(i); + EXPECT_TRUE(group_id); + EXPECT_LT(*group_id, num_groups); + } + EXPECT_FALSE(decomp.is_local_gid(num_cells)); + EXPECT_FALSE(decomp.local_group_from_gid(num_cells)); + } +} diff --git a/tests/validation/validate_ball_and_stick.hpp b/tests/validation/validate_ball_and_stick.hpp index da1652e6747e311479b07515faca3c799df8d923..786c6bc7b8c05592f81fb350a6d5e9c49b430ec8 100644 --- a/tests/validation/validate_ball_and_stick.hpp +++ b/tests/validation/validate_ball_and_stick.hpp @@ -49,7 +49,8 @@ void run_ncomp_convergence_test( seg->set_compartments(ncomp); } } - model m(singleton_recipe{c}, backend); + domain_decomposition decomp(singleton_recipe{c}, {1u, backend}); + model m(singleton_recipe{c}, decomp); runner.run(m, ncomp, t_end, dt, exclude); } diff --git a/tests/validation/validate_kinetic.hpp b/tests/validation/validate_kinetic.hpp index 535ed8697abf985893a00108435c51897575c332..ba21d0697a5bce98b3cb30643576fbee489f6f24 100644 --- a/tests/validation/validate_kinetic.hpp +++ b/tests/validation/validate_kinetic.hpp @@ -32,7 +32,8 @@ void run_kinetic_dt( convergence_test_runner<float> runner("dt", samplers, meta); runner.load_reference_data(ref_file); - model model(singleton_recipe{c}, backend); + domain_decomposition decomp(singleton_recipe{c}, {1u, backend}); + model model(singleton_recipe{c}, decomp); auto exclude = stimulus_ends(c); diff --git a/tests/validation/validate_soma.hpp b/tests/validation/validate_soma.hpp index 0c533faa044c9bf84bcfbbd279b77fbddcdc5846..862e50304aaa776302f913fd3156772eb861d95e 100644 --- a/tests/validation/validate_soma.hpp +++ b/tests/validation/validate_soma.hpp @@ -18,7 +18,8 @@ void validate_soma(nest::mc::backend_policy backend) { cell c = make_cell_soma_only(); add_common_voltage_probes(c); - model model(singleton_recipe{c}, backend); + domain_decomposition decomp(singleton_recipe{c}, {1u, backend}); + model m(singleton_recipe{c}, decomp); float sample_dt = .025f; sampler_info samplers[] = {{"soma.mid", {0u, 0u}, simple_sampler(sample_dt)}}; @@ -43,9 +44,9 @@ void validate_soma(nest::mc::backend_policy backend) { double oo_dt = base/multiple; if (oo_dt>max_oo_dt) goto end; - model.reset(); + m.reset(); float dt = float(1./oo_dt); - runner.run(model, dt, t_end, dt, {}); + runner.run(m, dt, t_end, dt, {}); } } end: diff --git a/tests/validation/validate_synapses.hpp b/tests/validation/validate_synapses.hpp index c3f7f29413526747a982c62733b4e6ae08862969..83a77d36eb73bfee8533055a90cc1c872aecc38b 100644 --- a/tests/validation/validate_synapses.hpp +++ b/tests/validation/validate_synapses.hpp @@ -60,7 +60,8 @@ void run_synapse_test( for (int ncomp = 10; ncomp<max_ncomp; ncomp*=2) { c.cable(1)->set_compartments(ncomp); - model m(singleton_recipe{c}, backend); + domain_decomposition decomp(singleton_recipe{c}, {1u, backend}); + model m(singleton_recipe{c}, decomp); m.group(0).enqueue_events(synthetic_events); runner.run(m, ncomp, t_end, dt, exclude);