From 6ba39a922326a9b36bb2f472e4343895d706375f Mon Sep 17 00:00:00 2001
From: Ben Cumming <bcumming@cscs.ch>
Date: Fri, 22 Jun 2018 17:29:03 +0200
Subject: [PATCH] Benchmark cell type (#500)

Add a new cell type, and corresponding cell_group implementation, for benchmarking the simulator library architecture.

Add an benchmark_cell_group, where each cell in the group

generates a spike train prescribed by a time_seq
takes a prescribed time interval per cell to perform the cell_group::advance method.
With this cell type, one can easily build arbitrary networks with prescribed spiking and cell update overheads.
A miniapp that uses this cell type to build a benchmark model is implemented in example/bench.

Fixes #493
Fixes #501
---
 example/CMakeLists.txt              |   1 +
 example/bench/CMakeLists.txt        |  15 ++++
 example/bench/bench.cpp             |  78 +++++++++++++++++++
 example/bench/parameters.cpp        | 111 ++++++++++++++++++++++++++++
 example/bench/parameters.hpp        |  43 +++++++++++
 example/bench/readme.md             |  57 ++++++++++++++
 example/bench/recipe.cpp            |  71 ++++++++++++++++++
 example/bench/recipe.hpp            |  23 ++++++
 example/miniapp/miniapp.cpp         |   1 -
 example/miniapp/miniapp_recipes.cpp |   2 +-
 src/CMakeLists.txt                  |   1 +
 src/benchmark_cell.hpp              |  21 ++++++
 src/benchmark_cell_group.cpp        |  90 ++++++++++++++++++++++
 src/benchmark_cell_group.hpp        |  41 ++++++++++
 src/cell_group_factory.cpp          |   4 +
 src/common_types.hpp                |   7 +-
 src/common_types_io.cpp             |   2 +
 src/spike_source_cell.hpp           |  14 ++++
 tests/unit/test_lif_cell_group.cpp  |   2 +-
 19 files changed, 578 insertions(+), 6 deletions(-)
 create mode 100644 example/bench/CMakeLists.txt
 create mode 100644 example/bench/bench.cpp
 create mode 100644 example/bench/parameters.cpp
 create mode 100644 example/bench/parameters.hpp
 create mode 100644 example/bench/readme.md
 create mode 100644 example/bench/recipe.cpp
 create mode 100644 example/bench/recipe.hpp
 create mode 100644 src/benchmark_cell.hpp
 create mode 100644 src/benchmark_cell_group.cpp
 create mode 100644 src/benchmark_cell_group.hpp
 create mode 100644 src/spike_source_cell.hpp

diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt
index 31e71224..0238e1f1 100644
--- a/example/CMakeLists.txt
+++ b/example/CMakeLists.txt
@@ -1,3 +1,4 @@
 add_subdirectory(miniapp)
 add_subdirectory(generators)
 add_subdirectory(brunel)
+add_subdirectory(bench)
diff --git a/example/bench/CMakeLists.txt b/example/bench/CMakeLists.txt
new file mode 100644
index 00000000..e5bf57c6
--- /dev/null
+++ b/example/bench/CMakeLists.txt
@@ -0,0 +1,15 @@
+add_executable(bench.exe bench.cpp recipe.cpp parameters.cpp)
+
+target_link_libraries(bench.exe LINK_PUBLIC ${ARB_LIBRARIES})
+target_link_libraries(bench.exe LINK_PUBLIC ${EXTERNAL_LIBRARIES})
+
+if(ARB_WITH_MPI)
+    target_link_libraries(bench.exe LINK_PUBLIC ${MPI_C_LIBRARIES})
+    set_property(TARGET bench.exe APPEND_STRING PROPERTY LINK_FLAGS "${MPI_C_LINK_FLAGS}")
+endif()
+
+set_target_properties(
+    bench.exe
+    PROPERTIES
+    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/example"
+)
diff --git a/example/bench/bench.cpp b/example/bench/bench.cpp
new file mode 100644
index 00000000..58b80633
--- /dev/null
+++ b/example/bench/bench.cpp
@@ -0,0 +1,78 @@
+/*
+ * Miniapp that uses the artificial benchmark cell type to test
+ * the simulator infrastructure.
+ */
+#include <fstream>
+#include <iomanip>
+#include <iostream>
+
+#include <json/json.hpp>
+
+#include <common_types.hpp>
+#include <communication/distributed_context.hpp>
+#include <hardware/node_info.hpp>
+#include <load_balance.hpp>
+#include <profiling/meter_manager.hpp>
+#include <simulation.hpp>
+#include <util/ioutil.hpp>
+
+#include "recipe.hpp"
+
+using namespace arb;
+
+int main(int argc, char** argv) {
+    try {
+        distributed_context context;
+        #ifdef ARB_HAVE_MPI
+        mpi::scoped_guard guard(&argc, &argv);
+        context = mpi_context(MPI_COMM_WORLD);
+        #endif
+        const bool is_root =  context.id()==0;
+
+        std::cout << util::mask_stream(is_root);
+
+        bench_params params = read_options(argc, argv);
+
+        std::cout << params << "\n";
+
+        util::meter_manager meters(&context);
+        meters.start();
+
+        // Create an instance of our recipe.
+        bench_recipe recipe(params);
+        meters.checkpoint("recipe-build");
+
+        // Make the domain decomposition for the model
+        auto node = arb::hw::get_node_info();
+        auto decomp = arb::partition_load_balance(recipe, node, &context);
+        meters.checkpoint("domain-decomp");
+
+        // Construct the model.
+        arb::simulation sim(recipe, decomp, &context);
+        meters.checkpoint("model-build");
+
+        // Run the simulation for 100 ms, with time steps of 0.01 ms.
+        sim.run(params.duration, 0.01);
+        meters.checkpoint("model-run");
+
+        // write meters
+        auto report = util::make_meter_report(meters);
+        std::cout << report << "\n";
+
+        if (is_root==0) {
+            std::ofstream fid;
+            fid.exceptions(std::ios_base::badbit | std::ios_base::failbit);
+            fid.open("meters.json");
+            fid << std::setw(1) << util::to_json(report) << "\n";
+        }
+
+        // output profile and diagnostic feedback
+        auto profile = util::profiler_summary();
+        std::cout << profile << "\n";
+
+        std::cout << "there were " << sim.num_spikes() << " spikes\n";
+    }
+    catch (std::exception& e) {
+        std::cerr << "exception caught running benchmark miniapp:\n" << e.what() << std::endl;
+    }
+}
diff --git a/example/bench/parameters.cpp b/example/bench/parameters.cpp
new file mode 100644
index 00000000..dea52a70
--- /dev/null
+++ b/example/bench/parameters.cpp
@@ -0,0 +1,111 @@
+#include <exception>
+#include <fstream>
+#include <string>
+
+#include <util/optional.hpp>
+
+#include <json/json.hpp>
+
+#include "parameters.hpp"
+
+double bench_params::expected_advance_time() const {
+    return cell.realtime_ratio * duration*1e-3 * num_cells;
+}
+unsigned bench_params::expected_spikes() const {
+    return num_cells * duration*1e-3 * cell.spike_freq_hz;
+}
+unsigned bench_params::expected_spikes_per_interval() const {
+    return num_cells * network.min_delay*1e-3/2 * cell.spike_freq_hz;
+}
+unsigned bench_params::expected_events() const {
+    return expected_spikes() * network.fan_in;
+}
+unsigned bench_params::expected_events_per_interval() const {
+    return expected_spikes_per_interval() * network.fan_in;
+}
+
+std::ostream& operator<<(std::ostream& o, const bench_params& p) {
+    o << "benchmark parameters:\n"
+      << "  name:          " << p.name << "\n"
+      << "  num cells:     " << p.num_cells << "\n"
+      << "  duration:      " << p.duration << " ms\n"
+      << "  fan in:        " << p.network.fan_in << " connections/cell\n"
+      << "  min delay:     " << p.network.min_delay << " ms\n"
+      << "  spike freq:    " << p.cell.spike_freq_hz << " Hz\n"
+      << "  cell overhead: " << p.cell.realtime_ratio << " ms to advance 1 ms\n";
+    o << "expected:\n"
+      << "  cell advance: " << p.expected_advance_time() << " s\n"
+      << "  spikes:       " << p.expected_spikes() << "\n"
+      << "  events:       " << p.expected_events() << "\n"
+      << "  spikes:       " << p.expected_spikes_per_interval() << " per interval\n"
+      << "  events:       " << p.expected_events_per_interval()/p.num_cells << " per cell per interval";
+    return o;
+}
+
+using arb::util::optional;
+using nlohmann::json;
+
+// Search a json object for an entry with a given name.
+// If found, return the value and remove from json object.
+template<typename T>
+optional<T> extract(const char* name, json& j) {
+    auto it = j.find(name);
+    if (it==j.end()) {
+        return arb::util::nullopt;
+    }
+    T value = std::move(*it);
+    j.erase(name);
+    return std::move(value);
+}
+
+bench_params read_options(int argc, char** argv) {
+    bench_params params;
+    if (argc<2) {
+        std::cout << "Using default parameters.\n";
+        return params;
+    }
+    if (argc>2) {
+        throw std::runtime_error("More than command line one option not permitted.");
+    }
+
+    std::string fname = argv[1];
+    std::cout << "Loading parameters from file: " << fname << "\n";
+    std::ifstream f(fname);
+
+    if (!f.good()) {
+        throw std::runtime_error("Unable to open input parameter file: "+fname);
+    }
+
+    nlohmann::json json;
+    json << f;
+
+    if (auto o  = extract<std::string>("name", json)) {
+        params.name = *o;
+    }
+    if (auto o  = extract<unsigned>("num-cells", json)) {
+        params.num_cells = *o;
+    }
+    if (auto o  = extract<double>("duration", json)) {
+        params.duration = *o;
+    }
+    if (auto o  = extract<double>("min-delay", json)) {
+        params.network.min_delay = *o;
+    }
+    if (auto o  = extract<unsigned>("fan-in", json)) {
+        params.network.fan_in = *o;
+    }
+    if (auto o  = extract<double>("realtime-ratio", json)) {
+        params.cell.realtime_ratio = *o;
+    }
+    if (auto o  = extract<double>("spike-frequency", json)) {
+        params.cell.spike_freq_hz = *o;
+    }
+
+    for (auto it=json.begin(); it!=json.end(); ++it) {
+        std::cout << "  Warning: unused input parameter: \"" << it.key() << "\"\n";
+    }
+    std::cout << "\n";
+
+    return params;
+}
+
diff --git a/example/bench/parameters.hpp b/example/bench/parameters.hpp
new file mode 100644
index 00000000..52c0d164
--- /dev/null
+++ b/example/bench/parameters.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+#include <ostream>
+#include <string>
+
+#include <common_types.hpp>
+
+using arb::time_type;
+
+struct bench_params {
+    struct cell_params {
+        double spike_freq_hz = 10;   // Frequency in hz that cell will generate (poisson) spikes.
+        double realtime_ratio = 0.1; // Integration speed relative to real time, e.g. 10 implies
+                                     // that a cell is integrated 10 times slower than real time.
+    };
+    struct network_params {
+        unsigned fan_in = 5000;      // Number of incoming connections on each cell.
+        double min_delay = 10;       // Used as the delay on all connections.
+    };
+    std::string name = "default";    // Name of the model.
+    unsigned num_cells = 1000;       // Number of cells in model.
+    time_type duration = 100;          // Simulation duration in ms.
+
+    cell_params cell;                // Cell parameters for all cells in model.
+    network_params network;          // Description of the network.
+
+    // Expected simulation performance properties based on model parameters.
+
+    // Time to finish simulation if only cell overheads are counted.
+    double expected_advance_time() const;
+    // Total expected number of spikes generated by simulation.
+    unsigned expected_spikes() const;
+    // Expected number of spikes generated per min_delay/2 interval.
+    unsigned expected_spikes_per_interval() const;
+    // Expected number of post-synaptic events delivered over simulation.
+    unsigned expected_events() const;
+    // Expected number of post-synaptic events delivered per min_delay/2 interval.
+    unsigned expected_events_per_interval() const;
+};
+
+bench_params read_options(int argc, char** argv);
+
+std::ostream& operator<<(std::ostream& o, const bench_params& p);
diff --git a/example/bench/readme.md b/example/bench/readme.md
new file mode 100644
index 00000000..c6dbc2f9
--- /dev/null
+++ b/example/bench/readme.md
@@ -0,0 +1,57 @@
+# Artificial Infrastructure Benchmark
+
+This miniapp uses the `arb::benchmark_cell` to build a network of cells that
+have predictable cell integration run time and spike generation patterns. It is
+for benchmarking purposes only.
+
+The main application of this miniapp is to benchmark the Arbor simulation
+architecture. The architecture is everything that isn't cell state update,
+including
+  * Spike exchange.
+  * Event sorting and merging.
+  * Threading and MPI infrastructure.
+
+For example, the scaling behavior of the spike exchange can be studied as
+factors such as fan-in, network minimum delay, spiking frequency and spiking
+pattern are varied, without having to tweak parameters on a model that uses
+"biologically realistic" cells such as LIF cells.
+
+## Usage
+
+The model can be configured using a json configuration file:
+
+```
+./bench.exe params.json
+```
+
+An example parameter file is:
+```
+{
+    "name": "small test",
+    "num-cells": 2000,
+    "duration": 100,
+    "fan-in": 10000,
+    "min-delay": 10,
+    "spike-frequency": 20,
+    "realtime-ratio": 0.1
+}
+```
+
+The parameters in the file:
+  * `name`: a string with a name for the benchmark.
+  * `num-cells`: the total number of cells in the model. The cell population
+    is assumed to be homogoneous, that is the `spike-frequency` and
+    `cell-overhead` parameters are the same for all cells.
+  * `duration`: the length of the simulated time interval, in ms.
+  * `fan-in`: the number of incoming connections on each cell.
+  * `min-delay`: the minimum delay of the network.
+  * `spike-frequency`: frequency of the independent Poisson processes that
+    generate spikes for each cell.
+  * `realtime-ratio`: the ratio between time taken to advance a single cell in
+    the simulation and the simulated time. For example, a value of 1 indicates
+    that the cell is simulated in real time, while a value of 0.1 indicates
+    that 10s can be simulated in a single second.
+
+The network is randomly connected with no self-connections and `fan-in`
+incoming connections on each cell, with every connection having delay of
+`min-delay`.
diff --git a/example/bench/recipe.cpp b/example/bench/recipe.cpp
new file mode 100644
index 00000000..c3358c22
--- /dev/null
+++ b/example/bench/recipe.cpp
@@ -0,0 +1,71 @@
+#include <random>
+
+#include <benchmark_cell.hpp>
+#include <common_types.hpp>
+#include <time_sequence.hpp>
+
+#include "recipe.hpp"
+
+cell_size_type bench_recipe::num_cells() const {
+    return params_.num_cells;
+}
+
+arb::util::unique_any bench_recipe::get_cell_description(cell_gid_type gid) const {
+    using rng_type = std::mt19937_64;
+    arb::benchmark_cell cell;
+    cell.realtime_ratio = params_.cell.realtime_ratio;
+
+    // The time_sequence of the cell produces the series of time points at
+    // which it will spike. We use a poisson_time_seq with a random sequence
+    // seeded with the gid. In this way, a cell's random stream depends only
+    // on its gid, and will hence give reproducable results when run with
+    // different MPI ranks and threads.
+    cell.time_sequence =
+        arb::poisson_time_seq<rng_type>(
+                rng_type(gid), 0, 1e-3*params_.cell.spike_freq_hz);
+
+    return std::move(cell);
+}
+
+arb::cell_kind bench_recipe::get_cell_kind(arb::cell_gid_type gid) const {
+    return arb::cell_kind::benchmark;
+}
+
+std::vector<arb::cell_connection> bench_recipe::connections_on(cell_gid_type gid) const {
+    const auto n = params_.network.fan_in;
+    std::vector<arb::cell_connection> cons;
+    cons.reserve(n);
+    using rng_type = std::mt19937_64;
+    rng_type rng(gid);
+
+    // Generate n incoming connections on this cell with random sources, where
+    // the source can't equal gid (i.e. no self-connections).
+    // We want a random distribution that will uniformly draw values from the
+    // union of the two ranges: [0, gid-1] AND [gid+1, num_cells-1].
+    // To do this, we draw random values in the range [0, num_cells-2], then
+    // add 1 to values ≥ gid.
+
+    std::uniform_int_distribution<cell_gid_type> dist(0, params_.num_cells-2);
+    for (unsigned i=0; i<n; ++i) {
+        // Draw random source and adjust to avoid self-connections if neccesary.
+        cell_gid_type src = dist(rng);
+        if (src>=gid) ++src;
+        // Note: target is {gid, 0}, i.e. the first (and only) target on the cell.
+        arb::cell_connection con({src, 0}, {gid, 0}, 1.f, params_.network.min_delay);
+        cons.push_back(con);
+    }
+
+    return cons;
+}
+
+cell_size_type bench_recipe::num_targets(cell_gid_type gid) const {
+    // Only one target, to which all incoming connections connect.
+    // This could be parameterized, in which case the connections
+    // generated in connections_on should end on random cell-local targets.
+    return 1;
+}
+
+// one spike source per cell
+cell_size_type bench_recipe::num_sources(cell_gid_type gid) const {
+    return 1;
+}
diff --git a/example/bench/recipe.hpp b/example/bench/recipe.hpp
new file mode 100644
index 00000000..76a1a84f
--- /dev/null
+++ b/example/bench/recipe.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <common_types.hpp>
+#include <recipe.hpp>
+
+#include "parameters.hpp"
+
+using arb::cell_kind;
+using arb::cell_gid_type;
+using arb::cell_size_type;
+
+class bench_recipe: public arb::recipe {
+    bench_params params_;
+public:
+    bench_recipe(bench_params p): params_(std::move(p)) {}
+    cell_size_type num_cells() const override;
+    arb::util::unique_any get_cell_description(cell_gid_type gid) const override;
+    arb::cell_kind get_cell_kind(arb::cell_gid_type gid) const override;
+    cell_size_type num_targets(cell_gid_type gid) const override;
+    cell_size_type num_sources(cell_gid_type gid) const override;
+    std::vector<arb::cell_connection> connections_on(cell_gid_type) const override;
+};
+
diff --git a/example/miniapp/miniapp.cpp b/example/miniapp/miniapp.cpp
index 2aecfeed..943ad04d 100644
--- a/example/miniapp/miniapp.cpp
+++ b/example/miniapp/miniapp.cpp
@@ -8,7 +8,6 @@
 #include <json/json.hpp>
 
 #include <common_types.hpp>
-#include <communication/communicator.hpp>
 #include <communication/distributed_context.hpp>
 #include <cell.hpp>
 #include <hardware/gpu.hpp>
diff --git a/example/miniapp/miniapp_recipes.cpp b/example/miniapp/miniapp_recipes.cpp
index 16c88939..17e852cb 100644
--- a/example/miniapp/miniapp_recipes.cpp
+++ b/example/miniapp/miniapp_recipes.cpp
@@ -6,7 +6,7 @@
 #include <cell.hpp>
 #include <event_generator.hpp>
 #include <morphology.hpp>
-#include <spike_source_cell_group.hpp>
+#include <spike_source_cell.hpp>
 #include <time_sequence.hpp>
 #include <util/debug.hpp>
 
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 11fe3e27..aad5e5ed 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -2,6 +2,7 @@ set(arbor_cxx_sources
     backends/multicore/mechanism.cpp
     backends/multicore/shared_state.cpp
     backends/multicore/stimulus.cpp
+    benchmark_cell_group.cpp
     builtin_mechanisms.cpp
     cell_group_factory.cpp
     common_types_io.cpp
diff --git a/src/benchmark_cell.hpp b/src/benchmark_cell.hpp
new file mode 100644
index 00000000..6d3c928c
--- /dev/null
+++ b/src/benchmark_cell.hpp
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <time_sequence.hpp>
+
+namespace arb {
+
+// Cell description returned by recipe::cell_description(gid) for cells with
+// recipe::cell_kind(gid) returning cell_kind::benchmark
+
+struct benchmark_cell {
+    // Describes the time points at which spikes are to be generated.
+    time_seq time_sequence;
+
+    // Time taken in ms to advance the cell one ms of simulation time.
+    // If equal to 1, then a single cell can be advanced in realtime 
+    double realtime_ratio;
+};
+
+} // namespace arb
+
+
diff --git a/src/benchmark_cell_group.cpp b/src/benchmark_cell_group.cpp
new file mode 100644
index 00000000..cd2237ba
--- /dev/null
+++ b/src/benchmark_cell_group.cpp
@@ -0,0 +1,90 @@
+#include <chrono>
+#include <exception>
+
+#include <cell_group.hpp>
+#include <profiling/profiler.hpp>
+#include <recipe.hpp>
+#include <benchmark_cell.hpp>
+#include <benchmark_cell_group.hpp>
+#include <time_sequence.hpp>
+
+namespace arb {
+
+benchmark_cell_group::benchmark_cell_group(std::vector<cell_gid_type> gids,
+                                           const recipe& rec):
+    gids_(std::move(gids))
+{
+    cells_.reserve(gids_.size());
+    for (auto gid: gids_) {
+        cells_.push_back(util::any_cast<benchmark_cell>(rec.get_cell_description(gid)));
+    }
+
+    reset();
+}
+
+void benchmark_cell_group::reset() {
+    t_ = 0;
+
+    for (auto& c: cells_) {
+        c.time_sequence.reset();
+    }
+
+    clear_spikes();
+}
+
+cell_kind benchmark_cell_group::get_cell_kind() const {
+    return cell_kind::benchmark;
+}
+
+void benchmark_cell_group::advance(epoch ep,
+                                   time_type dt,
+                                   const event_lane_subrange& event_lanes)
+{
+    using std::chrono::high_resolution_clock;
+    using duration_type = std::chrono::duration<double, std::micro>;
+
+    PE(advance_bench_cell);
+    // Micro-seconds to advance in this epoch.
+    auto us = 1e3*(ep.tfinal-t_);
+    for (auto i: util::make_span(0, gids_.size())) {
+        auto& tseq = cells_[i].time_sequence;
+        // Expected time to complete epoch in micro seconds.
+        const double duration_us = cells_[i].realtime_ratio*us;
+        const auto gid = gids_[i];
+
+        // Start timer.
+        auto start = high_resolution_clock::now();
+
+        while (tseq.front()<ep.tfinal) {
+            spikes_.push_back({{gid, 0u}, tseq.front()});
+            tseq.pop();
+        }
+
+        // Wait until the expected time to advance has elapsed. Use a busy-wait
+        // so that the resources of this thread are tied up until the interval
+        // has elapsed, to emulate a "real" cell.
+        while (duration_type(high_resolution_clock::now()-start).count() < duration_us);
+    }
+    t_ = ep.tfinal;
+
+    PL();
+};
+
+const std::vector<spike>& benchmark_cell_group::spikes() const {
+    return spikes_;
+}
+
+void benchmark_cell_group::clear_spikes() {
+    spikes_.clear();
+}
+
+void benchmark_cell_group::add_sampler(sampler_association_handle h,
+                                   cell_member_predicate probe_ids,
+                                   schedule sched,
+                                   sampler_function fn,
+                                   sampling_policy policy)
+{
+    std::logic_error("A benchmark_cell group doen't support sampling of internal state!");
+}
+
+} // namespace arb
diff --git a/src/benchmark_cell_group.hpp b/src/benchmark_cell_group.hpp
new file mode 100644
index 00000000..536a7fa1
--- /dev/null
+++ b/src/benchmark_cell_group.hpp
@@ -0,0 +1,41 @@
+#pragma once
+
+#include <benchmark_cell.hpp>
+#include <cell_group.hpp>
+#include <recipe.hpp>
+#include <time_sequence.hpp>
+
+namespace arb {
+
+class benchmark_cell_group: public cell_group {
+public:
+    benchmark_cell_group(std::vector<cell_gid_type> gids, const recipe& rec);
+
+    cell_kind get_cell_kind() const override;
+
+    void advance(epoch ep, time_type dt, const event_lane_subrange& event_lanes) override;
+
+    void reset() override;
+
+    void set_binning_policy(binning_kind policy, time_type bin_interval) override {}
+
+    const std::vector<spike>& spikes() const override;
+
+    void clear_spikes() override;
+
+    void add_sampler(sampler_association_handle h, cell_member_predicate probe_ids, schedule sched, sampler_function fn, sampling_policy policy) override;
+
+    void remove_sampler(sampler_association_handle h) override {}
+
+    void remove_all_samplers() override {}
+
+private:
+    time_type t_;
+
+    std::vector<benchmark_cell> cells_;
+    std::vector<spike> spikes_;
+    std::vector<cell_gid_type> gids_;
+};
+
+} // namespace arb
+
diff --git a/src/cell_group_factory.cpp b/src/cell_group_factory.cpp
index 862dfdb0..4c613695 100644
--- a/src/cell_group_factory.cpp
+++ b/src/cell_group_factory.cpp
@@ -1,6 +1,7 @@
 #include <vector>
 
 #include <backends.hpp>
+#include <benchmark_cell_group.hpp>
 #include <cell_group.hpp>
 #include <domain_decomposition.hpp>
 #include <fvm_lowered_cell.hpp>
@@ -23,6 +24,9 @@ cell_group_ptr cell_group_factory(const recipe& rec, const group_description& gr
     case cell_kind::lif_neuron:
         return make_cell_group<lif_cell_group>(group.gids, rec);
 
+    case cell_kind::benchmark:
+        return make_cell_group<benchmark_cell_group>(group.gids, rec);
+
     default:
         throw std::runtime_error("unknown cell kind");
     }
diff --git a/src/common_types.hpp b/src/common_types.hpp
index d74bf31e..c365883c 100644
--- a/src/common_types.hpp
+++ b/src/common_types.hpp
@@ -68,9 +68,10 @@ using sample_size_type = std::int32_t;
 // group equal kinds in the same cell group.
 
 enum class cell_kind {
-    cable1d_neuron,      // Our own special mc neuron
-    lif_neuron,          // Leaky-integrate and fire neuron
-    spike_source,        // Cell that generates spikes at a user-supplied sequence of time points
+    cable1d_neuron,   // Our own special mc neuron.
+    lif_neuron,       // Leaky-integrate and fire neuron.
+    spike_source,     // Cell that generates spikes at a user-supplied sequence of time points.
+    benchmark,        // Proxy cell used for benchmarking.
 };
 
 } // namespace arb
diff --git a/src/common_types_io.cpp b/src/common_types_io.cpp
index 91360c0f..ada86a91 100644
--- a/src/common_types_io.cpp
+++ b/src/common_types_io.cpp
@@ -15,6 +15,8 @@ std::ostream& operator<<(std::ostream& o, arb::cell_kind k) {
         return o << "cable1d_neuron";
     case arb::cell_kind::lif_neuron:
         return o << "lif_neuron";
+    case arb::cell_kind::benchmark:
+        return o << "benchmark_cell";
     }
     return o;
 }
diff --git a/src/spike_source_cell.hpp b/src/spike_source_cell.hpp
new file mode 100644
index 00000000..489d1300
--- /dev/null
+++ b/src/spike_source_cell.hpp
@@ -0,0 +1,14 @@
+#pragma once
+
+#include <time_sequence.hpp>
+
+namespace arb {
+
+// Cell description returned by recipe::cell_description(gid) for cells with
+// recipe::cell_kind(gid) returning cell_kind::spike_source
+
+struct spike_source_cell {
+    time_seq seq;
+};
+
+} // namespace arb
diff --git a/tests/unit/test_lif_cell_group.cpp b/tests/unit/test_lif_cell_group.cpp
index 43785e12..7c54b74d 100644
--- a/tests/unit/test_lif_cell_group.cpp
+++ b/tests/unit/test_lif_cell_group.cpp
@@ -6,7 +6,7 @@
 #include <lif_cell_group.hpp>
 #include <load_balance.hpp>
 #include <simulation.hpp>
-#include <spike_source_cell_group.hpp>
+#include <spike_source_cell.hpp>
 #include <recipe.hpp>
 
 using namespace arb;
-- 
GitLab