diff --git a/miniapp/miniapp.cpp b/miniapp/miniapp.cpp
index b2f0d9ccbbd8760cbb8c56355d5526659c74cc5c..d7db89deab84daa8d49e09b30b6c1d4b86e5f61c 100644
--- a/miniapp/miniapp.cpp
+++ b/miniapp/miniapp.cpp
@@ -178,8 +178,8 @@ int main(int argc, char** argv) {
 
 void banner() {
     std::cout << "====================\n";
-    std::cout << "  starting miniapp\n";
-    std::cout << "  - " << threading::description() << " threading support\n";
+    std::cout << "  NestMC miniapp\n";
+    std::cout << "  - " << threading::description() << " threading support (" << threading::num_threads() << ")\n";
     std::cout << "  - communication policy: " << std::to_string(global_policy::kind()) << " (" << global_policy::size() << ")\n";
     std::cout << "  - gpu support: " << (config::has_cuda? "on": "off") << "\n";
     std::cout << "====================\n";
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 47b79f5fd7ac9e2a036c40eeb10b41bb3bc3be2c..93643789a848522927570eaed4a6833d3c0b6d80 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,7 +1,7 @@
 set(BASE_SOURCES
+    backends/multicore/fvm.cpp
     common_types_io.cpp
     cell.cpp
-    #cell_group_factory.cpp
     event_binner.cpp
     model.cpp
     morphology.cpp
@@ -11,14 +11,15 @@ set(BASE_SOURCES
     profiling/power_meter.cpp
     profiling/profiler.cpp
     swcio.cpp
-    threading/affinity.cpp
+    hardware/affinity.cpp
+    hardware/gpu.cpp
+    hardware/memory.cpp
+    hardware/power.cpp
+    threading/threading.cpp
     util/debug.cpp
     util/hostname.cpp
-    util/memory.cpp
     util/path.cpp
-    util/power.cpp
     util/unwind.cpp
-    backends/multicore/fvm.cpp
 )
 set(CUDA_SOURCES
     backends/gpu/fvm.cu
diff --git a/src/threading/affinity.cpp b/src/hardware/affinity.cpp
similarity index 79%
rename from src/threading/affinity.cpp
rename to src/hardware/affinity.cpp
index 16c24197d6e18e8d4964d9a8f2c87cb3adf8d398..8d1895a0d7cf26d0c303f6491ee9cfb0d1137cec 100644
--- a/src/threading/affinity.cpp
+++ b/src/hardware/affinity.cpp
@@ -1,6 +1,7 @@
+#include <cstdlib>
 #include <vector>
 
-#include <cstdlib>
+#include <util/optional.hpp>
 
 #ifdef __linux__
 
@@ -16,7 +17,7 @@
 
 namespace nest {
 namespace mc {
-namespace threading {
+namespace hw {
 
 #ifdef __linux__
 std::vector<int> get_affinity() {
@@ -51,10 +52,14 @@ std::vector<int> get_affinity() {
 }
 #endif
 
-unsigned count_available_cores() {
-    return get_affinity().size();
+util::optional<std::size_t> num_cores() {
+    auto cores = get_affinity();
+    if (cores.size()==0u) {
+        return util::nothing;
+    }
+    return cores.size();
 }
 
-} // namespace threading
+} // namespace hw
 } // namespace mc
 } // namespace nest
diff --git a/src/threading/affinity.hpp b/src/hardware/affinity.hpp
similarity index 86%
rename from src/threading/affinity.hpp
rename to src/hardware/affinity.hpp
index e46e829eb09017c9412156b1f4c6fdcadbb5e053..598917cdb08dcd0e615982ede0b409d5dec31b98 100644
--- a/src/threading/affinity.hpp
+++ b/src/hardware/affinity.hpp
@@ -1,10 +1,13 @@
 #pragma once
 
+#include <cstdint>
 #include <vector>
 
+#include <util/optional.hpp>
+
 namespace nest {
 namespace mc {
-namespace threading {
+namespace hw {
 
 // The list of cores for which the calling thread has affinity.
 // If calling from the main thread at application start up, before
@@ -22,8 +25,8 @@ std::vector<int> get_affinity();
 // been playing with thread affinity.
 //
 // Returns 0 if unable to determine the number of cores.
-unsigned count_available_cores();
+util::optional<std::size_t> num_cores();
 
-} // namespace threading
+} // namespace util
 } // namespace mc
 } // namespace nest
diff --git a/src/hardware/gpu.cpp b/src/hardware/gpu.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8ee70b1d3e5b62c4ea8581fc0cace736f4e007b5
--- /dev/null
+++ b/src/hardware/gpu.cpp
@@ -0,0 +1,23 @@
+#ifdef NMC_HAVE_GPU
+    #include <cuda_runtime.h>
+#endif
+
+namespace nest {
+namespace mc {
+namespace hw {
+
+#ifdef NMC_HAVE_GPU
+unsigned num_gpus() {
+    int n;
+    cudaGetDeviceCount(&n);
+    return n;
+}
+#else
+unsigned num_gpus() {
+    return 0u;
+}
+#endif
+
+} // namespace hw
+} // namespace mc
+} // namespace nest
diff --git a/src/hardware/gpu.hpp b/src/hardware/gpu.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..953509f7bc96bb5257379596b4bb7ade67822fda
--- /dev/null
+++ b/src/hardware/gpu.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+namespace nest {
+namespace mc {
+namespace hw {
+
+unsigned num_gpus();
+
+} // namespace hw
+} // namespace mc
+} // namespace nest
diff --git a/src/util/memory.cpp b/src/hardware/memory.cpp
similarity index 94%
rename from src/util/memory.cpp
rename to src/hardware/memory.cpp
index f6172d6e9f6675bfd8291345341e67cdefd17501..60427ac3a1b7f818dab3dad096be1ab78e112f95 100644
--- a/src/util/memory.cpp
+++ b/src/hardware/memory.cpp
@@ -12,7 +12,7 @@ extern "C" {
 
 namespace nest {
 namespace mc {
-namespace util {
+namespace hw {
 
 #if defined(__linux__)
 memory_size_type allocated_memory() {
@@ -39,6 +39,6 @@ memory_size_type gpu_allocated_memory() {
 }
 #endif
 
-} // namespace util
+} // namespace hw
 } // namespace mc
 } // namespace nest
diff --git a/src/util/memory.hpp b/src/hardware/memory.hpp
similarity index 95%
rename from src/util/memory.hpp
rename to src/hardware/memory.hpp
index 7dab29b80a67f3a5604a04c3a0c936cb97b43d3b..3688746b6a54e4dcc7956b58bfa32ac20aa60078 100644
--- a/src/util/memory.hpp
+++ b/src/hardware/memory.hpp
@@ -4,7 +4,7 @@
 
 namespace nest {
 namespace mc {
-namespace util {
+namespace hw {
 
 // Use a signed type to store memory sizes because it can be used to store
 // the difference between two readings, which may be negative.
@@ -21,6 +21,6 @@ memory_size_type allocated_memory();
 // Returns a negative value on error, or if not using the gpu
 memory_size_type gpu_allocated_memory();
 
-} // namespace util
+} // namespace hw
 } // namespace mc
 } // namespace nest
diff --git a/src/util/power.cpp b/src/hardware/power.cpp
similarity index 91%
rename from src/util/power.cpp
rename to src/hardware/power.cpp
index 07037ea8051844cad00d92b8fa4cae954beb3060..3c5c9306a84b94efa62f4b3c398f97967c6639b6 100644
--- a/src/util/power.cpp
+++ b/src/hardware/power.cpp
@@ -4,7 +4,7 @@
 
 namespace nest {
 namespace mc {
-namespace util {
+namespace hw {
 
 #ifdef NMC_HAVE_CRAY
 
@@ -27,6 +27,6 @@ energy_size_type energy() {
 
 #endif
 
-} // namespace util
+} // namespace hw
 } // namespace mc
 } // namespace nest
diff --git a/src/util/power.hpp b/src/hardware/power.hpp
similarity index 68%
rename from src/util/power.hpp
rename to src/hardware/power.hpp
index 7c13624795a00d40d4c5743101e46ce957f8a270..de1bebf710a93f806b84e83e59a439113d43c18c 100644
--- a/src/util/power.hpp
+++ b/src/hardware/power.hpp
@@ -4,14 +4,14 @@
 
 namespace nest {
 namespace mc {
-namespace util {
+namespace hw {
 
 // Energy in Joules (J)
 using energy_size_type = std::uint64_t;
 
-// Returns negative value if unable to read energy
+// Returns energy_size_type(-1) if unable to read energy
 energy_size_type energy();
 
-} // namespace util
+} // namespace hw
 } // namespace mc
 } // namespace nest
diff --git a/src/profiling/memory_meter.cpp b/src/profiling/memory_meter.cpp
index 5d894167b3014461596188b66f5c09ea9a7bfe71..a502255b913c1f3422e3e696553e8dae594af834 100644
--- a/src/profiling/memory_meter.cpp
+++ b/src/profiling/memory_meter.cpp
@@ -2,8 +2,10 @@
 #include <vector>
 
 #include <util/config.hpp>
+#include <hardware/memory.hpp>
 
 #include "memory_meter.hpp"
+#include "meter.hpp"
 
 namespace nest {
 namespace mc {
@@ -15,7 +17,7 @@ namespace util {
 
 class memory_meter: public meter {
 protected:
-    std::vector<memory_size_type> readings_;
+    std::vector<hw::memory_size_type> readings_;
 
 public:
     std::string name() override {
@@ -27,7 +29,7 @@ public:
     }
 
     void take_reading() override {
-        readings_.push_back(allocated_memory());
+        readings_.push_back(hw::allocated_memory());
     }
 
     std::vector<double> measurements() override {
@@ -61,7 +63,7 @@ public:
     }
 
     void take_reading() override {
-        readings_.push_back(gpu_allocated_memory());
+        readings_.push_back(hw::gpu_allocated_memory());
     }
 };
 
diff --git a/src/profiling/memory_meter.hpp b/src/profiling/memory_meter.hpp
index d19e1b84becd1451ee95efaa96915aca9c7a17e7..cf9d71529baaf19bcc4a93670043567e797a4d1f 100644
--- a/src/profiling/memory_meter.hpp
+++ b/src/profiling/memory_meter.hpp
@@ -1,10 +1,5 @@
 #pragma once
 
-#include <string>
-#include <vector>
-
-#include <util/memory.hpp>
-
 #include "meter.hpp"
 
 namespace nest {
diff --git a/src/profiling/power_meter.cpp b/src/profiling/power_meter.cpp
index 2601f114e0ba1037ecd140d00fd8d2575af858f8..f4cbd2b9e9208b3f24d2cf858a1e95ba182b392a 100644
--- a/src/profiling/power_meter.cpp
+++ b/src/profiling/power_meter.cpp
@@ -1,16 +1,17 @@
 #include <string>
 #include <vector>
 
-#include <util/config.hpp>
+#include "meter.hpp"
 
-#include "power_meter.hpp"
+#include <util/config.hpp>
+#include <hardware/power.hpp>
 
 namespace nest {
 namespace mc {
 namespace util {
 
 class power_meter: public meter {
-    std::vector<energy_size_type> readings_;
+    std::vector<hw::energy_size_type> readings_;
 
 public:
     std::string name() override {
@@ -32,7 +33,7 @@ public:
     }
 
     void take_reading() override {
-        readings_.push_back(energy());
+        readings_.push_back(hw::energy());
     }
 };
 
diff --git a/src/profiling/power_meter.hpp b/src/profiling/power_meter.hpp
index 40682acb5fd4816b6cdf54abaa0213fd4b19c482..7c54e15c93aee945f1eb62a6e34fdcab6c72f01f 100644
--- a/src/profiling/power_meter.hpp
+++ b/src/profiling/power_meter.hpp
@@ -1,10 +1,5 @@
 #pragma once
 
-#include <string>
-#include <vector>
-
-#include <util/power.hpp>
-
 #include "meter.hpp"
 
 namespace nest {
diff --git a/src/threading/cthread.cpp b/src/threading/cthread.cpp
index 399dc635eb81a8f5b71e2d9e13c6de8b0d1c9787..0495ffd51cfb1bf5565ad255ea247db9e19f475a 100644
--- a/src/threading/cthread.cpp
+++ b/src/threading/cthread.cpp
@@ -5,9 +5,10 @@
 #include <regex>
 
 #include "cthread.hpp"
-#include "affinity.hpp"
+#include "threading.hpp"
 
 using namespace nest::mc::threading::impl;
+using namespace nest::mc;
 
 // RAII owner for a task in flight
 struct task_pool::run_task {
@@ -133,55 +134,8 @@ void task_pool::wait(task_group* g) {
     run_tasks_while(g);
 }
 
-[[noreturn]]
-static void terminate(std::string msg) {
-    std::cerr << "NMC_NUM_THREADS_ERROR: " << msg << std::endl;
-    std::terminate();
-}
-
-// should check string, throw exception on missing or badly formed
-static size_t global_get_num_threads() {
-    const char* str;
-
-    // select variable to use:
-    //   If NMC_NUM_THREADS_VAR is set, use $NMC_NUM_THREADS_VAR
-    //   else if NMC_NUM_THREAD set, use it
-    //   else if OMP_NUM_THREADS set, use it
-    if (auto nthreads_var_name = std::getenv("NMC_NUM_THREADS_VAR")) {
-        str = std::getenv(nthreads_var_name);
-    }
-    else if (! (str = std::getenv("NMC_NUM_THREADS"))) {
-        str = std::getenv("OMP_NUM_THREADS");
-    }
-
-    // If the selected var is unset set the number of threads to
-    // the hint given by the standard library
-    if (!str) {
-        unsigned nthreads = nest::mc::threading::count_available_cores();
-        if (nthreads==0u) {
-            terminate(
-                "The number of threads was not set by the user, and I am unable "
-                "to determine a sane default number of threads on this system. "
-                "Use the NMC_NUM_THREADS environment variable to explicitly "
-                "set the number of threads.");
-        }
-        return nthreads;
-    }
-
-    auto nthreads = std::strtoul(str, nullptr, 10);
-
-    // check that the environment variable string describes a non-negative integer
-    if (nthreads==0 || errno==ERANGE ||
-        !std::regex_match(str, std::regex("\\s*\\d*[1-9]\\d*\\s*")))
-    {
-        terminate("The requested number of threads \""+std::string(str)
-            +"\" is not a reasonable positive integer");
-    }
-
-    return nthreads;
-}
-
 task_pool& task_pool::get_global_task_pool() {
-    static task_pool global_task_pool{global_get_num_threads()};
+    auto num_threads = threading::num_threads();
+    static task_pool global_task_pool(num_threads);
     return global_task_pool;
 }
diff --git a/src/threading/threading.cpp b/src/threading/threading.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7f13a42a57f2e8b42769440e961f873f993114cb
--- /dev/null
+++ b/src/threading/threading.cpp
@@ -0,0 +1,85 @@
+#include <cstdlib>
+#include <exception>
+#include <regex>
+#include <string>
+
+#include <util/optional.hpp>
+#include <hardware/affinity.hpp>
+
+#include "threading.hpp"
+
+namespace nest {
+namespace mc {
+namespace threading {
+
+// Test environment variables for user-specified count of threads.
+//
+// NMC_NUM_THREADS is used if set, otherwise OMP_NUM_THREADS is used.
+//
+// If neither variable is set, returns no value.
+//
+// Valid values for the environment variable are:
+//  0 : NestMC is responsible for picking the number of threads.
+//  >0: The number of threads to use.
+//
+// Throws std::runtime_error:
+//  NMC_NUM_THREADS or OMP_NUM_THREADS is set with invalid value.
+util::optional<size_t> get_env_num_threads() {
+    const char* str;
+
+    // select variable to use:
+    //   If NMC_NUM_THREADS_VAR is set, use $NMC_NUM_THREADS_VAR
+    //   else if NMC_NUM_THREAD set, use it
+    //   else if OMP_NUM_THREADS set, use it
+    if (auto nthreads_var_name = std::getenv("NMC_NUM_THREADS_VAR")) {
+        str = std::getenv(nthreads_var_name);
+    }
+    else if (! (str = std::getenv("NMC_NUM_THREADS"))) {
+        str = std::getenv("OMP_NUM_THREADS");
+    }
+
+    // If the selected var is unset set the number of threads to
+    // the hint given by the standard library
+    if (!str) {
+        return util::nothing;
+    }
+
+    auto nthreads = std::strtoul(str, nullptr, 10);
+
+    // check that the environment variable string describes a non-negative integer
+    if (errno==ERANGE ||
+        !std::regex_match(str, std::regex("\\s*\\d*[0-9]\\d*\\s*")))
+    {
+        throw std::runtime_error("The requested number of threads \""
+            +std::string(str)+"\" is not a valid value\n");
+    }
+
+    return nthreads;
+}
+
+size_t num_threads_init() {
+    auto env_threads = get_env_num_threads();
+    if (!env_threads || *env_threads==0u) {
+        auto detect_threads = hw::num_cores();
+        return detect_threads? *detect_threads: 1;
+    }
+    return *env_threads;
+}
+
+// Returns the number of threads used by the threading back end.
+// Throws:
+//      std::runtime_error if an invalid environment variable was set for the
+//      number of threads.
+size_t num_threads() {
+    // TODO: this is a bit of a hack until we have user-configurable threading.
+#if defined(NMC_HAVE_SERIAL)
+    return 1;
+#else
+    static size_t num_threads_cached = num_threads_init();
+    return num_threads_cached;
+#endif
+}
+
+} // namespace threading
+} // namespace mc
+} // namespace nest
diff --git a/src/threading/threading.hpp b/src/threading/threading.hpp
index 6ed393fde98fbc9d379d6c58d1e6382df31e9bdc..493b5260dc2788ef71b7b3c27acbb3eec163578e 100644
--- a/src/threading/threading.hpp
+++ b/src/threading/threading.hpp
@@ -1,5 +1,32 @@
 #pragma once
 
+#include <util/optional.hpp>
+
+namespace nest {
+namespace mc {
+namespace threading {
+
+// Test environment variables for user-specified count of threads.
+// Potential environment variables are tested in this order:
+//   1. use the environment variable specified by NMC_NUM_THREADS_VAR
+//   2. use NMC_NUM_THREADS
+//   3. use OMP_NUM_THREADS
+//   4. If no variable is set, returns no value.
+//
+// Valid values for the environment variable are:
+//      0 : NestMC is responsible for picking the number of threads.
+//     >0 : The number of threads to use.
+//
+// Throws std::runtime_error:
+//      Environment variable is set with invalid value.
+util::optional<size_t> get_env_num_threads();
+
+size_t num_threads();
+
+} // namespace threading
+} // namespace mc
+} // namespace nest
+
 #if defined(NMC_HAVE_TBB)
     #include "tbb.hpp"
 #elif defined(NMC_HAVE_CTHREAD)
@@ -8,4 +35,3 @@
     #define NMC_HAVE_SERIAL
     #include "serial.hpp"
 #endif
-