From 22cd237fefeed37f4ecff28558cf42e76e2a8856 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 12 Mar 2025 21:52:33 -0700 Subject: [PATCH 01/37] building a new class for gpu rng --- Simulator/Core/GPUModel.cpp | 4 +- Simulator/Core/GPUModel.h | 11 +- Simulator/Utils/RNG/MTRand_d.cpp | 321 ++++++++++++++++++ Simulator/Utils/RNG/MTRand_d.h | 155 +++++++++ Simulator/Utils/RNG/MersenneTwister_d.h | 2 +- .../Data/MersenneTwister_16384.dat | Bin 0 -> 262144 bytes 6 files changed, 482 insertions(+), 11 deletions(-) create mode 100644 Simulator/Utils/RNG/MTRand_d.cpp create mode 100644 Simulator/Utils/RNG/MTRand_d.h create mode 100644 build/RuntimeFiles/Data/MersenneTwister_16384.dat diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index 5aaf15571..e4c9f35fd 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -24,8 +24,8 @@ cudaEvent_t start, stop; __constant__ int d_debug_mask[1]; GPUModel::GPUModel() : - Model::Model(), edgeIndexMapDevice_(nullptr), randNoise_d(nullptr), allVerticesDevice_(nullptr), - allEdgesDevice_(nullptr) + Model::Model(), edgeIndexMapDevice_(nullptr), allVerticesDevice_(nullptr), + allEdgesDevice_(nullptr), MTRandGenerator_() { } diff --git a/Simulator/Core/GPUModel.h b/Simulator/Core/GPUModel.h index c3987f960..089597004 100644 --- a/Simulator/Core/GPUModel.h +++ b/Simulator/Core/GPUModel.h @@ -23,11 +23,7 @@ #include "AllEdges.h" #include "AllVertices.h" - -#ifdef VALIDATION_MODE - #include - #include -#endif // VALIDATION_MODE +#include "MTRand_d.h" #ifdef __CUDACC__ #include "Book.h" @@ -101,9 +97,6 @@ class GPUModel : public Model { /// @param[out] allEdgesDevice Memory location of the pointer to the edges list on device memory. virtual void deleteDeviceStruct(void **allVerticesDevice, void **allEdgesDevice); - /// Pointer to device random noise array. - float *randNoise_d; - #if defined(USE_GPU) /// Pointer to edge index map in device memory. EdgeIndexMapDevice *edgeIndexMapDevice_; @@ -136,6 +129,8 @@ class GPUModel : public Model { // TODO void createEdge(AllEdges &edges, int vertexIndex, int edgeIndex, Coordinate source, Coordinate dest, BGFLOAT deltaT, edgeType type); + + MTRand_d MTRandGenerator_; }; #if defined(__CUDACC__) diff --git a/Simulator/Utils/RNG/MTRand_d.cpp b/Simulator/Utils/RNG/MTRand_d.cpp new file mode 100644 index 000000000..da14e1842 --- /dev/null +++ b/Simulator/Utils/RNG/MTRand_d.cpp @@ -0,0 +1,321 @@ +/** + * @file MersenneTwister_d.cu + * + * @ingroup Simulator/Utils/RNG + * + * @brief MersenneTwister logic from Nvidia + * + * Copyright 1993-2010 NVIDIA Corporation. All rights reserved. + * + * Please refer to the NVIDIA end user license agreement (EULA) associated + * with this source code for terms and conditions that govern your use of + * this software. Any use, reproduction, disclosure, or distribution of + * this software and related documentation outside the terms of the EULA + * is strictly prohibited. + * + * + * + * Edited by Warner Smidt Sep 4th 2011 + * ds_MT now stores the seed state after each call to the random number generator. + * Each consecutive call to the random number generator will not produce the same + * results now. + * Note: iState has replaced seed in mt_struct_stripped, therefore the .dat files + * last parameter which was for the seed is now used for the iState. + * Also added RandomNormGPU which combines RandomGPU and BoxMuller for normalized + * random numbers without extra global memory transfers. + * + * Edit Sep 14th 2011 + * MT_RNG_COUNT is the max total threads that will be used. initMTGP is now used + * to setup RandomNormGPU/RandomGPU to be called from normalMTGPU/uniformMTGPU. + * Allows the random number generation to be more dynamic without relying as much + * on #defines as well as being able to make the calculations for the needed data + * at initialization only once, and not everytime the random numbers are needed. + */ + + +#include "MTRand_d.h" +#include +#include + +using namespace std; + +// __device__ static mt_struct_stripped ds_MT[MT_RNG_COUNT]; +// static mt_struct_stripped h_MT[MT_RNG_COUNT]; +// __device__ unsigned int mt[MT_RNG_COUNT * MT_NN]; + + +//#define MT_DATAFILE "MersenneTwister/data/MersenneTwister.dat" +/* +//globals +__device__ static mt_struct_stripped * ds_MT; +static mt_struct_stripped * h_MT; +__device__ unsigned int * mt; +*/ + +MTRand_d::MTRand_d() +{ +} + +MTRand_d::~MTRand_d() +{ + delete[] MT_; + deleteDeviceStruct(); +} + +void MTRand_d::allocHostStruct() +{ + // Allocate host memory + MT_ = new mt_struct_stripped[mt_rng_count_]; +} +void MTRand_d::allocDeviceStruct() +{ + // Allocate device memory + HANDLE_ERROR(cudaMalloc((void **)&mtNoise1_d, mt_noiseSize_)); + HANDLE_ERROR(cudaMalloc((void **)&mtNoise2_d, mt_noiseSize_)); + HANDLE_ERROR(cudaMalloc(&MT_d, mt_rng_count_ * sizeof(mt_struct_stripped))); + HANDLE_ERROR(cudaMalloc(&mt_d, mt_rng_count_ * NN * sizeof(unsigned int))); +} +void MTRand_d::deleteDeviceStruct() +{ + HANDLE_ERROR(cudaFree(mtNoise1_d)); + HANDLE_ERROR(cudaFree(mtNoise2_d)); + HANDLE_ERROR(cudaFree(MT_d)); + HANDLE_ERROR(cudaFree(mt_d)); +} +//Load twister configurations +void MTRand_d::loadMTGPU(const char *fname) +{ + FILE *fd = fopen(fname, "rb"); + if (!fd) { + cerr << "initMTGPU(): failed to open " << fname << endl << "FAILED" << endl; + exit(0); + } + if (!fread(MT_, mt_rng_count_ * sizeof(mt_struct_stripped), 1, fd)) { + cerr << "initMTGPU(): failed to load " << fname << endl << "FAILED" << endl; + exit(0); + } + fclose(fd); +} + +//initialize the seed to mt[] +__global__ void seedMTGPUState(unsigned int *mt, unsigned int seed) +{ + const int tid = blockDim.x * blockIdx.x + threadIdx.x; + int iState; + mt[MT_NN * tid] = seed; + for (iState = MT_NN * tid + 1; iState < MT_NN * (1 + tid); iState++) + mt[iState] = (1812433253U * (mt[iState - 1] ^ (mt[iState - 1] >> 30)) + iState) & MT_WMASK; +} + +//Initialize/seed twister for current GPU context +void MTRand_d::seedMTGPU(unsigned int seed) +{ + int i; + for (i = 0; i < mt_rng_count; i++) { + MT_[i].iState = i * MT_NN; + } + + //seed does need to be used to initialize mt[] elements. + int threadsPerBlock = 256; + //get ceil of MT_RNG_COUNT/threadsPerBlock + int blocksPerGrid = (mt_rng_count + threadsPerBlock - 1) / threadsPerBlock; + seedMTGPUState<<>>(seed); + + if (cudaMemcpyToSymbol(ds_MT, MT, mt_rng_count * sizeof(mt_struct_stripped)) != cudaSuccess) { + cerr << "seedMTGP failed" << endl; + exit(0); + } +} + + +//////////////////////////////////////////////////////////////////////////////// +// Write MT_RNG_COUNT vertical lanes of nPerRng random numbers to *d_Random. +// For coalesced global writes MT_RNG_COUNT should be a multiple of warp size. +// Initial states for each generator are the same, since the states are +// initialized from the global seed. In order to improve distribution properties +// on small NPerRng supply dedicated (local) seed to each twister. +// The local seeds, in their turn, can be extracted from global seed +// by means of any simple random number generator, like LCG. +//////////////////////////////////////////////////////////////////////////////// +__global__ void RandomGPU(float *d_Random, int nPerRng, int mt_rng_count) +{ + const int tid = blockDim.x * blockIdx.x + threadIdx.x; + int iState, iState1, iStateM, iOut; + unsigned int mti, mti1, mtiM, x; + unsigned int matrix_a, mask_b, mask_c; + + //Load bit-vector Mersenne Twister parameters + matrix_a = ds_MT[tid].matrix_a; + mask_b = ds_MT[tid].mask_b; + mask_c = ds_MT[tid].mask_c; + + iState = ds_MT[tid].iState; + mti1 = mt[iState]; + for (iOut = 0; iOut < nPerRng; iOut++) { + iState1 = iState + 1; + iStateM = iState + MT_MM; + if (iState1 >= MT_NN * (1 + tid)) + iState1 -= MT_NN; + if (iStateM >= MT_NN * (1 + tid)) + iStateM -= MT_NN; + mti = mti1; + mti1 = mt[iState1]; + mtiM = mt[iStateM]; + + // MT recurrence + x = (mti & MT_UMASK) | (mti1 & MT_LMASK); + x = mtiM ^ (x >> 1) ^ ((x & 1) ? matrix_a : 0); + + mt[iState] = x; + iState = iState1; + + //Tempering transformation + x ^= (x >> MT_SHIFT0); + x ^= (x << MT_SHIFTB) & mask_b; + x ^= (x << MT_SHIFTC) & mask_c; + x ^= (x >> MT_SHIFT1); + + //Convert to (0, 1] float and write to global memory + d_Random[tid + iOut * mt_rng_count] = ((float)x + 1.0f) / 4294967296.0f; + } + ds_MT[tid].iState = iState; +} + +//////////////////////////////////////////////////////////////////////////////// +// Transform each of MT_RNG_COUNT lanes of nPerRng uniformly distributed +// random samples, produced by RandomGPU(), to normally distributed lanes +// using Cartesian form of Box-Muller transformation. +// nPerRng must be even. +//////////////////////////////////////////////////////////////////////////////// +__device__ inline void BoxMuller(float &u1, float &u2) +{ + float r = sqrtf(-2.0f * logf(u1)); + float phi = 2 * PI * u2; + u1 = r * __cosf(phi); + u2 = r * __sinf(phi); +} + +__global__ void BoxMullerGPU(float *d_Random, int nPerRng, int mt_rng_count) +{ + const int tid = blockDim.x * blockIdx.x + threadIdx.x; + + for (int iOut = 0; iOut < nPerRng; iOut += 2) + BoxMuller(d_Random[tid + (iOut + 0) * mt_rng_count], + d_Random[tid + (iOut + 1) * mt_rng_count]); +} + + +//skip the seperate BoxMullerGPU for increased speed (uses register memory). +//nPerRng must be a multiple of 2 +__global__ void RandomNormGPU(float *d_Random, int nPerRng, int mt_rng_count) +{ + const int tid = blockDim.x * blockIdx.x + threadIdx.x; + int iState, iState1, iStateM, iOut; + unsigned int mti, mti1, mtiM, x; + unsigned int matrix_a, mask_b, mask_c; + + float regVal1, regVal2; //need 2 values for boxmuller + bool boxFlag = false; //will perform boxmuller transform on true + + //Load bit-vector Mersenne Twister parameters + matrix_a = ds_MT[tid].matrix_a; + mask_b = ds_MT[tid].mask_b; + mask_c = ds_MT[tid].mask_c; + + iState = ds_MT[tid].iState; + mti1 = mt[iState]; + for (iOut = 0; iOut < nPerRng; iOut++) { + iState1 = iState + 1; + iStateM = iState + MT_MM; + if (iState1 >= MT_NN * (1 + tid)) + iState1 -= MT_NN; + if (iStateM >= MT_NN * (1 + tid)) + iStateM -= MT_NN; + mti = mti1; + mti1 = mt[iState1]; + mtiM = mt[iStateM]; + + // MT recurrence + x = (mti & MT_UMASK) | (mti1 & MT_LMASK); + x = mtiM ^ (x >> 1) ^ ((x & 1) ? matrix_a : 0); + + mt[iState] = x; + iState = iState1; + + //Tempering transformation + x ^= (x >> MT_SHIFT0); + x ^= (x << MT_SHIFTB) & mask_b; + x ^= (x << MT_SHIFTC) & mask_c; + x ^= (x >> MT_SHIFT1); + + if (boxFlag) { + regVal2 = ((float)x + 1.0f) / 4294967296.0f; + BoxMuller(regVal1, regVal2); + d_Random[tid + (iOut - 1) * mt_rng_count] = regVal1; + d_Random[tid + iOut * mt_rng_count] = regVal2; + boxFlag = false; + } else { + regVal1 = ((float)x + 1.0f) / 4294967296.0f; + boxFlag = true; + } + } + ds_MT[tid].iState = iState; +} + +void MTRand_d::uniformMTGPU(float *d_random) +{ + RandomGPU<<>>(d_random, mt_nPerRng, mt_rng_count); +} + +void MTRand_d::normalMTGPU(float *d_random) +{ + RandomNormGPU<<>>(d_random, mt_nPerRng, mt_rng_count); +} + +//initialize globals and setup state +//Note: mt_rng_count must equal blocks*threads. mt_rng_count*nPerRng should equal the total number of randon numbers to be generated +void MTRand_d::initMTGPU(unsigned int seed, unsigned int totalVertices) +{ + mt_seed_ = seed; + mt_totalVertices_ = totalVertices; + mt_noiseSize_ = totalVertices * 10; //each noise buffer can hold 10 times totalVertices + // mt_blocks = blocks; + // mt_threads = threads; + // mt_nPerRng = nPerRng; + // mt_rng_count = mt_rng_c; + //mt_threads_ = 256; + const int y = 256; + const int max_xy = 2500; + int best_x = -1, best_z = -1; + int min_value = INT_MAX; + for (int x = 1; x * y <= max_xy; ++x) { + int xy = x * y; + int min_z = (totalVertices + xy - 1) / xy; // Compute the smallest z such that xy * z >= B + + if (min_z % 2 != 0) { + min_z++; // Ensure z is even + } + + int product = xy * min_z; + + if (product >= totalVertices && product < min_value) { + min_value = product; + best_x = x; + best_z = min_z; + } + } + if (best_x != -1) { + mt_blocks_ = best_x; + mt_threads_ = 256; + mt_nPerRng_ = best_z; + } else { + mt_blocks_ = 25; + mt_nPerRng_ = 4; + int rng_mt_rng_count + = totalVertices / mt_nPerRng_; //# of threads to generate for numVertices rand #s + int mt_threads_ = rng_mt_rng_count / mt_blocks_; //# threads per block needed + } + + allocDeviceStruct() loadMTGPU(MT_DATAFILE); + seedMTGPU(seed); +} diff --git a/Simulator/Utils/RNG/MTRand_d.h b/Simulator/Utils/RNG/MTRand_d.h new file mode 100644 index 000000000..bab7790ca --- /dev/null +++ b/Simulator/Utils/RNG/MTRand_d.h @@ -0,0 +1,155 @@ +/** +* @file MTRand_d.h +* +* @ingroup Simulator/Utils/RNG +* +* @brief Mersenne Twister logic from Nvidia +* +* This file has been modified by the UW Bothell Graphitti group, +* mostly to reorganize it and make it look more like typical C++ +* code. This includes splitting it into a .h and .cpp (instead of +* having everything in a .h file), and replacing enums previously +* used to define constants with consts. Given that this was designed +* to produce 32-bit random numbers, and have 32-bit internal state, +* the type uint32_t has been used throughout for precision of +* definition (now that compilers often use 64-bit ints). +* +* Mersenne Twister random number generator -- a C++ class MTRand_d +* Based on code by Makoto Matsumoto, Takuji Nishimura, and Shawn Cokus +* Richard J. Wagner v1.0 15 May 2003 rjwagner@writeme.com +* +* The Mersenne Twister is an algorithm for generating random numbers. It +* was designed with consideration of the flaws in various other generators. +* The period, 2^19937-1, and the order of equidistribution, 623 dimensions, +* are far greater. The generator is also fast; it avoids multiplication and +* division, and it benefits from caches and pipelines. For more information +* see the inventors' web page at http://www.math.keio.ac.jp/~matumoto/emt.html +* +* Reference +* M. Matsumoto and T. Nishimura, "Mersenne Twister: A 623-Dimensionally +* Equidistributed Uniform Pseudo-Random Number Generator", ACM Transactions on +* Modeling and Computer Simulation, Vol. 8, No. 1, January 1998, pp 3-30. +* +* Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, +* Copyright (C) 2000 - 2003, Richard J. Wagner +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions +* are met: +* +* 1. Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright +* notice, this list of conditions and the following disclaimer in the +* documentation and/or other materials provided with the distribution. +* +* 3. The names of its contributors may not be used to endorse or promote +* products derived from this software without specific prior written +* permission. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +* +* The original code included the following notice: +* +* When you use this, send an email to: matumoto@math.keio.ac.jp +* with an appropriate reference to your work. +* +* It would be nice to CC: rjwagner@writeme.com and Cokus@math.washington.edu +* when you write. +* +* Not thread safe (unless auto-initialization is avoided and each thread has +* its own MTRand_d object) +*/ + +#pragma once + +#include "BGTypes.h" // for BGFLOAT +#include +#include +#include +#include +#include +#include +// cereal +#include +#include +#include +#include +#include + +class MTRand_d { +public: + static constexpr const char *MT_DATAFILE = "RuntimeFiles/Data/MersenneTwister_16384.dat"; + + static constexpr std::size_t RNG_COUNT = 16384; // max threads + static constexpr std::size_t MM = 9; + static constexpr std::size_t NN = 19; + + static constexpr std::uint32_t WMASK = 0xFFFFFFFFU; + static constexpr std::uint32_t UMASK = 0xFFFFFFFEU; + static constexpr std::uint32_t LMASK = 0x1U; + + static constexpr int SHIFT0 = 12; + static constexpr int SHIFTB = 7; + static constexpr int SHIFTC = 15; + static constexpr int SHIFT1 = 18; + + // Constants related to DCMT and period + static constexpr std::uint32_t DCMT_SEED = 4172; + static constexpr std::uint32_t MT_RNG_PERIOD = 607; + + static constexpr float PI = 3.14159265358979f; + + struct mt_struct_stripped { + unsigned int matrix_a; + unsigned int mask_b; + unsigned int mask_c; + unsigned int iState; // Replaces seed + }; + +private: + mt_struct_stripped *MT_; // Host state + mt_struct_stripped *MT_d; // Device state + unsigned int *mt_d; // Device state values + + unsigned int mt_rng_count_; + unsigned int mt_blocks_; + unsigned int mt_threads_; + unsigned int mt_nPerRng_; + unsigned int mt_totalVertices_; + unsigned int + mt_noiseSize_; //integer multiple of totalVertices for the size of the noise buffers + unsigned int mt_seed_; + float *mtNoise1_d; + float *mtNoise2_d; + + void loadState(); // Function to load state from file + void loadMTGPU(const char *fname); + void seedMTGPU(unsigned int seed); + void uniformMTGPU(float *d_random); + void normalMTGPU(float *d_random); + void initMTGPU(unsigned int seed, unsigned int blocks, unsigned int threads, + unsigned int nPerRng, unsigned int mt_rng_c); + + //Methods +public: + MTRand_d(); + + ~MTRand_d(); + + void allocDeviceStruct(); + void deleteDeviceStruct(); + void allocHostStruct(); +}; diff --git a/Simulator/Utils/RNG/MersenneTwister_d.h b/Simulator/Utils/RNG/MersenneTwister_d.h index 04f1cd42b..7f7c888e8 100644 --- a/Simulator/Utils/RNG/MersenneTwister_d.h +++ b/Simulator/Utils/RNG/MersenneTwister_d.h @@ -50,4 +50,4 @@ struct mt_struct_stripped { #define MT_SHIFTC 15 #define MT_SHIFT1 18 -//#endif +//#endif \ No newline at end of file diff --git a/build/RuntimeFiles/Data/MersenneTwister_16384.dat b/build/RuntimeFiles/Data/MersenneTwister_16384.dat new file mode 100644 index 0000000000000000000000000000000000000000..ac041c512767b88a0d0566ef55ba75394ebfabe7 GIT binary patch literal 262144 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%0Dl{ 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 literal 0 HcmV?d00001 From 0e7008355414e5df851ee7eebffc5ec1f7f88fd5 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 13 Mar 2025 22:39:40 -0700 Subject: [PATCH 02/37] refactor --- Simulator/Utils/RNG/MTRand_d.cpp | 34 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Simulator/Utils/RNG/MTRand_d.cpp b/Simulator/Utils/RNG/MTRand_d.cpp index da14e1842..4b71ce90f 100644 --- a/Simulator/Utils/RNG/MTRand_d.cpp +++ b/Simulator/Utils/RNG/MTRand_d.cpp @@ -119,12 +119,10 @@ void MTRand_d::seedMTGPU(unsigned int seed) int threadsPerBlock = 256; //get ceil of MT_RNG_COUNT/threadsPerBlock int blocksPerGrid = (mt_rng_count + threadsPerBlock - 1) / threadsPerBlock; - seedMTGPUState<<>>(seed); + seedMTGPUState<<>>(mt_d, seed); - if (cudaMemcpyToSymbol(ds_MT, MT, mt_rng_count * sizeof(mt_struct_stripped)) != cudaSuccess) { - cerr << "seedMTGP failed" << endl; - exit(0); - } + HANDLE_ERROR( + cudaMemcpy(MT_d, MT_, mt_rng_count_ * sizeof(mt_struct_stripped), cudaMemcyptHostToDevice)); } @@ -137,7 +135,8 @@ void MTRand_d::seedMTGPU(unsigned int seed) // The local seeds, in their turn, can be extracted from global seed // by means of any simple random number generator, like LCG. //////////////////////////////////////////////////////////////////////////////// -__global__ void RandomGPU(float *d_Random, int nPerRng, int mt_rng_count) +__global__ void RandomGPU(mt_struct_stripped *ds_MT, unsigned int *mt, float *d_Random, int nPerRng, + int mt_rng_count) { const int tid = blockDim.x * blockIdx.x + threadIdx.x; int iState, iState1, iStateM, iOut; @@ -163,7 +162,7 @@ __global__ void RandomGPU(float *d_Random, int nPerRng, int mt_rng_count) mtiM = mt[iStateM]; // MT recurrence - x = (mti & MT_UMASK) | (mti1 & MT_LMASK); + x = (mti & MTRand_d::UMASK) | (mti1 & MTRand_d::LMASK); x = mtiM ^ (x >> 1) ^ ((x & 1) ? matrix_a : 0); mt[iState] = x; @@ -190,7 +189,7 @@ __global__ void RandomGPU(float *d_Random, int nPerRng, int mt_rng_count) __device__ inline void BoxMuller(float &u1, float &u2) { float r = sqrtf(-2.0f * logf(u1)); - float phi = 2 * PI * u2; + float phi = 2 * MTRand_d::PI * u2; u1 = r * __cosf(phi); u2 = r * __sinf(phi); } @@ -207,7 +206,8 @@ __global__ void BoxMullerGPU(float *d_Random, int nPerRng, int mt_rng_count) //skip the seperate BoxMullerGPU for increased speed (uses register memory). //nPerRng must be a multiple of 2 -__global__ void RandomNormGPU(float *d_Random, int nPerRng, int mt_rng_count) +__global__ void RandomNormGPU(mt_struct_stripped *ds_MT, unsigned int *mt, float *d_Random, + int nPerRng, int mt_rng_count) { const int tid = blockDim.x * blockIdx.x + threadIdx.x; int iState, iState1, iStateM, iOut; @@ -236,17 +236,17 @@ __global__ void RandomNormGPU(float *d_Random, int nPerRng, int mt_rng_count) mtiM = mt[iStateM]; // MT recurrence - x = (mti & MT_UMASK) | (mti1 & MT_LMASK); + x = (mti & MTRand_d::UMASK) | (mti1 & MTRand_d::LMASK); x = mtiM ^ (x >> 1) ^ ((x & 1) ? matrix_a : 0); mt[iState] = x; iState = iState1; //Tempering transformation - x ^= (x >> MT_SHIFT0); - x ^= (x << MT_SHIFTB) & mask_b; - x ^= (x << MT_SHIFTC) & mask_c; - x ^= (x >> MT_SHIFT1); + x ^= (x >> MTRand_d::SHIFT0); + x ^= (x << MTRand_d::SHIFTB) & mask_b; + x ^= (x << MTRand_d::SHIFTC) & mask_c; + x ^= (x >> MTRand_d::SHIFT1); if (boxFlag) { regVal2 = ((float)x + 1.0f) / 4294967296.0f; @@ -264,12 +264,12 @@ __global__ void RandomNormGPU(float *d_Random, int nPerRng, int mt_rng_count) void MTRand_d::uniformMTGPU(float *d_random) { - RandomGPU<<>>(d_random, mt_nPerRng, mt_rng_count); + RandomGPU<<>>(MT_d, mt_d, d_random, mt_nPerRng_, mt_rng_count_); } void MTRand_d::normalMTGPU(float *d_random) { - RandomNormGPU<<>>(d_random, mt_nPerRng, mt_rng_count); + RandomNormGPU<<>>(MT_d, mt_d, d_random, mt_nPerRng, mt_rng_count); } //initialize globals and setup state @@ -285,7 +285,7 @@ void MTRand_d::initMTGPU(unsigned int seed, unsigned int totalVertices) // mt_rng_count = mt_rng_c; //mt_threads_ = 256; const int y = 256; - const int max_xy = 2500; + const int max_xy = 4096; int best_x = -1, best_z = -1; int min_value = INT_MAX; for (int x = 1; x * y <= max_xy; ++x) { From 69b86c9f15b7cfd8ba250d858eae36ff502e31b4 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 30 Apr 2025 01:55:39 -0700 Subject: [PATCH 03/37] curand rng class --- Simulator/Utils/RNG/AsyncMT_d.cu | 84 ++++++++++++++++++++++++++++++++ Simulator/Utils/RNG/AsyncMT_d.h | 34 +++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 Simulator/Utils/RNG/AsyncMT_d.cu create mode 100644 Simulator/Utils/RNG/AsyncMT_d.h diff --git a/Simulator/Utils/RNG/AsyncMT_d.cu b/Simulator/Utils/RNG/AsyncMT_d.cu new file mode 100644 index 000000000..f7cdb240a --- /dev/null +++ b/Simulator/Utils/RNG/AsyncMT_d.cu @@ -0,0 +1,84 @@ +#include "AsyncMT_d.h" +#include +#include + +__global__ void generateKernel(curandStateMtgp32 *state, float *output, int samplesPerGen) +{ + int tid = threadIdx.x; + int gen_id = blockIdx.x; + if (gen_id >= gridDim.x) + return; + + curandStateMtgp32 localState = state[gen_id]; + for (int i = tid; i < samplesPerGen; i += blockDim.x) { + output[gen_id * samplesPerGen + i] = curand_normal(&localState); + } + state[gen_id] = localState; +} + +AsyncMT_d::AsyncMT_d(int samplesPerSegment, unsigned long seed) : + segmentSize(samplesPerSegment), seed(seed), currentBuffer(0), segmentIndex(0) +{ + totalSegments = 10; // Each buffer has 10 segments + bufferSize = segmentSize * totalSegments; + totalSamples = bufferSize * 2; + numGenerators = 50; //placeholder num of blocks + + // Create internal stream + cudaStreamCreate(&stream); + + // Allocate two large buffers + cudaMalloc(&buffers[0], bufferSize * sizeof(float)); + cudaMalloc(&buffers[1], bufferSize * sizeof(float)); + + // Allocate state and param memory + cudaMalloc(&d_states, numGenerators * sizeof(curandStateMtgp32)); + cudaMalloc(&d_params, numGenerators * sizeof(mtgp32_kernel_params_t)); + + + // Create local param buffer of correct type + mtgp32_kernel_params_t *h_params = new mtgp32_kernel_params_t[numGenerators]; + curandMakeMTGP32Constants(mtgp32dc_params_fast_11213, h_params); + cudaMemcpy(d_params, h_params, numGenerators * sizeof(mtgp32_kernel_params_t), + cudaMemcpyHostToDevice); + delete[] h_params; + + curandMakeMTGP32KernelState(d_states, mtgp32dc_params_fast_11213, d_params, numGenerators, seed); + + // Pre-fill both buffers + fillBuffer(0); + fillBuffer(1); +} + +AsyncMT_d::~AsyncMT_d() +{ + cudaFree(buffers[0]); + cudaFree(buffers[1]); + cudaFree(d_states); + cudaFree(d_params); + cudaStreamDestroy(stream); +} + +float *AsyncMT_d::requestSegment() +{ + if (segmentIndex >= totalSegments) { + // Switch buffer and launch async refill on the now-unused one + int refillBuffer = currentBuffer; + currentBuffer = 1 - currentBuffer; + segmentIndex = 0; + cudaStreamSynchronize(stream); // Ensure refillBuffer is done + fillBuffer(refillBuffer); + } + + float *segmentPtr = buffers[currentBuffer] + segmentIndex * segmentSize; + segmentIndex++; + return segmentPtr; +} + +void AsyncMT_d::fillBuffer(int bufferIndex) +{ + dim3 blocks(numGenerators); + dim3 threads(256); + generateKernel<<>>(d_states, buffers[bufferIndex], + bufferSize / numGenerators); +} diff --git a/Simulator/Utils/RNG/AsyncMT_d.h b/Simulator/Utils/RNG/AsyncMT_d.h new file mode 100644 index 000000000..cfd45948b --- /dev/null +++ b/Simulator/Utils/RNG/AsyncMT_d.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include +#include // Precomputed parameter table + +class AsyncMT_d { +public: + AsyncMT_d(int samplesPerGen, unsigned long seed); + ~AsyncMT_d(); + + float *requestSegment(); + +private: + int numGenerators; + int segmentSize; + int totalSegments; + int bufferSize; + int totalSamples; + unsigned long seed; + + cudaStream_t stream; + + float *buffers[2]; + int currentBuffer; + int segmentIndex; + + curandStateMtgp32 *d_states; + mtgp32_kernel_params_t *d_params; + + void fillBuffer(int bufferIndex); +}; From 8f537d028fc8454737c8daa14cd8027ec38ad103 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 30 Apr 2025 02:28:35 -0700 Subject: [PATCH 04/37] revert change --- Simulator/Core/GPUModel.cpp | 6 +++--- Simulator/Core/GPUModel.h | 13 +++++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index e4c9f35fd..10e196aca 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -24,8 +24,8 @@ cudaEvent_t start, stop; __constant__ int d_debug_mask[1]; GPUModel::GPUModel() : - Model::Model(), edgeIndexMapDevice_(nullptr), allVerticesDevice_(nullptr), - allEdgesDevice_(nullptr), MTRandGenerator_() + Model::Model(), edgeIndexMapDevice_(nullptr), randNoise_d(nullptr), allVerticesDevice_(nullptr), + allEdgesDevice_(nullptr) { } @@ -360,4 +360,4 @@ void GPUModel::copyCPUtoGPU() void GPUModel::printGPUEdgesPropsModel() const { connections_->getEdges().printGPUEdgesProps(allEdgesDevice_); -} +} \ No newline at end of file diff --git a/Simulator/Core/GPUModel.h b/Simulator/Core/GPUModel.h index 089597004..652c1becf 100644 --- a/Simulator/Core/GPUModel.h +++ b/Simulator/Core/GPUModel.h @@ -23,7 +23,11 @@ #include "AllEdges.h" #include "AllVertices.h" -#include "MTRand_d.h" + +#ifdef VALIDATION_MODE + #include + #include +#endif // VALIDATION_MODE #ifdef __CUDACC__ #include "Book.h" @@ -97,6 +101,9 @@ class GPUModel : public Model { /// @param[out] allEdgesDevice Memory location of the pointer to the edges list on device memory. virtual void deleteDeviceStruct(void **allVerticesDevice, void **allEdgesDevice); + /// Pointer to device random noise array. + float *randNoise_d; + #if defined(USE_GPU) /// Pointer to edge index map in device memory. EdgeIndexMapDevice *edgeIndexMapDevice_; @@ -129,8 +136,6 @@ class GPUModel : public Model { // TODO void createEdge(AllEdges &edges, int vertexIndex, int edgeIndex, Coordinate source, Coordinate dest, BGFLOAT deltaT, edgeType type); - - MTRand_d MTRandGenerator_; }; #if defined(__CUDACC__) @@ -139,4 +144,4 @@ void normalMTGPU(float *randNoise_d); void initMTGPU(unsigned int seed, unsigned int blocks, unsigned int threads, unsigned int nPerRng, unsigned int mt_rng_count); } -#endif +#endif \ No newline at end of file From 401461054d253caf1af0ec09af4c39558d8975d6 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 12 May 2025 10:12:12 -0700 Subject: [PATCH 05/37] fix performance metrics --- Simulator/Core/GPUModel.cpp | 27 +-- Simulator/Core/GPUModel.h | 3 +- Simulator/Core/Simulator.cpp | 2 +- Simulator/Utils/RNG/AsyncMT_d.cu | 10 +- Simulator/Utils/RNG/AsyncMT_d.h | 1 + Simulator/Utils/RNG/MTRand_d.cpp | 321 ------------------------------- Simulator/Utils/RNG/MTRand_d.h | 155 --------------- 7 files changed, 28 insertions(+), 491 deletions(-) delete mode 100644 Simulator/Utils/RNG/MTRand_d.cpp delete mode 100644 Simulator/Utils/RNG/MTRand_d.h diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index 10e196aca..a03f4a4fc 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -44,8 +44,8 @@ void GPUModel::allocDeviceStruct(void **allVerticesDevice, void **allEdgesDevice // Allocate memory for random noise array int numVertices = Simulator::getInstance().getTotalVertices(); - BGSIZE randNoise_d_size = numVertices * sizeof(float); // size of random noise array - HANDLE_ERROR(cudaMalloc((void **)&randNoise_d, randNoise_d_size)); + // BGSIZE randNoise_d_size = numVertices * sizeof(float); // size of random noise array + // HANDLE_ERROR(cudaMalloc((void **)&randNoise_d, randNoise_d_size)); // Copy host vertex and edge arrays into GPU device vertices.copyToDevice(*allVerticesDevice); @@ -72,7 +72,7 @@ void GPUModel::deleteDeviceStruct(void **allVerticesDevice, void **allEdgesDevic edges.copyEdgeDeviceToHost(*allEdgesDevice); // Deallocate device memory edges.deleteEdgeDeviceStruct(*allEdgesDevice); - HANDLE_ERROR(cudaFree(randNoise_d)); + //HANDLE_ERROR(cudaFree(randNoise_d)); } /// Sets up the Simulation. @@ -86,14 +86,16 @@ void GPUModel::setupSim() //initialize Mersenne Twister //assuming numVertices >= 100 and is a multiple of 100. Note rng_mt_rng_count must be <= MT_RNG_COUNT - int rng_blocks = 25; //# of blocks the kernel will use - int rng_nPerRng - = 4; //# of iterations per thread (thread granularity, # of rands generated per thread) - int rng_mt_rng_count = Simulator::getInstance().getTotalVertices() - / rng_nPerRng; //# of threads to generate for numVertices rand #s - int rng_threads = rng_mt_rng_count / rng_blocks; //# threads per block needed - initMTGPU(Simulator::getInstance().getNoiseRngSeed(), rng_blocks, rng_threads, rng_nPerRng, - rng_mt_rng_count); + // int rng_blocks = 25; //# of blocks the kernel will use + // int rng_nPerRng + // = 4; //# of iterations per thread (thread granularity, # of rands generated per thread) + // int rng_mt_rng_count = Simulator::getInstance().getTotalVertices() + // / rng_nPerRng; //# of threads to generate for numVertices rand #s + // int rng_threads = rng_mt_rng_count / rng_blocks; //# threads per block needed + // initMTGPU(Simulator::getInstance().getNoiseRngSeed(), rng_blocks, rng_threads, rng_nPerRng, + // rng_mt_rng_count); + AsyncGenerator = AsyncMT_d(Simulator::getInstance().getTotalVertices(), + Simulator::getInstance().getNoiseRngSeed()); #ifdef PERFORMANCE_METRICS cudaEventCreate(&start); @@ -159,7 +161,8 @@ void GPUModel::advance() // } cudaMemcpy(randNoise_d, randNoise_h.data(), verts * sizeof(float), cudaMemcpyHostToDevice); #else - normalMTGPU(randNoise_d); + //normalMTGPU(randNoise_d); + randNoise_d = AsyncGenerator.requestSegment(); #endif //LOG4CPLUS_DEBUG(vertexLogger_, "Index: " << index << " Vm: " << Vm); #ifdef PERFORMANCE_METRICS diff --git a/Simulator/Core/GPUModel.h b/Simulator/Core/GPUModel.h index 652c1becf..448202018 100644 --- a/Simulator/Core/GPUModel.h +++ b/Simulator/Core/GPUModel.h @@ -23,6 +23,7 @@ #include "AllEdges.h" #include "AllVertices.h" +#include "AsyncMT_d.h" #ifdef VALIDATION_MODE #include @@ -103,7 +104,7 @@ class GPUModel : public Model { /// Pointer to device random noise array. float *randNoise_d; - + AsyncMT_d AsyncGenerator; #if defined(USE_GPU) /// Pointer to edge index map in device memory. EdgeIndexMapDevice *edgeIndexMapDevice_; diff --git a/Simulator/Core/Simulator.cpp b/Simulator/Core/Simulator.cpp index e4a42d9df..0fd941c2d 100644 --- a/Simulator/Core/Simulator.cpp +++ b/Simulator/Core/Simulator.cpp @@ -173,7 +173,7 @@ void Simulator::simulate() double total_time = timer.lap() / 1000000.0; cout << "\ntotal_time: " << total_time << " seconds" << endl; - printPerformanceMetrics(total_time, currentEpoch); + printPerformanceMetrics(total_time, currentEpoch_); cout << endl; #endif } diff --git a/Simulator/Utils/RNG/AsyncMT_d.cu b/Simulator/Utils/RNG/AsyncMT_d.cu index f7cdb240a..e4ede2de1 100644 --- a/Simulator/Utils/RNG/AsyncMT_d.cu +++ b/Simulator/Utils/RNG/AsyncMT_d.cu @@ -1,7 +1,8 @@ #include "AsyncMT_d.h" #include #include - +#include +#include __global__ void generateKernel(curandStateMtgp32 *state, float *output, int samplesPerGen) { int tid = threadIdx.x; @@ -48,6 +49,7 @@ AsyncMT_d::AsyncMT_d(int samplesPerSegment, unsigned long seed) : // Pre-fill both buffers fillBuffer(0); fillBuffer(1); + cudaStreamSynchronize(stream); //wait for both buffers to be filled before the first request } AsyncMT_d::~AsyncMT_d() @@ -61,6 +63,7 @@ AsyncMT_d::~AsyncMT_d() float *AsyncMT_d::requestSegment() { + //auto start = std::chrono::high_resolution_clock::now(); if (segmentIndex >= totalSegments) { // Switch buffer and launch async refill on the now-unused one int refillBuffer = currentBuffer; @@ -68,10 +71,15 @@ float *AsyncMT_d::requestSegment() segmentIndex = 0; cudaStreamSynchronize(stream); // Ensure refillBuffer is done fillBuffer(refillBuffer); + //cudaStreamSynchronize(stream); } float *segmentPtr = buffers[currentBuffer] + segmentIndex * segmentSize; segmentIndex++; + + // auto end = std::chrono::high_resolution_clock::now(); + // std::cout << "Segment: " << segmentIndex << ", Launch time: " << (end - start).count() << " ns\n"; + return segmentPtr; } diff --git a/Simulator/Utils/RNG/AsyncMT_d.h b/Simulator/Utils/RNG/AsyncMT_d.h index cfd45948b..1e9b31fa2 100644 --- a/Simulator/Utils/RNG/AsyncMT_d.h +++ b/Simulator/Utils/RNG/AsyncMT_d.h @@ -8,6 +8,7 @@ class AsyncMT_d { public: + AsyncMT_d() = default; AsyncMT_d(int samplesPerGen, unsigned long seed); ~AsyncMT_d(); diff --git a/Simulator/Utils/RNG/MTRand_d.cpp b/Simulator/Utils/RNG/MTRand_d.cpp deleted file mode 100644 index 4b71ce90f..000000000 --- a/Simulator/Utils/RNG/MTRand_d.cpp +++ /dev/null @@ -1,321 +0,0 @@ -/** - * @file MersenneTwister_d.cu - * - * @ingroup Simulator/Utils/RNG - * - * @brief MersenneTwister logic from Nvidia - * - * Copyright 1993-2010 NVIDIA Corporation. All rights reserved. - * - * Please refer to the NVIDIA end user license agreement (EULA) associated - * with this source code for terms and conditions that govern your use of - * this software. Any use, reproduction, disclosure, or distribution of - * this software and related documentation outside the terms of the EULA - * is strictly prohibited. - * - * - * - * Edited by Warner Smidt Sep 4th 2011 - * ds_MT now stores the seed state after each call to the random number generator. - * Each consecutive call to the random number generator will not produce the same - * results now. - * Note: iState has replaced seed in mt_struct_stripped, therefore the .dat files - * last parameter which was for the seed is now used for the iState. - * Also added RandomNormGPU which combines RandomGPU and BoxMuller for normalized - * random numbers without extra global memory transfers. - * - * Edit Sep 14th 2011 - * MT_RNG_COUNT is the max total threads that will be used. initMTGP is now used - * to setup RandomNormGPU/RandomGPU to be called from normalMTGPU/uniformMTGPU. - * Allows the random number generation to be more dynamic without relying as much - * on #defines as well as being able to make the calculations for the needed data - * at initialization only once, and not everytime the random numbers are needed. - */ - - -#include "MTRand_d.h" -#include -#include - -using namespace std; - -// __device__ static mt_struct_stripped ds_MT[MT_RNG_COUNT]; -// static mt_struct_stripped h_MT[MT_RNG_COUNT]; -// __device__ unsigned int mt[MT_RNG_COUNT * MT_NN]; - - -//#define MT_DATAFILE "MersenneTwister/data/MersenneTwister.dat" -/* -//globals -__device__ static mt_struct_stripped * ds_MT; -static mt_struct_stripped * h_MT; -__device__ unsigned int * mt; -*/ - -MTRand_d::MTRand_d() -{ -} - -MTRand_d::~MTRand_d() -{ - delete[] MT_; - deleteDeviceStruct(); -} - -void MTRand_d::allocHostStruct() -{ - // Allocate host memory - MT_ = new mt_struct_stripped[mt_rng_count_]; -} -void MTRand_d::allocDeviceStruct() -{ - // Allocate device memory - HANDLE_ERROR(cudaMalloc((void **)&mtNoise1_d, mt_noiseSize_)); - HANDLE_ERROR(cudaMalloc((void **)&mtNoise2_d, mt_noiseSize_)); - HANDLE_ERROR(cudaMalloc(&MT_d, mt_rng_count_ * sizeof(mt_struct_stripped))); - HANDLE_ERROR(cudaMalloc(&mt_d, mt_rng_count_ * NN * sizeof(unsigned int))); -} -void MTRand_d::deleteDeviceStruct() -{ - HANDLE_ERROR(cudaFree(mtNoise1_d)); - HANDLE_ERROR(cudaFree(mtNoise2_d)); - HANDLE_ERROR(cudaFree(MT_d)); - HANDLE_ERROR(cudaFree(mt_d)); -} -//Load twister configurations -void MTRand_d::loadMTGPU(const char *fname) -{ - FILE *fd = fopen(fname, "rb"); - if (!fd) { - cerr << "initMTGPU(): failed to open " << fname << endl << "FAILED" << endl; - exit(0); - } - if (!fread(MT_, mt_rng_count_ * sizeof(mt_struct_stripped), 1, fd)) { - cerr << "initMTGPU(): failed to load " << fname << endl << "FAILED" << endl; - exit(0); - } - fclose(fd); -} - -//initialize the seed to mt[] -__global__ void seedMTGPUState(unsigned int *mt, unsigned int seed) -{ - const int tid = blockDim.x * blockIdx.x + threadIdx.x; - int iState; - mt[MT_NN * tid] = seed; - for (iState = MT_NN * tid + 1; iState < MT_NN * (1 + tid); iState++) - mt[iState] = (1812433253U * (mt[iState - 1] ^ (mt[iState - 1] >> 30)) + iState) & MT_WMASK; -} - -//Initialize/seed twister for current GPU context -void MTRand_d::seedMTGPU(unsigned int seed) -{ - int i; - for (i = 0; i < mt_rng_count; i++) { - MT_[i].iState = i * MT_NN; - } - - //seed does need to be used to initialize mt[] elements. - int threadsPerBlock = 256; - //get ceil of MT_RNG_COUNT/threadsPerBlock - int blocksPerGrid = (mt_rng_count + threadsPerBlock - 1) / threadsPerBlock; - seedMTGPUState<<>>(mt_d, seed); - - HANDLE_ERROR( - cudaMemcpy(MT_d, MT_, mt_rng_count_ * sizeof(mt_struct_stripped), cudaMemcyptHostToDevice)); -} - - -//////////////////////////////////////////////////////////////////////////////// -// Write MT_RNG_COUNT vertical lanes of nPerRng random numbers to *d_Random. -// For coalesced global writes MT_RNG_COUNT should be a multiple of warp size. -// Initial states for each generator are the same, since the states are -// initialized from the global seed. In order to improve distribution properties -// on small NPerRng supply dedicated (local) seed to each twister. -// The local seeds, in their turn, can be extracted from global seed -// by means of any simple random number generator, like LCG. -//////////////////////////////////////////////////////////////////////////////// -__global__ void RandomGPU(mt_struct_stripped *ds_MT, unsigned int *mt, float *d_Random, int nPerRng, - int mt_rng_count) -{ - const int tid = blockDim.x * blockIdx.x + threadIdx.x; - int iState, iState1, iStateM, iOut; - unsigned int mti, mti1, mtiM, x; - unsigned int matrix_a, mask_b, mask_c; - - //Load bit-vector Mersenne Twister parameters - matrix_a = ds_MT[tid].matrix_a; - mask_b = ds_MT[tid].mask_b; - mask_c = ds_MT[tid].mask_c; - - iState = ds_MT[tid].iState; - mti1 = mt[iState]; - for (iOut = 0; iOut < nPerRng; iOut++) { - iState1 = iState + 1; - iStateM = iState + MT_MM; - if (iState1 >= MT_NN * (1 + tid)) - iState1 -= MT_NN; - if (iStateM >= MT_NN * (1 + tid)) - iStateM -= MT_NN; - mti = mti1; - mti1 = mt[iState1]; - mtiM = mt[iStateM]; - - // MT recurrence - x = (mti & MTRand_d::UMASK) | (mti1 & MTRand_d::LMASK); - x = mtiM ^ (x >> 1) ^ ((x & 1) ? matrix_a : 0); - - mt[iState] = x; - iState = iState1; - - //Tempering transformation - x ^= (x >> MT_SHIFT0); - x ^= (x << MT_SHIFTB) & mask_b; - x ^= (x << MT_SHIFTC) & mask_c; - x ^= (x >> MT_SHIFT1); - - //Convert to (0, 1] float and write to global memory - d_Random[tid + iOut * mt_rng_count] = ((float)x + 1.0f) / 4294967296.0f; - } - ds_MT[tid].iState = iState; -} - -//////////////////////////////////////////////////////////////////////////////// -// Transform each of MT_RNG_COUNT lanes of nPerRng uniformly distributed -// random samples, produced by RandomGPU(), to normally distributed lanes -// using Cartesian form of Box-Muller transformation. -// nPerRng must be even. -//////////////////////////////////////////////////////////////////////////////// -__device__ inline void BoxMuller(float &u1, float &u2) -{ - float r = sqrtf(-2.0f * logf(u1)); - float phi = 2 * MTRand_d::PI * u2; - u1 = r * __cosf(phi); - u2 = r * __sinf(phi); -} - -__global__ void BoxMullerGPU(float *d_Random, int nPerRng, int mt_rng_count) -{ - const int tid = blockDim.x * blockIdx.x + threadIdx.x; - - for (int iOut = 0; iOut < nPerRng; iOut += 2) - BoxMuller(d_Random[tid + (iOut + 0) * mt_rng_count], - d_Random[tid + (iOut + 1) * mt_rng_count]); -} - - -//skip the seperate BoxMullerGPU for increased speed (uses register memory). -//nPerRng must be a multiple of 2 -__global__ void RandomNormGPU(mt_struct_stripped *ds_MT, unsigned int *mt, float *d_Random, - int nPerRng, int mt_rng_count) -{ - const int tid = blockDim.x * blockIdx.x + threadIdx.x; - int iState, iState1, iStateM, iOut; - unsigned int mti, mti1, mtiM, x; - unsigned int matrix_a, mask_b, mask_c; - - float regVal1, regVal2; //need 2 values for boxmuller - bool boxFlag = false; //will perform boxmuller transform on true - - //Load bit-vector Mersenne Twister parameters - matrix_a = ds_MT[tid].matrix_a; - mask_b = ds_MT[tid].mask_b; - mask_c = ds_MT[tid].mask_c; - - iState = ds_MT[tid].iState; - mti1 = mt[iState]; - for (iOut = 0; iOut < nPerRng; iOut++) { - iState1 = iState + 1; - iStateM = iState + MT_MM; - if (iState1 >= MT_NN * (1 + tid)) - iState1 -= MT_NN; - if (iStateM >= MT_NN * (1 + tid)) - iStateM -= MT_NN; - mti = mti1; - mti1 = mt[iState1]; - mtiM = mt[iStateM]; - - // MT recurrence - x = (mti & MTRand_d::UMASK) | (mti1 & MTRand_d::LMASK); - x = mtiM ^ (x >> 1) ^ ((x & 1) ? matrix_a : 0); - - mt[iState] = x; - iState = iState1; - - //Tempering transformation - x ^= (x >> MTRand_d::SHIFT0); - x ^= (x << MTRand_d::SHIFTB) & mask_b; - x ^= (x << MTRand_d::SHIFTC) & mask_c; - x ^= (x >> MTRand_d::SHIFT1); - - if (boxFlag) { - regVal2 = ((float)x + 1.0f) / 4294967296.0f; - BoxMuller(regVal1, regVal2); - d_Random[tid + (iOut - 1) * mt_rng_count] = regVal1; - d_Random[tid + iOut * mt_rng_count] = regVal2; - boxFlag = false; - } else { - regVal1 = ((float)x + 1.0f) / 4294967296.0f; - boxFlag = true; - } - } - ds_MT[tid].iState = iState; -} - -void MTRand_d::uniformMTGPU(float *d_random) -{ - RandomGPU<<>>(MT_d, mt_d, d_random, mt_nPerRng_, mt_rng_count_); -} - -void MTRand_d::normalMTGPU(float *d_random) -{ - RandomNormGPU<<>>(MT_d, mt_d, d_random, mt_nPerRng, mt_rng_count); -} - -//initialize globals and setup state -//Note: mt_rng_count must equal blocks*threads. mt_rng_count*nPerRng should equal the total number of randon numbers to be generated -void MTRand_d::initMTGPU(unsigned int seed, unsigned int totalVertices) -{ - mt_seed_ = seed; - mt_totalVertices_ = totalVertices; - mt_noiseSize_ = totalVertices * 10; //each noise buffer can hold 10 times totalVertices - // mt_blocks = blocks; - // mt_threads = threads; - // mt_nPerRng = nPerRng; - // mt_rng_count = mt_rng_c; - //mt_threads_ = 256; - const int y = 256; - const int max_xy = 4096; - int best_x = -1, best_z = -1; - int min_value = INT_MAX; - for (int x = 1; x * y <= max_xy; ++x) { - int xy = x * y; - int min_z = (totalVertices + xy - 1) / xy; // Compute the smallest z such that xy * z >= B - - if (min_z % 2 != 0) { - min_z++; // Ensure z is even - } - - int product = xy * min_z; - - if (product >= totalVertices && product < min_value) { - min_value = product; - best_x = x; - best_z = min_z; - } - } - if (best_x != -1) { - mt_blocks_ = best_x; - mt_threads_ = 256; - mt_nPerRng_ = best_z; - } else { - mt_blocks_ = 25; - mt_nPerRng_ = 4; - int rng_mt_rng_count - = totalVertices / mt_nPerRng_; //# of threads to generate for numVertices rand #s - int mt_threads_ = rng_mt_rng_count / mt_blocks_; //# threads per block needed - } - - allocDeviceStruct() loadMTGPU(MT_DATAFILE); - seedMTGPU(seed); -} diff --git a/Simulator/Utils/RNG/MTRand_d.h b/Simulator/Utils/RNG/MTRand_d.h deleted file mode 100644 index bab7790ca..000000000 --- a/Simulator/Utils/RNG/MTRand_d.h +++ /dev/null @@ -1,155 +0,0 @@ -/** -* @file MTRand_d.h -* -* @ingroup Simulator/Utils/RNG -* -* @brief Mersenne Twister logic from Nvidia -* -* This file has been modified by the UW Bothell Graphitti group, -* mostly to reorganize it and make it look more like typical C++ -* code. This includes splitting it into a .h and .cpp (instead of -* having everything in a .h file), and replacing enums previously -* used to define constants with consts. Given that this was designed -* to produce 32-bit random numbers, and have 32-bit internal state, -* the type uint32_t has been used throughout for precision of -* definition (now that compilers often use 64-bit ints). -* -* Mersenne Twister random number generator -- a C++ class MTRand_d -* Based on code by Makoto Matsumoto, Takuji Nishimura, and Shawn Cokus -* Richard J. Wagner v1.0 15 May 2003 rjwagner@writeme.com -* -* The Mersenne Twister is an algorithm for generating random numbers. It -* was designed with consideration of the flaws in various other generators. -* The period, 2^19937-1, and the order of equidistribution, 623 dimensions, -* are far greater. The generator is also fast; it avoids multiplication and -* division, and it benefits from caches and pipelines. For more information -* see the inventors' web page at http://www.math.keio.ac.jp/~matumoto/emt.html -* -* Reference -* M. Matsumoto and T. Nishimura, "Mersenne Twister: A 623-Dimensionally -* Equidistributed Uniform Pseudo-Random Number Generator", ACM Transactions on -* Modeling and Computer Simulation, Vol. 8, No. 1, January 1998, pp 3-30. -* -* Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, -* Copyright (C) 2000 - 2003, Richard J. Wagner -* All rights reserved. -* -* Redistribution and use in source and binary forms, with or without -* modification, are permitted provided that the following conditions -* are met: -* -* 1. Redistributions of source code must retain the above copyright -* notice, this list of conditions and the following disclaimer. -* -* 2. Redistributions in binary form must reproduce the above copyright -* notice, this list of conditions and the following disclaimer in the -* documentation and/or other materials provided with the distribution. -* -* 3. The names of its contributors may not be used to endorse or promote -* products derived from this software without specific prior written -* permission. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -* -* The original code included the following notice: -* -* When you use this, send an email to: matumoto@math.keio.ac.jp -* with an appropriate reference to your work. -* -* It would be nice to CC: rjwagner@writeme.com and Cokus@math.washington.edu -* when you write. -* -* Not thread safe (unless auto-initialization is avoided and each thread has -* its own MTRand_d object) -*/ - -#pragma once - -#include "BGTypes.h" // for BGFLOAT -#include -#include -#include -#include -#include -#include -// cereal -#include -#include -#include -#include -#include - -class MTRand_d { -public: - static constexpr const char *MT_DATAFILE = "RuntimeFiles/Data/MersenneTwister_16384.dat"; - - static constexpr std::size_t RNG_COUNT = 16384; // max threads - static constexpr std::size_t MM = 9; - static constexpr std::size_t NN = 19; - - static constexpr std::uint32_t WMASK = 0xFFFFFFFFU; - static constexpr std::uint32_t UMASK = 0xFFFFFFFEU; - static constexpr std::uint32_t LMASK = 0x1U; - - static constexpr int SHIFT0 = 12; - static constexpr int SHIFTB = 7; - static constexpr int SHIFTC = 15; - static constexpr int SHIFT1 = 18; - - // Constants related to DCMT and period - static constexpr std::uint32_t DCMT_SEED = 4172; - static constexpr std::uint32_t MT_RNG_PERIOD = 607; - - static constexpr float PI = 3.14159265358979f; - - struct mt_struct_stripped { - unsigned int matrix_a; - unsigned int mask_b; - unsigned int mask_c; - unsigned int iState; // Replaces seed - }; - -private: - mt_struct_stripped *MT_; // Host state - mt_struct_stripped *MT_d; // Device state - unsigned int *mt_d; // Device state values - - unsigned int mt_rng_count_; - unsigned int mt_blocks_; - unsigned int mt_threads_; - unsigned int mt_nPerRng_; - unsigned int mt_totalVertices_; - unsigned int - mt_noiseSize_; //integer multiple of totalVertices for the size of the noise buffers - unsigned int mt_seed_; - float *mtNoise1_d; - float *mtNoise2_d; - - void loadState(); // Function to load state from file - void loadMTGPU(const char *fname); - void seedMTGPU(unsigned int seed); - void uniformMTGPU(float *d_random); - void normalMTGPU(float *d_random); - void initMTGPU(unsigned int seed, unsigned int blocks, unsigned int threads, - unsigned int nPerRng, unsigned int mt_rng_c); - - //Methods -public: - MTRand_d(); - - ~MTRand_d(); - - void allocDeviceStruct(); - void deleteDeviceStruct(); - void allocHostStruct(); -}; From 6058b880131fdae2b15eb56f762e83b4f6eaea82 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 27 May 2025 01:48:16 -0700 Subject: [PATCH 06/37] Testing refactor --- Simulator/Core/GPUModel.cpp | 4 +++- Simulator/Utils/RNG/AsyncMT_d.cu | 7 +++++-- Simulator/Utils/RNG/AsyncMT_d.h | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index a03f4a4fc..e1dcf819f 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -94,7 +94,9 @@ void GPUModel::setupSim() // int rng_threads = rng_mt_rng_count / rng_blocks; //# threads per block needed // initMTGPU(Simulator::getInstance().getNoiseRngSeed(), rng_blocks, rng_threads, rng_nPerRng, // rng_mt_rng_count); - AsyncGenerator = AsyncMT_d(Simulator::getInstance().getTotalVertices(), + // AsyncGenerator = AsyncMT_d(Simulator::getInstance().getTotalVertices(), + // Simulator::getInstance().getNoiseRngSeed()); + AsyncGenerator.loadAsyncMT(Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getNoiseRngSeed()); #ifdef PERFORMANCE_METRICS diff --git a/Simulator/Utils/RNG/AsyncMT_d.cu b/Simulator/Utils/RNG/AsyncMT_d.cu index e4ede2de1..a1093452b 100644 --- a/Simulator/Utils/RNG/AsyncMT_d.cu +++ b/Simulator/Utils/RNG/AsyncMT_d.cu @@ -17,9 +17,12 @@ __global__ void generateKernel(curandStateMtgp32 *state, float *output, int samp state[gen_id] = localState; } -AsyncMT_d::AsyncMT_d(int samplesPerSegment, unsigned long seed) : - segmentSize(samplesPerSegment), seed(seed), currentBuffer(0), segmentIndex(0) +void AsyncMT_d::loadAsyncMT(int samplesPerSegment, unsigned long seed) { + segmentSize = samplesPerSegment; + seed = seed; + currentBuffer = 0; + segmentIndex = 0; totalSegments = 10; // Each buffer has 10 segments bufferSize = segmentSize * totalSegments; totalSamples = bufferSize * 2; diff --git a/Simulator/Utils/RNG/AsyncMT_d.h b/Simulator/Utils/RNG/AsyncMT_d.h index 1e9b31fa2..ae4111bc5 100644 --- a/Simulator/Utils/RNG/AsyncMT_d.h +++ b/Simulator/Utils/RNG/AsyncMT_d.h @@ -11,7 +11,7 @@ class AsyncMT_d { AsyncMT_d() = default; AsyncMT_d(int samplesPerGen, unsigned long seed); ~AsyncMT_d(); - + void loadAsyncMT(int samplesPerSegment, unsigned long seed); float *requestSegment(); private: From b0fa9ec7a742a86bf63e21ec9bc00cc45090ccbe Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 27 May 2025 08:43:57 -0700 Subject: [PATCH 07/37] fix performance metrics bug --- Simulator/Core/Simulator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Simulator/Core/Simulator.cpp b/Simulator/Core/Simulator.cpp index e4a42d9df..0fd941c2d 100644 --- a/Simulator/Core/Simulator.cpp +++ b/Simulator/Core/Simulator.cpp @@ -173,7 +173,7 @@ void Simulator::simulate() double total_time = timer.lap() / 1000000.0; cout << "\ntotal_time: " << total_time << " seconds" << endl; - printPerformanceMetrics(total_time, currentEpoch); + printPerformanceMetrics(total_time, currentEpoch_); cout << endl; #endif } From 4ea4b41308d68798338dcf3260d798b4c457da2f Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 27 May 2025 09:36:16 -0700 Subject: [PATCH 08/37] added logs and increased size of buffers --- Simulator/Utils/RNG/AsyncMT_d.cu | 9 ++++++--- Simulator/Utils/RNG/AsyncMT_d.h | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Simulator/Utils/RNG/AsyncMT_d.cu b/Simulator/Utils/RNG/AsyncMT_d.cu index a1093452b..84fa01ef2 100644 --- a/Simulator/Utils/RNG/AsyncMT_d.cu +++ b/Simulator/Utils/RNG/AsyncMT_d.cu @@ -19,11 +19,12 @@ __global__ void generateKernel(curandStateMtgp32 *state, float *output, int samp void AsyncMT_d::loadAsyncMT(int samplesPerSegment, unsigned long seed) { + consoleLogger_ = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("console")); segmentSize = samplesPerSegment; seed = seed; currentBuffer = 0; segmentIndex = 0; - totalSegments = 10; // Each buffer has 10 segments + totalSegments = 100000; // Each buffer has 10 segments bufferSize = segmentSize * totalSegments; totalSamples = bufferSize * 2; numGenerators = 50; //placeholder num of blocks @@ -66,6 +67,7 @@ AsyncMT_d::~AsyncMT_d() float *AsyncMT_d::requestSegment() { + //LOG4CPLUS_TRACE(consoleLogger_, "request segment"); //auto start = std::chrono::high_resolution_clock::now(); if (segmentIndex >= totalSegments) { // Switch buffer and launch async refill on the now-unused one @@ -78,11 +80,11 @@ float *AsyncMT_d::requestSegment() } float *segmentPtr = buffers[currentBuffer] + segmentIndex * segmentSize; - segmentIndex++; + segmentIndex += 1; // auto end = std::chrono::high_resolution_clock::now(); // std::cout << "Segment: " << segmentIndex << ", Launch time: " << (end - start).count() << " ns\n"; - + numRequests++; return segmentPtr; } @@ -90,6 +92,7 @@ void AsyncMT_d::fillBuffer(int bufferIndex) { dim3 blocks(numGenerators); dim3 threads(256); + //LOG4CPLUS_TRACE(consoleLogger_, "filling buffer:"); generateKernel<<>>(d_states, buffers[bufferIndex], bufferSize / numGenerators); } diff --git a/Simulator/Utils/RNG/AsyncMT_d.h b/Simulator/Utils/RNG/AsyncMT_d.h index ae4111bc5..986c3a70d 100644 --- a/Simulator/Utils/RNG/AsyncMT_d.h +++ b/Simulator/Utils/RNG/AsyncMT_d.h @@ -5,7 +5,7 @@ #include #include #include // Precomputed parameter table - +#include class AsyncMT_d { public: AsyncMT_d() = default; @@ -31,5 +31,8 @@ class AsyncMT_d { curandStateMtgp32 *d_states; mtgp32_kernel_params_t *d_params; + log4cplus::Logger + consoleLogger_; /// Logger for printing to the console as well as the logging file + void fillBuffer(int bufferIndex); }; From e67a65e575a8292e9297f16f24913804b1456546 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 27 May 2025 11:33:36 -0700 Subject: [PATCH 09/37] testing various gpu memory allocation sizes --- Simulator/Utils/RNG/AsyncMT_d.cu | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Simulator/Utils/RNG/AsyncMT_d.cu b/Simulator/Utils/RNG/AsyncMT_d.cu index 84fa01ef2..c0cc40bdc 100644 --- a/Simulator/Utils/RNG/AsyncMT_d.cu +++ b/Simulator/Utils/RNG/AsyncMT_d.cu @@ -24,7 +24,7 @@ void AsyncMT_d::loadAsyncMT(int samplesPerSegment, unsigned long seed) seed = seed; currentBuffer = 0; segmentIndex = 0; - totalSegments = 100000; // Each buffer has 10 segments + totalSegments = 10000; // Each buffer has 10000 segments bufferSize = segmentSize * totalSegments; totalSamples = bufferSize * 2; numGenerators = 50; //placeholder num of blocks @@ -84,7 +84,6 @@ float *AsyncMT_d::requestSegment() // auto end = std::chrono::high_resolution_clock::now(); // std::cout << "Segment: " << segmentIndex << ", Launch time: " << (end - start).count() << " ns\n"; - numRequests++; return segmentPtr; } From 2fa2f4c05b761263fd011fa6f672a451a5f11258 Mon Sep 17 00:00:00 2001 From: Andrew Madison Date: Tue, 27 May 2025 12:09:25 -0700 Subject: [PATCH 10/37] added include paths for CUDA include in cmakelists to fix a bug trying to #include --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 995c34067..52311af99 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,7 @@ if(ENABLE_CUDA) project(Graphitti LANGUAGES CXX CUDA C) #Verify CUDA package is present find_Package(CUDA REQUIRED) + include_directories(${CUDA_INCLUDE_DIRS}) #Set the USE_GPU preprocessor macro so that GPU code will be compiled. add_compile_definitions(USE_GPU) #Specify the CUDA architecture / gencode that will be targeted From 7278cb22a0ed0d9978165e959a05e9d1bf31a04d Mon Sep 17 00:00:00 2001 From: Andrew Madison Date: Tue, 3 Jun 2025 06:52:17 -0700 Subject: [PATCH 11/37] testing --- Simulator/Core/GPUModel.cpp | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index e1dcf819f..8900c44c9 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -12,6 +12,7 @@ #include "AllVertices.h" #include "Connections.h" #include "Global.h" +#include #ifdef VALIDATION_MODE #include "AllIFNeurons.h" #include "OperationManager.h" @@ -44,8 +45,8 @@ void GPUModel::allocDeviceStruct(void **allVerticesDevice, void **allEdgesDevice // Allocate memory for random noise array int numVertices = Simulator::getInstance().getTotalVertices(); - // BGSIZE randNoise_d_size = numVertices * sizeof(float); // size of random noise array - // HANDLE_ERROR(cudaMalloc((void **)&randNoise_d, randNoise_d_size)); + BGSIZE randNoise_d_size = numVertices * sizeof(float); // size of random noise array + HANDLE_ERROR(cudaMalloc((void **)&randNoise_d, randNoise_d_size)); // Copy host vertex and edge arrays into GPU device vertices.copyToDevice(*allVerticesDevice); @@ -72,7 +73,7 @@ void GPUModel::deleteDeviceStruct(void **allVerticesDevice, void **allEdgesDevic edges.copyEdgeDeviceToHost(*allEdgesDevice); // Deallocate device memory edges.deleteEdgeDeviceStruct(*allEdgesDevice); - //HANDLE_ERROR(cudaFree(randNoise_d)); + HANDLE_ERROR(cudaFree(randNoise_d)); } /// Sets up the Simulation. @@ -94,8 +95,7 @@ void GPUModel::setupSim() // int rng_threads = rng_mt_rng_count / rng_blocks; //# threads per block needed // initMTGPU(Simulator::getInstance().getNoiseRngSeed(), rng_blocks, rng_threads, rng_nPerRng, // rng_mt_rng_count); - // AsyncGenerator = AsyncMT_d(Simulator::getInstance().getTotalVertices(), - // Simulator::getInstance().getNoiseRngSeed()); + //cout << "blocks, threads, nPerRng, rng_rng_count: " << rng_blocks << " " << rng_threads << " " << rng_nPerRng << " " << rng_mt_rng_count << endl; AsyncGenerator.loadAsyncMT(Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getNoiseRngSeed()); @@ -165,6 +165,17 @@ void GPUModel::advance() #else //normalMTGPU(randNoise_d); randNoise_d = AsyncGenerator.requestSegment(); + // int verts = Simulator::getInstance().getTotalVertices(); + // float* h_data = new float[verts]; + // cudaDeviceSynchronize(); + // HANDLE_ERROR(cudaMemcpy(h_data, randNoise_d, verts * sizeof(float), cudaMemcpyDeviceToHost)); + // log4cplus::Logger vertexLogger_ = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("vertex")); + // for(int i=0; i< verts; i++){ + // LOG4CPLUS_DEBUG(vertexLogger_, endl + // << "Rand Index[" << i << "] :: Noise = " + // << h_data[i]); + // } + // delete[] h_data; #endif //LOG4CPLUS_DEBUG(vertexLogger_, "Index: " << index << " Vm: " << Vm); #ifdef PERFORMANCE_METRICS From 92bf648fe0fe38dae5b2b3406e35e105293656bd Mon Sep 17 00:00:00 2001 From: Andrew Madison Date: Tue, 3 Jun 2025 11:51:51 -0700 Subject: [PATCH 12/37] testing full simulation and adding in error handling --- Simulator/Core/GPUModel.cpp | 7 +++--- Simulator/Core/GPUModel.h | 1 + Simulator/Utils/RNG/AsyncMT_d.cu | 38 +++++++++++++++++++------------- Simulator/Utils/RNG/AsyncMT_d.h | 6 +++++ 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index 8900c44c9..38ae16d43 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -45,8 +45,8 @@ void GPUModel::allocDeviceStruct(void **allVerticesDevice, void **allEdgesDevice // Allocate memory for random noise array int numVertices = Simulator::getInstance().getTotalVertices(); - BGSIZE randNoise_d_size = numVertices * sizeof(float); // size of random noise array - HANDLE_ERROR(cudaMalloc((void **)&randNoise_d, randNoise_d_size)); + // BGSIZE randNoise_d_size = numVertices * sizeof(float); // size of random noise array + // HANDLE_ERROR(cudaMalloc((void **)&randNoise_d, randNoise_d_size)); // Copy host vertex and edge arrays into GPU device vertices.copyToDevice(*allVerticesDevice); @@ -73,7 +73,7 @@ void GPUModel::deleteDeviceStruct(void **allVerticesDevice, void **allEdgesDevic edges.copyEdgeDeviceToHost(*allEdgesDevice); // Deallocate device memory edges.deleteEdgeDeviceStruct(*allEdgesDevice); - HANDLE_ERROR(cudaFree(randNoise_d)); + // HANDLE_ERROR(cudaFree(randNoise_d)); } /// Sets up the Simulation. @@ -128,6 +128,7 @@ void GPUModel::setupSim() void GPUModel::finish() { // deallocates memories on CUDA device + AsyncGenerator.deleteDeviceStruct(); deleteDeviceStruct((void **)&allVerticesDevice_, (void **)&allEdgesDevice_); deleteEdgeIndexMap(); diff --git a/Simulator/Core/GPUModel.h b/Simulator/Core/GPUModel.h index 448202018..ed3cc550c 100644 --- a/Simulator/Core/GPUModel.h +++ b/Simulator/Core/GPUModel.h @@ -105,6 +105,7 @@ class GPUModel : public Model { /// Pointer to device random noise array. float *randNoise_d; AsyncMT_d AsyncGenerator; + float *randNoise_h; #if defined(USE_GPU) /// Pointer to edge index map in device memory. EdgeIndexMapDevice *edgeIndexMapDevice_; diff --git a/Simulator/Utils/RNG/AsyncMT_d.cu b/Simulator/Utils/RNG/AsyncMT_d.cu index c0cc40bdc..8b000fb59 100644 --- a/Simulator/Utils/RNG/AsyncMT_d.cu +++ b/Simulator/Utils/RNG/AsyncMT_d.cu @@ -19,7 +19,10 @@ __global__ void generateKernel(curandStateMtgp32 *state, float *output, int samp void AsyncMT_d::loadAsyncMT(int samplesPerSegment, unsigned long seed) { - consoleLogger_ = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("console")); + // hostBuffer = nullptr; + // cudaHostAlloc(&hostBuffer, samplesPerSegment * sizeof(float), cudaHostAllocDefault); + // logfile = std::fopen("mt_output.bin", "wb"); + //consoleLogger_ = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("console")); segmentSize = samplesPerSegment; seed = seed; currentBuffer = 0; @@ -30,22 +33,22 @@ void AsyncMT_d::loadAsyncMT(int samplesPerSegment, unsigned long seed) numGenerators = 50; //placeholder num of blocks // Create internal stream - cudaStreamCreate(&stream); + HANDLE_ERROR(cudaStreamCreate(&stream)); // Allocate two large buffers - cudaMalloc(&buffers[0], bufferSize * sizeof(float)); - cudaMalloc(&buffers[1], bufferSize * sizeof(float)); + HANDLE_ERROR(cudaMalloc(&buffers[0], bufferSize * sizeof(float))); + HANDLE_ERROR(cudaMalloc(&buffers[1], bufferSize * sizeof(float))); // Allocate state and param memory - cudaMalloc(&d_states, numGenerators * sizeof(curandStateMtgp32)); - cudaMalloc(&d_params, numGenerators * sizeof(mtgp32_kernel_params_t)); + HANDLE_ERROR(cudaMalloc(&d_states, numGenerators * sizeof(curandStateMtgp32))); + HANDLE_ERROR(cudaMalloc(&d_params, numGenerators * sizeof(mtgp32_kernel_params_t))); // Create local param buffer of correct type mtgp32_kernel_params_t *h_params = new mtgp32_kernel_params_t[numGenerators]; curandMakeMTGP32Constants(mtgp32dc_params_fast_11213, h_params); - cudaMemcpy(d_params, h_params, numGenerators * sizeof(mtgp32_kernel_params_t), - cudaMemcpyHostToDevice); + HANDLE_ERROR(cudaMemcpy(d_params, h_params, numGenerators * sizeof(mtgp32_kernel_params_t), + cudaMemcpyHostToDevice)); delete[] h_params; curandMakeMTGP32KernelState(d_states, mtgp32dc_params_fast_11213, d_params, numGenerators, seed); @@ -53,16 +56,19 @@ void AsyncMT_d::loadAsyncMT(int samplesPerSegment, unsigned long seed) // Pre-fill both buffers fillBuffer(0); fillBuffer(1); - cudaStreamSynchronize(stream); //wait for both buffers to be filled before the first request + HANDLE_ERROR(cudaStreamSynchronize(stream)); //wait for both buffers to be filled before the first request +} +void AsyncMT_d::deleteDeviceStruct(){ + // std::fclose(logfile); + // cudaFree(hostBuffer); + HANDLE_ERROR(cudaFree(buffers[0])); + HANDLE_ERROR(cudaFree(buffers[1])); + HANDLE_ERROR(cudaFree(d_states)); + HANDLE_ERROR(cudaFree(d_params)); + HANDLE_ERROR(cudaStreamDestroy(stream)); } - AsyncMT_d::~AsyncMT_d() { - cudaFree(buffers[0]); - cudaFree(buffers[1]); - cudaFree(d_states); - cudaFree(d_params); - cudaStreamDestroy(stream); } float *AsyncMT_d::requestSegment() @@ -84,6 +90,8 @@ float *AsyncMT_d::requestSegment() // auto end = std::chrono::high_resolution_clock::now(); // std::cout << "Segment: " << segmentIndex << ", Launch time: " << (end - start).count() << " ns\n"; + // cudaMemcpy(hostBuffer, segmentPtr, segmentSize * sizeof(float), cudaMemcpyDeviceToHost); + // std::fwrite(hostBuffer, sizeof(float), segmentSize, logfile); return segmentPtr; } diff --git a/Simulator/Utils/RNG/AsyncMT_d.h b/Simulator/Utils/RNG/AsyncMT_d.h index 986c3a70d..6f59a4468 100644 --- a/Simulator/Utils/RNG/AsyncMT_d.h +++ b/Simulator/Utils/RNG/AsyncMT_d.h @@ -6,12 +6,16 @@ #include #include // Precomputed parameter table #include +#include "Book.h" +#include +#include class AsyncMT_d { public: AsyncMT_d() = default; AsyncMT_d(int samplesPerGen, unsigned long seed); ~AsyncMT_d(); void loadAsyncMT(int samplesPerSegment, unsigned long seed); + void deleteDeviceStruct(); float *requestSegment(); private: @@ -31,6 +35,8 @@ class AsyncMT_d { curandStateMtgp32 *d_states; mtgp32_kernel_params_t *d_params; + FILE* logfile; + float* hostBuffer; log4cplus::Logger consoleLogger_; /// Logger for printing to the console as well as the logging file From 8f8f4ac1b32d1aea35bd38ec5965378218d9b20c Mon Sep 17 00:00:00 2001 From: Andrew Madison Date: Tue, 3 Jun 2025 15:16:57 -0700 Subject: [PATCH 13/37] changed rng to philox and performed an analysis on the distribution --- Simulator/Core/GPUModel.cpp | 4 +- Simulator/Core/GPUModel.h | 1 + Simulator/Utils/RNG/AsyncMT_d.cu | 107 +++++++++++++++++++++---------- Simulator/Utils/RNG/AsyncMT_d.h | 13 ++-- 4 files changed, 85 insertions(+), 40 deletions(-) diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index 38ae16d43..fd7f12775 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -74,6 +74,8 @@ void GPUModel::deleteDeviceStruct(void **allVerticesDevice, void **allEdgesDevic // Deallocate device memory edges.deleteEdgeDeviceStruct(*allEdgesDevice); // HANDLE_ERROR(cudaFree(randNoise_d)); + AsyncGenerator.deleteDeviceStruct(); + // closeFileMT(); } /// Sets up the Simulation. @@ -164,7 +166,7 @@ void GPUModel::advance() // } cudaMemcpy(randNoise_d, randNoise_h.data(), verts * sizeof(float), cudaMemcpyHostToDevice); #else - //normalMTGPU(randNoise_d); + // normalMTGPU(randNoise_d); randNoise_d = AsyncGenerator.requestSegment(); // int verts = Simulator::getInstance().getTotalVertices(); // float* h_data = new float[verts]; diff --git a/Simulator/Core/GPUModel.h b/Simulator/Core/GPUModel.h index ed3cc550c..b082fd9a2 100644 --- a/Simulator/Core/GPUModel.h +++ b/Simulator/Core/GPUModel.h @@ -145,5 +145,6 @@ extern "C" { void normalMTGPU(float *randNoise_d); void initMTGPU(unsigned int seed, unsigned int blocks, unsigned int threads, unsigned int nPerRng, unsigned int mt_rng_count); +void closeFileMT(); } #endif \ No newline at end of file diff --git a/Simulator/Utils/RNG/AsyncMT_d.cu b/Simulator/Utils/RNG/AsyncMT_d.cu index 8b000fb59..96f9702b7 100644 --- a/Simulator/Utils/RNG/AsyncMT_d.cu +++ b/Simulator/Utils/RNG/AsyncMT_d.cu @@ -3,25 +3,55 @@ #include #include #include -__global__ void generateKernel(curandStateMtgp32 *state, float *output, int samplesPerGen) +// __global__ void generateKernel(curandStateMtgp32 *state, float *output, int samplesPerGen) +// { +// int tid = threadIdx.x; +// int gen_id = blockIdx.x; +// if (gen_id >= gridDim.x) +// return; + +// curandStateMtgp32 localState = state[gen_id]; +// for (int i = tid; i < samplesPerGen; i += blockDim.x) { +// output[gen_id * samplesPerGen + i] = curand_normal(&localState); +// } +// state[gen_id] = localState; +// } + +__global__ void generatePhilox(curandStatePhilox4_32_10_t* states, float* output,int bufferSize) { - int tid = threadIdx.x; - int gen_id = blockIdx.x; - if (gen_id >= gridDim.x) - return; - - curandStateMtgp32 localState = state[gen_id]; - for (int i = tid; i < samplesPerGen; i += blockDim.x) { - output[gen_id * samplesPerGen + i] = curand_normal(&localState); - } - state[gen_id] = localState; + // Compute a unique global index for this thread + int threadId = threadIdx.x; + int blockId = blockIdx.x; + int threadsPerBlock = blockDim.x; + int totalThreads = gridDim.x * threadsPerBlock; + int gid = blockId * threadsPerBlock + threadId; + + // Load this thread’s Philox state + curandStatePhilox4_32_10_t local = states[gid]; + + // Stride‐loop: write one random per iteration until we cover bufferSize + for (int idx = gid; idx < bufferSize; idx += totalThreads) { + output[idx] = curand_normal(&local); + } + + // Store back the updated state + states[gid] = local; +} + + + + +__global__ void initPhilox(curandStatePhilox4_32_10_t* states, unsigned long seed,int totalThreads) { + int gid = blockIdx.x * blockDim.x + threadIdx.x; + if (gid >= totalThreads) return; + curand_init(seed, gid, 0, &states[gid]); } void AsyncMT_d::loadAsyncMT(int samplesPerSegment, unsigned long seed) { - // hostBuffer = nullptr; - // cudaHostAlloc(&hostBuffer, samplesPerSegment * sizeof(float), cudaHostAllocDefault); - // logfile = std::fopen("mt_output.bin", "wb"); + hostBuffer = nullptr; + cudaHostAlloc(&hostBuffer, samplesPerSegment * sizeof(float), cudaHostAllocDefault); + logfile = std::fopen("philox_output.bin", "wb"); //consoleLogger_ = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("console")); segmentSize = samplesPerSegment; seed = seed; @@ -29,8 +59,13 @@ void AsyncMT_d::loadAsyncMT(int samplesPerSegment, unsigned long seed) segmentIndex = 0; totalSegments = 10000; // Each buffer has 10000 segments bufferSize = segmentSize * totalSegments; - totalSamples = bufferSize * 2; - numGenerators = 50; //placeholder num of blocks + numBlocks = 50; //placeholder num of blocks + numThreads = 256; + + + + totalThreads = numThreads * numBlocks; + // Create internal stream HANDLE_ERROR(cudaStreamCreate(&stream)); @@ -39,19 +74,23 @@ void AsyncMT_d::loadAsyncMT(int samplesPerSegment, unsigned long seed) HANDLE_ERROR(cudaMalloc(&buffers[0], bufferSize * sizeof(float))); HANDLE_ERROR(cudaMalloc(&buffers[1], bufferSize * sizeof(float))); - // Allocate state and param memory - HANDLE_ERROR(cudaMalloc(&d_states, numGenerators * sizeof(curandStateMtgp32))); - HANDLE_ERROR(cudaMalloc(&d_params, numGenerators * sizeof(mtgp32_kernel_params_t))); + // // Allocate state and param memory + // HANDLE_ERROR(cudaMalloc(&d_states, numGenerators * sizeof(curandStateMtgp32))); + // HANDLE_ERROR(cudaMalloc(&d_params, numGenerators * sizeof(mtgp32_kernel_params_t))); + HANDLE_ERROR(cudaMalloc(&spStates, totalThreads * sizeof(curandStatePhilox4_32_10_t))); + + initPhilox<<>>(spStates,seed,totalThreads); + // Create local param buffer of correct type - mtgp32_kernel_params_t *h_params = new mtgp32_kernel_params_t[numGenerators]; - curandMakeMTGP32Constants(mtgp32dc_params_fast_11213, h_params); - HANDLE_ERROR(cudaMemcpy(d_params, h_params, numGenerators * sizeof(mtgp32_kernel_params_t), - cudaMemcpyHostToDevice)); - delete[] h_params; + // mtgp32_kernel_params_t *h_params = new mtgp32_kernel_params_t[numGenerators]; + // curandMakeMTGP32Constants(mtgp32dc_params_fast_11213, h_params); + // HANDLE_ERROR(cudaMemcpy(d_params, h_params, numGenerators * sizeof(mtgp32_kernel_params_t), + // cudaMemcpyHostToDevice)); + // delete[] h_params; - curandMakeMTGP32KernelState(d_states, mtgp32dc_params_fast_11213, d_params, numGenerators, seed); + // curandMakeMTGP32KernelState(d_states, mtgp32dc_params_fast_11213, d_params, numGenerators, seed); // Pre-fill both buffers fillBuffer(0); @@ -59,12 +98,12 @@ void AsyncMT_d::loadAsyncMT(int samplesPerSegment, unsigned long seed) HANDLE_ERROR(cudaStreamSynchronize(stream)); //wait for both buffers to be filled before the first request } void AsyncMT_d::deleteDeviceStruct(){ - // std::fclose(logfile); - // cudaFree(hostBuffer); + std::fclose(logfile); + cudaFree(hostBuffer); HANDLE_ERROR(cudaFree(buffers[0])); HANDLE_ERROR(cudaFree(buffers[1])); - HANDLE_ERROR(cudaFree(d_states)); - HANDLE_ERROR(cudaFree(d_params)); + HANDLE_ERROR(cudaFree(spStates)); + HANDLE_ERROR(cudaStreamDestroy(stream)); } AsyncMT_d::~AsyncMT_d() @@ -90,16 +129,14 @@ float *AsyncMT_d::requestSegment() // auto end = std::chrono::high_resolution_clock::now(); // std::cout << "Segment: " << segmentIndex << ", Launch time: " << (end - start).count() << " ns\n"; - // cudaMemcpy(hostBuffer, segmentPtr, segmentSize * sizeof(float), cudaMemcpyDeviceToHost); - // std::fwrite(hostBuffer, sizeof(float), segmentSize, logfile); + cudaMemcpy(hostBuffer, segmentPtr, segmentSize * sizeof(float), cudaMemcpyDeviceToHost); + std::fwrite(hostBuffer, sizeof(float), segmentSize, logfile); + return segmentPtr; } void AsyncMT_d::fillBuffer(int bufferIndex) { - dim3 blocks(numGenerators); - dim3 threads(256); //LOG4CPLUS_TRACE(consoleLogger_, "filling buffer:"); - generateKernel<<>>(d_states, buffers[bufferIndex], - bufferSize / numGenerators); + generatePhilox<<>>(spStates, buffers[bufferIndex], bufferSize); } diff --git a/Simulator/Utils/RNG/AsyncMT_d.h b/Simulator/Utils/RNG/AsyncMT_d.h index 6f59a4468..7d3a80d65 100644 --- a/Simulator/Utils/RNG/AsyncMT_d.h +++ b/Simulator/Utils/RNG/AsyncMT_d.h @@ -19,21 +19,26 @@ class AsyncMT_d { float *requestSegment(); private: - int numGenerators; + int numBlocks; + int numThreads; + int totalThreads; int segmentSize; int totalSegments; int bufferSize; - int totalSamples; unsigned long seed; + cudaStream_t stream; float *buffers[2]; int currentBuffer; int segmentIndex; - curandStateMtgp32 *d_states; - mtgp32_kernel_params_t *d_params; + curandStatePhilox4_32_10_t* spStates; + + + //curandStateMtgp32 *d_states; + //mtgp32_kernel_params_t *d_params; FILE* logfile; float* hostBuffer; From f56d31a420c7347ff367cb05af7440ce481dd048 Mon Sep 17 00:00:00 2001 From: Andrew Madison Date: Tue, 3 Jun 2025 15:22:19 -0700 Subject: [PATCH 14/37] turned off file logging and fixed double free bug --- Simulator/Core/GPUModel.cpp | 1 - Simulator/Utils/RNG/AsyncMT_d.cu | 14 +++++++------- Simulator/Utils/RNG/AsyncMT_d.h | 4 ++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index fd7f12775..f736db254 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -74,7 +74,6 @@ void GPUModel::deleteDeviceStruct(void **allVerticesDevice, void **allEdgesDevic // Deallocate device memory edges.deleteEdgeDeviceStruct(*allEdgesDevice); // HANDLE_ERROR(cudaFree(randNoise_d)); - AsyncGenerator.deleteDeviceStruct(); // closeFileMT(); } diff --git a/Simulator/Utils/RNG/AsyncMT_d.cu b/Simulator/Utils/RNG/AsyncMT_d.cu index 96f9702b7..bb2424e1f 100644 --- a/Simulator/Utils/RNG/AsyncMT_d.cu +++ b/Simulator/Utils/RNG/AsyncMT_d.cu @@ -49,9 +49,9 @@ __global__ void initPhilox(curandStatePhilox4_32_10_t* states, unsigned long see void AsyncMT_d::loadAsyncMT(int samplesPerSegment, unsigned long seed) { - hostBuffer = nullptr; - cudaHostAlloc(&hostBuffer, samplesPerSegment * sizeof(float), cudaHostAllocDefault); - logfile = std::fopen("philox_output.bin", "wb"); + // hostBuffer = nullptr; + // cudaHostAlloc(&hostBuffer, samplesPerSegment * sizeof(float), cudaHostAllocDefault); + // logfile = std::fopen("philox_output.bin", "wb"); //consoleLogger_ = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("console")); segmentSize = samplesPerSegment; seed = seed; @@ -98,8 +98,8 @@ void AsyncMT_d::loadAsyncMT(int samplesPerSegment, unsigned long seed) HANDLE_ERROR(cudaStreamSynchronize(stream)); //wait for both buffers to be filled before the first request } void AsyncMT_d::deleteDeviceStruct(){ - std::fclose(logfile); - cudaFree(hostBuffer); + // std::fclose(logfile); + // cudaFree(hostBuffer); HANDLE_ERROR(cudaFree(buffers[0])); HANDLE_ERROR(cudaFree(buffers[1])); HANDLE_ERROR(cudaFree(spStates)); @@ -129,8 +129,8 @@ float *AsyncMT_d::requestSegment() // auto end = std::chrono::high_resolution_clock::now(); // std::cout << "Segment: " << segmentIndex << ", Launch time: " << (end - start).count() << " ns\n"; - cudaMemcpy(hostBuffer, segmentPtr, segmentSize * sizeof(float), cudaMemcpyDeviceToHost); - std::fwrite(hostBuffer, sizeof(float), segmentSize, logfile); + // cudaMemcpy(hostBuffer, segmentPtr, segmentSize * sizeof(float), cudaMemcpyDeviceToHost); + // std::fwrite(hostBuffer, sizeof(float), segmentSize, logfile); return segmentPtr; } diff --git a/Simulator/Utils/RNG/AsyncMT_d.h b/Simulator/Utils/RNG/AsyncMT_d.h index 7d3a80d65..711a7e88a 100644 --- a/Simulator/Utils/RNG/AsyncMT_d.h +++ b/Simulator/Utils/RNG/AsyncMT_d.h @@ -40,8 +40,8 @@ class AsyncMT_d { //curandStateMtgp32 *d_states; //mtgp32_kernel_params_t *d_params; - FILE* logfile; - float* hostBuffer; + // FILE* logfile; + // float* hostBuffer; log4cplus::Logger consoleLogger_; /// Logger for printing to the console as well as the logging file From d30ca17a595bd73467a115d28a741a995ab65ec6 Mon Sep 17 00:00:00 2001 From: Andrew Madison Date: Thu, 5 Jun 2025 11:06:58 -0700 Subject: [PATCH 15/37] fixed non-blocking stream --- Simulator/Connections/Connections.cpp | 3 +- Simulator/Connections/Connections.h | 10 ++- Simulator/Connections/Neuro/ConnGrowth.cpp | 1 + Simulator/Connections/Neuro/ConnGrowth.h | 6 +- Simulator/Connections/Neuro/ConnGrowth_d.cpp | 7 ++- Simulator/Core/GPUModel.cpp | 18 +++++- Simulator/Core/GPUModel.h | 5 ++ Simulator/Edges/AllEdges.cpp | 9 ++- Simulator/Edges/AllEdges.h | 11 ++++ Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp | 6 +- Simulator/Edges/Neuro/AllSpikingSynapses.h | 6 +- .../Edges/Neuro/AllSpikingSynapses_d.cpp | 4 +- Simulator/Utils/RNG/AsyncMT_d.cu | 63 +++++++++++++------ Simulator/Utils/RNG/AsyncMT_d.h | 15 ++--- Simulator/Utils/RNG/MersenneTwister_d.cu | 29 ++++++++- Simulator/Vertices/AllVertices.cpp | 8 +++ Simulator/Vertices/AllVertices.h | 10 +++ Simulator/Vertices/Neuro/AllIFNeurons.h | 4 ++ Simulator/Vertices/Neuro/AllIFNeurons_d.cpp | 2 + Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp | 2 +- Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp | 3 +- .../Vertices/Neuro/AllSpikingNeurons.cpp | 2 + Simulator/Vertices/Neuro/AllSpikingNeurons.h | 1 + .../Vertices/Neuro/AllSpikingNeurons_d.cpp | 4 +- 24 files changed, 183 insertions(+), 46 deletions(-) diff --git a/Simulator/Connections/Connections.cpp b/Simulator/Connections/Connections.cpp index a8a63fc0e..410e5df80 100644 --- a/Simulator/Connections/Connections.cpp +++ b/Simulator/Connections/Connections.cpp @@ -91,7 +91,8 @@ bool Connections::updateConnections(AllVertices &vertices) #if defined(USE_GPU) void Connections::updateEdgesWeights(int numVertices, AllVertices &vertices, AllEdges &edges, AllVerticesDeviceProperties *allVerticesDevice, - AllEdgesDeviceProperties *allEdgesDevice, Layout &layout) + AllEdgesDeviceProperties *allEdgesDevice, Layout &layout + ,cudaStream_t stream) { } #else diff --git a/Simulator/Connections/Connections.h b/Simulator/Connections/Connections.h index d8bcc8596..a594d56d2 100644 --- a/Simulator/Connections/Connections.h +++ b/Simulator/Connections/Connections.h @@ -33,6 +33,11 @@ // cereal #include +#ifdef USE_GPU +#include +#endif + + using namespace std; class Connections { @@ -87,7 +92,10 @@ class Connections { /// @param layout Layout information of the graph network. virtual void updateEdgesWeights(int numVertices, AllVertices &vertices, AllEdges &edges, AllVerticesDeviceProperties *allVerticesDevice, - AllEdgesDeviceProperties *allEdgesDevice, Layout &layout); + AllEdgesDeviceProperties *allEdgesDevice, Layout &layout + ,cudaStream_t stream); + + #else public: /// Update the weight of the edges in the simulation. diff --git a/Simulator/Connections/Neuro/ConnGrowth.cpp b/Simulator/Connections/Neuro/ConnGrowth.cpp index beaad1660..3a60930af 100644 --- a/Simulator/Connections/Neuro/ConnGrowth.cpp +++ b/Simulator/Connections/Neuro/ConnGrowth.cpp @@ -75,6 +75,7 @@ void ConnGrowth::setup() // Initialize connection frontier distance change matrix with the current distances Layout &layout = Simulator::getInstance().getModel().getLayout(); delta_ = layout.dist_; + // Register VertorMatrix radii_ for Recording if need // Recorder &recorder = Simulator::getInstance().getModel().getRecorder(); diff --git a/Simulator/Connections/Neuro/ConnGrowth.h b/Simulator/Connections/Neuro/ConnGrowth.h index e6a88078c..263d96dca 100644 --- a/Simulator/Connections/Neuro/ConnGrowth.h +++ b/Simulator/Connections/Neuro/ConnGrowth.h @@ -73,6 +73,9 @@ #include "Simulator.h" #include #include + + + // cereal #include #include @@ -128,7 +131,8 @@ class ConnGrowth : public Connections { virtual void updateEdgesWeights(int numVertices, AllVertices &vertices, AllEdges &edges, AllVerticesDeviceProperties *allVerticesDevice, AllEdgesDeviceProperties *allEdgesDevice, - Layout &layout) override; + Layout &layout, cudaStream_t stream) override; + #else /// Update the weights of the Synapses in the simulation. To be clear, /// iterates through all source and destination neurons and updates their diff --git a/Simulator/Connections/Neuro/ConnGrowth_d.cpp b/Simulator/Connections/Neuro/ConnGrowth_d.cpp index 148834fea..c0a293d81 100644 --- a/Simulator/Connections/Neuro/ConnGrowth_d.cpp +++ b/Simulator/Connections/Neuro/ConnGrowth_d.cpp @@ -29,7 +29,9 @@ */ void ConnGrowth::updateEdgesWeights(int numVertices, AllVertices &vertices, AllEdges &edges, AllVerticesDeviceProperties *allVerticesDevice, - AllEdgesDeviceProperties *allEdgesDevice, Layout &layout) + AllEdgesDeviceProperties *allEdgesDevice, Layout &layout + ,cudaStream_t stream + ) { Simulator &simulator = Simulator::getInstance(); // For now, we just set the weights to equal the areas. We will later @@ -64,11 +66,12 @@ void ConnGrowth::updateEdgesWeights(int numVertices, AllVertices &vertices, AllE cudaMemcpyHostToDevice)); blocksPerGrid = (simulator.getTotalVertices() + threadsPerBlock - 1) / threadsPerBlock; - updateSynapsesWeightsDevice<<>>( + updateSynapsesWeightsDevice<<>>( simulator.getTotalVertices(), deltaT, W_d, simulator.getMaxEdgesPerVertex(), (AllSpikingNeuronsDeviceProperties *)allVerticesDevice, (AllSpikingSynapsesDeviceProperties *)allEdgesDevice, neuronTypeMapD); + // free memories HANDLE_ERROR(cudaFree(W_d)); delete[] W_h; diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index f736db254..ed333ac79 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -54,6 +54,11 @@ void GPUModel::allocDeviceStruct(void **allVerticesDevice, void **allEdgesDevice // Allocate edge inverse map in device memory allocEdgeIndexMap(numVertices); + + // Create gpu model stream + HANDLE_ERROR(cudaStreamCreate(&stream)); + + } /// Copies device memories to host memories and deallocates them. @@ -75,6 +80,8 @@ void GPUModel::deleteDeviceStruct(void **allVerticesDevice, void **allEdgesDevic edges.deleteEdgeDeviceStruct(*allEdgesDevice); // HANDLE_ERROR(cudaFree(randNoise_d)); // closeFileMT(); + HANDLE_ERROR(cudaStreamDestroy(stream)); + } /// Sets up the Simulation. @@ -99,6 +106,7 @@ void GPUModel::setupSim() //cout << "blocks, threads, nPerRng, rng_rng_count: " << rng_blocks << " " << rng_threads << " " << rng_nPerRng << " " << rng_mt_rng_count << endl; AsyncGenerator.loadAsyncMT(Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getNoiseRngSeed()); + #ifdef PERFORMANCE_METRICS cudaEventCreate(&start); @@ -123,16 +131,22 @@ void GPUModel::setupSim() // set some parameters used for advanceEdgesDevice edges.setAdvanceEdgesDeviceParams(); + AllVertices &vertices = layout_->getVertices(); + vertices.SetStream(stream); + edges.SetStream(stream); + + } /// Performs any finalization tasks on network following a simulation. void GPUModel::finish() { // deallocates memories on CUDA device - AsyncGenerator.deleteDeviceStruct(); + AsyncGenerator.deleteDeviceStruct(); deleteDeviceStruct((void **)&allVerticesDevice_, (void **)&allEdgesDevice_); deleteEdgeIndexMap(); + #ifdef PERFORMANCE_METRICS cudaEventDestroy(start); cudaEventDestroy(stop); @@ -257,7 +271,7 @@ void GPUModel::updateConnections() // Update Connections data if (connections_->updateConnections(vertices)) { connections_->updateEdgesWeights(Simulator::getInstance().getTotalVertices(), vertices, edges, - allVerticesDevice_, allEdgesDevice_, getLayout()); + allVerticesDevice_, allEdgesDevice_, getLayout(),stream); // create edge index map connections_->createEdgeIndexMap(); // copy index map to the device memory diff --git a/Simulator/Core/GPUModel.h b/Simulator/Core/GPUModel.h index b082fd9a2..b679eb97c 100644 --- a/Simulator/Core/GPUModel.h +++ b/Simulator/Core/GPUModel.h @@ -24,6 +24,7 @@ #include "AllEdges.h" #include "AllVertices.h" #include "AsyncMT_d.h" +#include #ifdef VALIDATION_MODE #include @@ -106,6 +107,9 @@ class GPUModel : public Model { float *randNoise_d; AsyncMT_d AsyncGenerator; float *randNoise_h; + /// Cuda Stream for kernel use + cudaStream_t stream; + #if defined(USE_GPU) /// Pointer to edge index map in device memory. EdgeIndexMapDevice *edgeIndexMapDevice_; @@ -117,6 +121,7 @@ class GPUModel : public Model { /// vertex structure in device memory. AllVerticesDeviceProperties *allVerticesDevice_; + private: void allocEdgeIndexMap(int count); diff --git a/Simulator/Edges/AllEdges.cpp b/Simulator/Edges/AllEdges.cpp index ecdd1c21a..5ad138160 100644 --- a/Simulator/Edges/AllEdges.cpp +++ b/Simulator/Edges/AllEdges.cpp @@ -217,9 +217,16 @@ void AllEdges::createEdgeIndexMap(EdgeIndexMap &edgeIndexMap) } } +#if defined(USE_GPU) +// Set GPU stream for edge kernels -#if !defined(USE_GPU) +void AllEdges::SetStream(cudaStream_t pStream) +{ + stream = pStream; +} +#endif +#if !defined(USE_GPU) /// Advance all the edges in the simulation. /// /// @param vertices The vertices. diff --git a/Simulator/Edges/AllEdges.h b/Simulator/Edges/AllEdges.h index 72605ef53..db7bd467d 100644 --- a/Simulator/Edges/AllEdges.h +++ b/Simulator/Edges/AllEdges.h @@ -15,6 +15,9 @@ #include // cereal #include "cereal/types/vector.hpp" +#ifdef USE_GPU +#include +#endif class AllVertices; struct AllEdgesDeviceProperties; @@ -92,7 +95,15 @@ class AllEdges { log4cplus::Logger edgeLogger_; #if defined(USE_GPU) + + /// Cuda Stream for Edge Kernels + cudaStream_t stream; public: + + // Set GPU stream for edge kernels + + void SetStream(cudaStream_t pStream); + /// Allocate GPU memories to store all edges' states, /// and copy them from host to GPU memory. /// diff --git a/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp b/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp index 9527d87c2..3fb469cdf 100644 --- a/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp +++ b/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp @@ -89,6 +89,8 @@ void AllSTDPSynapses::allocDeviceStruct(AllSTDPSynapsesDeviceProperties &allEdge HANDLE_ERROR(cudaMalloc((void **)&allEdgesDevice.Apos_, maxTotalSynapses * sizeof(BGFLOAT))); HANDLE_ERROR(cudaMalloc((void **)&allEdgesDevice.mupos_, maxTotalSynapses * sizeof(BGFLOAT))); HANDLE_ERROR(cudaMalloc((void **)&allEdgesDevice.muneg_, maxTotalSynapses * sizeof(BGFLOAT))); + + } /// Delete GPU memories. @@ -268,10 +270,10 @@ void AllSTDPSynapses::advanceEdges(void *allEdgesDevice, void *allVerticesDevice const int threadsPerBlock = 256; int blocksPerGrid = (totalEdgeCount_ + threadsPerBlock - 1) / threadsPerBlock; // Advance synapses -------------> - advanceSTDPSynapsesDevice<<>>( + advanceSTDPSynapsesDevice<<>>( totalEdgeCount_, (EdgeIndexMapDevice *)edgeIndexMapDevice, g_simulationStep, Simulator::getInstance().getDeltaT(), (AllSTDPSynapsesDeviceProperties *)allEdgesDevice, - (AllSpikingNeuronsDeviceProperties *)allVerticesDevice, maxSpikes); + (AllSpikingNeuronsDeviceProperties *)allVerticesDevice, maxSpikes); } /// Set synapse class ID defined by enumClassSynapses for the caller's Synapse class. diff --git a/Simulator/Edges/Neuro/AllSpikingSynapses.h b/Simulator/Edges/Neuro/AllSpikingSynapses.h index 0cd04821b..d55c93c9f 100644 --- a/Simulator/Edges/Neuro/AllSpikingSynapses.h +++ b/Simulator/Edges/Neuro/AllSpikingSynapses.h @@ -29,6 +29,10 @@ #include #include +#ifdef USE_GPU +#include +#endif + struct AllSpikingSynapsesDeviceProperties; using fpPreSynapsesSpikeHit_t = void (*)(const BGSIZE, AllSpikingSynapsesDeviceProperties *); @@ -170,7 +174,7 @@ class AllSpikingSynapses : public AllNeuroEdges { /// @param allVerticesDevice GPU address of the allNeurons struct on device memory. /// @param edgeIndexMapDevice GPU address of the EdgeIndexMap on device memory. virtual void advanceEdges(void *allEdgesDevice, void *allVerticesDevice, - void *edgeIndexMapDevice) override; + void *edgeIndexMapDevice ) override; /// Set some parameters used for advanceEdgesDevice. /// Currently we set a member variable: m_fpChangePSR_h. diff --git a/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp b/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp index 94364e4a2..02dc839e4 100644 --- a/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp +++ b/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp @@ -85,6 +85,7 @@ void AllSpikingSynapses::allocDeviceStruct(AllSpikingSynapsesDeviceProperties &a HANDLE_ERROR(cudaMalloc((void **)&allEdgesDevice.delayIndex_, maxTotalSynapses * sizeof(int))); HANDLE_ERROR( cudaMalloc((void **)&allEdgesDevice.delayQueueLength_, maxTotalSynapses * sizeof(int))); + } /// Delete GPU memories. @@ -330,9 +331,10 @@ void AllSpikingSynapses::advanceEdges(void *allEdgesDevice, void *allVerticesDev const int threadsPerBlock = 256; int blocksPerGrid = (totalEdgeCount_ + threadsPerBlock - 1) / threadsPerBlock; // Advance synapses -------------> - advanceSpikingSynapsesDevice<<>>( + advanceSpikingSynapsesDevice<<>>( totalEdgeCount_, (EdgeIndexMapDevice *)edgeIndexMapDevice, g_simulationStep, Simulator::getInstance().getDeltaT(), (AllSpikingSynapsesDeviceProperties *)allEdgesDevice); + } /// Prints GPU SynapsesProps data. diff --git a/Simulator/Utils/RNG/AsyncMT_d.cu b/Simulator/Utils/RNG/AsyncMT_d.cu index bb2424e1f..777cb96d2 100644 --- a/Simulator/Utils/RNG/AsyncMT_d.cu +++ b/Simulator/Utils/RNG/AsyncMT_d.cu @@ -1,8 +1,10 @@ #include "AsyncMT_d.h" #include -#include #include #include + +#include "NvtxHelper.h" + // __global__ void generateKernel(curandStateMtgp32 *state, float *output, int samplesPerGen) // { // int tid = threadIdx.x; @@ -51,47 +53,49 @@ void AsyncMT_d::loadAsyncMT(int samplesPerSegment, unsigned long seed) { // hostBuffer = nullptr; // cudaHostAlloc(&hostBuffer, samplesPerSegment * sizeof(float), cudaHostAllocDefault); - // logfile = std::fopen("philox_output.bin", "wb"); + // logfile = std::fopen("philox_output_32_10.bin", "wb"); //consoleLogger_ = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("console")); segmentSize = samplesPerSegment; seed = seed; currentBuffer = 0; segmentIndex = 0; - totalSegments = 10000; // Each buffer has 10000 segments + + totalSegments = 10; + +#ifdef ENABLE_NVTX + nvtxMarker = 10000 / totalSegments; // make a marker every nvtxMarker buffer fills; + nvtxCurrentMarker = nvtxMarker; // count down to color flip +#endif bufferSize = segmentSize * totalSegments; - numBlocks = 50; //placeholder num of blocks - numThreads = 256; + numBlocks = 64; //placeholder num of blocks + numThreads = 64; totalThreads = numThreads * numBlocks; + int leastPriority, greatestPriority; + HANDLE_ERROR(cudaDeviceGetStreamPriorityRange(&leastPriority, &greatestPriority)); + // └─ leastPriority is the numerically largest value → lowest actual priority + // └─ greatestPriority is the numerically smallest value → highest actual priority + + HANDLE_ERROR(cudaStreamCreateWithPriority(&stream, + cudaStreamNonBlocking, + leastPriority)); + + // Create internal stream - HANDLE_ERROR(cudaStreamCreate(&stream)); + // HANDLE_ERROR(cudaStreamCreate(&stream)); // Allocate two large buffers HANDLE_ERROR(cudaMalloc(&buffers[0], bufferSize * sizeof(float))); HANDLE_ERROR(cudaMalloc(&buffers[1], bufferSize * sizeof(float))); - // // Allocate state and param memory - // HANDLE_ERROR(cudaMalloc(&d_states, numGenerators * sizeof(curandStateMtgp32))); - // HANDLE_ERROR(cudaMalloc(&d_params, numGenerators * sizeof(mtgp32_kernel_params_t))); - - HANDLE_ERROR(cudaMalloc(&spStates, totalThreads * sizeof(curandStatePhilox4_32_10_t))); initPhilox<<>>(spStates,seed,totalThreads); - // Create local param buffer of correct type - // mtgp32_kernel_params_t *h_params = new mtgp32_kernel_params_t[numGenerators]; - // curandMakeMTGP32Constants(mtgp32dc_params_fast_11213, h_params); - // HANDLE_ERROR(cudaMemcpy(d_params, h_params, numGenerators * sizeof(mtgp32_kernel_params_t), - // cudaMemcpyHostToDevice)); - // delete[] h_params; - - // curandMakeMTGP32KernelState(d_states, mtgp32dc_params_fast_11213, d_params, numGenerators, seed); - // Pre-fill both buffers fillBuffer(0); fillBuffer(1); @@ -114,8 +118,27 @@ float *AsyncMT_d::requestSegment() { //LOG4CPLUS_TRACE(consoleLogger_, "request segment"); //auto start = std::chrono::high_resolution_clock::now(); + static bool flipColor; if (segmentIndex >= totalSegments) { // Switch buffer and launch async refill on the now-unused one + + #ifdef ENABLE_NVTX + if(nvtxCurrentMarker <= 0){ + nvtxPop(); + if(flipColor == true) + nvtxPushColor("10,000 time steps", Color::RED); + else + nvtxPushColor("10,000 time steps", Color::BLUE); + + flipColor = !flipColor; + nvtxCurrentMarker = nvtxMarker; + } + else + --nvtxCurrentMarker; + #endif + + + int refillBuffer = currentBuffer; currentBuffer = 1 - currentBuffer; segmentIndex = 0; diff --git a/Simulator/Utils/RNG/AsyncMT_d.h b/Simulator/Utils/RNG/AsyncMT_d.h index 711a7e88a..55d926b05 100644 --- a/Simulator/Utils/RNG/AsyncMT_d.h +++ b/Simulator/Utils/RNG/AsyncMT_d.h @@ -1,10 +1,6 @@ #pragma once - -#include #include -#include -#include -#include // Precomputed parameter table +#include #include #include "Book.h" #include @@ -27,6 +23,11 @@ class AsyncMT_d { int bufferSize; unsigned long seed; + #ifdef ENABLE_NVTX + int nvtxMarker; + int nvtxCurrentMarker; + #endif + cudaStream_t stream; @@ -36,10 +37,6 @@ class AsyncMT_d { curandStatePhilox4_32_10_t* spStates; - - //curandStateMtgp32 *d_states; - //mtgp32_kernel_params_t *d_params; - // FILE* logfile; // float* hostBuffer; log4cplus::Logger diff --git a/Simulator/Utils/RNG/MersenneTwister_d.cu b/Simulator/Utils/RNG/MersenneTwister_d.cu index e90151495..67c61b155 100644 --- a/Simulator/Utils/RNG/MersenneTwister_d.cu +++ b/Simulator/Utils/RNG/MersenneTwister_d.cu @@ -35,7 +35,8 @@ #include #include - +#include +#include using namespace std; #include "MersenneTwister_d.h" @@ -57,6 +58,8 @@ unsigned int mt_blocks; unsigned int mt_threads; unsigned int mt_nPerRng; + + //Load twister configurations void loadMTGPU(const char *fname){ FILE *fd = fopen(fname, "rb"); @@ -242,12 +245,24 @@ __global__ void RandomNormGPU( ds_MT[tid].iState = iState; } + + + extern "C" void uniformMTGPU(float * d_random){ RandomGPU<<>>(d_random, mt_nPerRng, mt_rng_count); } + + float* hostBuffer = nullptr; + FILE* logfile; + + extern "C" void normalMTGPU(float * d_random){ - RandomNormGPU<<>>(d_random, mt_nPerRng, mt_rng_count); + RandomNormGPU<<>>(d_random, mt_nPerRng, mt_rng_count); + + cudaMemcpy(hostBuffer, d_random, mt_rng_count*mt_nPerRng * sizeof(float), cudaMemcpyDeviceToHost); + std::fwrite(hostBuffer, sizeof(float), mt_rng_count*mt_nPerRng, logfile); + } //initialize globals and setup state @@ -260,5 +275,15 @@ extern "C" void initMTGPU(unsigned int seed, unsigned int blocks, unsigned int t loadMTGPU(MT_DATAFILE); seedMTGPU(seed); + + cudaHostAlloc(&hostBuffer, mt_rng_count*nPerRng * sizeof(float), cudaHostAllocDefault); + logfile = std::fopen("mt_old_output.bin", "wb"); + +} + +extern "C" void closeFileMT(){ + + std::fclose(logfile); + cudaFree(hostBuffer); } diff --git a/Simulator/Vertices/AllVertices.cpp b/Simulator/Vertices/AllVertices.cpp index 107848c34..6f4d82562 100644 --- a/Simulator/Vertices/AllVertices.cpp +++ b/Simulator/Vertices/AllVertices.cpp @@ -49,3 +49,11 @@ void AllVertices::loadEpochInputs(uint64_t currentStep, uint64_t endStep) // This is an empty implementation so that Neural Network simulation works // normally } + + +#ifdef USE_GPU +// Set Cuda Stream for Vertices kernels +void AllVertices::SetStream(cudaStream_t pStream){ + stream = pStream; +} +#endif \ No newline at end of file diff --git a/Simulator/Vertices/AllVertices.h b/Simulator/Vertices/AllVertices.h index d005c689c..581b9180a 100644 --- a/Simulator/Vertices/AllVertices.h +++ b/Simulator/Vertices/AllVertices.h @@ -32,6 +32,9 @@ using namespace std; #include // cereal #include "cereal/types/vector.hpp" +#ifdef USE_GPU +#include +#endif class Layout; class AllEdges; @@ -92,7 +95,14 @@ class AllVertices { log4cplus::Logger vertexLogger_; // Logs to Output/Debug/neurons.txt #if defined(USE_GPU) + /// Cuda Stream for Vertices Kernels + cudaStream_t stream; public: + + // Set GPU stream for Vertices kernels + + void SetStream(cudaStream_t pStream); + /// Allocate GPU memories to store all vertices' states, /// and copy them from host to GPU memory. /// diff --git a/Simulator/Vertices/Neuro/AllIFNeurons.h b/Simulator/Vertices/Neuro/AllIFNeurons.h index b199dd32d..fdb216f74 100644 --- a/Simulator/Vertices/Neuro/AllIFNeurons.h +++ b/Simulator/Vertices/Neuro/AllIFNeurons.h @@ -25,6 +25,10 @@ #include "AllSpikingNeurons.h" #include "Global.h" +#ifdef USE_GPU +#include +#endif + // cereal #include #include diff --git a/Simulator/Vertices/Neuro/AllIFNeurons_d.cpp b/Simulator/Vertices/Neuro/AllIFNeurons_d.cpp index 572ff8b97..bbbdb2c14 100644 --- a/Simulator/Vertices/Neuro/AllIFNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllIFNeurons_d.cpp @@ -65,6 +65,7 @@ void AllIFNeurons::allocDeviceStruct(AllIFNeuronsDeviceProperties &allVerticesDe HANDLE_ERROR(cudaMalloc((void **)&allVerticesDevice.bufferEnd_, count * sizeof(int))); HANDLE_ERROR(cudaMalloc((void **)&allVerticesDevice.epochStart_, count * sizeof(int))); HANDLE_ERROR(cudaMalloc((void **)&allVerticesDevice.numElementsInEpoch_, count * sizeof(int))); + } /// Delete GPU memories. @@ -110,6 +111,7 @@ void AllIFNeurons::deleteDeviceStruct(AllIFNeuronsDeviceProperties &allVerticesD HANDLE_ERROR(cudaFree(allVerticesDevice.hasFired_)); HANDLE_ERROR(cudaFree(allVerticesDevice.numStepsInRefractoryPeriod_)); HANDLE_ERROR(cudaFree(allVerticesDevice.summationPoints_)); + #ifdef VALIDATION_MODE HANDLE_ERROR(cudaFree(allVerticesDevice.spValidation_)); #endif diff --git a/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp b/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp index 2e0c8d2a6..f001479ca 100644 --- a/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp @@ -195,7 +195,7 @@ void AllIZHNeurons::advanceVertices(AllEdges &synapses, void *allVerticesDevice, int blocksPerGrid = (vertex_count + threadsPerBlock - 1) / threadsPerBlock; // Advance neurons -------------> - advanceIZHNeuronsDevice<<>>( + advanceIZHNeuronsDevice<<>>( vertex_count, Simulator::getInstance().getMaxEdgesPerVertex(), maxSpikes, Simulator::getInstance().getDeltaT(), g_simulationStep, randNoise, (AllIZHNeuronsDeviceProperties *)allVerticesDevice, diff --git a/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp b/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp index 25fcdf3be..ed43480a7 100644 --- a/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp @@ -55,12 +55,13 @@ void AllLIFNeurons::advanceVertices(AllEdges &synapses, void *allVerticesDevice, int blocksPerGrid = (vertex_count + threadsPerBlock - 1) / threadsPerBlock; // Advance neurons -------------> - advanceLIFNeuronsDevice<<>>( + advanceLIFNeuronsDevice<<>>( vertex_count, Simulator::getInstance().getMaxEdgesPerVertex(), maxSpikes, Simulator::getInstance().getDeltaT(), g_simulationStep, randNoise, (AllIFNeuronsDeviceProperties *)allVerticesDevice, (AllSpikingSynapsesDeviceProperties *)allEdgesDevice, edgeIndexMapDevice, fAllowBackPropagation_); + } ///@} diff --git a/Simulator/Vertices/Neuro/AllSpikingNeurons.cpp b/Simulator/Vertices/Neuro/AllSpikingNeurons.cpp index a2769fa0a..dfcc08417 100644 --- a/Simulator/Vertices/Neuro/AllSpikingNeurons.cpp +++ b/Simulator/Vertices/Neuro/AllSpikingNeurons.cpp @@ -58,6 +58,8 @@ void AllSpikingNeurons::clearSpikeCounts() #if !defined(USE_GPU) + + /// Update internal state of the indexed Neuron (called by every simulation step). /// Notify outgoing synapses if neuron has fired. /// diff --git a/Simulator/Vertices/Neuro/AllSpikingNeurons.h b/Simulator/Vertices/Neuro/AllSpikingNeurons.h index 675370a26..cd9ceaf41 100644 --- a/Simulator/Vertices/Neuro/AllSpikingNeurons.h +++ b/Simulator/Vertices/Neuro/AllSpikingNeurons.h @@ -79,6 +79,7 @@ class AllSpikingNeurons : public AllVertices { /// /// @param allVerticesDevice GPU address of the allVertices struct on device memory. void clearDeviceSpikeCounts(AllSpikingNeuronsDeviceProperties &allVerticesDevice); + #else // !defined(USE_GPU) public: /// Update internal state of the indexed Neuron (called by every simulation step). diff --git a/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp b/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp index 1fbc9f492..155c435db 100644 --- a/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp @@ -32,6 +32,8 @@ __global__ void calcSummationPointDevice(int totalVertices, EdgeIndexMapDevice *edgeIndexMapDevice, AllSpikingSynapsesDeviceProperties *allEdgesDevice); + + void AllSpikingNeurons::copyToDevice(void *deviceAddress) { AllSpikingNeuronsDeviceProperties allVerticesDevice; @@ -197,7 +199,7 @@ void AllSpikingNeurons::integrateVertexInputs(void *allVerticesDevice, = (Simulator::getInstance().getTotalVertices() + threadsPerBlock - 1) / threadsPerBlock; int vertex_count = Simulator::getInstance().getTotalVertices(); - calcSummationPointDevice<<>>( + calcSummationPointDevice<<>>( vertex_count, (AllSpikingNeuronsDeviceProperties *)allVerticesDevice, edgeIndexMapDevice, (AllSpikingSynapsesDeviceProperties *)allEdgesDevice); } From e40cdf10a6ced29b873dada37004a3a8d1159d83 Mon Sep 17 00:00:00 2001 From: Andrew Madison Date: Thu, 5 Jun 2025 11:08:33 -0700 Subject: [PATCH 16/37] improved nvtx support --- CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 52311af99..b576e529a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -359,7 +359,7 @@ add_library(Matrix ${Matrix_Source}) # Create RNG library -file(GLOB RNG_Source Simulator/Utils/RNG/*.cpp Simulator/Utils/RNG/*.h Simulator/Utils/RNG/*.cu) +file(GLOB RNG_Source Simulator/Utils/RNG/*.cpp Simulator/Utils/RNG/*.h Simulator/Utils/RNG/*.cu ) # Remove demo from file list as it contains a main and it will cause compilation errors list(REMOVE_ITEM RNG_Source "${CMAKE_CURRENT_SOURCE_DIR}/Simulator/Utils/RNG/MersenneTwister_demo.cu") add_library(RNG STATIC ${RNG_Source}) @@ -391,9 +391,13 @@ add_library(Utils ${Utils_Source}) # Only link NVTX if it was found if(NVTX_LIBRARY) + + message(STATUS "Adding NVTX: to Utils lib") target_link_libraries(Utils PRIVATE ${NVTX_LIBRARY}) endif() +message(STATUS "Linking RNG against Utils") +target_link_libraries(RNG PRIVATE Utils) # Used to locate and run other CMakeLists.txt files from Third Party resources for further compilation of the project. add_subdirectory(ThirdParty) From a3dd0f27f50f06e918625b94380afe08262de5f8 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 5 Jun 2025 11:21:29 -0700 Subject: [PATCH 17/37] clang-format --- Simulator/Core/GPUModel.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index ed333ac79..898a658a1 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -55,10 +55,8 @@ void GPUModel::allocDeviceStruct(void **allVerticesDevice, void **allEdgesDevice // Allocate edge inverse map in device memory allocEdgeIndexMap(numVertices); - // Create gpu model stream + // Create gpu model stream HANDLE_ERROR(cudaStreamCreate(&stream)); - - } /// Copies device memories to host memories and deallocates them. @@ -81,7 +79,6 @@ void GPUModel::deleteDeviceStruct(void **allVerticesDevice, void **allEdgesDevic // HANDLE_ERROR(cudaFree(randNoise_d)); // closeFileMT(); HANDLE_ERROR(cudaStreamDestroy(stream)); - } /// Sets up the Simulation. @@ -106,7 +103,7 @@ void GPUModel::setupSim() //cout << "blocks, threads, nPerRng, rng_rng_count: " << rng_blocks << " " << rng_threads << " " << rng_nPerRng << " " << rng_mt_rng_count << endl; AsyncGenerator.loadAsyncMT(Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getNoiseRngSeed()); - + #ifdef PERFORMANCE_METRICS cudaEventCreate(&start); @@ -134,15 +131,13 @@ void GPUModel::setupSim() AllVertices &vertices = layout_->getVertices(); vertices.SetStream(stream); edges.SetStream(stream); - - } /// Performs any finalization tasks on network following a simulation. void GPUModel::finish() { // deallocates memories on CUDA device - AsyncGenerator.deleteDeviceStruct(); + AsyncGenerator.deleteDeviceStruct(); deleteDeviceStruct((void **)&allVerticesDevice_, (void **)&allEdgesDevice_); deleteEdgeIndexMap(); @@ -271,7 +266,7 @@ void GPUModel::updateConnections() // Update Connections data if (connections_->updateConnections(vertices)) { connections_->updateEdgesWeights(Simulator::getInstance().getTotalVertices(), vertices, edges, - allVerticesDevice_, allEdgesDevice_, getLayout(),stream); + allVerticesDevice_, allEdgesDevice_, getLayout(), stream); // create edge index map connections_->createEdgeIndexMap(); // copy index map to the device memory From 339bca4ec0f1a38846e89d31862e44f3a68f6fb4 Mon Sep 17 00:00:00 2001 From: Andrew Madison Date: Mon, 9 Jun 2025 21:29:15 -0700 Subject: [PATCH 18/37] comments and renaming --- Simulator/Core/GPUModel.cpp | 13 +---- Simulator/Core/GPUModel.h | 4 +- .../RNG/{AsyncMT_d.cu => AsyncPhilox_d.cu} | 54 ++++++++----------- .../RNG/{AsyncMT_d.h => AsyncPhilox_d.h} | 24 +++++++-- 4 files changed, 43 insertions(+), 52 deletions(-) rename Simulator/Utils/RNG/{AsyncMT_d.cu => AsyncPhilox_d.cu} (84%) rename Simulator/Utils/RNG/{AsyncMT_d.h => AsyncPhilox_d.h} (57%) diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index 898a658a1..429273182 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -101,7 +101,7 @@ void GPUModel::setupSim() // initMTGPU(Simulator::getInstance().getNoiseRngSeed(), rng_blocks, rng_threads, rng_nPerRng, // rng_mt_rng_count); //cout << "blocks, threads, nPerRng, rng_rng_count: " << rng_blocks << " " << rng_threads << " " << rng_nPerRng << " " << rng_mt_rng_count << endl; - AsyncGenerator.loadAsyncMT(Simulator::getInstance().getTotalVertices(), + AsyncGenerator.loadAsyncPhilox(Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getNoiseRngSeed()); @@ -176,17 +176,6 @@ void GPUModel::advance() #else // normalMTGPU(randNoise_d); randNoise_d = AsyncGenerator.requestSegment(); - // int verts = Simulator::getInstance().getTotalVertices(); - // float* h_data = new float[verts]; - // cudaDeviceSynchronize(); - // HANDLE_ERROR(cudaMemcpy(h_data, randNoise_d, verts * sizeof(float), cudaMemcpyDeviceToHost)); - // log4cplus::Logger vertexLogger_ = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("vertex")); - // for(int i=0; i< verts; i++){ - // LOG4CPLUS_DEBUG(vertexLogger_, endl - // << "Rand Index[" << i << "] :: Noise = " - // << h_data[i]); - // } - // delete[] h_data; #endif //LOG4CPLUS_DEBUG(vertexLogger_, "Index: " << index << " Vm: " << Vm); #ifdef PERFORMANCE_METRICS diff --git a/Simulator/Core/GPUModel.h b/Simulator/Core/GPUModel.h index b679eb97c..32df3612e 100644 --- a/Simulator/Core/GPUModel.h +++ b/Simulator/Core/GPUModel.h @@ -23,7 +23,7 @@ #include "AllEdges.h" #include "AllVertices.h" -#include "AsyncMT_d.h" +#include "AsyncPhilox_d.h" #include #ifdef VALIDATION_MODE @@ -105,7 +105,7 @@ class GPUModel : public Model { /// Pointer to device random noise array. float *randNoise_d; - AsyncMT_d AsyncGenerator; + AsyncPhilox_d AsyncGenerator; float *randNoise_h; /// Cuda Stream for kernel use cudaStream_t stream; diff --git a/Simulator/Utils/RNG/AsyncMT_d.cu b/Simulator/Utils/RNG/AsyncPhilox_d.cu similarity index 84% rename from Simulator/Utils/RNG/AsyncMT_d.cu rename to Simulator/Utils/RNG/AsyncPhilox_d.cu index 777cb96d2..9e430ec3e 100644 --- a/Simulator/Utils/RNG/AsyncMT_d.cu +++ b/Simulator/Utils/RNG/AsyncPhilox_d.cu @@ -1,24 +1,24 @@ -#include "AsyncMT_d.h" +/** + * @file AsyncPhilox_d.cu + * + * @ingroup Simulator/Utils/RNG + * + * @brief Asynchronous Philox RNG using curand to fill GPU buffers + * + * AsyncPhilox_d class maintains two large GPU buffers for noise. + * GPUModel calls loadAsyncPhilox to initialize states and + * fill the buffers, then, each advance requestSegment + * returns a float* slice of a buffer for use in + * advanceVertices + */ + +#include "AsyncPhilox_d.h" #include #include #include #include "NvtxHelper.h" -// __global__ void generateKernel(curandStateMtgp32 *state, float *output, int samplesPerGen) -// { -// int tid = threadIdx.x; -// int gen_id = blockIdx.x; -// if (gen_id >= gridDim.x) -// return; - -// curandStateMtgp32 localState = state[gen_id]; -// for (int i = tid; i < samplesPerGen; i += blockDim.x) { -// output[gen_id * samplesPerGen + i] = curand_normal(&localState); -// } -// state[gen_id] = localState; -// } - __global__ void generatePhilox(curandStatePhilox4_32_10_t* states, float* output,int bufferSize) { // Compute a unique global index for this thread @@ -40,16 +40,13 @@ __global__ void generatePhilox(curandStatePhilox4_32_10_t* states, float* output states[gid] = local; } - - - __global__ void initPhilox(curandStatePhilox4_32_10_t* states, unsigned long seed,int totalThreads) { int gid = blockIdx.x * blockDim.x + threadIdx.x; if (gid >= totalThreads) return; curand_init(seed, gid, 0, &states[gid]); } -void AsyncMT_d::loadAsyncMT(int samplesPerSegment, unsigned long seed) +void AsyncPhilox_d::loadAsyncPhilox(int samplesPerSegment, unsigned long seed) { // hostBuffer = nullptr; // cudaHostAlloc(&hostBuffer, samplesPerSegment * sizeof(float), cudaHostAllocDefault); @@ -70,24 +67,18 @@ void AsyncMT_d::loadAsyncMT(int samplesPerSegment, unsigned long seed) numBlocks = 64; //placeholder num of blocks numThreads = 64; - - totalThreads = numThreads * numBlocks; - int leastPriority, greatestPriority; HANDLE_ERROR(cudaDeviceGetStreamPriorityRange(&leastPriority, &greatestPriority)); // └─ leastPriority is the numerically largest value → lowest actual priority // └─ greatestPriority is the numerically smallest value → highest actual priority + // Create internal stream HANDLE_ERROR(cudaStreamCreateWithPriority(&stream, cudaStreamNonBlocking, leastPriority)); - - // Create internal stream - // HANDLE_ERROR(cudaStreamCreate(&stream)); - // Allocate two large buffers HANDLE_ERROR(cudaMalloc(&buffers[0], bufferSize * sizeof(float))); HANDLE_ERROR(cudaMalloc(&buffers[1], bufferSize * sizeof(float))); @@ -101,7 +92,7 @@ void AsyncMT_d::loadAsyncMT(int samplesPerSegment, unsigned long seed) fillBuffer(1); HANDLE_ERROR(cudaStreamSynchronize(stream)); //wait for both buffers to be filled before the first request } -void AsyncMT_d::deleteDeviceStruct(){ +void AsyncPhilox_d::deleteDeviceStruct(){ // std::fclose(logfile); // cudaFree(hostBuffer); HANDLE_ERROR(cudaFree(buffers[0])); @@ -110,11 +101,11 @@ void AsyncMT_d::deleteDeviceStruct(){ HANDLE_ERROR(cudaStreamDestroy(stream)); } -AsyncMT_d::~AsyncMT_d() +AsyncPhilox_d::~AsyncPhilox_d() { } -float *AsyncMT_d::requestSegment() +float *AsyncPhilox_d::requestSegment() { //LOG4CPLUS_TRACE(consoleLogger_, "request segment"); //auto start = std::chrono::high_resolution_clock::now(); @@ -136,15 +127,12 @@ float *AsyncMT_d::requestSegment() else --nvtxCurrentMarker; #endif - - int refillBuffer = currentBuffer; currentBuffer = 1 - currentBuffer; segmentIndex = 0; cudaStreamSynchronize(stream); // Ensure refillBuffer is done fillBuffer(refillBuffer); - //cudaStreamSynchronize(stream); } float *segmentPtr = buffers[currentBuffer] + segmentIndex * segmentSize; @@ -158,7 +146,7 @@ float *AsyncMT_d::requestSegment() return segmentPtr; } -void AsyncMT_d::fillBuffer(int bufferIndex) +void AsyncPhilox_d::fillBuffer(int bufferIndex) { //LOG4CPLUS_TRACE(consoleLogger_, "filling buffer:"); generatePhilox<<>>(spStates, buffers[bufferIndex], bufferSize); diff --git a/Simulator/Utils/RNG/AsyncMT_d.h b/Simulator/Utils/RNG/AsyncPhilox_d.h similarity index 57% rename from Simulator/Utils/RNG/AsyncMT_d.h rename to Simulator/Utils/RNG/AsyncPhilox_d.h index 55d926b05..6771b1021 100644 --- a/Simulator/Utils/RNG/AsyncMT_d.h +++ b/Simulator/Utils/RNG/AsyncPhilox_d.h @@ -1,3 +1,17 @@ +/** + * @file AsyncPhilox_d.h + * + * @ingroup Simulator/Utils/RNG + * + * @brief Asynchronous Philox RNG using curand to fill GPU buffers + * + * AsyncPhilox_d class maintains two large GPU buffers for noise. + * GPUModel calls loadAsyncPhilox to initialize states and + * fill the buffers, then, each advance requestSegment + * returns a float* slice of a buffer for use in + * advanceVertices + */ + #pragma once #include #include @@ -5,12 +19,12 @@ #include "Book.h" #include #include -class AsyncMT_d { +class AsyncPhilox_d { public: - AsyncMT_d() = default; - AsyncMT_d(int samplesPerGen, unsigned long seed); - ~AsyncMT_d(); - void loadAsyncMT(int samplesPerSegment, unsigned long seed); + AsyncPhilox_d() = default; + AsyncPhilox_d(int samplesPerGen, unsigned long seed); + ~AsyncPhilox_d(); + void loadAsyncPhilox(int samplesPerSegment, unsigned long seed); void deleteDeviceStruct(); float *requestSegment(); From 00c418ecfcc54215accbcee56110a93132df8a20 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 9 Jun 2025 22:00:47 -0700 Subject: [PATCH 19/37] clang --- Simulator/Core/GPUModel.cpp | 2 +- Simulator/Utils/RNG/AsyncPhilox_d.cu | 82 ++++++++++++++-------------- Simulator/Utils/RNG/AsyncPhilox_d.h | 12 ++-- 3 files changed, 48 insertions(+), 48 deletions(-) diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index 429273182..cf600261b 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -102,7 +102,7 @@ void GPUModel::setupSim() // rng_mt_rng_count); //cout << "blocks, threads, nPerRng, rng_rng_count: " << rng_blocks << " " << rng_threads << " " << rng_nPerRng << " " << rng_mt_rng_count << endl; AsyncGenerator.loadAsyncPhilox(Simulator::getInstance().getTotalVertices(), - Simulator::getInstance().getNoiseRngSeed()); + Simulator::getInstance().getNoiseRngSeed()); #ifdef PERFORMANCE_METRICS diff --git a/Simulator/Utils/RNG/AsyncPhilox_d.cu b/Simulator/Utils/RNG/AsyncPhilox_d.cu index 9e430ec3e..ffc93f945 100644 --- a/Simulator/Utils/RNG/AsyncPhilox_d.cu +++ b/Simulator/Utils/RNG/AsyncPhilox_d.cu @@ -13,37 +13,38 @@ */ #include "AsyncPhilox_d.h" +#include "NvtxHelper.h" #include -#include #include +#include -#include "NvtxHelper.h" - -__global__ void generatePhilox(curandStatePhilox4_32_10_t* states, float* output,int bufferSize) +__global__ void generatePhilox(curandStatePhilox4_32_10_t *states, float *output, int bufferSize) { - // Compute a unique global index for this thread - int threadId = threadIdx.x; - int blockId = blockIdx.x; - int threadsPerBlock = blockDim.x; - int totalThreads = gridDim.x * threadsPerBlock; - int gid = blockId * threadsPerBlock + threadId; - - // Load this thread’s Philox state - curandStatePhilox4_32_10_t local = states[gid]; - - // Stride‐loop: write one random per iteration until we cover bufferSize - for (int idx = gid; idx < bufferSize; idx += totalThreads) { - output[idx] = curand_normal(&local); - } - - // Store back the updated state - states[gid] = local; + // Compute a unique global index for this thread + int threadId = threadIdx.x; + int blockId = blockIdx.x; + int threadsPerBlock = blockDim.x; + int totalThreads = gridDim.x * threadsPerBlock; + int gid = blockId * threadsPerBlock + threadId; + + // Load this thread’s Philox state + curandStatePhilox4_32_10_t local = states[gid]; + + // Stride‐loop: write one random per iteration until we cover bufferSize + for (int idx = gid; idx < bufferSize; idx += totalThreads) { + output[idx] = curand_normal(&local); + } + + // Store back the updated state + states[gid] = local; } -__global__ void initPhilox(curandStatePhilox4_32_10_t* states, unsigned long seed,int totalThreads) { - int gid = blockIdx.x * blockDim.x + threadIdx.x; - if (gid >= totalThreads) return; - curand_init(seed, gid, 0, &states[gid]); +__global__ void initPhilox(curandStatePhilox4_32_10_t *states, unsigned long seed, int totalThreads) +{ + int gid = blockIdx.x * blockDim.x + threadIdx.x; + if (gid >= totalThreads) + return; + curand_init(seed, gid, 0, &states[gid]); } void AsyncPhilox_d::loadAsyncPhilox(int samplesPerSegment, unsigned long seed) @@ -57,11 +58,11 @@ void AsyncPhilox_d::loadAsyncPhilox(int samplesPerSegment, unsigned long seed) currentBuffer = 0; segmentIndex = 0; - totalSegments = 10; + totalSegments = 10; #ifdef ENABLE_NVTX - nvtxMarker = 10000 / totalSegments; // make a marker every nvtxMarker buffer fills; - nvtxCurrentMarker = nvtxMarker; // count down to color flip + nvtxMarker = 10000 / totalSegments; // make a marker every nvtxMarker buffer fills; + nvtxCurrentMarker = nvtxMarker; // count down to color flip #endif bufferSize = segmentSize * totalSegments; numBlocks = 64; //placeholder num of blocks @@ -75,9 +76,7 @@ void AsyncPhilox_d::loadAsyncPhilox(int samplesPerSegment, unsigned long seed) // └─ greatestPriority is the numerically smallest value → highest actual priority // Create internal stream - HANDLE_ERROR(cudaStreamCreateWithPriority(&stream, - cudaStreamNonBlocking, - leastPriority)); + HANDLE_ERROR(cudaStreamCreateWithPriority(&stream, cudaStreamNonBlocking, leastPriority)); // Allocate two large buffers HANDLE_ERROR(cudaMalloc(&buffers[0], bufferSize * sizeof(float))); @@ -85,14 +84,16 @@ void AsyncPhilox_d::loadAsyncPhilox(int samplesPerSegment, unsigned long seed) HANDLE_ERROR(cudaMalloc(&spStates, totalThreads * sizeof(curandStatePhilox4_32_10_t))); - initPhilox<<>>(spStates,seed,totalThreads); + initPhilox<<>>(spStates, seed, totalThreads); // Pre-fill both buffers fillBuffer(0); fillBuffer(1); - HANDLE_ERROR(cudaStreamSynchronize(stream)); //wait for both buffers to be filled before the first request + HANDLE_ERROR(cudaStreamSynchronize( + stream)); //wait for both buffers to be filled before the first request } -void AsyncPhilox_d::deleteDeviceStruct(){ +void AsyncPhilox_d::deleteDeviceStruct() +{ // std::fclose(logfile); // cudaFree(hostBuffer); HANDLE_ERROR(cudaFree(buffers[0])); @@ -113,20 +114,19 @@ float *AsyncPhilox_d::requestSegment() if (segmentIndex >= totalSegments) { // Switch buffer and launch async refill on the now-unused one - #ifdef ENABLE_NVTX - if(nvtxCurrentMarker <= 0){ +#ifdef ENABLE_NVTX + if (nvtxCurrentMarker <= 0) { nvtxPop(); - if(flipColor == true) + if (flipColor == true) nvtxPushColor("10,000 time steps", Color::RED); else nvtxPushColor("10,000 time steps", Color::BLUE); flipColor = !flipColor; - nvtxCurrentMarker = nvtxMarker; - } - else + nvtxCurrentMarker = nvtxMarker; + } else --nvtxCurrentMarker; - #endif +#endif int refillBuffer = currentBuffer; currentBuffer = 1 - currentBuffer; diff --git a/Simulator/Utils/RNG/AsyncPhilox_d.h b/Simulator/Utils/RNG/AsyncPhilox_d.h index 6771b1021..28a8a90e0 100644 --- a/Simulator/Utils/RNG/AsyncPhilox_d.h +++ b/Simulator/Utils/RNG/AsyncPhilox_d.h @@ -13,12 +13,12 @@ */ #pragma once -#include -#include -#include #include "Book.h" #include #include +#include +#include +#include class AsyncPhilox_d { public: AsyncPhilox_d() = default; @@ -37,10 +37,10 @@ class AsyncPhilox_d { int bufferSize; unsigned long seed; - #ifdef ENABLE_NVTX +#ifdef ENABLE_NVTX int nvtxMarker; int nvtxCurrentMarker; - #endif +#endif cudaStream_t stream; @@ -49,7 +49,7 @@ class AsyncPhilox_d { int currentBuffer; int segmentIndex; - curandStatePhilox4_32_10_t* spStates; + curandStatePhilox4_32_10_t *spStates; // FILE* logfile; // float* hostBuffer; From 51da776d95e0bb5a948240d876c713c9249929fe Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 9 Jun 2025 22:15:32 -0700 Subject: [PATCH 20/37] cleaned up changes --- CMakeLists.txt | 2 +- Simulator/Connections/Neuro/ConnGrowth.cpp | 1 - Simulator/Core/GPUModel.cpp | 1 - Simulator/Core/GPUModel.h | 15 +++++++-------- Simulator/Edges/Neuro/AllSpikingSynapses.h | 5 +---- Simulator/Vertices/Neuro/AllSpikingNeurons.cpp | 2 -- Simulator/Vertices/Neuro/AllSpikingNeurons.h | 1 - .../Data/MersenneTwister_16384.dat | Bin 262144 -> 0 bytes 8 files changed, 9 insertions(+), 18 deletions(-) delete mode 100644 build/RuntimeFiles/Data/MersenneTwister_16384.dat diff --git a/CMakeLists.txt b/CMakeLists.txt index 7674ff854..120267b31 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -359,7 +359,7 @@ add_library(Matrix ${Matrix_Source}) # Create RNG library -file(GLOB RNG_Source Simulator/Utils/RNG/*.cpp Simulator/Utils/RNG/*.h Simulator/Utils/RNG/*.cu ) +file(GLOB RNG_Source Simulator/Utils/RNG/*.cpp Simulator/Utils/RNG/*.h Simulator/Utils/RNG/*.cu) # Remove demo from file list as it contains a main and it will cause compilation errors list(REMOVE_ITEM RNG_Source "${CMAKE_CURRENT_SOURCE_DIR}/Simulator/Utils/RNG/MersenneTwister_demo.cu") add_library(RNG STATIC ${RNG_Source}) diff --git a/Simulator/Connections/Neuro/ConnGrowth.cpp b/Simulator/Connections/Neuro/ConnGrowth.cpp index 3a60930af..beaad1660 100644 --- a/Simulator/Connections/Neuro/ConnGrowth.cpp +++ b/Simulator/Connections/Neuro/ConnGrowth.cpp @@ -75,7 +75,6 @@ void ConnGrowth::setup() // Initialize connection frontier distance change matrix with the current distances Layout &layout = Simulator::getInstance().getModel().getLayout(); delta_ = layout.dist_; - // Register VertorMatrix radii_ for Recording if need // Recorder &recorder = Simulator::getInstance().getModel().getRecorder(); diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index cf600261b..8aa17fea1 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -12,7 +12,6 @@ #include "AllVertices.h" #include "Connections.h" #include "Global.h" -#include #ifdef VALIDATION_MODE #include "AllIFNeurons.h" #include "OperationManager.h" diff --git a/Simulator/Core/GPUModel.h b/Simulator/Core/GPUModel.h index 32df3612e..df50a4835 100644 --- a/Simulator/Core/GPUModel.h +++ b/Simulator/Core/GPUModel.h @@ -145,11 +145,10 @@ class GPUModel : public Model { Coordinate dest, BGFLOAT deltaT, edgeType type); }; -#if defined(__CUDACC__) -extern "C" { -void normalMTGPU(float *randNoise_d); -void initMTGPU(unsigned int seed, unsigned int blocks, unsigned int threads, unsigned int nPerRng, - unsigned int mt_rng_count); -void closeFileMT(); -} -#endif \ No newline at end of file +// #if defined(__CUDACC__) +// extern "C" { +// void normalMTGPU(float *randNoise_d); +// void initMTGPU(unsigned int seed, unsigned int blocks, unsigned int threads, unsigned int nPerRng, +// unsigned int mt_rng_count); +// } +// #endif \ No newline at end of file diff --git a/Simulator/Edges/Neuro/AllSpikingSynapses.h b/Simulator/Edges/Neuro/AllSpikingSynapses.h index d55c93c9f..4e464732c 100644 --- a/Simulator/Edges/Neuro/AllSpikingSynapses.h +++ b/Simulator/Edges/Neuro/AllSpikingSynapses.h @@ -29,9 +29,6 @@ #include #include -#ifdef USE_GPU -#include -#endif struct AllSpikingSynapsesDeviceProperties; @@ -174,7 +171,7 @@ class AllSpikingSynapses : public AllNeuroEdges { /// @param allVerticesDevice GPU address of the allNeurons struct on device memory. /// @param edgeIndexMapDevice GPU address of the EdgeIndexMap on device memory. virtual void advanceEdges(void *allEdgesDevice, void *allVerticesDevice, - void *edgeIndexMapDevice ) override; + void *edgeIndexMapDevice) override; /// Set some parameters used for advanceEdgesDevice. /// Currently we set a member variable: m_fpChangePSR_h. diff --git a/Simulator/Vertices/Neuro/AllSpikingNeurons.cpp b/Simulator/Vertices/Neuro/AllSpikingNeurons.cpp index dfcc08417..a2769fa0a 100644 --- a/Simulator/Vertices/Neuro/AllSpikingNeurons.cpp +++ b/Simulator/Vertices/Neuro/AllSpikingNeurons.cpp @@ -58,8 +58,6 @@ void AllSpikingNeurons::clearSpikeCounts() #if !defined(USE_GPU) - - /// Update internal state of the indexed Neuron (called by every simulation step). /// Notify outgoing synapses if neuron has fired. /// diff --git a/Simulator/Vertices/Neuro/AllSpikingNeurons.h b/Simulator/Vertices/Neuro/AllSpikingNeurons.h index 2922ce231..1d1b35726 100644 --- a/Simulator/Vertices/Neuro/AllSpikingNeurons.h +++ b/Simulator/Vertices/Neuro/AllSpikingNeurons.h @@ -80,7 +80,6 @@ class AllSpikingNeurons : public AllVertices { /// /// @param allVerticesDevice GPU address of the allVertices struct on device memory. void clearDeviceSpikeCounts(AllSpikingNeuronsDeviceProperties &allVerticesDevice); - #else // !defined(USE_GPU) public: /// Update internal state of the indexed Neuron (called by every simulation step). diff --git a/build/RuntimeFiles/Data/MersenneTwister_16384.dat b/build/RuntimeFiles/Data/MersenneTwister_16384.dat deleted file mode 100644 index ac041c512767b88a0d0566ef55ba75394ebfabe7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 262144 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
N$dmgi%Q=5bXVq=o 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%0Dl{ zr%HQEd@Z$ppACHp{d1A?^Zm$_2Vmc5Uzb}2hqdADhT#1uxju!TXv8ZLIp0ca(J0)f z2Y9l{udA&5Vv+P2#t$a`S6gitKp#S%srdIAD+BR@0Pvs6UlTut^1kyp&cD{0gYzY6 zFMvNeKIaQdgnmiq|Ld*afsaQwLVx{(f8<}A_~^OOI=%t*Vqw1g^(O1D->|>Eg88YR zIA2T{=&Ste->hnL&<}GE@JW1Zwn7naDryOOQ+~Es{mzT9O`D-l%jNo$t$q{H`T+A! zuY#|lHSn$S*Mv9jJ@97$|4*@={#oQzPXvDwAKNX@X84!&ef8Hntg|D;JI{`X{^I;Q zrM@ap!SinFi{Gse%0)@Nw!ZWFyR47SiTHtdR{85ctkZ>}a&92#$N6?!1J;R>#-N8f zU%bcq@dC~Zoko6u;}gDNw?=@UNUwdCdq$MEX%G1zzwEbqEfFD2qT%0hzophw6ZYpI z$3JL2Hy-EzmSBHZr5^|MyXVVaAGVq-6BlHE%L?NAh&2xW3}pH5{Pj`mb=%rx*RPOzX$dV%U9SKyQwJ(t2yVC~Q{;_+R^4Hnc{R#N5aXz~@;E$L5!TF<({s4aAemB?pX`#q_C&ZV(&a?hH zC!+2`pR4@!8S5d$-!d>?BKhsC^}tCH+vE#8Q{SAk%7=@MX_H`IDUau^N!zrjxfuUZ z;_rgh5&AU)y$Gh2x^jGT=Ln^IfrOO%UE4 zwl9Bu)e1f)?w$nwN;y999q|$Dv&vr=Sot?l|F;bMK>S^^PNw31jb6Yf=~HM$+!V>O zA5X6;eII)--j}~FwtA$A#)y6YtsnlhQgd)V2l9TL@LadvoGgmvePQ2l{u|b9+@Dkj z@)lIJKaX#+82ex1oA4FiTkG2|mRcX-K9L@4upcFV^MCKE9O&z4NuP3S?Hm!@7VGas zf9fym+KLH_{m zSMORwaz)hX4Zug0e%ciB2i8mcR9e%PifcPSuhhE|9?kByP27;@F;xCq!P|2a^daBB z->yhRyafJrIpw38{Sy4g#o)g$iLVFjMZ>Y*M!ZASUq5IMLcczF9x;&oK=?~e9|n9w z<#Z3*?Z=2dZGnH4zxLQ4t`{XWYWe&*-JY^d47lxw_#*M4zSm&yVu%mZZi(~hO|V{7 ze{I?Gufm^!zoGKiw*7XBC<(y&E>hn(_G#RwA>*yP_`PetH&ui+!+s-{@I7KDl%szy z_$8D4!u3a-f;^X#-ygG=ZbW@2#*3uk{HN_* zsF!#G@8e7ze$Uvgai5OtPoVCwnA8@&`s?TIm=nlvVm&GzO#Br3XMsOCUOhX1 zu4obb8|Xp&)wlQV6vf@vLw*TQ1G_rT>!t#qD&F;?J#UIAxziHyNy^hp_JJaip0e1N zzy6QC9Q8x~1F&A=|7E*)o#-s%^NJn~?N2hrfYd0^oAbY7FFYl(t{w8_uV1yte~)-0 z^zA|7uaP}$nh0}-`103WUwGLK*kjW3b-U+b5%YL$#2*OH8+OX zzZA%y%3sU%qu*mU-~PIZZD)(y&pieC9kS_w>82NsC`&*o^$^*R0K7VF+TO}&az&L324?MV_bY2g3q5pp~~ zJNh(AC9nDXWq*56o(N5!1NkDnl#gI}UPk4w`Rwg;$>*;H*=0DNauoBcek1?2!^>fR zAusQf9-Zw6_KBhop&wQLx{Hnb77AY&JstF=ed%rAHwpJw zZS>`@``GW}JW5l*qxy$ap0YE5Z1;cmK*?4{JIW>m~lf>@7Htkq3LO?!WG5 zYpBPO@$+q@f4H6Z1MZXS3;Ysa5q9U_Mfow%N7Y|M+L=ehCb|EJWIp#>yYon~^FHYR zAnN1(_SfjgF86nV)Mo?iM=yxW>9u|FiYWW~E)oAM{Kvdf$qxhVMG2xH?G*HZ@{ilKZW-{=lZAE?sY9ho}a71`0Z5tmlTn_3HoLupA+qP+^5@i4)WLhp7P&Co~L}L zYW#wR82=*I!~f&bp??a9kD2z9h`)7{@<4pevOmoixBOurmnHlIU)+0`zX$aN;SGHj z{_p|D?>Js@FzlD|=jYljmWpf-{EPN{f1aIj7NMv^;Ai54-xoK4JyZE>ejoX|>+=VH zu{#_>Kdo@=m$?1~b_(i6X2U-Dli&G%u+)Dlf4xZJw;&?}^dY>9?bzcYUDkJ}-;wYy zv3KFVp?ddx{>M^#E9|qZ$5r*$%WTxc#`!@%U7-9fx1TyG^8J9nkRkH>6?VxDQL)W} zzryF0Hu_!0H$i;tL#}6)eGB(V2W0#FmDM)l4NMdIdLHa+75%)WkT2Cgoa2?}K92n- z@wLe=gMHp`z;|D6lKnjHi_Wp(|59K6W;ah0dA&jJ2f5zO_Rom#KU~}AA91|MAuV9f zSbv>tZ$p28o2D;+z11F$`1w*T`>s>?xTdG++?yPoBi@Uc2^rF(@AKzyQkMK6?W8D|34K^4I(A z|l(q>Mf7h zk+Xz11^B+j@s8T-j)<6zkT;dTK4x#`xrX z1L<|r?t2LLonihA>hn`}akj{4jrUc2Bg>wQ^JYijU#t9ew*A;O?BAgelgK}(?FHY9 z66AhGIqeDIE0_vU#hmB)$SYj)j9;&z=EAb))3`j@{BeOWVH!dGP9cSjWD)P(*fyv6nu@Mqc*;Di1; z-wz!c0eTak*X@L*qP!vCD@~W*->|)FwF3Xmz$f*0iQOpy^O_ z6SNksK~I&xF0<#&5d&m=Soz20_CttYr5^(SSCixaWxtgy-h2=Iw4eAQJmp>ReKYEh z+xF@0=-;#z@DRQV`*OahxCeRbNqP_;F_WP`&yrqu?eoYdw~6z`Blv$5_TFOZNI4&$ z%UXi}RQ)xdL+h^a<*)B^p4^Q479s!Y{P_LO5ac`G-{H$&^ZkOBL!d7R?*mTVKXINq z3i#*z4?2zhL_Z9yCnHPF_mK0+YV>Er^C|NG!_G_hL`gf?qfLC@<20Fv{bj5F7cVH6 z_EqJt4X5%f>a#jxJ^WtbC!;I$Qx$(jZ7_WKYuhO(6HVWPewsplP`vR{VpX>~5gf)PC~$G3OBaue`nn`$5`=>dvw(+;{z!FMrMP zqdteeQueWi^J=9Q)f4zp`3B;vq$TuC1?^!?XVww)D~A4UpDf4c_i+c0g1?E6r<@C8 zv|)MB$0{Bm|JMrAz%OBh?-{3jst8Y>4Spnl)N)pA#(oj|fpq>~+ZloWSWGcc=Qh^|3BxP!~LxHpr5x<|JQYLR*T%B+hFe~PtQAZ@(9wUdM~gp;{AecSw}G=~ttfl%DLhmDUUXht z41e?yywCq%azax?^O>MmUKM>~$6$TE2=B|z#uW571iz^B5e=P3#)|=W4g#Jk`WNNy zgZ$F}d(|lu;#T)3;ooxojhq#kBI5D|(7#H5Bxkq5dCq_7pY9ETKf?Eh(`KoN8+;e~ zl>fi!466{?r&HjMMacCvc8&%H z;ap!RB7-4MnWWFV&Y9_=yf@$rh?DTObm}7C`)&mM9zA$NSDVV{yI<|EqY>2hK;iqDLR-19cwhL+8;Wu!mT0I>-CSId%}|W1yc@ z{<@7b{D_uw3E!8Jer=skrlX(6i-3>sw{zldqMrUV>;c#JvGd#}(c*X{)lr&{AHq~Z7Ja8{GFUpx5bvJm|x|uiLcVO@Sg%GFMiIF zVc4GlAJwbyTeiI^{9*E6fHUnV`b#$le#w7<4$j8}uLpjL32%@Su^ad6Rf6BSevV&p zvOeU2&z+sL(V|%H=Suk8#mU0`?WN(EpZihji=4nrU;TAgr^nBTmjPbIFCk7L?%&N= z1bak%L;UePPbT3Fb@m}%vkl{SBL8%Ez9}W~fZ=Ab1Vo(prZzs;z*O`m{pwd6eru>FE0{zNc`9nW><@^1d z73i;UYb@X){lcAgYteuHSHwTb4-rmB^y|L|{`;KpaQ@r(wnKlC9$asZ><9Yy{@-~r zLBtG50RA}M0B3TU7TXp6kjh_2Ig9@k{o=u2>i>bxeDs6rg7H-TI@)`LDsY{oOyD^M{wMMm&=A9^|wbfmRvV|N9f(SZCzVkgq+UFV7G1 z{g52kgU-ayU5dS|4eL{RBIQnAISo;5&)JOdO)ae}9Z_YQ; zIfngJ5&W-g(r1)2tOVz0fZz0RNw3ik;&t)&AwQHsdHLRHbWB7(@;&Tpr2K!3a}V{L z@;u#n(&q=~%s$l9V*EaYXRNd09`0MZ2zyBTGtOyLfO^(w=wp6A-kFO0)ZLenzv2JH zckycQZxG>`=nVNy^dE}v{W#u_&QjD1=Y9=*5ucNsUN|oy&r_=U>&Z@wR1tO&>)EaL zyH4|I;*{*4r0n5T=L4MQX+0GCd5)jxESV>w<@pYkznC71>!s|mnWpn+r93AJga>3tT((5Ou=#238dI9la!aLg; zdt6-a33;y|J-A+P>oV{s`Dw26;c9XI^t-@6;hX35{a!>3S>da{p6^^5gL*yiufosI zPVP#PU1ufq0q6h4`7=u-ymk=wkn3OItRE-VKLLEykj~=RBH(`^E!&`Qr7?|I$QJ zz~jId?a2n`JEb>@H`d_UP4y;elZ^U4FsKcsi~yO@7B`CmPwcK7f4 z>lA0$K@p88>EHL0@c*(HDbM_#38L~H=!{f}}zzdKFF0)Nmq7b#D> zoN4G!HmyJSi}e4)Y4r!rkAfa5f6eg+h!253#%K0838#^djDr4SJYlbM^0Mfk0saai zzV|tcXX1XWRe(Q6%Kv_+Fhwhn`s^U>*8!&``UPADzG7&f)cDtc4;4S-bLMIAlZtm8 zcJi;_{LmUa(|y-*jga@-=zz-=i6C&v5QPCkFU~ zzxq&~GM$|L=obfksQmRwXWTr|C4Hk$ADwa@zKj0B_2K_!OMGNGUG`|+t{8s>@x%3Y zxd;6eOn%^Vwp?!~(ksX5e+K8XKp(|l{NKy-U6kKEXX*&-55S)rNxw7B+-W%P2Ysvb z)mf+ib`dYn*QosUIcM)LB4Y;lGm!fFyz^2P&Np4deB}QN&QqnL??U)DA>^lv&T8B* zay1b0O?z<32|SPZ57w*BYh88{(O*IC$JBYXD-QCvfkBX0Rew!*qwciY zMXi_5$w3`~FXH=}Q~0xp8u$_H3HiU!X;p~&q4B=_b&<0a^;kP5f5x+N_mFVXnq;c^Y(smwm0;(%3t&Q*u)s$`JG$NYp^#rGr-^f z?5D(_%AUwylV3T0%-~Yr{nvM#akww4F61wW`%{jWoSy8{*L;qDrvuij$Q(uX&jI5w|Flc^pBSRKkPm@66ZZ?BY$1>f6n6NzWg=U7rYhnl0|+u z-1n9VZxF^)_CU=q`_~>P|61;K)MssQ@PCFJpZ`bQHw^h}j?eMp>UIV{bAH#oa#NIV zS^)m!dLD7V+$mbz!~e%AZ;!eW`PkpXzLs+Q$K0UPBJ?uo5l?yM_>uQL@O$6g^P{cq#wHPf0rDTgivQ-%XrH$RqqO zxo@5jcb!X!Z<5}MUftGV|HJ=Zc1JD~WkFcK$~QE0lXi*zy;cLCl#f^3qRApW2k_P; ze6PB{%@mcJ{CxVlk^2bFqsxA`sl$f4lz76_^`Rm5+@GDwEFYHf(!sPo++&6zkzd+D4H&Q-xytoR)%ap&}%pH6| z^bVK;dh!3}?iaWpCU^|;*W|~y+_iT^=Zv3y`RljcdZWbIGYc_3_0>DBxF&cXo629e zaKFI$y*iCx-$=iA-S?M?hBAJr^4BfhWb}i3+22=x-O3HbeGa9t=hHdfdv5R_V!%7l zm%)_R*6z$vsJDlHRr%}p-QKvLdhR>0AH?qm?lY)g3z`Z1QlEY3{)YYst>!`B^MBH- z_xCG6Z_=xcdj{txqelV131Wt>*t4fx(o`~0!nWxWUqXa@Rl z{7>9J4~qQr@JE&Was23&v*8c(`}S`Cu{aM4eG@`?`^-&3|BjT)c>kaMt5&`P@THTV zI=XizAl?*@_1}~9`P?0KL_}4O#{Qi2_`+?RfOz&5@E84?PHr>w&y@A7nat;X={|8l z6og_wq3W;w+*&{4emAU7`9J>dM(o#ReziT}4{&dsMf?xr*DI6r1-duC7yU!w-?SnA zgWTHLA~vlR;GzBeulwLw^veZ4T9O{*pXC2Rz7@T>{>UM)XR7`>*mXuA--G?3>L1?K z{qB|sPTdNA;r#r5Q20UEFY2>yZmmS@H=vKE@c&RZZ?fhc27HDRAKl%Z2XG!T4Elih z>*2PYB}y|uk5bZ?;|*!@5d0U)$JcJ+B-966*dI~9_H-@WFS(---%~&Ja%ZKAik=;P z_h0vR^KoB|^lv7S-}|@&;ZJOZekrTb} zWo65tk9giB%sq~LWXgHqkNT;fn~8p4>F{?{{yN;vSs((tMfvhQ5$?faar++RQPp2Z zx;2N1m>NBO{PwN;N~Y*?8vc)}zwYlYIVH9~__43Q<^Z?&Pn>UrJ?KpQMY)YDkveJ& zdXv5b-NkoAaYyifDDgvlmETN)^{@4|9<(i(d%-Tu%6hyYCj@s4wWL>aP>r z4|8#TrxEZ;|7V12&lU08A>UzD{1SC~A@K8;}Sno3G1I{;e6ZGX->aXwJ zh08?b4*0Vwf6f21YlZss+YfH-bD~6x!FZJavF>EtpSK+EPoeyea|@P<1X=HvNB?HL zYoUHH5%Lm9{W8IQ=B^kf`wQIU`W1a{H-)_;zwtS#59IAA`I*mgkM{w6xc`{!#^fQM z_Y(98^Vd_{+USQ|41IW<`emwHgnob;#`}cuCAwd&M80%_FMmDFU7sMrv*C~BjFkA8 z?#}-n`5(|nov-11Ez*JC5BdE}_s~x0L+CSg-fourpR1y41K5utaQ2NPplW=cd@>aq~{{H?MmE_3Vf*g z>&0%n`N;o(-YS31@4daTzPp5Hsk_ zLjEMYv8Vrw{59#3zKJdf~) z+xZsGN5Q`cq(0c~eu4P>MfmeqIsP8^KJ*7V4Ek+U`+xV1Iam+$O>_EJ``m4)Cz17o zDu2D-?SuOS)4^Zr{_6wo0i3T5f&6Z~E9pyohK5XlKBWCTwXd=a%W1p0{lc-*~uUBncCAKH}US@U)vaf`K}QkneMbB zBKsN4r}Eb)-ET*Up$|Om%Ws`>=MEDUy|=^PB0aL)Y}{v-{)2Qj zzeoRmtXJi)bKE4HSKS7GrS$MW@({NZ_NR>Uk>|GfU5mR4dfLR#8Mo~{QTzblP33rJ z-7NI;miN`E{Pj84TOcA5!H=r``n>xh{FnD@0iP9e{0nX#@)>ECPydlWqNROqM*h3x zJ~9vYX>US&f%@jME0Q$t7<}K8{=yZv1o5TB-oQWcb=9qoetR+=ulk4QyC423`lbPY zQ-{d;xc>ENyP%&aU)S6dyS0!VpieIKW1-t<80s@)pda~vk^3pmZ>NAhg@mWr4L>Bh zwZyaXC;xP(qMwD#=XapJy6y&NiXeI3Hshq6?}q#1KIlvESLbN?T;ldF6xUj>_tjtD zbZeqM=5&@XUQp_uUy69@KF9<4r_BBBo`|Z6?@K8^^;ZL;S271u}Y z#eRnJR!xhZuX#%_zq6Y`$~BseuC5AhW?@cG__INL|M!!-~HG8 z-`fiQ&o^S~$R@tD>I@!d4=r^-)qzLZeNlk$%xeYD7%)!_dTfBgSOx7F~6IX~%F zBJ(31IDSp7+fK|6eXI6c{68=T>#ae1@RYXjF!XH@{9D5Nv^G3n6l`k@|DX8a`!V~X zfp5ZBOZ#<_hziB`0er5lB^(o9y$AYx`F$O&;+%-B^)={4d_Swr-78iFfgaga`g}Wl z*T3sM>S`O&f91aCeB(W@tzINb{b29fbNm;yD8wTJ9bf-&u0OojV$hfNqQ2Jc9O4C- zFP-{E(dQEERRHIIQCofoSNp+UDEz*peLPO2$o}gpKl>kT`p+V^|5zXWU)JX5i=eaT|4ue2m)fgZ>Dje7&vJ zLH(@%S|9wxUov3%yZ*X`b|V|-0iYjM|L}LU+vOr_O9;QZ!zS~YaeFps- zz`v@0IN^(w=a14k-Y44gcSUj%G%FALTO{x=-XsNPZx{Hr@+=O2yB+Yq0;_^2URn z++X+5p3lMg00DhOd-Ii6{RGZ~O8JQX=lkG5=)W||TTktk<*@INr*h8UON;#l@mlbM zs#ha?CDI>L=NI~Dy^jd*`{1ur)KCA@{Lo*j7Vz-}^~X2ba-3(8=R;I}h2O^oV!R6C zFHGBYNdzYL2R-=QPm}j4+y%Zy@;O|qb4i?fE*A77d=Z*8Q-n3f_??Ikj`!V=`M?+P z@vU~>5)tLu3wup^_17BWyt+JJ9_*Ft9iXKYi|{AF&*@d`@7ojn7s&sK&-gXKzkj%V zKU%wi`;=Nm`uc~*Xk!vY#2RY-J2p;@=qz{VYt?IyXZIgFz82p znV^j-5|OfAEs^$Ug!aQG5%Lh8RsNdrhQ~$t_6CtnflV85q zrp-aTqc!Xg*T?TKUrL1hlO8{4`9F!|&C4-A{~xOrkH`LLBm5)UOTHi66!N9+zaFnm zm?|o|LSOVDy(efbhKr)9u%9Y_JyBbAL5uhA4f{%Z{-|Z)KI3axzaRN$l4ky-Wv4Cx zez>2QtaU;DayRU$%HMLnm~BnqpL6`F+PzFIulp+CgZl^m9~m$T@KS$H)ADx%Kaf|I zzn-r3JuV`>yU{hXYCYP+eh~g&v`v%HPaE>Co)>8E6o|YXz~}!_b=`4YRqy}q`)Su#&8D_A z+r!b&)XGtrnz0fDv>YiQDZ(c~<(N5g=0Mz`vpEPQNr%Z zdcqrw|NX8MEJQpN^xj2zT&kQvzE?orWqlX%>HXn%_k7%PWia};rHur<SP0Dny6e5;gB$Pf8}KV&{_wQ^y(C_IVv_|V>Q{7OH_Pj&uU>5Q{F z|I}ZvRXQb#{wHg((Z&P%{&WyQGahx z>^Zofu^INW>@NZPw?-OB8|AJb?_aL!@8JD0`Ry0Qbn)`u0%9@2DqAB<_=s))M2KKkd7|+q)r~A?8f@ZKgSe(s_1(j?`!kfZx`Xk5c8rRZ;oUt8RUPq-Oy1X%6Xm zO*uMDBzIck=C7|SRtoxuVSHc8H@}ZNfbox#KW->rOcEu#AaAn%x?Fh&{m-0!3G)8y z3MCuoe=jG2KZxH-<$}}Sz8CPv`EDv-Bi`inL-osY@b|Z}6!nY+h!@V}__vg6rAiod z`9JaCDrNo#k$VvIuFdt`RwkYhsWGr0oru3XN|U>y_+Dqo1MNBSUA!(E`)7Vn_@gq{ z!9OSc6?N6GBB~tplm3#bj=Bu}x)}N)*MUbVrg5Jcxp{_Xy zf2k4jW7K!1S`+t?yxax)p?bdfOz_J#j%TZ*|5DPs7GXTb>pki;)B~15-Um>A9r%>k zBH+`P__|-sm?mNyLm$fg^#kgq!?5Sj_fmg8sIEYL@$gx$K6^;rJx%17VLseRiQmI& z=Vds*_&)NXq~9ZIyWeqtjBxeSqw43&a6dqI;Dh|?;7j5C&fCcE|5FFg5#E|TT=}S> z9z^|G9{8n@>zDlQ+tAHl^SSa*tiO!!iSNpjurHa!cWw2_GotE=3s^trdqQ1t0`@Qz z^dvt#seW@*l$Kl2*VGSBsqNN6pL_s)LjHeR9gvLghrqtjf8+S28xDYeT;H>5veUl? z&++u%>!@cBhz;40r(Ej0y6OuH;EzH7<`6&6sX?Xak2C}Ev%U_#o>!kL5`7imzl!$i z1+_o=$t5GcQW@^Ne^H$`L!_jEAHOENFRAFqk>3XLyoB@DQ}a_rnv?I8_1E>)yXB}4 z2LI>ZbN>IbTB8{CH}EfOas3U{get`6@PC=VenmZi`+J@E;~2vGs(NR%IG?*6_#u3+ zsmF2NGYkBd%lTec`%G7o@^-*p6Tfe$ZO`F;%fYZ8)Q@kfKaN5DLm~V<%F|n_v0S9p zEr31e^V@1i=kfP0=>#(ob{4Eao!_BicY8H+s4h$p`HugeNcuNb2P3{)0sRpg>AY{I))*u5GdDwC=#PA)Za{rz%Sz}|`hU$; z<9Fm&fbVBHe+zYL2I5QbZ)N^kuGa_lS)MofSS>z|^GefEe@%PL_i>K?ZB6=rsz&b< zeI5q9^8V{q>goivPCAVE8{uuOPFyV_60?9`uCI;yTZJfj4)kr!XO36=##HEYK7Xd} zOcs$&KeGV(gL1wWX|RXfuY93aEW>@b(APDp{~wio2kR$%U#jcSk0d?Vm1p9w>?Gt7 zduaSRsPF#{dSiZ>zwW4hIui9H;Lj7}$FI~saDK&l4gV*h32pI>PMeZ+e60JYy0 z;dSyIwW*&8@6~eTr`{vKbyl;BaQ=U%o9_u!dyW#`DR<$&lK(kh&;Y#8;P_qC#IZ;% z0p5YsH^J(Id*Sc)M0}V0%I{-hhGD&YAF9qAiStD`e=W}|bXDJ&g8UEGAIkN0Q&;0Y zai?Ec!bk^y->Sowic%*Y->I5EbDemFoWHv|d9EmS&gc3G=ldRNpToG%u0Hky|A99P z{4eL{dMiVKzl1R7dp>vDlc$K)#CLBseyiwn7XICWXy<(&wR;Bo7cO%1*J0}W^F-{a zi}*kDvEgdmF|jslwwu3}f!&ge>m|S zpA3Coi~Jg+Hg)c!fPdPK>+`BtWQm_N=*WXV^+&9l zf%?B7z%zjKiBlJ#zvIDt$UE)nAa!7&66(}jb>w`KpPc=e5BDoW)LBDC{N3-|{Vd^2 z$%OvuMEnZ%#$*wGGQzDt9I6igQ$!5`znS#EUs6AfQm4S)mFeIo%KI;BIrNj? zJw>eJ_oLOs>mu0MA7zmL#;A*CqF)B^QAYkB>*(8vxuCzyUyoDkrXjwb;O4K#tIz(0 z{vRFO{^1kUoHDmy47pBV2i!s!kap%38jN`s?cPV$v|*9@2N3`aq#5 z)8Su8`ylZ-Hvsu->Vp~TAG?rm8w31CI{2TdHvU<7Bd}hXzn-NYLciJD(V!3UJzH(K zP+YG+#jU@dqn4c(ef0hqztH*rTs8KLDD1Kk^rQWsr@oB)0=mLJPbEFcAC*%UyX&8? z`kxj-k3!$e{Ivt0Qt1Q#DXE(N?9VWp`gV~TwOGV80)Mx@=ghZQy}1DV0C^Zl|C{Rz zaq7=x{q^r^+nvbw>~Qnfe4pft`D<0{$4Dn$AHeaKt1WR~avJoXJa4i>eHHgLPmYJZ z;d=Q0s4ELVU*enV@kSqpKUDp`q7U?!AL+G59kp8Y&FBevBY*v&_85!(B;-rxuQ^_& zQ=iwE^hi?wK>d9$(C0n!=Q?%mHJq>d5dIP6Wxd)B{TsSB$9{?Q+Mo{HBjQf3!2in~ zd~m$Wjk4YR^+t6=f_SSse@%Gf2DHWgf#b>bIQ@HM{5(b7UV`&Nuy6OO^?O7b)-U^q zr>cjKh}iDn_dV42Y3l8<;%co2fj{mK($(*9zv!l;ZvFLEb>BEqm0chCXv!bw&m91K z$^7+p^|$>ZtOoc?_7C5o{(ThZN0%VKNc`?p^QPcF1NaxRK9m2CJbMH7jqBg7rr>_* z%$Gr5;*0C?W+K0_p78Hg&!-4)>?fc%-|tgjF2i{t&`;*C_p9%pLVOAQB=gq?R5KC% zkwKqv{QjUi7xh}1ouThYzeDP#64A|xhwS6~!|JrLN}1!Y%KY^awLucTKLYw!IrKQH z_Lw0G3%1~U>Vsn{`ZJZ}PDXt)`Q=ab(_|$mx)|_~ei>?3CgLHh|6l!eU>?>l>#wua z51f7}-@u<^JRnEtS|8=Jo3Gh-z+CR&!}B;aK85g=qJkaS@rI1 z5&L~JoZsUAP=|F2II*f;Xqc@_P?ysxi^J|I6|P-~$+)j9uIkMLYnPi#Oxr6lmD zJYPlq(j4}^Cg;1XiZiIcJqUZp@ru=9_r!Y>2SUEdKUdT>=x3h~eIn0ym8ge*5>eag zg1&hUzOJfih=+&Ffj!`UqEtn{B%Z%Hu*G>V@pBLU{T0&xn%Wrsrw~gO0Wtr+kM&2s zp_u$qrVhn@=>Fe9zNw#XsLSt&NbwowBmCv+JA1(Ip#L%2w+huaSNLT{x#vwP)$K<` zOaS!vZPK6f7d!rx%wPYlmY^SX8otjUzHX^4wyN^fWaU`SfR?Pi1_<(vHkUeO9oWzqYkA zlh7Yv0`N`vs1M?X0{>a$=lisG(a*ej1>!f=@E4o-{{iyD1DgGZh;IHY_@B=YYCEQg zxcTisU&8m0w(U6bEwC4UgzsT3b*(7*4F1&tj`xU`__wH>)7Y)QepFkNBRa1GK4t#; zF)eSIV17A^@H1VeW;~9u|kv=K)yP0J;YCN z!uy6+W0{EKc~Ii(P3_ucQFt8dk0d{E{jmjjmiJ#1o{IL6k8-|$ zN87y$_tW;q{*Ls0SNr^T(Z|_8_)-5f)V`gF^L^l7nZL$K#q+q&_#D>9?;C4hfgjT0 zkNXUA*55=smW|eRM}S|-?|a&t8R&;P2Ks^f$M?1S6GfL8ceUSQ{ z@J436g6C@auk?XE&8dDa?EwCf{Lb~1e*TbKf6f2*9Rh#9g!pKoHM=fyJ0)YhYW*MX z^pBJESRZQ*{uG_Jj01e6&nH@!vr4F6Lwrwsf2u7-f9>Xg--q(hN?V+Y{*E27KFVKf zt!x7B?}C3Q`-ivD9^NJ5LIU0WNn7ptbI2dUpQ#``pJ`owhre|Q>*4d~+7R?3sJQ{- z6TUCBvO*E`F8K2@=WC}8tQ6PJ!k)?g;ap$Y+4i6(;cKrwnk~HDTRho7;AbEo z(X$&WMUSZ^3^f{X1!WRw2GO!;L3${D^}Ne!0K*(Vm_wDi6ax z$^5mi7C&Ev`AkIqn*aCH-a-G|I-T)<;)DNB4}m_a;(P(xiLJOl4eKc(y}!|R7l`yC zf9yX<&(7MXI3MWbH)T9FP#c^l(w+NBJ5}qWTj$yV-&_yZSN;*!lg;^pwXCD?A5c${ z%l9GLVdPhT#QZXU9jdjPiu)g+4`u$ktF|2H#nwIG%1<}#+C@?9n}Pak${WYOkPm%( zf%K91?f`u#&yRN3#=%}a4*e|SaXqy764C!$J>;($pXB#(H#z`b(yy1+AwvwSi}^E% zFOFa03#)e;uLa2@%$KHS{0p z#rNU258(S~M;;@!+*=|Lu?$g*`2RtBzD$(Wg}u$Ce(LAw@6b%(BZ%Kj8FJ-n@@1h{?bsb-DuQzehGN# zzYzYiHs{fwjQAL)z5fUFU#wey&G!*beUt1TK3p5L5Anl6ZvLA1i1RhwdaRM!*bEWh zJ_75h9^Zd9@PCf<{zdx>{pTCud)Xgrv{rr`@#Q$gKZx%!+PYh!YCQ0nLwb$XcI;M) zcR>D*alPZT2c|1wy|DfR9FOw{bvoicPtc~LUgqo$H^2C+Rw>ZGz9-^Uq|Zcc%Xu-Z z68u?3{^0+kqTdBQxPGp8bHhb$K7ER|H(B)02fw%WI`rfFu;gsS$0MERsoL}+#M|KC zgz^80+MH1$#;N~3MtV)t(q=;c!Jqt3|8Q>x?EOU2Ylc=nM)Yy|=gIu_Os(*=2wp!M z{KffZX*H&ZpqBH1PxcR=tu?p_#5ubKSw)?`;7fJxcO_Yr_#BfRN9Yu+MFB6 zPs5%__I;4f zcjhNPO0$|l9vS~!q6KUcrFX$!6RYJXY&ZO`*~HIM?W+qSt{doef&24i+S=cR*MVQ= zLzZiS$oHl_0e!*mS7?*bpYF{V=y%$ymD(pbuhVcU=Hvc^^Lv~84*F4kR%<@LAwGn7 zp3Gmb(VjRfu7?0#AJY2|Eg$=b^T2P9TnB!x&+B*G?H`_`-A4Z+pBJGY=)bSi25c1( zajT%8sejjNm-0Xl(5pA`;mnWwy?~zpelPi{UOe`{)JGe&*GA(!k1yao^Y46B^PoTW zk-s-%Wvdkgd>{HfZX7eu8$^l5F*pQeq^5#AT> z!5&c_(zVWqMRew3*n6Ho*{XF*5D^bVLtk*c+q8}8u-83+55C{7&Ce4Td@-Nw54b~n zbh#K7zZ?8V|8A$&=VwuTJ`eCweu(cbA#T2&mWun?x-N%& z5MO(>w>F6&#~wyfUiN8U%tinI@feTv-LHMURs_t61V2+h9?fVX!}yQW|1Z!&a6jHT*t^VX{_*(j3BJF08NT~sRcD}DepHtf2BJ5wm zAM*a|)7sWN(Pup5$&d1NMtcYS6J7>=I*~ulYEO<9*Pp}svof6jpVJ0h5Piy_U!Lat z=e14f-_p)+bzb@B` z#-e{(8uSzC!~X~SZN~nB{aq@xZh7K_bDviaj(=0@u^au~@qH%s6XEM0)D!#TNC%&{ zwB&s3M<9=~{<=!LdK3K?AdjFaez&!g#mHZ`M!dFKpT^fJfPX^!aaWt1i}(}tUnu9} z_-9VTFMr+J$rneFKAJuW^)AoC|C0Jv*KeH=11jL}c)319SFyjZ zGYw`Q^zo9vAJG%n!oFaH)}?<^bO>lb;{cPme+WUFee>+OPlV@uv{4f;*@!yMP$ze=o8|D@P=IgzNGzoUf-}xbnb-x zgv?*Rpud_SLXWm{{oxn&1xuhGx?uileoC1N`j%54*3%=WAb(RI`hxq9`uf!QO4N-N zfd8%o-^=>*S73ji1V1u<) z%R=e_UjF}eJ$ZphS~wZ}M1Fijf8eBunuYbs{PmmqGkLgQ5$ludeM^sUVWACA|w7W{!4#MgWJUz2gZZ7k%E_V0bYU<>l~WzYwlkKbRP2YN?SUOv<{ z^apbKEA}G)G}SL&5m&Pw!~Tf;*i28v`RuH4=mYZ4M>_V8RcYXFoBELWiZ6u!`55`T zg92n%Ial^|8qb0ss8Q_oEHMV5+fXX zw$fWAh}ih25#M6Ik?>!u5Bn_phquv}{UJ(gO~LvIe_K6$rHE_;`;kF@k@#so65kU) zpX*;OQqn{HVc$9b7y6zn81eS-R}onC%S^c#8>{7wD)rM~2@2p#Yx_=)suuft!B zL9XMU`>#9bIq(O&Kt4Kgy&d(NlSF*ULCjBn`%3=<{Rzv1!LRYodOGPh*NUs<;NL*5 zkMLDSfqzaAJ|F$V?IJAgZ^%FOhp+x7_`RULTYv4RH=mF5pDAwrwZFa^@u(Ki|3@fq z0s3X=^H+giS${3ppB4yzkn+)4--PoxHNao5l3s!OTk}M&0{>6uuY>fRhfrFz82IFR zNUy7ZzmEL{V-%DS=U)&pl{Yob7J>mB@f_%yRb#LAN3+L@)Am4)sQ&HP*wwAU{RucYhNlE#ut$bzeQANNo5D_AGd?L*GcfZYA_N=qKs@ zgZ>!qcgO=hWc@YAFD(E+d`|q6J~1(SV4sK&K1V;*AO0ozae%(`9PZ<}0C=enqV)%n z|7%(7=C5P)NA`=Pud$vC(#xwK#(nj7Tfts)JkA#r^#lBI(qo`LBoFeu8up9$i`6rh zi?D^TU*jmxae9NTN>rD-kdNyBN8Hxk`>zM<=&u*i4D^=u*Bn2-2keu~U&re{r$pbD zH{AG=(6?_C700n&+0T2Zz6bI5nt(U7di}9ZKij#Sf0%ycu80^g0`X|d%g_38ocCz@ zh`T=+t}Ey-*Bt+s_17cxuW{dC9oU;4r2j~L@NC4J#=8F8C_Ui3hz^7Om+|Xg^be=u z{NN@xe=X^85b`7Q*JJeCs2~2Q1L#NkOZg0W&8@#4r^g%V43!>fST(|M-5QzI~;*^Z1+4_gwEJy~i985(Zi$i68fp+ zgC4S8XNo>_y14pkLpOh&pby-P{;fSBKh#H@Kl{BPd{6zEsGnPd`^Y*%UdcavAMv?+lA|nYz0 z^hL+eUlj6mg8uA6eZw@|7d#c~r~X=`x4wmXYuL||9B;8c6!l`w=Kx;X!zFsqP|>d^ z^uayqkKgsz&noeQ`a&Pkel69PO~QQ|8Q?G4N5UVs8~R@6uL-}`uLD{q zU-NxL-g4|`_@2*0(jlKRf6eElmk%KyOM3G;sypy?sQUX{Cm)m;>FA5K`r$pIv?2JX zBjqzm-!n~ww88j|DbMTl_a=#|ci)1&;rsP^BgoItg|Od;od0jo-$K8i7TbW&o6d8x z-gzGG&w~DZi1^v4_s0E)>FuE(xc*K0q=llX<4?=_>&<%pbR`P81aXJ-C%nU)eikx+ zy+uz(J@#EZ@8f!ikHWO3ZvAzd{`XoXU<>qHIpIs!Poo|_67dY#Kb+s6_-KfmzuuOP_ZU{a-dB zzRmUR)-TN#XX=3eB8bmDI`SP?KX?Q3OnCR|q1m{PrVH@T{Q}ol{uuO$tiN{PQ`m1r z=C2Rvz2Seq3w^hN;~&)j$``o~eE!_89MV_ez6+lvcqacJ)}xk*5+BgFobw;iabCD8 z8~C3|_>bzZ;QWtszWWP~cT7LHP89zP`pf+FpZf0yl_;kkSk_+?zBu2n6tSqUgO5zz z{!M(>4f;XWU-LcXZ3_M0Y<==gk-H@T`zP9m9KAGMlqEyoO8@4#-svpj;URAR`h?!5 zN+gBP27MWiJ*mgf6LHI-PvYX7`ADz00VeG6F6TK<-%x`79w)G0BtAL+z{oL>mjTZE z0{y{*$WK7uhUw08p*|n=c3F)P5AizB9IxN!6ENRx=lPWWG4RzI@EoIl;`{LP3D7^} zpELR()E7GOtwCJRSv~iHNOtVSN5tnjJqG*9=p5KXKJ)*%oq+!=+N%rtszc&h_73o? z9M7RIVI3WxdTB(DP@khx}2jH%J%W-1?9&zQ3Z6MSrT9z*iyH zQ=;QMbxg+R@P|3yRo(B5D0zA%_>1%{)z6H;`H>>5m(PFclaC|5-`@4-uIWE*7rE;W zy7}wt`e(?OJq`bC7vU|_e=QY#o%4ZLxV{^DqkZC*bACkT-^%s3k-w}Q3Vq7;ROnw{ z6tN#Y19}m@NjK~SNZw&hUJ^BNr=SfFiLph&ewEInzg)aa- zsJ~3(5aKgg;8#Dsw~QAGMM;+|oJS(QY-4Z*>Q%s>|FPd?PQF~8FTBraav7z@<6V74 z_=69@{$$bLc);j?3HST{0sbI99yESM{L+ChhxC5Ph*%|pj$u6k(GEZJ|8YTxe*{GS z`&{De@1=cz)cEX-2y2CSn#?CWW;~dU`fS)2KhpbuM%}q0J-Z?1r~K709)Ez-;_yx z@uczG4(y-r2R{>k{C~{ZXg7cTv{C;s;-~(2&-L?t)cRSNpYrgm(eRuIO1=dDhwtkc z4R(pjW1TTS{cV09wg~#I55IrTxHVUlIq`_Bf5%fwePKU*xZW3xl2sx;z5w_mJTDs7 zEfMO-|1sLXmyGVHXIu|@$^3Oaqrn2?6Tp8#)&9+eTF{^W$zKn?(Hi*X`vykE2@&W6 ze>#ify<)tcE-GpW_@|ujRU=tIze7LC{Pkr;4cL3?%*tm^&So-@eo@pBNkFiT)o$pT%*$PmKnM*Ke2(dhofGapq?c>*UvmlU}Wj z&O1b|(~sj0@!iG{DWVTz8UNJ7w>8dP5YbU%fv<2!9zQcCuNC>{m%-i>-p`E{%S2#r z_)kHU-!F`{RU*Q%FIi#E_w9_{IU=Iv5Pa`-p1(92OvHIh;M14%Y;Syyd_?_V)L)aI zIvCe~K|RwTH-FvHn0NsFK7F7s8GroBc;|?SarCE*$8|Cm{33e#PIAwGl7Bio{%a1$ z_c1CGao(&u=*#u`8uyaLiA>mES%2+kJe(}b?(RZ-hU@V+hOQQ&jUI&kp!@|GYfgzF zb)m1a$p7CMQ}>7@ryl1V*F*X2dJ6n3^=Y8d=YlA4&L3nEAA~2Q4)kXN^+y-ubKDQy zs4ehCehoIxp#DB|G1f!+gcyaRlu8GFFP}q=p0h-JA>iM}|93V1Tp_%t!5?v4UpHgr za@4!QK1=@n)~H1PQs;gJKfZV1Q!1|lzmo}Xcf-g={rf06j1>k7b2`^oX*Uasx(XQWYi6!!i+K9D~Bjosr#_|wr?FX>7A_ic{#Ro-#tX~+K^!1Z|zab96Rt^me|e>CnNBa&Z)K0e0v3^X2^FIM}59}aN* zSfd&4cjyTDkoD(r#&@?wVKewMy@}sJ#s@!(DCc};AmgEfjmOX7K3vQv^KV0pXMe%f zGT=9Pek0zvR4h_@=fU5ky%vUrez6T+27mT-)-%)?Fiwfeta9_$KN(Mq5NAR@gS>FQ zVa7|ye`a8PGT;8Q0e>oN_$zMydbr_}B4QNi%g2bX5ykWdhPn#zF|0R( z_HLB%+)%`)VSnWLieHSflQ`cGdis%{MjJ=hqW(1>`j7j~F~)*l#pTRw@Eh?p)~JE= zL+Je>T9cnhkIh+E!7s$`cq4h7h+EhW`!DW~Cm54)UNz?k=*9Jro>AfO2NLc&>*up~ z0PJ@y;$xB_W{Ri@n7^F%d$Q3O=i!>qaP!wwjPOb5pM4(jX|6ZHn4TjB#z201QJ+sW zqGyZPdfy}dOMXl=CZhkcvtP|5zexU!fWH?^c%~aiui*Zc3dleGvl+%_*!!Gdj7NRO z?<1W3vdmx4GH(8j_}#dCQQQl81N0?5ml}J}uTXyp{x_eO z8U4~kq+{P@{@S6BQsR3Z{=CotbYeAN4V3iu+O@%}KLnv3{}0e_tHuQdjuKSA~h$UFHV$p~F0 z?mG1Un)F*|pcW^&5b_jBcu1d9{*cdXzTaTz`G|kDclkNl*s)Di%=rTG9ny27(djzk zslaz3{fA8k`kVgP5B{R`cQza6SBT0P9l^h(7soGNkpX*3{Uo1LzXbo1-l>MUQG|OF zK!3hZGa8%`eO`)j^VjLd%-N!E$QAeg>#at}Qj~TsfIQ_o>)&R~!};F5jUkVu_jY5| z-?(3QvRi+>!x@1_X{1t{EULy0?`;Ck=5!I$A?0fZlZzSYb-hVCSH3#&O z`RhYQ@h_-1fj*P<*N2T4u8F7tJHQ{L#}Om%fRf&=2>2s^5Wb3!?!)^mXZ^>F54VWI zkVS}J68=Ap4%0>}*dVB+V5vHi3d_&oHFFULD+RP7ZJQ)UC7 z)Ylw8%9*bgzt1yXyo!ENt=#(Sd?R6rGNf5|=m)+pFnX;-eggVg_75*KP%o43?60N2 zSY!;U63Jg;`~m#_l(7u=t$1NCeTgrLUq7r@=C98f<`Tr$i=oe{&(0c`rlG#N7UYxb zIcF%pim-%iH-Ald3!gdc-hX|;XunNVwk!vKay=J~nAM`ni9eQ6-YywEk?*Sn{iJ_# z**N{XsK|sq3ne{@jm2Bx&ke*g^&`ixJYEm+xN820Y`NS$A8^&!ep1}JiTQmwU#W2% z{fS$GpDQ`vU&fZ-a3A|V(3AhaW^|m4c-1O5e|_E9yH%8Seh~VkI=_+DA`SMXy8hbh zcL@G&-+$>}`s5pkm(YK&Fwh?_en1=eW6U=a-g|TW!S7+t_cx7>uzw9fkB~^``ER5B z4D4?Juk^QX8LbzJJI8%MFTSrb+Tpx=PkjF;;k|9lnu+r=(Qf@U=@nOdJmi`7^{%0( zh!2}jfPSTX-ZOp^;<`WVRRaAH#SEMz&h8GvdyYr=; z^rk+z&#XO4VZTM0zrNo*zD(RHf_;?r*AJM}cZ&!oKO_AW=l@F70MJ|J6A5pOFYpn~ z^*n4gJqZ6J(DjEOF}t9DT2^z|+e!!iN6pCbqI6Yf@HhGQG4t_JqVPj!J&d3I&%Bi- z%6j$%{LB~DFpr@>-l?CVuSt)Z=85fMNQ>U!FY-q%6a8Wur-MHO=)XN~Cgc9qIiubB z>)Pgn!_aRRrTnc>4`s=67AIe2# z7Wln3{Tt$M=-+ss%Jn{L222!JPo4#SiSIh*!}C%9_Y?3%d97=n+afCKBtoClpCP_d zLg1eVaQ)Al7ZK0M?*My9{qur3zEC8sfV|WoJTIE>p%eIMb$ln{U^ucb9yoC-8sr9pS>&d-TSX!H7^}R{m%jTTl61Z zGaF`$sMIpZH{pNX{A-kmZvy*xl=klpvvs-1TLb*&kX~<^X*-dh13!oJ`?t&;r*R&# z1@K3I`E9e&OyTXh*sZ@NyiGn^fqF&ihj-076U3FR81Ee6Z)kQvKM~Ou`k>U|pGM~6 zm$7w%J^Cj<1b!58&iSWn)SpevWc1hgC<^;QKEG$K-Y-6E0e+PIlsI49P|!a#!Wr)a z^BDF|L4YS=u=D((xp#)R5Cr@m^ksWUjv} zB8w)%{t@2hW-9cTv;R0jerRFdLj7vXQjAai)6(pbiu-7>zSUeW;i<}mJS`x;KQZl< zqWEA2;vXFEQ}h01;woa<|KvMbnNR#K`kx&Ke~|pu+B}vaLPDT_UL`-aF&|uw{@#!W znZIspo<{$SZh*fB>G7GF`?Dx<>Yrr%lJomz&H;YN9}+(;pnsN9AGI^1^F@Tye@ym^ z{n8w{Sa_o$pZoc~z1ixvh)e`L3DhSY%xlMS-%=jN)I1v^Ld=#_5BX}ACBi|&MHEF za~Al6`rh9hJ3(A40RPH<$pL1MzeRf7CHM=(?>D9=MU;jl{lEN8MJn)dmh=iV+a4FC z*It4=bA3T((NWZsrC~h&pZH1fh5!F4=MOg9C8D1t=qLS^5HlR{FyEPO{yNl5TrEz9 zEpqeMUCm<&h*wSkyoC<`bTj9disBa&kiX`B;9K+B3DHkLpGbb^d=<+8pUhu(H#bdH ziZd|ZA;R0koQ3|>&UqafPv~jx%7s0{__BX^FSFM&QBg}l{)7JM_h!O&#p~PC&0qI6 z7atMf70}-@f8EDSC{l8rdQ;iYBFwys{gKnZM*2(PX67o?+cbf_<@}^qaL#7XGul~C zU-S4Z#IG@)>>nO!&O40zJUSu2NP7NYhQr@UngaVke(PrjZxeA&z3oiyU;3K`$kz@Y z< zM?W3syqt`0^Z%7;mC$GWf4tcj=MBBbfKSS+F#q@o^#p?;-+U(i;+^<>tw;xdKbajz zh?RbTU)Em_GmAHgs|M_MG5w#P&CA!s$s2$_nDFrbF+QOGHR5xGx%o7Xrd$Di_ zeS-MT;RjiNJ<1F&5xE~>ez{-b|D(DbLi~;N9Bm#sB*M>ue(NbuW6b(T5FdcNyhnJ) zny6p6`e_{exqA-0hGYIG5_L^?~ zdZLN`-f_{dx%ul!=E;%BPksq_bDi-go1M^)&wJGM=ckw%xKFDs@PCT-lkkVu84vy< zJ*JxR!^JRlFzgZSQ=-{!2V6PmCwc$%H1l2TFP!}O6u#&GFE?E8)?d#s%Zo*1W59ET z{4~=%ctZ(l3H;S0J!hG39u&hqhJKX!>)Gb6Wn%DhthYATKgWD474fcktdI1VYu=uY z`Z~z}dcr%;?2Y^G{oo(Scp~Tbp2z&qS`f;oUKK~{@9sljpRy=>s z@t2wjqtWlZ5AwVGewo>8lM=RP6X3bye81d0IYQjaYW)B5*C*>N1OAC0@_X0kAYZb7 z_$qT=z9@g}LEww_)OoK&IOiX$Xn)q2o>L;ZcSraB>p#rD&cQ!}d`A$!Yt1iCi+K3H zA|k?>Kgq0hOkB|C7y-xgx49 z_@fE;2OG_1IPdc|*C|oAKZUVd*KI>r*2|wxocg7GmK62Q6=VzS10RP@2K989F zN>PvOb@SIp&F^#3Uj^&?i1<8a&OrWfF2>8?^Pi?ULga=(US+*mhWSj6D6Jpj@+;+~ z?5mdGw`zXy7DNDk{y*D%0R5O}K_BLDe!>^z3w%VAUdPSa=ug`+5%!AsK4CVQDzcn< z9~r+nX?}?N)E3nPe^GupUs&b>z{BzRT;=p%ONe&x$?-2c@zq3*UtrEd|4U!!FPXnC zG+)j`eHG{%&hd)OpmY)C#PhN^-YIiz3GTP(f&C``f7(QRE-rK_;t7$?_-D)@+)uk6 z`YMR{KWi4^zD2(>(3k$!IrG_}qOdFUqlE9g`9PlN+;T4H&G9do@kfNW{9V*vlm9Q8 zGjl~*MiuM{;kjhaJ19!#IQoIlm(AX#IFH>J@bz`(FE)*2cNUhyL0(9>Dq3c<2A6=IuQqsQfDMO@1T(`ad-j^rJqwW{%#e zL}kywc-8a_@(2B8{<_TEdrqlz`blJwUN_7n+%M*whm-eTmzxh4h|1^cgTB@D3UTTs zvb@gxm1g%@N>tuQfS>EXY1)V<`{R88@lE=DR|x$VKziOX$6iNk`c-)5ex=IXiGByO zySwsy+w>HPB{iHA7U9#a_jZe@2=MP@;?J;7ARm|oe^G~|7gOPf_tcjjYsOen8VGuolRxgWzC5CM#|?o0L;T%uJ-QnE zlZP=s^~nQPomsf=4DxrG`^5*Xk^6U7|P`GLo*AJ2(^Ea=-He*ZsfK#AxV0DT$sZ~R9I>pThg=KGq~ z>|7-!BMS4=o^t#c#A5%s|N3z&J6ps!^{^$xM{Vozq2lV(ke~n5U&kcFe#!dlC#|m+ ziNJ;!U-Iu$*6MwtpBMAX`fJjo(*Lj{)qn+QFC7)bVG$r2i|H zivBVm0G~3y{i^lsWbBtAe=>jln)Ty7@vf5(koDKZcU;_7?EeYx8&>~i$UkDenbqs5 zx(obQaeRJXQHA}QtiOKSTAGXVh%4Os>vybHe+X|^=;J&5{#`5Zrl|PnVbFu)6W_TC z^m9&x!(WZ8h?$~t{czZCzHe-;oQU%mhrut@H%+WvOK|=Q_{rk;?^(FtBgG&3M&_^I zw+ber9|qPV^Vc6(CsB{=4|$RK>kqA~caX2g{-+n^t*P}i?q^Bw;hrCDW~GjRKE-^w zqz~aMwK^dF#_yY3X$yq6<+Fg7`m=@A3;pNQUj+TQzLwUYb7H+CzrN+pdOo&%mLo$M z2!Dmo951=7FYE{9?^CPUAF%(}KlzcLTUl#y-~51uh>vi6t*ueGf1w`uJDT#)#)`vv zH|KumEb6niRxspwBi2`o`rtF`3EW540qY5(fAG2W?OgP0!T*DZPtq$g^c?tu^4iX_ zj)-_a#7nkO{=T$^Ur>e?Vt-u4^|!Z{4OfaY4?;ew;V*d!^JVdUM{9E-_KTgNzeu02 ztS48CK2E$a!0W8PlXYSy&d)CgzB&Kb*5PsJZ-nu@gxANy{xPg3o#T07`d>bn5z@jpeCQ_oSG<2(3O;+_6&sM*4=vvnpx zRJ3>!|G(or2U=NIa31JUxBfcFdUvyUZ#veqmhgA6G7?41kYUhYoS*oLxHB8}f%qZ) zuAfiBdO3fnHS?;t>fBG`rF?X?uJ0G+e)mAX>i=KfjqhR8@%z@=ctE7&ws!N^9KY1@ ze*;J_KF5!I+|6J2uu@Kn!k%leUXI_>I*@>VB}d>taQt4@_cO%RIvv5ElHvpb94t(L(2OGt`w9nmmaD=rY zTU4}ZkN7zGy|4A|Mp3#G@(@ORM_S#kBEGf=@N)bgtkLk#QX!wR{<@!aXsjqM0RKHk z``q8sAkXEOfgkR7qpSmoIFIefPqZ`t0P88l1C#K+vbtYFa2Lb_Wd1tFntl%XYv{LN z?q9vu=hH-xFXjs%K7O>Go}&a0E(X0g-azYR_~#keFUa^e=~q?&{T##bONBCd4@K5uYhB_Z-D%{9W||B&9ptzTw{=q_t9 z-@p8g{R!-`>>obTdZ|dn7D4~F`_X}Kl+_dUd+?=12I2X|T6YosK7bz?PnP`f{1a~d zwdA+I{h=S|uZ^|NZxUf`;13-meB-QFf5z1v5r`j9Uip3SR_OB_;%kC+W+BcuL*K8b z{1E=oQ)$pw&aHl;iBy4Hn0bLKgF7RNMt+l@8JK; zcNg+2<(u>8z72bkN%>2(+ALHedqBU){58jq$s7lMVSHh_bs6W$oqi!1;4D#31-}9`!x3M3GyhcVi?+H&_U988S^qX(h#d*B4#ek3a;(GESlmGM& zUub>)tElJ_2K!I?JMWd)T=0*~UoW=SWQ*{-(coW>x5S!+^R@>+Am&@Gt4J#(D$&^hP}c{81hW&){V6kMu|QoZB4u+{N*d ztb$|_)aqHpXGrgLR@(}Z6r2M7BE9&1r4R5U^Vgg&$%#K^5`W3o4eW>7%z?i`e%NR= zLA>cI@QWN1ew(a{g?JP6r##QZ@xshV;E(dN#aeezlm)|Imi5=E z7V?2!_{#s}uO&Uh?|`2PU%K@o{Dn-wQ=9AAYHi)Cc%Azuj!{4G`=}#>AP?McY`4y% z-X{w9yH}0BxPtH8{598G=G2efA$@jP4Tzj5vaJ-< zecr`)99U{qp|nGuEMWac}V$@O!xPJ@GSmJ>(@M z!g-eT3GsveA%4zVKMWPuyS@QTeyTBgH`s<5UJNVZfAP=(s`jXZ92JZ7NbN$uJ z)@s}r;_$ybA5?7BnT&po*e}&0zOPs>sB7_lPHJ$CsH4kS)W`J z=L3u!#?<@jqde_d`3I45F5VNay}uCQw2zSyb#ke}!G#Fw|1qfaRBe7+j` zKhTHz{)Th`7$uz!bs$>n;htZ`RxKh_njm;7-@aqb zNBzTve5_~4zx3#T1?x*BKisqC+!Q%^o1w3X55=~#M40bt@IU=c)qWxk{m#K3In;-m z-E$_+x556*roPhcKc_0)LUZ8%P(K*$UCr zCq?Btz$f$99=p|S+;`g$`Ri)?(%%>S70mb!|G(L}KVIgqiLa1Q;IEYQde9z)dP2wF z&LO`&WFNk!c=Iv7tiOKP{(GMYybJlMLHHiA-%SxgefB{9?mGB+)Si#~o9ny_eZcpR z*^x!)Z-)6~{`!A**WKcBD%O92@DrY>tR(D@DBm^hQ9p@c_X430`2Sk=q6H#*8u)D_ z<%i#=)P(=^81*&b@3U|V^abI4!me{pi3;uE)?Ytqcit<$D}+CjL;K9{bDjN|y#Jcd z{@MLOPtxxhdm{S(4)8^Mi~b1b3%ilw=CAA6O=pRe0LWX92xq-@?SdTCZ$q9ls>i>) z4*Go{`HAEA-8#X|U%z0_T!8-U;D^p!AIJaU5}xIGg_rCx+r*I2uD~b9=l{J~&^HOB zXMMZxD$(@gAlR2`|M>QEZ@Tk0u!oHn;W6MJ>0i8J-#-H9nSj5O#P_TApQ}ZF9`JpF z@V#bt8jbVQ-(!5*v)Ao@$3$^(BI>WJ@jdV!@b@D1HQ|l5F1Y#Yx9mpf*YG&>V=elN zl0Lrhw;GdQ-?2yE6~ml-n#^BwJrT#iPn#(Z4ednqcN+x#agOrZ$gZ0!QnEW?f5G>S z?TQ5Cn=roI?=-Q;91@{f;P0ihZ|~WCSK@s3OK$!3`*tDv!(74lvi|x5dqE}Yt)Wj7 zIR1zBeK&Fb4)i-fdNsAnc8hL%a>38UM>G5I91*ky@)AmYPIxP~;(LGMkI(F1d!za} zu5lB{JN?a;cBc)Zw5X$-ANtrng?LVV|b_!s1l*7ksTA}IJA@-x-^6jq@^o(DVsZ)>+s6_p>(#CYV7&+IDnZ!QRM z{kzZY@--qVt1-qS{l2i*&%^zxLtvkXk9Ky4rSR`gxcTcZ?FZ3swdiHQOaFlDjeHaG zx{mbiV2?f`dOP=%`4FCtcB>*$@k}4cAIJO3ZixO$uML7fPW*PVe_V`uRq&&XCwy&N zV^E(p5cSvmzmFZf4E>gXkABqWzIOX9qQnA!ntb-N19zbRvLEa>^^?CnF+Ndm!_kjz zGU&tioIl;kUw8kPpOm1iMzEKK4*iJVsMptne&Nn@cROT>xY88QGJoB}_MeRWdaCOW z^|W7Fp$zMR{mE70yO-Sw=j~pHzDpwf9M9Vi_I)<-*V|5AD$cinzTU<4^s#SULH}&5 z|3CVvbM|lW_o=_b?Rj?u@Bcl*@d%?+j_}gJz z{||Q9QSn`4$eUb$Kl}K85gPv@^j|gq#F{}yA%tjBp$&|l`Sx!%ja=RsdnU;JoW^F?L!2>3@_FW+A;fPRRf zK8>|=H!5X5z;9!IA7`gtguR6PcBVcVWQYDD`W#;3=I;mFU+)vquSH}3aL<8vh&^Hi z&ck9nc|JPcUiJ&_SA@L8b3S38TcT8Mo$KbWhuVjS;XDZZ_qN2(Pxi0Zh1bbv2aunK z+5V$MY~+)O2NOR(+q2O>?i|)rllw2?KP>taH-A0CUR@+2yUcOxuerX+mQNwS$oWUv zv3C@2B*vHd>tF0w$BPI@A4~hj@nSQZV}1O7j9s(^^&a?M=C8-vsn`#H4||nB{EV~P z<9?{y;4hiK9&g9|iueuuHJQKW{JG5ryZ2xJYWJ8V%D;sCWN)CdPIigQj=r@_ao@1}We$Tm&JHqS0JJ)^?_R@)e2627!>=!FU_Kmg3KM=pa z+343_F#z%?^Vjq3$Mey8y&d?C`g(!AsY(Rq4Z!-yUkmMVlSIm`H}O6HzsUX?{cf`% zKl1+T#Wv0le`rIq<$NrJbbD@*h+k44{7U<{)%I+{d98TRhx2W-zuqtI zWP(3(INo;qXPob!2z`=6|7eFDwOS%Wu@3UuY77@8vUn1w5?Z?(5 z9tnFFNB$!}Twa%i@i_mV_MB;=ThDQzH{WO2-FJh(vENJ~J~@9}Ht;R;*L=>s+aCYt z`)qr`YEiLu1MnZ=@B_z>7zF(m!10dTCr63kRNzDAXHVG6#)+tK$jdSQ|D^4k1^ze; zdqMni{J^Z)@DI4YJUe)%h;Q*U=tuh_=imAZ#v}X%_WM7J&AyYM&-p#)k9=5g{QQ58Q}39=_h;=RW5osM{wc{X z=j`S4kw5N$^;LiGb?WbO_~7uyqYf5}78mlwFcEB2_F$_b}mK{=lZ-&J4ij}LIZt9FAF5q~EY;}Jil z_K^zQ2M+nF!S9K$l$wj6AFAPvbM9x7`RnU;ABc+x9ODD|i6%Wc zUS$E~tBmxkupirp^APU?{%ZSp!Kwe3_18D;h;8D%T>}9h<%|E13IP8~|NEBx^9r1| z#d-rcUX^{{DG}?ycZ~6%+xCX#h-X7zq6QAXJNAdrcfq5<-}I+AUQos?_=m*jJ$v#& z(SJSoCx_$leQUn#yD0S-fWc{_~`7Tog{)zv~c&_fLmx=SouwOxy&U}Wa z&t(zQ$FU!@52mLyNr?}h3HT`gmL~=J>giROpYYh8skk581ixiOI{zpB?!5$j$@ALx zd7fFLtj~mf38epezo#qm&CLLx%wH4UyECAlVmSVTp8Mg?IOnAUNY95nSJ8hxYd*#& zJ~@6$;wHELn$K|+_&$U9;Ip@x4tf*5$2|F{&))!fmid(bd6rC5yrGcK|MlS1r%ZRibYK@axOx$2~Xqi`b8!0e-9ZCvk=F2Rd`UCp?>{iE`)s z%Te;rlb$nqBKWTJf70hE&w$HH)DXZ|#rIEpzCgajIZqcs{6FKFjr-Z1c=B-0_pImb z4BUSQ{0`>#bv*G?L{jDs*b{zV*K_cQlJ5`wmJ{LN|2a=@+<#mEeeO?t{=8>Xk_gNM zesU=fFL)lBB0@fL_@l4${fnMX==bF0^Zogr_zUO;e$C|jdY*0bMQ~7z+dsU%=Y@+R z+#mAWtD4`U+JHY~{dEJ+Q@GE?$wx|k{EFws7!f%R>+el}=~d5<$QMpY1-u;pHBWjH z`hmhFH$U>v+n&6MB5K80 z*emkmJD$Ne(C@zo^a=6#u4l(uq#8h9Kk7?oy$a9M$@pU`6r4Kz;m8fU{ ze)MC!t*Hm`-qKdZ&|i!v@&6(B3c>$`kI%WyZ@K!hx#uC==T-!KR8qcMcn;t`37`8B zf8+mKdMwn#H;4a_L3lp)*odE29CY;&;jQ$05&D$;^Qq@D&i}V~1pntV@!@s)HnP{PUS72=U5zz~77Wf9|<)2lYZnU?0dY zUwGCE5f`)C&0n|k3_63J^Qz#z76q%bNqjMPn&ZhETbLf=l->WC!<7E zgsextnB#Ty%)xoX<_X{@+MBODkEDrjTY%nDK00|y&xx2x@V90B=4;STtPBAD+fiTm zcs{)VeGC4(MEUadyg3i~nKu!yig4f|{>w9=AF8f8&;Fi6zvDhY=nt8{4)83!qm zeq{aiH=f*|5HEl~Ap39g|53S+&#%dkfgYb#qRRg|{72%O??3z)_VO6#@8W56OjLXo zi1-cH7wkE^R5WY>d7SvKyea9S@K5D=!cb2u_@`m7fWL_UuAW)9QNIoSUP*c2_>nse z;BSZn|F@o{=+|{`7yJX(Uw`LWcpmiypoiQ~clTtY-(k^yz#r{=-@{X5GU|=50AIu} z$Mfbu-eSoAy*$1X;17ep&Jq6aJyS1>= zl7GWI8S_NtXV4!LsXxO#otI-j0Db=~$BXd%UM8A4{++D9CjZ7b@!-jf7esp6|AhYk z&@Y*!&kvr>+i^ayDb~m5ex4SnA9n1sl>h#o`*5BYv2$^u+P`8wwDd0qc%~f^rEg9E zKFNR49u51Shc5!()HgAn11m+;(}PieP5JbCHg6HJ${6Se+Up-Z+t6R(aq#Cc!Z*|UXHQ3Ex$9fJGp*|uS_~!cKJSVb6m}AcxQ+@||mQNSyAHyG(_1A+vi&8~U8sL%j z*F!w7q29JpN9b$XXUc1FYrxZq^bwvmOGNRyM693uGSqW@H2PD6K8fVFpFD4%KR^`x z8+l%9nCBM!(e||sF)9V=Ir2zJo&yzfhri-LJ@K*)-YqDqBE)h~V4)~*fnc`Ui|2tK{Unp?+ zKfyEmAhrgOj~-m#RL`0tINu5XYzOg4e$1beg8X&0JbN8`CF`$=uPDbKll9l~Ie9kn z*VXdLddu2`XO_oIL_dagz#qq(?P)wl#23N;tE`?cJHE3UPn+ur$`rwOpnqlkHOKFq z41W5Lzh3O@2d0uA2=9P0&|9AGT;O?hq=>w;68Q>_ztHpJ6x{!e@6T7;pNPbA_;2Jl zj^F1T;FtT6C7yOC(2t=5_}%N^>vzxT5u(axB;IE`&;0)S*=2|i(Ecs+;D0d`}15ZE8Ce}(7xbk9^W0JZ-?brFUMQ!dH$$!IRnpLexKxd;D*9}54AYnI#2o? z^!LDfnZI7|*?L1<=?Qt3=bblrjv^j48|#<#*OLB!0bf$z^0}mkznj0_f+#o<8AX)CX4HJ>H%KTcf04|HA=iwpC$L3 zJ3K)+AB~_!;qa%!B*+ zye%Jr{vo~p^z24GN&@7)GTh;>4A1v-P;WZb&0lAF($OEwiDxb$zh`;s{a;mA9#>V> zwQp~eXRaK|*C8v-%0|Z=LVHWgOwANX5zw4U1XASQ6qT)}R?Y*O=2Xsej;JWkBMvDb zDgv4_2qXxkpnlJOb|?Dof9H43-FwYz?Y;I|dx8k-3w>AnKW8dT-7G@Sw1a&mJ<=-t z6_0rLBJ2h2+t~`q$3dUEcu#mJ-##fX<7uBs51+ILvm)gD=@sI!-`pMi4kEoSRCxc2 zD2`hX{o#IKMui~U|IiKgQ=R|Jtnk`W(Z3YTC%g66mn+PW`+JaYmCu#%d4j*fe4QEZU9I5TBnqBr z40)wJxK?4rX_1%G0Q4vR#K&6~@metT_eO=>^CHqu>MP~(W`zx7aQ+eNa}{rASJ*j5 zWZy{x{EQEBDtxgR=ampIg_2*2J~@!DE|iDd3Q1dWzhN@?%jdib7uO-bzZCjeK7YKt zA5!J7`TvdGr$e47|9sA?8S3V*3o1;!B69o?Zz+5WD`+!C;e7a)S)|XM3Zt{Ien9;4 z9{EvJ;jte?PRLK7FY#0OMqs`J<@P%vrHz}vCcjGtLY|d>zgwXv&f`pk{Hpvl-{;nW zepKXmr4{~*cp;>hyZ^xVg>}NA&y?5u6~tZOe;V}sCx3%_bjZ)Y^4CK{M`Ha+`|8p9 zEX4VQey}H`|0CMj<*1*i=jN~fqqVvx&esAwQ;FZBTCJaOAAJh+oBN}WX@}B9_JQtL zzotw1d|V4(?I~$i41CJ*-IZAd_K5I2sWrJT^5POfKk~1F_BQGld#}U(7U?PR@x(@h zA48}gq~99}kT=yo+|X7X7DKlJpJejO)WUFnI2G?ze#p|QjKO)!Ey&N2-?lad{pR=2 z!g`SQz|qD`7ruvxACfr!Q(DO?5q<{#M#ooeWwS@m8epR&a$)ejY@W);0FOI|BKLzo zo2h^N{^5M+pUPkJIY-tLHzPfs*Sc&K@frUCJsJPh(yk((g|n6-hx~d$tBdn~Qr?ni ze_zyETofg>Uvu-BnfZl5VW#wEA^)^@gnw5k;jB8PX3?s7i7Xd{i}a? zbQi?WN9Ye4Yo)lqAs+GFebTFmRt52@?8l_~hrg@MdLZJfe*kzHKQ`5d{e=5GATKI^ z-AoHT;z>X(`}h6V&9!*kw+3G!3JGrut;Khqp%-JY|3>+KPiv2SdZp{|U*Aah{Iny# ziiijBpQ`@4r8fKr5&0P8Wgz*{N_)OQWE5h)#|a@)dczvdsn#m>wa2dnuq;|yK+3zKXge`>|ann2|v%fo~OOy z`*2zBr`DHYTGV2Z-eMKxt^B<&5AuA8@P}(1e-;@b6W#l-IiAl4dZ&^9d=B=5zVzq+ zztMJ&M?3?2IfMRlptfMWI4}KuU%nrtRl@x+A4A?%eQK2U-Fnmm)xh_p?_lkvc#)rs z_^Unn8?E(6ed;dQt7pj%uNH*+x8DT39Vrh!%@6f60l-J)ufNsaJtzjga0vXSJPgs+ zpua_9FXE?EN#CJb;|(J0xe?euDaSXg3+%VW=V4mxX4JRBz9tj@;aY2)7YxCATG_J^ zTEJWp6<7!QN_b+mReMF0oImxR9A9Y00TK1#9XEeHQhQ`G_}$v=A3jQZdm`$`AfH9F zFQc_hSdY)^5B(y&zS9QH6SvAzK!3g`zgA@&NBog4$LDii)yi)D^*C)x8P4wjUX{Q8 zUMorRl(f2x_?zXY`v|9d4oleG%yx9lAY{7Ank+S=11HY*U{bH1rs!7AbP zj>a?jF-`j!`Oo5Iz@PFwT{}@Cye(j_tC9c2Z>a1Srs}WbwECAt{)G(eH<3PkA5;nQ zsq)vev?)_Xj_jATgyZr3y-fJ8w7U{M!k;epV=K}B&CzP2-*X7;T?+lf&)WG@B4a7! zX&~{Rt5rx9{o%{S9nxo>Hs(8g5BX1Jd^TU}A@h&%pf~kxfp+#3>gPh>-tD66z85`%Vc&8}|A8+`=D(DFFV-d%d1A&vepUZ);!`5wSNZFu+S=c6 z-|cSLAJTi7w&sS&f2_S*f4yAu;(X8QHg5iUg*Ijy?hl20b|yboYGF5F?*{`P;=4-w zVjb?Q0lnMO{;bx%S%md)Hsp==aE%r>4fWh>0YATAtF@mZ3eltQ@BG<1jOPgod96@d<|zEnfO7dMmFEvsK75`7+wK=Ux_-66_VQtG_*uMmQRQ~jcHs~V! zv+nZysCMW!`l~NPyukUAv>n;F|86GUbG&0(SJ=M=(BFUcU-32`gz<>q32iFsufl zeiDAAKM(rid&ZloTJ!ND=qc!rqTdC`sD+;pF}M6+pC})sPx{~!!0&;?m(O7{=DPLQ#oEpJI6n$_)%nB{ zt;QYnn}U3J$-lp}-8jE}1^!pb-(9WE72s19<8yu`U;gJn?{GPOsrDuMlk|QK^d*1G zw0Wo(2;KmGa(#VYi^cg|+219O{C%K(e-Y>9V84SYPY<>4w~L`6?XX`$dUl8^Ov0{ zkA^-H`O7;e!T$(J4^tmGU&PFUyq+PvmR?jSz8MMqO!vw6ww^BUk3>A08YZ6|ecz8F zUHUUsfBlsHb&3d@27aYP%J)y}-42S_J-bkUP5FFAw-#YPr4af`|58!^9`~7bg}jDw ze@XJk!}^k-Z{+_g>!WafQ06n#{u}WxSO|V8`|+&)%c65 zV>lnX8T2JR{;StS{fj*RsPfl;({m&6A-}8ZEytn17UbnB@vWgRIEwn)MCdc)kDB_h z-$cUtmH0kN!uPyhhW-)Vzk_^m{91Z1+<&s?dB`{Q^#xrUiT$PdcqTnx)PG9E{Tqwj z{qL9bHl+{>T~Xhkd~0gcEp$C$F3Ow|IFvN^{l<3xCi`)%HQyP$!D+N|9mgu z_mpX*Zt`w{G|s=sceS3$r2Tj1XT^0%?R1?PJo!2V4qJ(}qD zJ**!b;6wR&SNBX55!L&;`}s}vUC0;7^CSMGZ!^8e3Qx%c$cMWBy1AYs@Bf$#dNH0O zyrm&^VNW^!dwQSSB1fLDQT@aH^sckT-H;BzpX;lZ`r^|f=2z(dO@7}>e{Q9SkoH#X zSG}*lcSQ{RdYzlUR^!Y5;HqE92m1eh7F{=OL%d1*|DnDz4fP7pht}k8YrX4g5k3$4 zbehj?^jqkE`p9U+7qoXD>4nIzm*H8#`?0=lo`}9$hP|6ks{c;)UCh%M87)+ z>ob4IH|f<*Pgo@q{1M+Ilm4ITGjYBl82J4wpW>?|`9DU=dk6hUmWav1_iF#9qnKwqVgfqG0b@@2!^{59zt-Z;*!zvi>g zAM&E^zwV~TreS{|6Y@iQAEc+O6h3+Xo628z*Y6=e>o*(ro%9XXJK+8!nU7QcJVal$ zTa!1K{H0!hCi)xVS@jR^r8hi{ zQuKbXuiOvictc}?V4n$JA073g+3&%>eNB4w)uZ+x|B3n2Xixj;?`4U|Sm@Jc^6xAC zbM!;r^`c9kul0#ZqQrX=>ruv2{q?UlApQn^0nw74VR}RKW0Lz7Dt|pde{F^6FZ00} zK?Dh<~>1Kc?hs zkZ#Y%ehB$md6xzCTg`<2o9*0G*Yx!h?{`y<}rDMW7crxNMz8|8`g1pG;G~NKTdJ~N9dck ziINVWPaOF@Qf~qIDulmM`Rh^o@yWQ)z=!=!>gQ1 z7=7MaF)&~a@Fl&*>L2H${uUbz`LsXd^mi}d{v7NtJ!=^{R%`|Sa{M3kncw4n zCh(_(@!1<50$Y1mSlk}E( zV#E`W7gc}F@uE|ZOMD2=AlVd0Q&AtAf_!cHdSK}KNp8Hv|3|)u_(JWE&(|xa zz<&PW`foLU)(P;N@~}{!eNKcmT8I5h+Ph!$<5R_evLx6)@`vAtq@Q*3*NgPv1QE3b z@#eqI3uVdu+`+VuOZ3BgMEK)?U!8wks=qZw_!_ps`iA*e;#;y4@xfKf_i}ypDeMPf zf1nHTS)rdgB}%Kp9;y0k&fmY{D7XH4m7a71rIYs|pM-CW}u!2`rL^8=6u^bFTr>me}ld#Pk5W$ zbm1fXG2xKE66)6`{k0Qf*YkdeC&;hO`V+^o-noYOhVr*XKZN=}*^gDlC-M5wJTb5b z;?E@hf2;m1`oCvFU+zar`Prtozm9%wkAvUzPuul((cdvG1ooEv;r!i}LmpK9^$vY= zJo>redzHW5sn3}sGTOwz{*WJudV@)#bQ1iD%3tr&gV7&n-&EKi!n<34?7AoC_i*HQ z`AqzSI>DZ(cy+IS4)?2`0Q?!Wr~C9Cd$1m#<;u@~{qklpx?}|W598khdRNq&JcRri zgqPzrlk3}A5fVQ>d;2{Le5kL7_198F`96F#_^M`1l;T8}>z=hd!?xGeo4!r_`W4 zrRz^72-bTim;b*+)~BfRiWz$I=_2OhGPi$tral4tEy-^K-*5>}mj28Y5z`d@M&++L zUwDI=*nj&MzVYCHF!B9EPb$UvORS$%{`#_h7xzIGL0qq!=^dHd=unX*o%3mveYyx<%k{`J;evP^g`j4Z$^8e}0G5%EIm#^n-6rrsk zf2#iaPyI>svkZuXKcK$c*6Yp_CFXUP{ssCASz?IH=ie%?w+c64zh@DiJNilF_hdd# z<*$o$`;NFS>zCF2*Ts6-dNDfoAn@b&gs0y@=#yH%|D~^ArsaM-ezb68bq#@>|KLJWqa# z_V1zI=7R9mg?_2??;fK7{eT|=K7S#+GL6_-Gs%l&tWtpCVjJXP7a5ht>1pMm@n-zrApb&)Uk`&Is$|Ie$8{g0Fj zlHSi5Jw}O$*lM5;^`ok>>4q36?`Opx9)85XBnA9c_1FBrug^Ke3#3nVcj<EG)b zmCuTFdA_ia^nb;uvmX6t;UA8d(>Eg#`dS+K5B|Z@Us{B(p3x5RL^R}KApifm(R8&a zXx#zpb3VUeto%(3ErUNQEXTk9OQ3gPsT}VuW9K>a3ws0pmhce17#;HYuv{KXDnowN z{nz!4J_|+mjtg%7x`DA`ktnTl)y-cwG`9UHZp!nzYQLnBF$ndEuOJ?&#(2H4@#Ye= zj>r1t1L{i?qj;(amHE0@+IyuR8I8an+NY*Q!)YS;BI4;Q)c0n_3wP1qU=#dhmW02# z@g?NZ?_J0P$7^ArA9Bbg$cw7KmiVCm1;&da{(i>n1!CaKz$b$G!1;<>V*Ccw&sIj( z4D^4&|7(!`?;BU~zy8FixlTlfzXW>meLJJgev$W}J>-}F|J3NTT->V(eaD$Z{Ms7>Hlsgq6*qs~ z!Ds_`7aOoIK3bbO~Re(c!9S?m@hw z^4DJ&i8!Ai*ZV!`f4UmYrhB69K;BjTbvNU$Gsw?W1^s9*f{ewR5FbL`RsOoWQ7=Ob z-2;7}L4O-;G}$Pk;-ayhAwMbKQRrFwcmHrcZ>*Ex=C9TJYJ=VUbx&jRX%XJ)EBH6s zFXBJEB;3WXmvQtVTJ66MdXnC~jj+`sR-WgpOnKnc zKcnI?5%f9qQRT0{GA5v(T?q6cnfQNggd7yjD?|T}(BAYnKAa&6>j1vtj4#8CDHB02 z@Kfcl2N+Ga<2)IjRsA*bNtgW<8}s`JBlisK71pywoS*L#y0mul*Mv7V3-WxaoL({h zC-MDx2|vFt-jDGr(SAl5>C=&aoP+q9_G7TI`ZCTpK^{VgKfjN!vkmKS$^)OHigNKB zDaZ2}oe<9s{txU0_2paR#9Z7@yao1;-w!c%pq^qT_;ZHy4>dxTim)AX-TZZoG5rVh zb6eove?81NG#2O81MvMn@Oui+g5Tl2pswnWl{{Jg~T@nR< zHNz|M9c#?qFLIKfMtsZh#u;Uqh&N!LZxi0{jXa#+34wm8{Phn;@OnW)g*ogCFHKe=}-&w|{(PCgN%%}3#KN)v^6=8nEp)a&& zvkmVA5mXc7sr>aEBlrR0%Sh0-T)#?c7J{Grey*{hScK1l{mSx5ddxGzW{J{wXSne& znfY z@pCW6BR~0^lL2{bNBXTYina@H)HiPan(udoV*f_vuh$r}PKX>!!bAMl8ingTT#u{# z!#d-MIU>C5QP7j)5x;>SUB~y7ul2^5A5ow36ZlVivB4N~NEGy_$ zDP=SKpOF09W^_9&aw>xVMU=Pg#u|b9WG3MISouD|xQ2e<+3!O>xL>xzcx$6bs0(|e z@}Z)X*Sn2(G9j-Tz9;^Bj5Ww_4z?g4l>fcP zy0Ic+7wm_k&pzY)NKaIBJ*>yd<#G7yZ2&Ky4;WAG7Tx6jq{?3(G}g}%q5VKFmA^h@ z;JjnR49M?v%F|(E);+{yh*z-3j34>Y?F--=O8y@;CLM)8hd+v>d?gw8u|BWS8}CV< zW5$q!fWHXzC%umw`_R9r(MiNV{PcuP2RU-1k)ke6!2rsD6@Mz70a%_-R5z<=I*LLYXAe30KM#)|8Rrx$}B952;)@dsS(2z?6T_h*bh z_K46+z+XL+{^wqS{Y&HfvqoE-x4r{;@h{h(D8KpeccjmGv`R$(A8DWH-!B@+E{X69s{jxA zcgdKvO$=>RjQEB5z(0(F`J%zpiSGL3vJnjX9C-r!$>sdd@{|9kzrJe3?GgD6AU|rq zi~I~}gZKS?lE1`1=12tQXMXO6@y93;)i7McZ{D#BmV_? zc%1YtGS>bs(pwG%|M>r6k&{lKtr-&^$@$S39Lp)vP}Nb^VhrQ&aoxnU3bTZ5nJ{6G1Vi?yk^$?wU(nD7RW zf5Q8yc`r^B_5nXT6W_0)CJ-mCmI;V+f+O((x>Gd>09xoW!m-;VkCT#?;i1NMgr?^9;cIIMRde`>w?wD~X% z^{Y!DpXA3gW+c`xq0(Lx|BB{zd8x32!3atNX7BZ_IywhJO5mANpy(27GDXUNe^>e-n!JpUPho-hzw3U!6~W z-F$qdD3$eP=gZ}{C;;-L@^5dN*H4LD`>gzJGcy_c9bz%;UwQtd zFca`9|Ixy%k&B~Uum`PU<#_LzFHaW1#}hy=`g29!OGgp!(m%B{XCD&b)nGqW{dFs| z?gE_GhCIYkUif`UGasHQ&;K)@%0T?G3I3h_?gR6`zlg|=pyyQb=R?yvB%)J6?|J3& zvdiCe?M)lA*M8i8@I2&)<9}q$*4wIoxRRGz(4Tu8kIy4yeT1sNQ}Qq4 z@e;x}$b9p%DEtWaE}!uzzfTNBe5mr*gU$cU$N9^7SZ`DQqD|jq5fMKb|EGL;&CQFz zf7rX$eD;|~QpC{V)v-TA`g6W?zXHrpehx7!-4Okw0pCoHH`Ls}S42lmfWDOTJ9v2Z z|GWP>APfGiZ=}S3xY=f%2(JWtr22=CFxy`cw|lIEJub&LziS!fkMbeR;)x>X^H*?ld+`oGa^Qrv9 z3^QJco=@WY6z2cp%#lghFGc)#i~N{rMx}a6@`By^>se-c4z~Va4^{s9C-Z-^aQ+qc zp|D*4gW|5a`Rh66j4|k+4t|Ex9#H z&o>v|!g>htor;(F|Adrrh=+*ZLi5i;G2qO0$TR)NFJ@=NJF;Jwve*3H_ciQOXU1!b z%qFYEQ0Y%p{qAIJH^ zN8R{$xw&^P&X+93{u$+Eh55s;BC}#6H{M=p9{nEoOT)jY{PimH@I}#N;t)4~z1rM5 zMyyK%Kk70braU%z3-1F-|Fve1Z6bXG{9!QdC*McO^O==N@88U>3F!9-dy~QW*PD+# z5Ce+fAIH<*ZZHST7V*gKh-Zo4MspqRAFfageE9t)vm*9uQ{WH&wf_<72l^@hvBmsr zEBbAM{%FFbvA8Gu?>|d>wa;vfdZOHP$RFvu-#n5hVq&pgOQF3wV19x3 zlc5iRd_HJSxq|aK(D!+KK4dV z78&ApU-*jy;S&BNv(`l6yA1w3OZpr$f5d$UEx-?zzdmj@Tp@gaLH;sIi&v!CP%?f`on$$T#9TYL`ss_g$Mb13@Z$n&Qve|_3qd|c#=1^p^g-;&LZI6s^M z{MC7@6m#7?)VBpdpUIz8^X&(yuj>waQohcZs}7*PeK6vea(?A+IfeaP(&Mc8?kBtlt@-yA6j(q3f)^7g#f;j~F!`l9C{dI=mum)7U_N4tUpa;+lYr3Ma%aE=Dlm8U_=eX_r$-@?1cLwWc(h-e$JfVm)Rcnm-)jY zGjIydyB=`!*Tv@jr6MB~@~HZUmzbFwknch~q4@om+3SS3^(Xvy80F!v*&s)TXO6GS2e@Gv`FFgo(Q1J)xN$(nuc$xU~*;}y} z@}C?}w;GJYc_YLd!8}i6SeueXe#Uf+H%{`$w8kcj=qi&@A53|&tfq+~yrLK5k$<+O z{e=36^KN|MSS@j$CK2+Q%Y40r$MfK2$b+iCe%dM+hx|l0H-G(%wS2S4@IySI^4Aru zPWwfZEwEQngs+nI%vjjNI{zF+G95%3qT{rJ2w_)jzztbr<(jkE{xR%kgViyTPAq z*fW*Cu4&yoBl41`y7yl{Z#6>wRAe9I7imvwS!KvCKR*=yf$`o8*6%nE9RYb!_1ENA zY|s6OpZWbuR^Vh&Qu9Z5zS@>?MPyZsgT1A`*0KB^BA&T}{2J{a;oaU4@}ctAb*%~L z_v(Kf{3Cx}v3|q-=5x9r-sSVF)>iEI7KOX}x35{%(O-5x?5oOO*R!tvBnmeza{GtB zZpEMV__}QVf9ubYcfUitO?=+8)?$6#x*hCUiIf-O6WlNw_MZ0ZZL7&v;hPA1*^%GBZx(|$FwH0)nC`U3pre2uNe8KST+ z?0;;eoWF_n?ov+-?*pa$y=z^_5!)*sfWFcGH?@wZ;(oHlh#v?~GpjP5A2fu0`Pcft zcMasP@5}L8SQRgNGUWbKNs)Yh&+4&P4EG@(Q1zL9R&J@t{ir7XPkC!;wLIa8DjW@a zNd0VO1*6}CALLopU%zk7*eYVbflBV4+w7n>{Tk`?+>goWg_Y%`2Udd`JvT$ zsR;cH^z2zKuTjIsLcYrBS-QJ3=*|2!={Iz<5Bu+Y|FJbaUu?@-iTo&IUk4L!r*WRjkQgmA}0sf8j?qGe4 z{euA5XSF}m(aQY;=l_G;{PkznwM3EEbtmj4<)M@H-er-Ui~Y*z6Ox{vTUF4Hu%@IZ z>CxGWz95?T!Jb=`uP)XQ+@D$q`Y8KA{5GzJeNy$;0agd>C&=?}Dt{el75^q8WxhDI zRF40J6<&gT59F^c^|z~a`>+_@au@Qul)r9P-O1=bbQ|z+{vhk?eInr^;`iRaO(d{Yh`@`v+o+?B}TR*L|#C7Kv|U zzT?bUIe%a4k*UHfKEi%5|4;h(q(5p*fBTj73C_>SeE1o{LwixUv;pET@}s|%vkdhQ zC;InZ$9ksR9}c<#`n5}t<9%zLzK?#Z7*FM| zhgh2tKV5`A1d?Avt#xZfr1T$QgfGULf%~px{ZldFA7Cc0+9e=2zzZk-g!pMzg& ze`JI;H4XJR^PsP^N3mAl?eK4*=pRmdLwe_XIv{=~Kl$wQoq#{&_k7O#X(#M8<&)1@ zGGE`8`uLso`VJA(?FYyk-;c4z-ADZ)>{%fHKh}Esq$qjiS=ZiTymq588_SHT7-0m9$nA-hh0o_0|Nd`4!YdfPR&~ zmGqow{k%)$4}$%Ami(Dy^)40uDT(k0gm1F-@sG&Yz`v{h;Zv+LJJHW_6~-gHQ>|%x zM8QtjhZ6pOnzin}I3EW8)sFB@x5{pdm=e$@i1eRf$@@+h?{M?iaaQacksG=Z;}L%1 zcdsGntM(&iS+)1ze%xgA4<|l9Sw}AkUmWnyq5RIap8FO158#(S;UT@fo*Q`P{PG$8 z4*U(|_j9eQIf!3_@t*P}=RJ!Sqs zn&bUq9mjb$SwE)si+;6UABX+%A+YC!XOWeN_1i|sw}OZGmCE{Fb-r+kbqoFLy9a{5 z{C=s0ek!H!y#ji~O8zXfGLDPfR>NHRSZ+PBOGKo$b@5qYc~^=aHz(nl->^+v1hH1rb#{L{(Z6F%jAY@vJ}nV}iAA9rjBSkWZ!l?y!1dzcu5IE5AFfU6;f+e}aB0f1PLz z7$;&c9Roef;m!LX2J%6E@3vb0CUV~fKB|BC9_!svxW5SgN#%?8TB)V5M{$VX7{Bkc z=As_y@Z+F&dHfUmusY@^JO`{Qmqq^>;FsFZqx_ZxEP(&u{D-WiOGMum?J*wZgYQFS zK2zndk66!N6{8;+4*2-}QESCr5!HJzzUOn2h5jYEe&BC%q@?#T%X3HM`GKB+Jbu0e12p_x_{+{}M!&*EZ_2r;f7ry8B-qe=pAI|x+t+eGLU+zy| zC%gBYib_yzmq^;;=}K~QDYH5q)U7Ytwh{F#2k|}a1Mv&X*zy15qd`^Zqsm|3wT@r&bWNED z`Vqc+)`eXnp%La+`QuV6B}e20ZN~mE<*Urvhx5O|@Lw^Mulv?hYeary&{yTdIA5vX z%b1`0Jr6C^YedR=x#^_8$KJeA3`y(@e)9j1*e7rwP0bL{oAl@ZgW?chsQqF-=LaFa zoyGq@X20;K2z_-K^nvtv+^%#U{dT|)Re$}2T^;8&KEnDuiTe4Zy&wJZ-gfYR!e7B| zxB#WlV?YnC4>Wr(`ZZKTekGOhg>F~k9JM{Qfz6(@v3oCe!U7Ue$hjfjH+6dY6*^|Ftu5-ZvKT^dtV&?4V>3m3SHZF@&eO zefEUNoxcI{N%^Z`f43OtDI2-_<2CJu%S2Ec@LT1tpSOMJM=j64o#J@4Y$p@vpDV&X zl3p*^N3s8W$H0Cg;d{}Zmo3uec^6fG{gOR0O%#j{0>3FQwe1!ep2C)RF5&)M9UJ$l zrnia)|AtHbc-fvk*He;M2>VBR)wSPRivGyZpS1h({VR58q9~~a`8>e!UbUn5ix9a! zPNMym^!7wucn|iP^r&Z#!}*8+yzfK!$p26opQ!a4pQDQ)|8dm!H|-I7Md1^Wha%$p zmfb#0G}sf2{59$Kww=DslaUGj*Wh~M9s9ZIVqhTj_d4aFzO5&Uh;Hyd70J&Ab~gH* z3>*jhOncPO{?BUMcj5>9IDR9$_HNY2_HpNHY%e_}eBVO;uXDewiH-G6x?FE3QGedG zm+nWuO6Xr_;@8wpJ&gTV(C;esp_y%CzUViRzaA{<)!cq28~q+jVK11Ul=ymLp2h!9 z5x)2A7xrL%gm^~fAN*|ZXxw*(_#ut(wX}zC76EA+u^&Ksx3cX~qVU94H-3NLp1Dv& z4f_!LXG7(Dq-WHFpJA{0{Rj5bIA2*2@^*^vKeT^O5`n2pfIs1(J4;C`$8zb7ma@*79_=xXQ96ZvQAL%z%ZU-CpFw|{t$ zz3+ghc`L}%P>$E#UUfu7G>rm(86Oay?OnhR)jvGMUY;qo$oeLgzYevZf&Giwjr9@v zL--1NLtj+>x~E-fI^qx5vl7b7m-Y&*-=79O4{*L-b`t!zwEx%m+}qxCMijP${Hgf7 zkNrj>_JzM)es-4(>oeTaD|8Tn-?vD({`=5zlg#A@6>b3h| zy&5g~M|?{{KtGj_BfTQ$O@uttzYnzQjlzCYcjyDF0 zFQk7TY%km+`c@f){TUy!2f>@dXm3Bdz<`x`L+k~6!T&b!7aVV>U6v^dx58ei{^2q99mFfWhhseI!!R55Mo}pkZ!qn_ zaQoHOxXfxdpOo-wK1RS zA3nlip#Kpr2XP zr*Zc3RFOGiC!Wcl@9k=*klzP?8hItX$d9Nj$Xg-hkN=Me#r!Fx_jtSE9#6~}4fu0D zzAu*dFV7-;iogC(x%rJr_J$iGVH^0>nf`6Ez4Ey5K7{>9;{NLtI~C{m`i3Chjga$C zwWr+{-Y2hOzvQ8Oo@T$2BC?a$!QPTT)9p^Hao^v|h?m0T`x*9In??WNLeyXL|D3;h z>n9M8k>4}z-nd`*OjFnX%(54wf5?6Kb5(!+lN||qwL$#2jPgF)uCxpLgDBnCz$do=l8@8e%!6Uo^SUq6w!V@=sV$C zV4pxfDWxauDd$^gzm5BC`a%9w{q-;QtpenW0FSD_{?#5b5Bt$e-2IeA_Qnaww?e*E z{(7-J9`Uv8fAT)<75SGgg!6oUJ>p?guM)+Jgl-OjP?ZMZ1ms$cD4Q61ML6wgTJMGtg-#B zBfm2j_JH!V)}CJ`60X5ttN!8S&-Hq+U!jbLf3r_~=kYxbc~$k->+SbbMf0AUL4U4y zH`w3b@nl{675N3q!$$in)T`f%hCC7eO?I96=m!h^Q}z6t?WgABK8<8If4#+iHXikC z7VKxZlo!t5OrEEiMfz;DXDt&&ZRWWB!?)Q@E{e#O3qW7uyWKuD1Nw*gRQ@`_4*wbV z>%pJrQ@(cC*RpZ{-&)8c@!x5e;l8pLo^$ipi8juo#K?TlbdI;nM!(ja3yXk1<5BW! zcu_d~&1DJS9=rK*kr9vv`qRJewIi>IU7?7F`Vn8kA9@M)LgjP!+jnsPTFN;$pLxJO zc3O=72=pFDc|B;)D-iiZ-f-)$57~*QM1b7?jv~Aq?^4ZnZvOg+?ZN%ya{U@e`*ze0 zT<*z{=VMg+6(4;hNefk@$xzQ{B<$l0r;)* z*XQlQ7ew0c$KCvOx;+5(ee)2HR_1&c?4@@-zK)ID{^1#R;8F2sk1V%;c&1%8Qw)6M zFyedikNhsp#C}#~%J)TkS32TR@T(E|b;B`F(`EUw%8^U$*!EEF!;x z{;B*m@gE)!`N<(YuiCgDF5Pb#^nu@Bv%mgBq;~~;uhM>Aw` zK61X>Km4XWe};&c=Z7kjADq9dyx%N|_AbYsiSrNN{^ZtQ-?DRm5yh<6rM%|aht?tg z4u7Ze*Ln7qTO#r)tiM$Kb-vwuvxwq(I>y(8KPD9Rwj=$mqEEn8@Q41gz+Sx_^{n8B zs=qF@j~?^*##KlBQ65j^uZ8>`A$&!)cYz3xg8#k3=VH5HsVF^+_Zb|o#Li!ce!;Lm z{==od{bf(Ti28!1?)k*K_FKu~oa~>Z^4It5eR+sS;4gXY-}ziFRH z&*q7MH?UN~_oUP6q6liZ-_2iFa5^A9slCCizt)_2@nT~Y&?AfV)tzThKh`JCz5m*9 zlG1TrANr;8*AhNY*E5h0RbOK{<|=Wm8t7Y%^tPRPX(B@2zo_!pj&mO8NoI7#eB{ql zPRqTbzwB3*!TFwc+Ak5eqA|b9Uq9mvEWrIMfM4aWmAqVqz4Pbym7KJ-*iY}}_7AV@ zEI_?MIOdO1`U3bw><-vlmA@uE?$>-1`D>q~$8*k~$j29fAL@KmRc8bGHMJ^p_b>kI z#9tCI{rb82>uS#beiX$WfNvV<&GEv!L*7mjpBm0PWuhYv zCW(ajTYxuR(wp)Uc5oZ;p?%@_-o9Jh{PjD|Q%l64F!DSn)zr&N^2YFKdtBDhGTAZ)8*{#2R z*YW;>_&37MpEh-Bqux%|=gf+9;YB|q&{O5Fn>(MupUC}`&4j;&^JShG)MY&ECEF4ov2mb%cU!R|m;O4JM-yrNA ziQ$Cj1LxpPoL{dGdw5U6$M+@D|F@+)wstP=!ukO6x1}8ZsML*!&&t>94>MndeWbj6 z>?Gb6XPyPVJ&8|SXWtf)Q0V~RXFT$W6E;Qke+l}p>aW{5^A~!2Hv?dwcz*9w=f~@) z--7*7`Rn#hpXnkYA2|Z^9VdM{Ij1Lx zU6)!yp3h4BK9}|`swfQp*DIepJFnq9jbEaB|26T?3jlx4l*@0JTpy|Zg8;`~Aik0L ztRm7Q(9tiTo}~xYr_@*e-<#Dd>l|4uyalj7Du3P2`Dh~UI~n5UufKAVQawR3|E==ZUprNAie9005HE3m zq`xx*>+dcG*5Bk$nDgKg)>FZ5{Wa&`6$*LVNq&YqI{MiK;C(XTiEz4XK)-a%ulDOB zop)x5tP49JulFTCzj6Lpi~Tp?r}EbWo!Qe-S_=CXNPZ4-E+F6As4<=?-<&W1Vn_FT zKF7S!74e@>ju-8e!C#}-#NYjhypFRK=jZWWosaN2qp&}J<{iM3FTW=}gD&iZ{xaSg z;xyWa`{nd6Ig-FQMH36n2KZ2dC;%alUIj;w$3+qmvAN&NzzrK3e{t@D?mYe49o7PH=M2 z|9AlWi^^Y5bj%|nKeH|7g+|oxNdRKSK`C@2KKvx@yOrl&ZCpX!!4a555#YVQw{g0?gl^8NWVDe?fo0*L)wI0{%lM{>IM}E9>?AX)otEW$3rN2KFG8>&2g) zPjH_{Db_2h{(7#Hi1^Kax{LojC*UyTU6(?{bTqqmA_u(R9PXiDz(6S>f>T(;-AP56o6jj z*Ai!CwkW6#`&vl)as2SI)tI0DcbVhG{g$ubeKzC6<<9S@|Go4h@a6X_oXo$_pW=7u zFY#UJJj@b(zh4FV5#CkK_2t6XBLedA&;B?1_d|YE{(6n`Wr2tC7{vHyt%H7J8HvS^ zcg6>#clPC7?s&h+cwtCm=zl85TkkZRje5)mh)>DS4bE!x?@fLPf5K<^e^1Ho1@Ir_ z|0d`04X9tkdZaz&bF)(q`=R%{13u2b#n~`Lczb&=KjDve+Mf_ZW&GNi{MqWXT`D34 z;up2wu+8})M zTZn!w(1#4l$3AEGF}UV)kQct^_xb({5x;T%1J1269`<)v`;P~mlwGi&Vc;+Q*&*k# z@3DUdd#cW3A9m9Aijgg!ck8c@IGGnjub!~~YW$#hS2BoG-~4`#tKVLEmsb zA9Jd$5J57&QTglR&g5*-Ed%m2ob*27Y*;FC*Y1NqA-zsI^ZyVnTfG5&BRzh1;oZ{0HfuYu z)rn2O{$vK|$M@Hqap*UPwbI}9bl08fTSTlp|GtOw-*9SSf4pKp@R#`AbY9NH{ey41 z@|*1h{Y?J&5Ue$VVKO>}@Pw**wxK9lAl9|K)za&KKGnV)DL+Y0bkBn>O6H`_@up3{=LizMg4L&*zZ2nzx&R; ti(-41k6iu|{^nikB7aT$_0U-|3iX_4K`;6n&r=I#iS92?hrOac{T~uqgZ%&i From 383e054a8d8bf801ec8627cf2dbb450f5faced3c Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 9 Jun 2025 22:24:50 -0700 Subject: [PATCH 21/37] clang --- Simulator/Connections/Neuro/ConnGrowth.h | 7 ++----- Simulator/Core/GPUModel.cpp | 2 -- Simulator/Core/GPUModel.h | 1 - Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp | 6 ++---- Simulator/Vertices/Neuro/AllIFNeurons.h | 4 ---- Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp | 3 +-- 6 files changed, 5 insertions(+), 18 deletions(-) diff --git a/Simulator/Connections/Neuro/ConnGrowth.h b/Simulator/Connections/Neuro/ConnGrowth.h index 263d96dca..965554403 100644 --- a/Simulator/Connections/Neuro/ConnGrowth.h +++ b/Simulator/Connections/Neuro/ConnGrowth.h @@ -73,9 +73,6 @@ #include "Simulator.h" #include #include - - - // cereal #include #include @@ -130,8 +127,8 @@ class ConnGrowth : public Connections { /// @param layout The Layout object. virtual void updateEdgesWeights(int numVertices, AllVertices &vertices, AllEdges &edges, AllVerticesDeviceProperties *allVerticesDevice, - AllEdgesDeviceProperties *allEdgesDevice, - Layout &layout, cudaStream_t stream) override; + AllEdgesDeviceProperties *allEdgesDevice, Layout &layout, + cudaStream_t stream) override; #else /// Update the weights of the Synapses in the simulation. To be clear, diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index 8aa17fea1..b68df8054 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -103,7 +103,6 @@ void GPUModel::setupSim() AsyncGenerator.loadAsyncPhilox(Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getNoiseRngSeed()); - #ifdef PERFORMANCE_METRICS cudaEventCreate(&start); cudaEventCreate(&stop); @@ -140,7 +139,6 @@ void GPUModel::finish() deleteDeviceStruct((void **)&allVerticesDevice_, (void **)&allEdgesDevice_); deleteEdgeIndexMap(); - #ifdef PERFORMANCE_METRICS cudaEventDestroy(start); cudaEventDestroy(stop); diff --git a/Simulator/Core/GPUModel.h b/Simulator/Core/GPUModel.h index df50a4835..8d5bf8158 100644 --- a/Simulator/Core/GPUModel.h +++ b/Simulator/Core/GPUModel.h @@ -121,7 +121,6 @@ class GPUModel : public Model { /// vertex structure in device memory. AllVerticesDeviceProperties *allVerticesDevice_; - private: void allocEdgeIndexMap(int count); diff --git a/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp b/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp index 3fb469cdf..99e6aaf25 100644 --- a/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp +++ b/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp @@ -89,8 +89,6 @@ void AllSTDPSynapses::allocDeviceStruct(AllSTDPSynapsesDeviceProperties &allEdge HANDLE_ERROR(cudaMalloc((void **)&allEdgesDevice.Apos_, maxTotalSynapses * sizeof(BGFLOAT))); HANDLE_ERROR(cudaMalloc((void **)&allEdgesDevice.mupos_, maxTotalSynapses * sizeof(BGFLOAT))); HANDLE_ERROR(cudaMalloc((void **)&allEdgesDevice.muneg_, maxTotalSynapses * sizeof(BGFLOAT))); - - } /// Delete GPU memories. @@ -270,10 +268,10 @@ void AllSTDPSynapses::advanceEdges(void *allEdgesDevice, void *allVerticesDevice const int threadsPerBlock = 256; int blocksPerGrid = (totalEdgeCount_ + threadsPerBlock - 1) / threadsPerBlock; // Advance synapses -------------> - advanceSTDPSynapsesDevice<<>>( + advanceSTDPSynapsesDevice<<>>( totalEdgeCount_, (EdgeIndexMapDevice *)edgeIndexMapDevice, g_simulationStep, Simulator::getInstance().getDeltaT(), (AllSTDPSynapsesDeviceProperties *)allEdgesDevice, - (AllSpikingNeuronsDeviceProperties *)allVerticesDevice, maxSpikes); + (AllSpikingNeuronsDeviceProperties *)allVerticesDevice, maxSpikes); } /// Set synapse class ID defined by enumClassSynapses for the caller's Synapse class. diff --git a/Simulator/Vertices/Neuro/AllIFNeurons.h b/Simulator/Vertices/Neuro/AllIFNeurons.h index 07d7cae58..99f3b8140 100644 --- a/Simulator/Vertices/Neuro/AllIFNeurons.h +++ b/Simulator/Vertices/Neuro/AllIFNeurons.h @@ -26,10 +26,6 @@ #include "AllSpikingNeurons.h" #include "DeviceVector.h" #include "Global.h" -#ifdef USE_GPU -#include -#endif - // cereal #include #include diff --git a/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp b/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp index cddcc6de6..dcca032df 100644 --- a/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp @@ -56,14 +56,13 @@ void AllLIFNeurons::advanceVertices(AllEdges &synapses, void *allVerticesDevice, int blocksPerGrid = (vertex_count + threadsPerBlock - 1) / threadsPerBlock; // Advance neurons -------------> - advanceLIFNeuronsDevice<<>>( + advanceLIFNeuronsDevice<<>>( vertex_count, Simulator::getInstance().getMaxEdgesPerVertex(), maxSpikes, Simulator::getInstance().getDeltaT(), g_simulationStep, randNoise, hasFired_, summationPoints_, Vm_, Trefract_, numStepsInRefractoryPeriod_, Vthresh_, Vreset_, I0_, Inoise_, C2_, C1_, (AllIFNeuronsDeviceProperties *)allVerticesDevice, (AllSpikingSynapsesDeviceProperties *)allEdgesDevice, edgeIndexMapDevice, fAllowBackPropagation_); - } ///@} From 8719559db857a3bff36a639f2dd925eafbc636a4 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 9 Jun 2025 22:35:13 -0700 Subject: [PATCH 22/37] format --- Simulator/Connections/Connections.h | 8 +- Simulator/Connections/Neuro/ConnGrowth_d.cpp | 8 +- .../Edges/Neuro/AllSpikingSynapses_d.cpp | 4 +- Simulator/Utils/RNG/MersenneTwister_d.cu | 338 +++++++++--------- .../Vertices/Neuro/AllSpikingNeurons_d.cpp | 1 - 5 files changed, 169 insertions(+), 190 deletions(-) diff --git a/Simulator/Connections/Connections.h b/Simulator/Connections/Connections.h index a594d56d2..400a7deed 100644 --- a/Simulator/Connections/Connections.h +++ b/Simulator/Connections/Connections.h @@ -34,7 +34,7 @@ #include #ifdef USE_GPU -#include + #include #endif @@ -92,10 +92,8 @@ class Connections { /// @param layout Layout information of the graph network. virtual void updateEdgesWeights(int numVertices, AllVertices &vertices, AllEdges &edges, AllVerticesDeviceProperties *allVerticesDevice, - AllEdgesDeviceProperties *allEdgesDevice, Layout &layout - ,cudaStream_t stream); - - + AllEdgesDeviceProperties *allEdgesDevice, Layout &layout, + cudaStream_t stream); #else public: /// Update the weight of the edges in the simulation. diff --git a/Simulator/Connections/Neuro/ConnGrowth_d.cpp b/Simulator/Connections/Neuro/ConnGrowth_d.cpp index c0a293d81..12b178002 100644 --- a/Simulator/Connections/Neuro/ConnGrowth_d.cpp +++ b/Simulator/Connections/Neuro/ConnGrowth_d.cpp @@ -29,9 +29,8 @@ */ void ConnGrowth::updateEdgesWeights(int numVertices, AllVertices &vertices, AllEdges &edges, AllVerticesDeviceProperties *allVerticesDevice, - AllEdgesDeviceProperties *allEdgesDevice, Layout &layout - ,cudaStream_t stream - ) + AllEdgesDeviceProperties *allEdgesDevice, Layout &layout, + cudaStream_t stream) { Simulator &simulator = Simulator::getInstance(); // For now, we just set the weights to equal the areas. We will later @@ -66,12 +65,11 @@ void ConnGrowth::updateEdgesWeights(int numVertices, AllVertices &vertices, AllE cudaMemcpyHostToDevice)); blocksPerGrid = (simulator.getTotalVertices() + threadsPerBlock - 1) / threadsPerBlock; - updateSynapsesWeightsDevice<<>>( + updateSynapsesWeightsDevice<<>>( simulator.getTotalVertices(), deltaT, W_d, simulator.getMaxEdgesPerVertex(), (AllSpikingNeuronsDeviceProperties *)allVerticesDevice, (AllSpikingSynapsesDeviceProperties *)allEdgesDevice, neuronTypeMapD); - // free memories HANDLE_ERROR(cudaFree(W_d)); delete[] W_h; diff --git a/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp b/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp index 02dc839e4..404ac9bd4 100644 --- a/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp +++ b/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp @@ -85,7 +85,6 @@ void AllSpikingSynapses::allocDeviceStruct(AllSpikingSynapsesDeviceProperties &a HANDLE_ERROR(cudaMalloc((void **)&allEdgesDevice.delayIndex_, maxTotalSynapses * sizeof(int))); HANDLE_ERROR( cudaMalloc((void **)&allEdgesDevice.delayQueueLength_, maxTotalSynapses * sizeof(int))); - } /// Delete GPU memories. @@ -331,10 +330,9 @@ void AllSpikingSynapses::advanceEdges(void *allEdgesDevice, void *allVerticesDev const int threadsPerBlock = 256; int blocksPerGrid = (totalEdgeCount_ + threadsPerBlock - 1) / threadsPerBlock; // Advance synapses -------------> - advanceSpikingSynapsesDevice<<>>( + advanceSpikingSynapsesDevice<<>>( totalEdgeCount_, (EdgeIndexMapDevice *)edgeIndexMapDevice, g_simulationStep, Simulator::getInstance().getDeltaT(), (AllSpikingSynapsesDeviceProperties *)allEdgesDevice); - } /// Prints GPU SynapsesProps data. diff --git a/Simulator/Utils/RNG/MersenneTwister_d.cu b/Simulator/Utils/RNG/MersenneTwister_d.cu index 67c61b155..38a2718f2 100644 --- a/Simulator/Utils/RNG/MersenneTwister_d.cu +++ b/Simulator/Utils/RNG/MersenneTwister_d.cu @@ -35,14 +35,12 @@ #include #include -#include -#include using namespace std; #include "MersenneTwister_d.h" __device__ static mt_struct_stripped ds_MT[MT_RNG_COUNT]; static mt_struct_stripped h_MT[MT_RNG_COUNT]; -__device__ unsigned int mt[MT_RNG_COUNT*MT_NN]; +__device__ unsigned int mt[MT_RNG_COUNT * MT_NN]; //#define MT_DATAFILE "MersenneTwister/data/MersenneTwister.dat" @@ -59,54 +57,54 @@ unsigned int mt_threads; unsigned int mt_nPerRng; - //Load twister configurations -void loadMTGPU(const char *fname){ - FILE *fd = fopen(fname, "rb"); - if(!fd){ - cerr << "initMTGPU(): failed to open " << fname << endl << "FAILED" << endl; - exit(0); - } - if( !fread(h_MT, mt_rng_count*sizeof(mt_struct_stripped), 1, fd) ){ - cerr << "initMTGPU(): failed to load " << fname << endl << "FAILED" << endl; - exit(0); - } - fclose(fd); +void loadMTGPU(const char *fname) +{ + FILE *fd = fopen(fname, "rb"); + if (!fd) { + cerr << "initMTGPU(): failed to open " << fname << endl << "FAILED" << endl; + exit(0); + } + if (!fread(h_MT, mt_rng_count * sizeof(mt_struct_stripped), 1, fd)) { + cerr << "initMTGPU(): failed to load " << fname << endl << "FAILED" << endl; + exit(0); + } } //initialize the seed to mt[] -__global__ void seedMTGPUState(unsigned int seed){ - const int tid = blockDim.x * blockIdx.x + threadIdx.x; - int iState; - mt[MT_NN*tid] = seed; - for (iState = MT_NN*tid+1; iState < MT_NN*(1+tid); iState++) - mt[iState] = (1812433253U * (mt[iState - 1] ^ (mt[iState - 1] >> 30)) + iState) & MT_WMASK; - +__global__ void seedMTGPUState(unsigned int seed) +{ + const int tid = blockDim.x * blockIdx.x + threadIdx.x; + int iState; + mt[MT_NN * tid] = seed; + for (iState = MT_NN * tid + 1; iState < MT_NN * (1 + tid); iState++) + mt[iState] = (1812433253U * (mt[iState - 1] ^ (mt[iState - 1] >> 30)) + iState) & MT_WMASK; } //Initialize/seed twister for current GPU context -void seedMTGPU(unsigned int seed){ - int i; - //Need to be thread-safe - mt_struct_stripped *MT = (mt_struct_stripped *)malloc(mt_rng_count * sizeof(mt_struct_stripped)); +void seedMTGPU(unsigned int seed) +{ + int i; + //Need to be thread-safe + mt_struct_stripped *MT = (mt_struct_stripped *)malloc(mt_rng_count * sizeof(mt_struct_stripped)); - for(i = 0; i < mt_rng_count; i++){ - MT[i] = h_MT[i]; - MT[i].iState = i*MT_NN; - } + for (i = 0; i < mt_rng_count; i++) { + MT[i] = h_MT[i]; + MT[i].iState = i * MT_NN; + } //seed does need to be used to initialize mt[] elements. - int threadsPerBlock = 256; - //get ceil of MT_RNG_COUNT/threadsPerBlock - int blocksPerGrid = (mt_rng_count+threadsPerBlock-1)/threadsPerBlock; - seedMTGPUState<<>>(seed); + int threadsPerBlock = 256; + //get ceil of MT_RNG_COUNT/threadsPerBlock + int blocksPerGrid = (mt_rng_count + threadsPerBlock - 1) / threadsPerBlock; + seedMTGPUState<<>>(seed); - if(cudaMemcpyToSymbol(ds_MT, MT, mt_rng_count*sizeof(mt_struct_stripped))!=cudaSuccess){ - cerr << "seedMTGP failed" << endl; - exit(0); - } + if (cudaMemcpyToSymbol(ds_MT, MT, mt_rng_count * sizeof(mt_struct_stripped)) != cudaSuccess) { + cerr << "seedMTGP failed" << endl; + exit(0); + } - free(MT); + free(MT); } @@ -119,171 +117,159 @@ void seedMTGPU(unsigned int seed){ // The local seeds, in their turn, can be extracted from global seed // by means of any simple random number generator, like LCG. //////////////////////////////////////////////////////////////////////////////// -__global__ void RandomGPU( - float *d_Random, - int nPerRng, int mt_rng_count) +__global__ void RandomGPU(float *d_Random, int nPerRng, int mt_rng_count) { - const int tid = blockDim.x * blockIdx.x + threadIdx.x; - int iState, iState1, iStateM, iOut; - unsigned int mti, mti1, mtiM, x; - unsigned int matrix_a, mask_b, mask_c; - - //Load bit-vector Mersenne Twister parameters - matrix_a = ds_MT[tid].matrix_a; - mask_b = ds_MT[tid].mask_b; - mask_c = ds_MT[tid].mask_c; - - iState = ds_MT[tid].iState; - mti1 = mt[iState]; - for (iOut = 0; iOut < nPerRng; iOut++) { - iState1 = iState + 1; - iStateM = iState + MT_MM; - if(iState1 >= MT_NN*(1+tid)) iState1 -= MT_NN; - if(iStateM >= MT_NN*(1+tid)) iStateM -= MT_NN; - mti = mti1; - mti1 = mt[iState1]; - mtiM = mt[iStateM]; - - // MT recurrence - x = (mti & MT_UMASK) | (mti1 & MT_LMASK); - x = mtiM ^ (x >> 1) ^ ((x & 1) ? matrix_a : 0); - - mt[iState] = x; - iState = iState1; - - //Tempering transformation - x ^= (x >> MT_SHIFT0); - x ^= (x << MT_SHIFTB) & mask_b; - x ^= (x << MT_SHIFTC) & mask_c; - x ^= (x >> MT_SHIFT1); - - //Convert to (0, 1] float and write to global memory - d_Random[tid + iOut * mt_rng_count] = ((float)x + 1.0f) / 4294967296.0f; - } - ds_MT[tid].iState = iState; + const int tid = blockDim.x * blockIdx.x + threadIdx.x; + int iState, iState1, iStateM, iOut; + unsigned int mti, mti1, mtiM, x; + unsigned int matrix_a, mask_b, mask_c; + + //Load bit-vector Mersenne Twister parameters + matrix_a = ds_MT[tid].matrix_a; + mask_b = ds_MT[tid].mask_b; + mask_c = ds_MT[tid].mask_c; + + iState = ds_MT[tid].iState; + mti1 = mt[iState]; + for (iOut = 0; iOut < nPerRng; iOut++) { + iState1 = iState + 1; + iStateM = iState + MT_MM; + if (iState1 >= MT_NN * (1 + tid)) + iState1 -= MT_NN; + if (iStateM >= MT_NN * (1 + tid)) + iStateM -= MT_NN; + mti = mti1; + mti1 = mt[iState1]; + mtiM = mt[iStateM]; + + // MT recurrence + x = (mti & MT_UMASK) | (mti1 & MT_LMASK); + x = mtiM ^ (x >> 1) ^ ((x & 1) ? matrix_a : 0); + + mt[iState] = x; + iState = iState1; + + //Tempering transformation + x ^= (x >> MT_SHIFT0); + x ^= (x << MT_SHIFTB) & mask_b; + x ^= (x << MT_SHIFTC) & mask_c; + x ^= (x >> MT_SHIFT1); + + //Convert to (0, 1] float and write to global memory + d_Random[tid + iOut * mt_rng_count] = ((float)x + 1.0f) / 4294967296.0f; + } + ds_MT[tid].iState = iState; } //////////////////////////////////////////////////////////////////////////////// -// Transform each of MT_RNG_COUNT lanes of nPerRng uniformly distributed +// Transform each of MT_RNG_COUNT lanes of nPerRng uniformly distributed // random samples, produced by RandomGPU(), to normally distributed lanes // using Cartesian form of Box-Muller transformation. // nPerRng must be even. //////////////////////////////////////////////////////////////////////////////// #define PI 3.14159265358979f -__device__ inline void BoxMuller(float& u1, float& u2){ - float r = sqrtf(-2.0f * logf(u1)); - float phi = 2 * PI * u2; - u1 = r * __cosf(phi); - u2 = r * __sinf(phi); +__device__ inline void BoxMuller(float &u1, float &u2) +{ + float r = sqrtf(-2.0f * logf(u1)); + float phi = 2 * PI * u2; + u1 = r * __cosf(phi); + u2 = r * __sinf(phi); } -__global__ void BoxMullerGPU(float *d_Random, int nPerRng, int mt_rng_count){ - const int tid = blockDim.x * blockIdx.x + threadIdx.x; +__global__ void BoxMullerGPU(float *d_Random, int nPerRng, int mt_rng_count) +{ + const int tid = blockDim.x * blockIdx.x + threadIdx.x; - for (int iOut = 0; iOut < nPerRng; iOut += 2) - BoxMuller( - d_Random[tid + (iOut + 0) * mt_rng_count], - d_Random[tid + (iOut + 1) * mt_rng_count] - ); + for (int iOut = 0; iOut < nPerRng; iOut += 2) + BoxMuller(d_Random[tid + (iOut + 0) * mt_rng_count], + d_Random[tid + (iOut + 1) * mt_rng_count]); } -//skip the seperate BoxMullerGPU for increased speed (uses register memory). +//skip the seperate BoxMullerGPU for increased speed (uses register memory). //nPerRng must be a multiple of 2 -__global__ void RandomNormGPU( - float *d_Random, - int nPerRng, int mt_rng_count) +__global__ void RandomNormGPU(float *d_Random, int nPerRng, int mt_rng_count) { - const int tid = blockDim.x * blockIdx.x + threadIdx.x; - int iState, iState1, iStateM, iOut; - unsigned int mti, mti1, mtiM, x; - unsigned int matrix_a, mask_b, mask_c; - - float regVal1, regVal2; //need 2 values for boxmuller - bool boxFlag = false; //will perform boxmuller transform on true - - //Load bit-vector Mersenne Twister parameters - matrix_a = ds_MT[tid].matrix_a; - mask_b = ds_MT[tid].mask_b; - mask_c = ds_MT[tid].mask_c; - - iState = ds_MT[tid].iState; - mti1 = mt[iState]; - for (iOut = 0; iOut < nPerRng; iOut++) { - iState1 = iState + 1; - iStateM = iState + MT_MM; - if(iState1 >= MT_NN*(1+tid)) iState1 -= MT_NN; - if(iStateM >= MT_NN*(1+tid)) iStateM -= MT_NN; - mti = mti1; - mti1 = mt[iState1]; - mtiM = mt[iStateM]; - - // MT recurrence - x = (mti & MT_UMASK) | (mti1 & MT_LMASK); - x = mtiM ^ (x >> 1) ^ ((x & 1) ? matrix_a : 0); - - mt[iState] = x; - iState = iState1; - - //Tempering transformation - x ^= (x >> MT_SHIFT0); - x ^= (x << MT_SHIFTB) & mask_b; - x ^= (x << MT_SHIFTC) & mask_c; - x ^= (x >> MT_SHIFT1); - - if(boxFlag){ - regVal2 = ((float)x + 1.0f) / 4294967296.0f; - BoxMuller(regVal1,regVal2); - d_Random[tid + (iOut-1) * mt_rng_count] = regVal1; - d_Random[tid + iOut * mt_rng_count] = regVal2; - boxFlag = false; - }else{ - regVal1 = ((float)x + 1.0f) / 4294967296.0f; - boxFlag = true; - } - } - ds_MT[tid].iState = iState; + const int tid = blockDim.x * blockIdx.x + threadIdx.x; + int iState, iState1, iStateM, iOut; + unsigned int mti, mti1, mtiM, x; + unsigned int matrix_a, mask_b, mask_c; + + float regVal1, regVal2; //need 2 values for boxmuller + bool boxFlag = false; //will perform boxmuller transform on true + + //Load bit-vector Mersenne Twister parameters + matrix_a = ds_MT[tid].matrix_a; + mask_b = ds_MT[tid].mask_b; + mask_c = ds_MT[tid].mask_c; + + iState = ds_MT[tid].iState; + mti1 = mt[iState]; + for (iOut = 0; iOut < nPerRng; iOut++) { + iState1 = iState + 1; + iStateM = iState + MT_MM; + if (iState1 >= MT_NN * (1 + tid)) + iState1 -= MT_NN; + if (iStateM >= MT_NN * (1 + tid)) + iStateM -= MT_NN; + mti = mti1; + mti1 = mt[iState1]; + mtiM = mt[iStateM]; + + // MT recurrence + x = (mti & MT_UMASK) | (mti1 & MT_LMASK); + x = mtiM ^ (x >> 1) ^ ((x & 1) ? matrix_a : 0); + + mt[iState] = x; + iState = iState1; + + //Tempering transformation + x ^= (x >> MT_SHIFT0); + x ^= (x << MT_SHIFTB) & mask_b; + x ^= (x << MT_SHIFTC) & mask_c; + x ^= (x >> MT_SHIFT1); + + if (boxFlag) { + regVal2 = ((float)x + 1.0f) / 4294967296.0f; + BoxMuller(regVal1, regVal2); + d_Random[tid + (iOut - 1) * mt_rng_count] = regVal1; + d_Random[tid + iOut * mt_rng_count] = regVal2; + boxFlag = false; + } else { + regVal1 = ((float)x + 1.0f) / 4294967296.0f; + boxFlag = true; + } + } + ds_MT[tid].iState = iState; } - - -extern "C" void uniformMTGPU(float * d_random){ - RandomGPU<<>>(d_random, mt_nPerRng, mt_rng_count); +extern "C" void uniformMTGPU(float *d_random) +{ + RandomGPU<<>>(d_random, mt_nPerRng, mt_rng_count); } - float* hostBuffer = nullptr; - FILE* logfile; - - -extern "C" void normalMTGPU(float * d_random){ - RandomNormGPU<<>>(d_random, mt_nPerRng, mt_rng_count); +extern "C" void normalMTGPU(float *d_random) +{ + RandomNormGPU<<>>(d_random, mt_nPerRng, mt_rng_count); - cudaMemcpy(hostBuffer, d_random, mt_rng_count*mt_nPerRng * sizeof(float), cudaMemcpyDeviceToHost); - std::fwrite(hostBuffer, sizeof(float), mt_rng_count*mt_nPerRng, logfile); - + cudaMemcpy(hostBuffer, d_random, mt_rng_count * mt_nPerRng * sizeof(float), + cudaMemcpyDeviceToHost); } //initialize globals and setup state //Note: mt_rng_count must equal blocks*threads. mt_rng_count*nPerRng should equal the total number of randon numbers to be generated -extern "C" void initMTGPU(unsigned int seed, unsigned int blocks, unsigned int threads, unsigned int nPerRng, unsigned int mt_rng_c){ - mt_blocks = blocks; - mt_threads = threads; - mt_nPerRng = nPerRng; - mt_rng_count = mt_rng_c; - - loadMTGPU(MT_DATAFILE); - seedMTGPU(seed); - - cudaHostAlloc(&hostBuffer, mt_rng_count*nPerRng * sizeof(float), cudaHostAllocDefault); - logfile = std::fopen("mt_old_output.bin", "wb"); - -} +extern "C" void initMTGPU(unsigned int seed, unsigned int blocks, unsigned int threads, + unsigned int nPerRng, unsigned int mt_rng_c) +{ + mt_blocks = blocks; + mt_threads = threads; + mt_nPerRng = nPerRng; + mt_rng_count = mt_rng_c; -extern "C" void closeFileMT(){ + loadMTGPU(MT_DATAFILE); + seedMTGPU(seed); - std::fclose(logfile); - cudaFree(hostBuffer); + cudaHostAlloc(&hostBuffer, mt_rng_count * nPerRng * sizeof(float), cudaHostAllocDefault); } - diff --git a/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp b/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp index f7482ce65..c60a24e74 100644 --- a/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp @@ -32,7 +32,6 @@ __global__ void calcSummationPointDevice(int totalVertices, BGFLOAT *summationPo EdgeIndexMapDevice *edgeIndexMapDevice, AllSpikingSynapsesDeviceProperties *allEdgesDevice); - void AllSpikingNeurons::copyToDevice(void *deviceAddress) { AllSpikingNeuronsDeviceProperties allVerticesDevice; From 74104c8ebc77ae999c95fdee9b626c3ba828690e Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 9 Jun 2025 22:39:10 -0700 Subject: [PATCH 23/37] format --- Simulator/Edges/Neuro/AllSpikingSynapses.h | 1 - Simulator/Utils/RNG/MersenneTwister_d.cu | 12 +++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/Simulator/Edges/Neuro/AllSpikingSynapses.h b/Simulator/Edges/Neuro/AllSpikingSynapses.h index 4e464732c..0cd04821b 100644 --- a/Simulator/Edges/Neuro/AllSpikingSynapses.h +++ b/Simulator/Edges/Neuro/AllSpikingSynapses.h @@ -29,7 +29,6 @@ #include #include - struct AllSpikingSynapsesDeviceProperties; using fpPreSynapsesSpikeHit_t = void (*)(const BGSIZE, AllSpikingSynapsesDeviceProperties *); diff --git a/Simulator/Utils/RNG/MersenneTwister_d.cu b/Simulator/Utils/RNG/MersenneTwister_d.cu index 38a2718f2..6e4878609 100644 --- a/Simulator/Utils/RNG/MersenneTwister_d.cu +++ b/Simulator/Utils/RNG/MersenneTwister_d.cu @@ -35,6 +35,7 @@ #include #include + using namespace std; #include "MersenneTwister_d.h" @@ -56,7 +57,6 @@ unsigned int mt_blocks; unsigned int mt_threads; unsigned int mt_nPerRng; - //Load twister configurations void loadMTGPU(const char *fname) { @@ -69,6 +69,7 @@ void loadMTGPU(const char *fname) cerr << "initMTGPU(): failed to load " << fname << endl << "FAILED" << endl; exit(0); } + fclose(fd); } //initialize the seed to mt[] @@ -243,19 +244,14 @@ __global__ void RandomNormGPU(float *d_Random, int nPerRng, int mt_rng_count) ds_MT[tid].iState = iState; } - extern "C" void uniformMTGPU(float *d_random) { RandomGPU<<>>(d_random, mt_nPerRng, mt_rng_count); } - extern "C" void normalMTGPU(float *d_random) { RandomNormGPU<<>>(d_random, mt_nPerRng, mt_rng_count); - - cudaMemcpy(hostBuffer, d_random, mt_rng_count * mt_nPerRng * sizeof(float), - cudaMemcpyDeviceToHost); } //initialize globals and setup state @@ -270,6 +266,4 @@ extern "C" void initMTGPU(unsigned int seed, unsigned int blocks, unsigned int t loadMTGPU(MT_DATAFILE); seedMTGPU(seed); - - cudaHostAlloc(&hostBuffer, mt_rng_count * nPerRng * sizeof(float), cudaHostAllocDefault); -} +} \ No newline at end of file From 6d6b56fda3fe8b38dfd2f1e8542df32ea3e06c6b Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 9 Jun 2025 22:54:16 -0700 Subject: [PATCH 24/37] fixed cpp version of utils library and added appropriate include for Book.h --- CMakeLists.txt | 4 +++- Simulator/Utils/Book.h | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 120267b31..96f3648ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -368,7 +368,9 @@ add_library(RNG STATIC ${RNG_Source}) # Create Utils library file(GLOB Utils_Source Simulator/Utils/*.cpp Simulator/Utils/*.h) list(REMOVE_ITEM Utils_Source "${CMAKE_CURRENT_SOURCE_DIR}/Simulator/Utils/Factory.cpp") - +if(NOT ENABLE_CUDA) + list(REMOVE_ITEM Utils_Source "${CMAKE_CURRENT_SOURCE_DIR}/Simulator/Utils/Book.h") +endif() if(CMAKE_BUILD_TYPE STREQUAL "Profiling") if(ENABLE_CUDA) # Find NVTX Library diff --git a/Simulator/Utils/Book.h b/Simulator/Utils/Book.h index f71b1e952..c0dbb6919 100644 --- a/Simulator/Utils/Book.h +++ b/Simulator/Utils/Book.h @@ -22,6 +22,7 @@ #pragma once +#include #include //! CUDA Exception handler static void HandleError(cudaError_t err, const char *file, int line) From f0a60a29f0c4561888397686488ec6190b299111 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 10 Jun 2025 15:34:15 -0700 Subject: [PATCH 25/37] revert clang changes on MersenneTwister --- Simulator/Utils/RNG/MersenneTwister_d.cu | 316 +++++++++++------------ 1 file changed, 155 insertions(+), 161 deletions(-) diff --git a/Simulator/Utils/RNG/MersenneTwister_d.cu b/Simulator/Utils/RNG/MersenneTwister_d.cu index 6e4878609..2c5c60345 100644 --- a/Simulator/Utils/RNG/MersenneTwister_d.cu +++ b/Simulator/Utils/RNG/MersenneTwister_d.cu @@ -41,7 +41,7 @@ using namespace std; __device__ static mt_struct_stripped ds_MT[MT_RNG_COUNT]; static mt_struct_stripped h_MT[MT_RNG_COUNT]; -__device__ unsigned int mt[MT_RNG_COUNT * MT_NN]; +__device__ unsigned int mt[MT_RNG_COUNT*MT_NN]; //#define MT_DATAFILE "MersenneTwister/data/MersenneTwister.dat" @@ -58,54 +58,52 @@ unsigned int mt_threads; unsigned int mt_nPerRng; //Load twister configurations -void loadMTGPU(const char *fname) -{ - FILE *fd = fopen(fname, "rb"); - if (!fd) { - cerr << "initMTGPU(): failed to open " << fname << endl << "FAILED" << endl; - exit(0); - } - if (!fread(h_MT, mt_rng_count * sizeof(mt_struct_stripped), 1, fd)) { - cerr << "initMTGPU(): failed to load " << fname << endl << "FAILED" << endl; - exit(0); - } - fclose(fd); +void loadMTGPU(const char *fname){ + FILE *fd = fopen(fname, "rb"); + if(!fd){ + cerr << "initMTGPU(): failed to open " << fname << endl << "FAILED" << endl; + exit(0); + } + if( !fread(h_MT, mt_rng_count*sizeof(mt_struct_stripped), 1, fd) ){ + cerr << "initMTGPU(): failed to load " << fname << endl << "FAILED" << endl; + exit(0); + } + fclose(fd); } //initialize the seed to mt[] -__global__ void seedMTGPUState(unsigned int seed) -{ - const int tid = blockDim.x * blockIdx.x + threadIdx.x; - int iState; - mt[MT_NN * tid] = seed; - for (iState = MT_NN * tid + 1; iState < MT_NN * (1 + tid); iState++) - mt[iState] = (1812433253U * (mt[iState - 1] ^ (mt[iState - 1] >> 30)) + iState) & MT_WMASK; +__global__ void seedMTGPUState(unsigned int seed){ + const int tid = blockDim.x * blockIdx.x + threadIdx.x; + int iState; + mt[MT_NN*tid] = seed; + for (iState = MT_NN*tid+1; iState < MT_NN*(1+tid); iState++) + mt[iState] = (1812433253U * (mt[iState - 1] ^ (mt[iState - 1] >> 30)) + iState) & MT_WMASK; + } //Initialize/seed twister for current GPU context -void seedMTGPU(unsigned int seed) -{ - int i; - //Need to be thread-safe - mt_struct_stripped *MT = (mt_struct_stripped *)malloc(mt_rng_count * sizeof(mt_struct_stripped)); +void seedMTGPU(unsigned int seed){ + int i; + //Need to be thread-safe + mt_struct_stripped *MT = (mt_struct_stripped *)malloc(mt_rng_count * sizeof(mt_struct_stripped)); - for (i = 0; i < mt_rng_count; i++) { - MT[i] = h_MT[i]; - MT[i].iState = i * MT_NN; - } + for(i = 0; i < mt_rng_count; i++){ + MT[i] = h_MT[i]; + MT[i].iState = i*MT_NN; + } //seed does need to be used to initialize mt[] elements. - int threadsPerBlock = 256; - //get ceil of MT_RNG_COUNT/threadsPerBlock - int blocksPerGrid = (mt_rng_count + threadsPerBlock - 1) / threadsPerBlock; - seedMTGPUState<<>>(seed); + int threadsPerBlock = 256; + //get ceil of MT_RNG_COUNT/threadsPerBlock + int blocksPerGrid = (mt_rng_count+threadsPerBlock-1)/threadsPerBlock; + seedMTGPUState<<>>(seed); - if (cudaMemcpyToSymbol(ds_MT, MT, mt_rng_count * sizeof(mt_struct_stripped)) != cudaSuccess) { - cerr << "seedMTGP failed" << endl; - exit(0); - } + if(cudaMemcpyToSymbol(ds_MT, MT, mt_rng_count*sizeof(mt_struct_stripped))!=cudaSuccess){ + cerr << "seedMTGP failed" << endl; + exit(0); + } - free(MT); + free(MT); } @@ -118,152 +116,148 @@ void seedMTGPU(unsigned int seed) // The local seeds, in their turn, can be extracted from global seed // by means of any simple random number generator, like LCG. //////////////////////////////////////////////////////////////////////////////// -__global__ void RandomGPU(float *d_Random, int nPerRng, int mt_rng_count) +__global__ void RandomGPU( + float *d_Random, + int nPerRng, int mt_rng_count) { - const int tid = blockDim.x * blockIdx.x + threadIdx.x; - int iState, iState1, iStateM, iOut; - unsigned int mti, mti1, mtiM, x; - unsigned int matrix_a, mask_b, mask_c; - - //Load bit-vector Mersenne Twister parameters - matrix_a = ds_MT[tid].matrix_a; - mask_b = ds_MT[tid].mask_b; - mask_c = ds_MT[tid].mask_c; - - iState = ds_MT[tid].iState; - mti1 = mt[iState]; - for (iOut = 0; iOut < nPerRng; iOut++) { - iState1 = iState + 1; - iStateM = iState + MT_MM; - if (iState1 >= MT_NN * (1 + tid)) - iState1 -= MT_NN; - if (iStateM >= MT_NN * (1 + tid)) - iStateM -= MT_NN; - mti = mti1; - mti1 = mt[iState1]; - mtiM = mt[iStateM]; - - // MT recurrence - x = (mti & MT_UMASK) | (mti1 & MT_LMASK); - x = mtiM ^ (x >> 1) ^ ((x & 1) ? matrix_a : 0); - - mt[iState] = x; - iState = iState1; - - //Tempering transformation - x ^= (x >> MT_SHIFT0); - x ^= (x << MT_SHIFTB) & mask_b; - x ^= (x << MT_SHIFTC) & mask_c; - x ^= (x >> MT_SHIFT1); - - //Convert to (0, 1] float and write to global memory - d_Random[tid + iOut * mt_rng_count] = ((float)x + 1.0f) / 4294967296.0f; - } - ds_MT[tid].iState = iState; + const int tid = blockDim.x * blockIdx.x + threadIdx.x; + int iState, iState1, iStateM, iOut; + unsigned int mti, mti1, mtiM, x; + unsigned int matrix_a, mask_b, mask_c; + + //Load bit-vector Mersenne Twister parameters + matrix_a = ds_MT[tid].matrix_a; + mask_b = ds_MT[tid].mask_b; + mask_c = ds_MT[tid].mask_c; + + iState = ds_MT[tid].iState; + mti1 = mt[iState]; + for (iOut = 0; iOut < nPerRng; iOut++) { + iState1 = iState + 1; + iStateM = iState + MT_MM; + if(iState1 >= MT_NN*(1+tid)) iState1 -= MT_NN; + if(iStateM >= MT_NN*(1+tid)) iStateM -= MT_NN; + mti = mti1; + mti1 = mt[iState1]; + mtiM = mt[iStateM]; + + // MT recurrence + x = (mti & MT_UMASK) | (mti1 & MT_LMASK); + x = mtiM ^ (x >> 1) ^ ((x & 1) ? matrix_a : 0); + + mt[iState] = x; + iState = iState1; + + //Tempering transformation + x ^= (x >> MT_SHIFT0); + x ^= (x << MT_SHIFTB) & mask_b; + x ^= (x << MT_SHIFTC) & mask_c; + x ^= (x >> MT_SHIFT1); + + //Convert to (0, 1] float and write to global memory + d_Random[tid + iOut * mt_rng_count] = ((float)x + 1.0f) / 4294967296.0f; + } + ds_MT[tid].iState = iState; } //////////////////////////////////////////////////////////////////////////////// -// Transform each of MT_RNG_COUNT lanes of nPerRng uniformly distributed +// Transform each of MT_RNG_COUNT lanes of nPerRng uniformly distributed // random samples, produced by RandomGPU(), to normally distributed lanes // using Cartesian form of Box-Muller transformation. // nPerRng must be even. //////////////////////////////////////////////////////////////////////////////// #define PI 3.14159265358979f -__device__ inline void BoxMuller(float &u1, float &u2) -{ - float r = sqrtf(-2.0f * logf(u1)); - float phi = 2 * PI * u2; - u1 = r * __cosf(phi); - u2 = r * __sinf(phi); +__device__ inline void BoxMuller(float& u1, float& u2){ + float r = sqrtf(-2.0f * logf(u1)); + float phi = 2 * PI * u2; + u1 = r * __cosf(phi); + u2 = r * __sinf(phi); } -__global__ void BoxMullerGPU(float *d_Random, int nPerRng, int mt_rng_count) -{ - const int tid = blockDim.x * blockIdx.x + threadIdx.x; +__global__ void BoxMullerGPU(float *d_Random, int nPerRng, int mt_rng_count){ + const int tid = blockDim.x * blockIdx.x + threadIdx.x; - for (int iOut = 0; iOut < nPerRng; iOut += 2) - BoxMuller(d_Random[tid + (iOut + 0) * mt_rng_count], - d_Random[tid + (iOut + 1) * mt_rng_count]); + for (int iOut = 0; iOut < nPerRng; iOut += 2) + BoxMuller( + d_Random[tid + (iOut + 0) * mt_rng_count], + d_Random[tid + (iOut + 1) * mt_rng_count] + ); } -//skip the seperate BoxMullerGPU for increased speed (uses register memory). +//skip the seperate BoxMullerGPU for increased speed (uses register memory). //nPerRng must be a multiple of 2 -__global__ void RandomNormGPU(float *d_Random, int nPerRng, int mt_rng_count) +__global__ void RandomNormGPU( + float *d_Random, + int nPerRng, int mt_rng_count) { - const int tid = blockDim.x * blockIdx.x + threadIdx.x; - int iState, iState1, iStateM, iOut; - unsigned int mti, mti1, mtiM, x; - unsigned int matrix_a, mask_b, mask_c; - - float regVal1, regVal2; //need 2 values for boxmuller - bool boxFlag = false; //will perform boxmuller transform on true - - //Load bit-vector Mersenne Twister parameters - matrix_a = ds_MT[tid].matrix_a; - mask_b = ds_MT[tid].mask_b; - mask_c = ds_MT[tid].mask_c; - - iState = ds_MT[tid].iState; - mti1 = mt[iState]; - for (iOut = 0; iOut < nPerRng; iOut++) { - iState1 = iState + 1; - iStateM = iState + MT_MM; - if (iState1 >= MT_NN * (1 + tid)) - iState1 -= MT_NN; - if (iStateM >= MT_NN * (1 + tid)) - iStateM -= MT_NN; - mti = mti1; - mti1 = mt[iState1]; - mtiM = mt[iStateM]; - - // MT recurrence - x = (mti & MT_UMASK) | (mti1 & MT_LMASK); - x = mtiM ^ (x >> 1) ^ ((x & 1) ? matrix_a : 0); - - mt[iState] = x; - iState = iState1; - - //Tempering transformation - x ^= (x >> MT_SHIFT0); - x ^= (x << MT_SHIFTB) & mask_b; - x ^= (x << MT_SHIFTC) & mask_c; - x ^= (x >> MT_SHIFT1); - - if (boxFlag) { - regVal2 = ((float)x + 1.0f) / 4294967296.0f; - BoxMuller(regVal1, regVal2); - d_Random[tid + (iOut - 1) * mt_rng_count] = regVal1; - d_Random[tid + iOut * mt_rng_count] = regVal2; - boxFlag = false; - } else { - regVal1 = ((float)x + 1.0f) / 4294967296.0f; - boxFlag = true; - } - } - ds_MT[tid].iState = iState; + const int tid = blockDim.x * blockIdx.x + threadIdx.x; + int iState, iState1, iStateM, iOut; + unsigned int mti, mti1, mtiM, x; + unsigned int matrix_a, mask_b, mask_c; + + float regVal1, regVal2; //need 2 values for boxmuller + bool boxFlag = false; //will perform boxmuller transform on true + + //Load bit-vector Mersenne Twister parameters + matrix_a = ds_MT[tid].matrix_a; + mask_b = ds_MT[tid].mask_b; + mask_c = ds_MT[tid].mask_c; + + iState = ds_MT[tid].iState; + mti1 = mt[iState]; + for (iOut = 0; iOut < nPerRng; iOut++) { + iState1 = iState + 1; + iStateM = iState + MT_MM; + if(iState1 >= MT_NN*(1+tid)) iState1 -= MT_NN; + if(iStateM >= MT_NN*(1+tid)) iStateM -= MT_NN; + mti = mti1; + mti1 = mt[iState1]; + mtiM = mt[iStateM]; + + // MT recurrence + x = (mti & MT_UMASK) | (mti1 & MT_LMASK); + x = mtiM ^ (x >> 1) ^ ((x & 1) ? matrix_a : 0); + + mt[iState] = x; + iState = iState1; + + //Tempering transformation + x ^= (x >> MT_SHIFT0); + x ^= (x << MT_SHIFTB) & mask_b; + x ^= (x << MT_SHIFTC) & mask_c; + x ^= (x >> MT_SHIFT1); + + if(boxFlag){ + regVal2 = ((float)x + 1.0f) / 4294967296.0f; + BoxMuller(regVal1,regVal2); + d_Random[tid + (iOut-1) * mt_rng_count] = regVal1; + d_Random[tid + iOut * mt_rng_count] = regVal2; + boxFlag = false; + }else{ + regVal1 = ((float)x + 1.0f) / 4294967296.0f; + boxFlag = true; + } + } + ds_MT[tid].iState = iState; } -extern "C" void uniformMTGPU(float *d_random) -{ - RandomGPU<<>>(d_random, mt_nPerRng, mt_rng_count); +extern "C" void uniformMTGPU(float * d_random){ + RandomGPU<<>>(d_random, mt_nPerRng, mt_rng_count); } -extern "C" void normalMTGPU(float *d_random) -{ - RandomNormGPU<<>>(d_random, mt_nPerRng, mt_rng_count); +extern "C" void normalMTGPU(float * d_random){ + RandomNormGPU<<>>(d_random, mt_nPerRng, mt_rng_count); } //initialize globals and setup state //Note: mt_rng_count must equal blocks*threads. mt_rng_count*nPerRng should equal the total number of randon numbers to be generated -extern "C" void initMTGPU(unsigned int seed, unsigned int blocks, unsigned int threads, - unsigned int nPerRng, unsigned int mt_rng_c) -{ - mt_blocks = blocks; - mt_threads = threads; - mt_nPerRng = nPerRng; - mt_rng_count = mt_rng_c; - - loadMTGPU(MT_DATAFILE); - seedMTGPU(seed); +extern "C" void initMTGPU(unsigned int seed, unsigned int blocks, unsigned int threads, unsigned int nPerRng, unsigned int mt_rng_c){ + mt_blocks = blocks; + mt_threads = threads; + mt_nPerRng = nPerRng; + mt_rng_count = mt_rng_c; + + loadMTGPU(MT_DATAFILE); + seedMTGPU(seed); } \ No newline at end of file From 2872d15585f291a10238c12782b2e554678d682a Mon Sep 17 00:00:00 2001 From: Andrew Madison Date: Tue, 10 Jun 2025 16:39:02 -0700 Subject: [PATCH 26/37] fixing cpu errors with cuda includes --- Simulator/Core/GPUModel.h | 2 ++ Simulator/Utils/Book.h | 2 ++ Simulator/Utils/RNG/AsyncPhilox_d.h | 2 ++ 3 files changed, 6 insertions(+) diff --git a/Simulator/Core/GPUModel.h b/Simulator/Core/GPUModel.h index 8d5bf8158..f508841de 100644 --- a/Simulator/Core/GPUModel.h +++ b/Simulator/Core/GPUModel.h @@ -24,7 +24,9 @@ #include "AllEdges.h" #include "AllVertices.h" #include "AsyncPhilox_d.h" +#ifdef USE_GPU #include +#endif #ifdef VALIDATION_MODE #include diff --git a/Simulator/Utils/Book.h b/Simulator/Utils/Book.h index c0dbb6919..3322bb9bb 100644 --- a/Simulator/Utils/Book.h +++ b/Simulator/Utils/Book.h @@ -22,7 +22,9 @@ #pragma once +#ifdef USE_GPU #include +#endif #include //! CUDA Exception handler static void HandleError(cudaError_t err, const char *file, int line) diff --git a/Simulator/Utils/RNG/AsyncPhilox_d.h b/Simulator/Utils/RNG/AsyncPhilox_d.h index 28a8a90e0..1d50341ea 100644 --- a/Simulator/Utils/RNG/AsyncPhilox_d.h +++ b/Simulator/Utils/RNG/AsyncPhilox_d.h @@ -16,8 +16,10 @@ #include "Book.h" #include #include +#ifdef USE_GPU #include #include +#endif #include class AsyncPhilox_d { public: From 522c85f2281dc50660c5796147cd1aec51999e4e Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 10 Jun 2025 17:20:36 -0700 Subject: [PATCH 27/37] cleaned up linking and unnecessary includes --- CMakeLists.txt | 3 +++ Simulator/Core/Serializer.cpp | 4 +++- Simulator/Core/Simulator.cpp | 4 +++- Simulator/Utils/Book.h | 2 -- Simulator/Utils/RNG/AsyncPhilox_d.h | 2 -- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 96f3648ed..38ef52922 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -362,6 +362,9 @@ add_library(Matrix ${Matrix_Source}) file(GLOB RNG_Source Simulator/Utils/RNG/*.cpp Simulator/Utils/RNG/*.h Simulator/Utils/RNG/*.cu) # Remove demo from file list as it contains a main and it will cause compilation errors list(REMOVE_ITEM RNG_Source "${CMAKE_CURRENT_SOURCE_DIR}/Simulator/Utils/RNG/MersenneTwister_demo.cu") +if(NOT ENABLE_CUDA) + list(REMOVE_ITEM Utils_Source "${CMAKE_CURRENT_SOURCE_DIR}/Simulator/Utils/AsyncPhilox_d.cu") +endif() add_library(RNG STATIC ${RNG_Source}) diff --git a/Simulator/Core/Serializer.cpp b/Simulator/Core/Serializer.cpp index 4b90fbc04..21652f37e 100644 --- a/Simulator/Core/Serializer.cpp +++ b/Simulator/Core/Serializer.cpp @@ -25,7 +25,9 @@ #include "Serializer.h" #include "ConnGrowth.h" -#include "GPUModel.h" +#if defined(USE_GPU) + #include "GPUModel.h" +#endif #include // About CEREAL_XML_STRING_VALUE diff --git a/Simulator/Core/Simulator.cpp b/Simulator/Core/Simulator.cpp index 0fd941c2d..593cd5c48 100644 --- a/Simulator/Core/Simulator.cpp +++ b/Simulator/Core/Simulator.cpp @@ -9,7 +9,9 @@ #include "Simulator.h" #include "CPUModel.h" -#include "GPUModel.h" +#if defined(USE_GPU) + #include "GPUModel.h" +#endif #include "OperationManager.h" #include "ParameterManager.h" #include "Utils/Factory.h" diff --git a/Simulator/Utils/Book.h b/Simulator/Utils/Book.h index 3322bb9bb..c0dbb6919 100644 --- a/Simulator/Utils/Book.h +++ b/Simulator/Utils/Book.h @@ -22,9 +22,7 @@ #pragma once -#ifdef USE_GPU #include -#endif #include //! CUDA Exception handler static void HandleError(cudaError_t err, const char *file, int line) diff --git a/Simulator/Utils/RNG/AsyncPhilox_d.h b/Simulator/Utils/RNG/AsyncPhilox_d.h index 1d50341ea..28a8a90e0 100644 --- a/Simulator/Utils/RNG/AsyncPhilox_d.h +++ b/Simulator/Utils/RNG/AsyncPhilox_d.h @@ -16,10 +16,8 @@ #include "Book.h" #include #include -#ifdef USE_GPU #include #include -#endif #include class AsyncPhilox_d { public: From 55ba665fa08dda5198a08a0a3efc15b6a56c8836 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 10 Jun 2025 17:30:15 -0700 Subject: [PATCH 28/37] clang format issues --- Simulator/Connections/Connections.cpp | 4 ++-- Simulator/Core/GPUModel.h | 2 +- Simulator/Edges/AllEdges.h | 4 ++-- Simulator/Vertices/AllVertices.cpp | 3 ++- Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Simulator/Connections/Connections.cpp b/Simulator/Connections/Connections.cpp index 410e5df80..b531b3ca1 100644 --- a/Simulator/Connections/Connections.cpp +++ b/Simulator/Connections/Connections.cpp @@ -91,8 +91,8 @@ bool Connections::updateConnections(AllVertices &vertices) #if defined(USE_GPU) void Connections::updateEdgesWeights(int numVertices, AllVertices &vertices, AllEdges &edges, AllVerticesDeviceProperties *allVerticesDevice, - AllEdgesDeviceProperties *allEdgesDevice, Layout &layout - ,cudaStream_t stream) + AllEdgesDeviceProperties *allEdgesDevice, Layout &layout, + cudaStream_t stream) { } #else diff --git a/Simulator/Core/GPUModel.h b/Simulator/Core/GPUModel.h index f508841de..297f82eee 100644 --- a/Simulator/Core/GPUModel.h +++ b/Simulator/Core/GPUModel.h @@ -25,7 +25,7 @@ #include "AllVertices.h" #include "AsyncPhilox_d.h" #ifdef USE_GPU -#include + #include #endif #ifdef VALIDATION_MODE diff --git a/Simulator/Edges/AllEdges.h b/Simulator/Edges/AllEdges.h index db7bd467d..28a1f46d3 100644 --- a/Simulator/Edges/AllEdges.h +++ b/Simulator/Edges/AllEdges.h @@ -16,7 +16,7 @@ // cereal #include "cereal/types/vector.hpp" #ifdef USE_GPU -#include + #include #endif class AllVertices; @@ -98,8 +98,8 @@ class AllEdges { /// Cuda Stream for Edge Kernels cudaStream_t stream; -public: +public: // Set GPU stream for edge kernels void SetStream(cudaStream_t pStream); diff --git a/Simulator/Vertices/AllVertices.cpp b/Simulator/Vertices/AllVertices.cpp index 6f4d82562..fbb48ecfe 100644 --- a/Simulator/Vertices/AllVertices.cpp +++ b/Simulator/Vertices/AllVertices.cpp @@ -53,7 +53,8 @@ void AllVertices::loadEpochInputs(uint64_t currentStep, uint64_t endStep) #ifdef USE_GPU // Set Cuda Stream for Vertices kernels -void AllVertices::SetStream(cudaStream_t pStream){ +void AllVertices::SetStream(cudaStream_t pStream) +{ stream = pStream; } #endif \ No newline at end of file diff --git a/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp b/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp index 79f1deb8c..1dce4251e 100644 --- a/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp @@ -171,7 +171,7 @@ void AllIZHNeurons::advanceVertices(AllEdges &synapses, void *allVerticesDevice, int blocksPerGrid = (vertex_count + threadsPerBlock - 1) / threadsPerBlock; // Advance neurons -------------> - advanceIZHNeuronsDevice<<>>( + advanceIZHNeuronsDevice<<>>( vertex_count, Simulator::getInstance().getMaxEdgesPerVertex(), maxSpikes, Simulator::getInstance().getDeltaT(), g_simulationStep, randNoise, hasFired_, summationPoints_, Vm_, Aconst_, Bconst_, u_, numStepsInRefractoryPeriod_, Vthresh_, Trefract_, From 13af9d72d70fc12e66a2806d4906f62c819dc28b Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 10 Jun 2025 17:33:33 -0700 Subject: [PATCH 29/37] clang format issues --- Simulator/Vertices/AllVertices.h | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Simulator/Vertices/AllVertices.h b/Simulator/Vertices/AllVertices.h index 581b9180a..19c7dd96e 100644 --- a/Simulator/Vertices/AllVertices.h +++ b/Simulator/Vertices/AllVertices.h @@ -32,8 +32,8 @@ using namespace std; #include // cereal #include "cereal/types/vector.hpp" -#ifdef USE_GPU -#include +#if defined(USE_GPU) + #include #endif class Layout; @@ -97,10 +97,9 @@ class AllVertices { #if defined(USE_GPU) /// Cuda Stream for Vertices Kernels cudaStream_t stream; -public: +public: // Set GPU stream for Vertices kernels - void SetStream(cudaStream_t pStream); /// Allocate GPU memories to store all vertices' states, From 935fe1f75fff6cd6e7b412c2ab9806bb8769434b Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 10 Jun 2025 18:00:01 -0700 Subject: [PATCH 30/37] removed unnecessary file removal --- CMakeLists.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 38ef52922..62c8ecf1b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -371,9 +371,6 @@ add_library(RNG STATIC ${RNG_Source}) # Create Utils library file(GLOB Utils_Source Simulator/Utils/*.cpp Simulator/Utils/*.h) list(REMOVE_ITEM Utils_Source "${CMAKE_CURRENT_SOURCE_DIR}/Simulator/Utils/Factory.cpp") -if(NOT ENABLE_CUDA) - list(REMOVE_ITEM Utils_Source "${CMAKE_CURRENT_SOURCE_DIR}/Simulator/Utils/Book.h") -endif() if(CMAKE_BUILD_TYPE STREQUAL "Profiling") if(ENABLE_CUDA) # Find NVTX Library From 5d092ba92dd8542c9f31f3497e0171a2cd876a82 Mon Sep 17 00:00:00 2001 From: Andrew Madison Date: Mon, 23 Jun 2025 07:54:56 -0700 Subject: [PATCH 31/37] Wrote better comments with doxygen in mind and renamed variables according to the code style --- Simulator/Connections/Connections.cpp | 2 +- Simulator/Connections/Connections.h | 3 +- Simulator/Connections/Neuro/ConnGrowth.h | 3 +- Simulator/Connections/Neuro/ConnGrowth_d.cpp | 5 +- Simulator/Core/GPUModel.cpp | 21 +++-- Simulator/Core/GPUModel.h | 9 +- Simulator/Edges/AllEdges.cpp | 14 ++- Simulator/Edges/AllEdges.h | 15 ++-- Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp | 2 +- .../Edges/Neuro/AllSpikingSynapses_d.cpp | 2 +- Simulator/Utils/RNG/AsyncPhilox_d.cu | 89 ++++++++++++------- Simulator/Utils/RNG/AsyncPhilox_d.h | 66 ++++++++++---- Simulator/Vertices/AllVertices.cpp | 12 ++- Simulator/Vertices/AllVertices.h | 14 ++- Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp | 2 +- Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp | 2 +- .../Vertices/Neuro/AllSpikingNeurons_d.cpp | 2 +- 17 files changed, 180 insertions(+), 83 deletions(-) diff --git a/Simulator/Connections/Connections.cpp b/Simulator/Connections/Connections.cpp index b531b3ca1..37f4803e8 100644 --- a/Simulator/Connections/Connections.cpp +++ b/Simulator/Connections/Connections.cpp @@ -92,7 +92,7 @@ bool Connections::updateConnections(AllVertices &vertices) void Connections::updateEdgesWeights(int numVertices, AllVertices &vertices, AllEdges &edges, AllVerticesDeviceProperties *allVerticesDevice, AllEdgesDeviceProperties *allEdgesDevice, Layout &layout, - cudaStream_t stream) + cudaStream_t simulationStream) { } #else diff --git a/Simulator/Connections/Connections.h b/Simulator/Connections/Connections.h index 400a7deed..99f2694e0 100644 --- a/Simulator/Connections/Connections.h +++ b/Simulator/Connections/Connections.h @@ -90,10 +90,11 @@ class Connections { /// @param allVerticesDevice GPU address of the allVertices struct on device memory. /// @param allEdgesDevice GPU address of the allEdges struct on device memory. /// @param layout Layout information of the graph network. + /// @param simulationStream The cuda stream for all synchronous kernels. virtual void updateEdgesWeights(int numVertices, AllVertices &vertices, AllEdges &edges, AllVerticesDeviceProperties *allVerticesDevice, AllEdgesDeviceProperties *allEdgesDevice, Layout &layout, - cudaStream_t stream); + cudaStream_t simulationStream); #else public: /// Update the weight of the edges in the simulation. diff --git a/Simulator/Connections/Neuro/ConnGrowth.h b/Simulator/Connections/Neuro/ConnGrowth.h index 965554403..399a2665c 100644 --- a/Simulator/Connections/Neuro/ConnGrowth.h +++ b/Simulator/Connections/Neuro/ConnGrowth.h @@ -125,10 +125,11 @@ class ConnGrowth : public Connections { /// @param allVerticesDevice GPU address of the AllVertices struct in device memory. /// @param allEdgesDevice GPU address of the AllEdges struct in device memory. /// @param layout The Layout object. + /// @param simulationStream The cuda stream for all synchronous kernels. virtual void updateEdgesWeights(int numVertices, AllVertices &vertices, AllEdges &edges, AllVerticesDeviceProperties *allVerticesDevice, AllEdgesDeviceProperties *allEdgesDevice, Layout &layout, - cudaStream_t stream) override; + cudaStream_t simulationStream) override; #else /// Update the weights of the Synapses in the simulation. To be clear, diff --git a/Simulator/Connections/Neuro/ConnGrowth_d.cpp b/Simulator/Connections/Neuro/ConnGrowth_d.cpp index 12b178002..19a6d9395 100644 --- a/Simulator/Connections/Neuro/ConnGrowth_d.cpp +++ b/Simulator/Connections/Neuro/ConnGrowth_d.cpp @@ -26,11 +26,12 @@ * @param allVerticesDevice GPU address to the AllVertices struct in device memory. * @param allEdgesDevice GPU address to the AllEdges struct in device memory. * @param layout The Layout object. + * @param simulationStream The cuda stream for all synchronous kernels. */ void ConnGrowth::updateEdgesWeights(int numVertices, AllVertices &vertices, AllEdges &edges, AllVerticesDeviceProperties *allVerticesDevice, AllEdgesDeviceProperties *allEdgesDevice, Layout &layout, - cudaStream_t stream) + cudaStream_t simulationStream) { Simulator &simulator = Simulator::getInstance(); // For now, we just set the weights to equal the areas. We will later @@ -65,7 +66,7 @@ void ConnGrowth::updateEdgesWeights(int numVertices, AllVertices &vertices, AllE cudaMemcpyHostToDevice)); blocksPerGrid = (simulator.getTotalVertices() + threadsPerBlock - 1) / threadsPerBlock; - updateSynapsesWeightsDevice<<>>( + updateSynapsesWeightsDevice<<>>( simulator.getTotalVertices(), deltaT, W_d, simulator.getMaxEdgesPerVertex(), (AllSpikingNeuronsDeviceProperties *)allVerticesDevice, (AllSpikingSynapsesDeviceProperties *)allEdgesDevice, neuronTypeMapD); diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index b68df8054..b1563292d 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -54,8 +54,11 @@ void GPUModel::allocDeviceStruct(void **allVerticesDevice, void **allEdgesDevice // Allocate edge inverse map in device memory allocEdgeIndexMap(numVertices); - // Create gpu model stream - HANDLE_ERROR(cudaStreamCreate(&stream)); + // Create the CUDA stream used to launch synchronous GPU kernels during the simulation. + // This stream is passed to components like AllEdges and used consistently for kernel launches. + // For stream behavior and management, see: + // https://docs.nvidia.com/cuda/cuda-runtime-api/group__CUDART__STREAM.html + HANDLE_ERROR(cudaStreamCreate(&simulationStream_)); } /// Copies device memories to host memories and deallocates them. @@ -77,7 +80,7 @@ void GPUModel::deleteDeviceStruct(void **allVerticesDevice, void **allEdgesDevic edges.deleteEdgeDeviceStruct(*allEdgesDevice); // HANDLE_ERROR(cudaFree(randNoise_d)); // closeFileMT(); - HANDLE_ERROR(cudaStreamDestroy(stream)); + HANDLE_ERROR(cudaStreamDestroy(simulationStream_)); } /// Sets up the Simulation. @@ -100,7 +103,7 @@ void GPUModel::setupSim() // initMTGPU(Simulator::getInstance().getNoiseRngSeed(), rng_blocks, rng_threads, rng_nPerRng, // rng_mt_rng_count); //cout << "blocks, threads, nPerRng, rng_rng_count: " << rng_blocks << " " << rng_threads << " " << rng_nPerRng << " " << rng_mt_rng_count << endl; - AsyncGenerator.loadAsyncPhilox(Simulator::getInstance().getTotalVertices(), + AsyncGenerator_.loadAsyncPhilox(Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getNoiseRngSeed()); #ifdef PERFORMANCE_METRICS @@ -127,15 +130,15 @@ void GPUModel::setupSim() // set some parameters used for advanceEdgesDevice edges.setAdvanceEdgesDeviceParams(); AllVertices &vertices = layout_->getVertices(); - vertices.SetStream(stream); - edges.SetStream(stream); + vertices.SetStream(simulationStream_); + edges.SetStream(simulationStream_); } /// Performs any finalization tasks on network following a simulation. void GPUModel::finish() { // deallocates memories on CUDA device - AsyncGenerator.deleteDeviceStruct(); + AsyncGenerator_.deleteDeviceStruct(); deleteDeviceStruct((void **)&allVerticesDevice_, (void **)&allEdgesDevice_); deleteEdgeIndexMap(); @@ -172,7 +175,7 @@ void GPUModel::advance() cudaMemcpy(randNoise_d, randNoise_h.data(), verts * sizeof(float), cudaMemcpyHostToDevice); #else // normalMTGPU(randNoise_d); - randNoise_d = AsyncGenerator.requestSegment(); + randNoise_d = AsyncGenerator_.requestSegment(); #endif //LOG4CPLUS_DEBUG(vertexLogger_, "Index: " << index << " Vm: " << Vm); #ifdef PERFORMANCE_METRICS @@ -252,7 +255,7 @@ void GPUModel::updateConnections() // Update Connections data if (connections_->updateConnections(vertices)) { connections_->updateEdgesWeights(Simulator::getInstance().getTotalVertices(), vertices, edges, - allVerticesDevice_, allEdgesDevice_, getLayout(), stream); + allVerticesDevice_, allEdgesDevice_, getLayout(), simulationStream_); // create edge index map connections_->createEdgeIndexMap(); // copy index map to the device memory diff --git a/Simulator/Core/GPUModel.h b/Simulator/Core/GPUModel.h index 297f82eee..7bfbf1de5 100644 --- a/Simulator/Core/GPUModel.h +++ b/Simulator/Core/GPUModel.h @@ -107,10 +107,15 @@ class GPUModel : public Model { /// Pointer to device random noise array. float *randNoise_d; - AsyncPhilox_d AsyncGenerator; + + /// Async RNG class instance used to load generator with a seed and request noise device pointers. + AsyncPhilox_d AsyncGenerator_; +#ifdef VALIDATION_MODE + /// Buffer used in the validation mode to copy cgraphitti's noise generation into the device noise buffer. float *randNoise_h; +#endif /// Cuda Stream for kernel use - cudaStream_t stream; + cudaStream_t simulationStream_; #if defined(USE_GPU) /// Pointer to edge index map in device memory. diff --git a/Simulator/Edges/AllEdges.cpp b/Simulator/Edges/AllEdges.cpp index 5ad138160..8c570e24c 100644 --- a/Simulator/Edges/AllEdges.cpp +++ b/Simulator/Edges/AllEdges.cpp @@ -218,11 +218,17 @@ void AllEdges::createEdgeIndexMap(EdgeIndexMap &edgeIndexMap) } #if defined(USE_GPU) -// Set GPU stream for edge kernels - -void AllEdges::SetStream(cudaStream_t pStream) +/// Set the CUDA stream to be used by GPU edge kernels in derived classes. +/// +/// This assigns a CUDA stream to the base class, allowing subclasses +/// (e.g., AllSpikingSynapses_d, AllSTDPSynapses_d) to launch kernels on +/// the correct stream. The stream is typically created by GPUModel and +/// passed down during simulation setup. +/// +/// @param simulationStream A valid CUDA stream (`cudaStream_t`) managed by the caller. +void AllEdges::SetStream(cudaStream_t simulationStream) { - stream = pStream; + simulationStream_ = simulationStream; } #endif diff --git a/Simulator/Edges/AllEdges.h b/Simulator/Edges/AllEdges.h index 28a1f46d3..d5592dc35 100644 --- a/Simulator/Edges/AllEdges.h +++ b/Simulator/Edges/AllEdges.h @@ -95,14 +95,19 @@ class AllEdges { log4cplus::Logger edgeLogger_; #if defined(USE_GPU) - /// Cuda Stream for Edge Kernels - cudaStream_t stream; + cudaStream_t simulationStream_; public: - // Set GPU stream for edge kernels - - void SetStream(cudaStream_t pStream); + /// Set the CUDA stream to be used by GPU edge kernels in derived classes. + /// + /// This assigns a CUDA stream to the base class, allowing subclasses + /// (e.g., AllSpikingSynapses_d, AllSTDPSynapses_d) to launch kernels on + /// the correct stream. The stream is typically created by GPUModel and + /// passed down during simulation setup. + /// + /// @param simulationStream A valid CUDA stream (`cudaStream_t`) managed by the caller. + void SetStream(cudaStream_t simulationStream); /// Allocate GPU memories to store all edges' states, /// and copy them from host to GPU memory. diff --git a/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp b/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp index 99e6aaf25..f68b9add4 100644 --- a/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp +++ b/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp @@ -268,7 +268,7 @@ void AllSTDPSynapses::advanceEdges(void *allEdgesDevice, void *allVerticesDevice const int threadsPerBlock = 256; int blocksPerGrid = (totalEdgeCount_ + threadsPerBlock - 1) / threadsPerBlock; // Advance synapses -------------> - advanceSTDPSynapsesDevice<<>>( + advanceSTDPSynapsesDevice<<>>( totalEdgeCount_, (EdgeIndexMapDevice *)edgeIndexMapDevice, g_simulationStep, Simulator::getInstance().getDeltaT(), (AllSTDPSynapsesDeviceProperties *)allEdgesDevice, (AllSpikingNeuronsDeviceProperties *)allVerticesDevice, maxSpikes); diff --git a/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp b/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp index 404ac9bd4..011c3ef9f 100644 --- a/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp +++ b/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp @@ -330,7 +330,7 @@ void AllSpikingSynapses::advanceEdges(void *allEdgesDevice, void *allVerticesDev const int threadsPerBlock = 256; int blocksPerGrid = (totalEdgeCount_ + threadsPerBlock - 1) / threadsPerBlock; // Advance synapses -------------> - advanceSpikingSynapsesDevice<<>>( + advanceSpikingSynapsesDevice<<>>( totalEdgeCount_, (EdgeIndexMapDevice *)edgeIndexMapDevice, g_simulationStep, Simulator::getInstance().getDeltaT(), (AllSpikingSynapsesDeviceProperties *)allEdgesDevice); } diff --git a/Simulator/Utils/RNG/AsyncPhilox_d.cu b/Simulator/Utils/RNG/AsyncPhilox_d.cu index ffc93f945..485910da0 100644 --- a/Simulator/Utils/RNG/AsyncPhilox_d.cu +++ b/Simulator/Utils/RNG/AsyncPhilox_d.cu @@ -18,6 +18,15 @@ #include #include +/// @brief Kernel to generate Gaussian (normal) random numbers using Philox. +/// +/// Each thread loads its own Philox RNG state, generates one or more +/// random floats using a strided loop, and writes them into the output buffer. +/// The updated RNG state is written back to global device memory. +/// +/// @param states Array of Philox RNG states, one per thread. +/// @param output Output buffer for generated random floats. +/// @param bufferSize Total number of floats to generate (length of output buffer). __global__ void generatePhilox(curandStatePhilox4_32_10_t *states, float *output, int bufferSize) { // Compute a unique global index for this thread @@ -39,6 +48,14 @@ __global__ void generatePhilox(curandStatePhilox4_32_10_t *states, float *output states[gid] = local; } +/// @brief Kernel to initialize Philox RNG states for each thread. +/// +/// Each thread initializes its entry in the RNG state array using a fixed seed. +/// This is typically called once before generating random numbers. +/// +/// @param states Array to hold initialized Philox RNG states. +/// @param seed Seed value used to initialize curand. +/// @param totalThreads Total number of threads that will use RNG states. __global__ void initPhilox(curandStatePhilox4_32_10_t *states, unsigned long seed, int totalThreads) { int gid = blockIdx.x * blockDim.x + threadIdx.x; @@ -47,28 +64,31 @@ __global__ void initPhilox(curandStatePhilox4_32_10_t *states, unsigned long see curand_init(seed, gid, 0, &states[gid]); } +/// Initializes generator and allocates device memory +/// @param samplesPerSegment Number of total vertices +/// @param seed RNG seed. void AsyncPhilox_d::loadAsyncPhilox(int samplesPerSegment, unsigned long seed) { // hostBuffer = nullptr; // cudaHostAlloc(&hostBuffer, samplesPerSegment * sizeof(float), cudaHostAllocDefault); // logfile = std::fopen("philox_output_32_10.bin", "wb"); //consoleLogger_ = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("console")); - segmentSize = samplesPerSegment; - seed = seed; - currentBuffer = 0; - segmentIndex = 0; + segmentSize_ = samplesPerSegment; + seed_ = seed; + currentBuffer_ = 0; + segmentIndex_ = 0; - totalSegments = 10; + totalSegments_ = 10; #ifdef ENABLE_NVTX nvtxMarker = 10000 / totalSegments; // make a marker every nvtxMarker buffer fills; nvtxCurrentMarker = nvtxMarker; // count down to color flip #endif - bufferSize = segmentSize * totalSegments; - numBlocks = 64; //placeholder num of blocks - numThreads = 64; + bufferSize_ = segmentSize_ * totalSegments_; + numBlocks_ = 64; //placeholder num of blocks + numThreads_ = 64; - totalThreads = numThreads * numBlocks; + totalThreads_ = numThreads_ * numBlocks_; int leastPriority, greatestPriority; HANDLE_ERROR(cudaDeviceGetStreamPriorityRange(&leastPriority, &greatestPriority)); @@ -76,46 +96,53 @@ void AsyncPhilox_d::loadAsyncPhilox(int samplesPerSegment, unsigned long seed) // └─ greatestPriority is the numerically smallest value → highest actual priority // Create internal stream - HANDLE_ERROR(cudaStreamCreateWithPriority(&stream, cudaStreamNonBlocking, leastPriority)); + HANDLE_ERROR(cudaStreamCreateWithPriority(&RNG_stream_, cudaStreamNonBlocking, leastPriority)); // Allocate two large buffers - HANDLE_ERROR(cudaMalloc(&buffers[0], bufferSize * sizeof(float))); - HANDLE_ERROR(cudaMalloc(&buffers[1], bufferSize * sizeof(float))); + HANDLE_ERROR(cudaMalloc(&buffers_d[0], bufferSize_ * sizeof(float))); + HANDLE_ERROR(cudaMalloc(&buffers_d[1], bufferSize_ * sizeof(float))); - HANDLE_ERROR(cudaMalloc(&spStates, totalThreads * sizeof(curandStatePhilox4_32_10_t))); + HANDLE_ERROR(cudaMalloc(&spStates_d, totalThreads_ * sizeof(curandStatePhilox4_32_10_t))); - initPhilox<<>>(spStates, seed, totalThreads); + initPhilox<<>>(spStates_d, seed_, totalThreads_); // Pre-fill both buffers fillBuffer(0); fillBuffer(1); HANDLE_ERROR(cudaStreamSynchronize( - stream)); //wait for both buffers to be filled before the first request + RNG_stream_)); //wait for both buffers to be filled before the first request } + +/// Free device memory void AsyncPhilox_d::deleteDeviceStruct() { // std::fclose(logfile); // cudaFree(hostBuffer); - HANDLE_ERROR(cudaFree(buffers[0])); - HANDLE_ERROR(cudaFree(buffers[1])); - HANDLE_ERROR(cudaFree(spStates)); + HANDLE_ERROR(cudaFree(buffers_d[0])); + HANDLE_ERROR(cudaFree(buffers_d[1])); + HANDLE_ERROR(cudaFree(spStates_d)); - HANDLE_ERROR(cudaStreamDestroy(stream)); + HANDLE_ERROR(cudaStreamDestroy(RNG_stream_)); } + AsyncPhilox_d::~AsyncPhilox_d() { } +/// Request a new segment of generated noise. +/// @return Pointer to a slice of device memory containing noise. float *AsyncPhilox_d::requestSegment() { //LOG4CPLUS_TRACE(consoleLogger_, "request segment"); //auto start = std::chrono::high_resolution_clock::now(); +#ifdef ENABLE_NVTX static bool flipColor; - if (segmentIndex >= totalSegments) { +#endif + if (segmentIndex_ >= totalSegments_) { // Switch buffer and launch async refill on the now-unused one #ifdef ENABLE_NVTX - if (nvtxCurrentMarker <= 0) { + if (nvtxCurrentMarker_ <= 0) { nvtxPop(); if (flipColor == true) nvtxPushColor("10,000 time steps", Color::RED); @@ -123,20 +150,20 @@ float *AsyncPhilox_d::requestSegment() nvtxPushColor("10,000 time steps", Color::BLUE); flipColor = !flipColor; - nvtxCurrentMarker = nvtxMarker; + nvtxCurrentMarker_ = nvtxMarker_; } else - --nvtxCurrentMarker; + --nvtxCurrentMarker_; #endif - int refillBuffer = currentBuffer; - currentBuffer = 1 - currentBuffer; - segmentIndex = 0; - cudaStreamSynchronize(stream); // Ensure refillBuffer is done + int refillBuffer = currentBuffer_; + currentBuffer_ = 1 - currentBuffer_; + segmentIndex_ = 0; + cudaStreamSynchronize(RNG_stream_); // Ensure refillBuffer is done fillBuffer(refillBuffer); } - float *segmentPtr = buffers[currentBuffer] + segmentIndex * segmentSize; - segmentIndex += 1; + float *segmentPtr = buffers_d[currentBuffer_] + segmentIndex_ * segmentSize_; + segmentIndex_ += 1; // auto end = std::chrono::high_resolution_clock::now(); // std::cout << "Segment: " << segmentIndex << ", Launch time: " << (end - start).count() << " ns\n"; @@ -146,8 +173,10 @@ float *AsyncPhilox_d::requestSegment() return segmentPtr; } +/// Internal helper to fill a specified buffer with random floats. +/// @param bufferIndex Index (0 or 1) of the buffer to fill. void AsyncPhilox_d::fillBuffer(int bufferIndex) { //LOG4CPLUS_TRACE(consoleLogger_, "filling buffer:"); - generatePhilox<<>>(spStates, buffers[bufferIndex], bufferSize); + generatePhilox<<>>(spStates_d, buffers_d[bufferIndex], bufferSize_); } diff --git a/Simulator/Utils/RNG/AsyncPhilox_d.h b/Simulator/Utils/RNG/AsyncPhilox_d.h index 28a8a90e0..030dd2ac1 100644 --- a/Simulator/Utils/RNG/AsyncPhilox_d.h +++ b/Simulator/Utils/RNG/AsyncPhilox_d.h @@ -22,39 +22,73 @@ class AsyncPhilox_d { public: AsyncPhilox_d() = default; - AsyncPhilox_d(int samplesPerGen, unsigned long seed); + ~AsyncPhilox_d(); + + /// Initializes generator and allocates device memory + /// @param samplesPerSegment Number of total vertices + /// @param seed RNG seed. void loadAsyncPhilox(int samplesPerSegment, unsigned long seed); + + /// Free device memory void deleteDeviceStruct(); + + /// Request a new segment of generated noise. + /// @return Pointer to a slice of device memory containing noise. float *requestSegment(); private: - int numBlocks; - int numThreads; - int totalThreads; - int segmentSize; - int totalSegments; - int bufferSize; - unsigned long seed; + /// Number of CUDA blocks to launch per kernel call. + int numBlocks_; + + /// Number of threads per CUDA block. + int numThreads_; + + /// Total number of threads = numBlocks × numThreads. + int totalThreads_; + + /// Number of random floats per segment. + int segmentSize_; + + /// Number of total segments in each buffer. + int totalSegments_; + + /// Number of random floats per buffer. + int bufferSize_; + + /// RNG seed. + unsigned long seed_; #ifdef ENABLE_NVTX - int nvtxMarker; - int nvtxCurrentMarker; + /// Marker index for NVTX profiling (if enabled). + int nvtxMarker_; + + /// Tracks current NVTX marker for alternating regions. + int nvtxCurrentMarker_; #endif + /// CUDA stream used for asynchronous kernel launches. + cudaStream_t RNG_stream_; + + /// Double-buffered random number output on device. + float *buffers_d[2]; - cudaStream_t stream; + /// Index of currently active buffer. + int currentBuffer_; - float *buffers[2]; - int currentBuffer; - int segmentIndex; + /// Index of the next segment to serve. + int segmentIndex_; - curandStatePhilox4_32_10_t *spStates; + /// Device-side array of Philox curand RNG states. + curandStatePhilox4_32_10_t *spStates_d; // FILE* logfile; // float* hostBuffer; + /// Logger for printing to the console as well as the logging file log4cplus::Logger - consoleLogger_; /// Logger for printing to the console as well as the logging file + consoleLogger_; + /// Internal helper to fill a specified buffer with random floats. + /// @param bufferIndex Index (0 or 1) of the buffer to fill. void fillBuffer(int bufferIndex); }; diff --git a/Simulator/Vertices/AllVertices.cpp b/Simulator/Vertices/AllVertices.cpp index fbb48ecfe..6d804e2d5 100644 --- a/Simulator/Vertices/AllVertices.cpp +++ b/Simulator/Vertices/AllVertices.cpp @@ -52,9 +52,15 @@ void AllVertices::loadEpochInputs(uint64_t currentStep, uint64_t endStep) #ifdef USE_GPU -// Set Cuda Stream for Vertices kernels -void AllVertices::SetStream(cudaStream_t pStream) +/// Set the CUDA stream to be used by GPU vertices kernels in derived classes. +/// +/// This assigns a CUDA stream to the base class, allowing subclasses +/// to launch kernels on the correct stream. The stream is typically +/// created by GPUModel and passed down during simulation setup. +/// +/// @param simulationStream A valid CUDA stream (`cudaStream_t`) managed by the caller. +void AllVertices::SetStream(cudaStream_t simulationStream) { - stream = pStream; + simulationStream_ = simulationStream; } #endif \ No newline at end of file diff --git a/Simulator/Vertices/AllVertices.h b/Simulator/Vertices/AllVertices.h index 19c7dd96e..919bd8dce 100644 --- a/Simulator/Vertices/AllVertices.h +++ b/Simulator/Vertices/AllVertices.h @@ -95,12 +95,18 @@ class AllVertices { log4cplus::Logger vertexLogger_; // Logs to Output/Debug/neurons.txt #if defined(USE_GPU) - /// Cuda Stream for Vertices Kernels - cudaStream_t stream; + /// Cuda Stream for Edge Kernels + cudaStream_t simulationStream_; public: - // Set GPU stream for Vertices kernels - void SetStream(cudaStream_t pStream); + /// Set the CUDA stream to be used by GPU vertices kernels in derived classes. + /// + /// This assigns a CUDA stream to the base class, allowing subclasses + /// to launch kernels on the correct stream. The stream is typically + /// created by GPUModel and passed down during simulation setup. + /// + /// @param simulationStream A valid CUDA stream (`cudaStream_t`) managed by the caller. + void SetStream(cudaStream_t simulationStream); /// Allocate GPU memories to store all vertices' states, /// and copy them from host to GPU memory. diff --git a/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp b/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp index 1dce4251e..c3b7fa622 100644 --- a/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp @@ -171,7 +171,7 @@ void AllIZHNeurons::advanceVertices(AllEdges &synapses, void *allVerticesDevice, int blocksPerGrid = (vertex_count + threadsPerBlock - 1) / threadsPerBlock; // Advance neurons -------------> - advanceIZHNeuronsDevice<<>>( + advanceIZHNeuronsDevice<<>>( vertex_count, Simulator::getInstance().getMaxEdgesPerVertex(), maxSpikes, Simulator::getInstance().getDeltaT(), g_simulationStep, randNoise, hasFired_, summationPoints_, Vm_, Aconst_, Bconst_, u_, numStepsInRefractoryPeriod_, Vthresh_, Trefract_, diff --git a/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp b/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp index dcca032df..bf46fbacc 100644 --- a/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp @@ -56,7 +56,7 @@ void AllLIFNeurons::advanceVertices(AllEdges &synapses, void *allVerticesDevice, int blocksPerGrid = (vertex_count + threadsPerBlock - 1) / threadsPerBlock; // Advance neurons -------------> - advanceLIFNeuronsDevice<<>>( + advanceLIFNeuronsDevice<<>>( vertex_count, Simulator::getInstance().getMaxEdgesPerVertex(), maxSpikes, Simulator::getInstance().getDeltaT(), g_simulationStep, randNoise, hasFired_, summationPoints_, Vm_, Trefract_, numStepsInRefractoryPeriod_, Vthresh_, Vreset_, I0_, diff --git a/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp b/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp index c60a24e74..c0a2f504d 100644 --- a/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp @@ -194,7 +194,7 @@ void AllSpikingNeurons::integrateVertexInputs(void *allVerticesDevice, = (Simulator::getInstance().getTotalVertices() + threadsPerBlock - 1) / threadsPerBlock; int vertex_count = Simulator::getInstance().getTotalVertices(); - calcSummationPointDevice<<>>( + calcSummationPointDevice<<>>( vertex_count, summationPoints_, edgeIndexMapDevice, (AllSpikingSynapsesDeviceProperties *)allEdgesDevice); } From 4fdd64340609ba3759424ac971227bec5f3f4c0f Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 23 Jun 2025 08:24:31 -0700 Subject: [PATCH 32/37] clang --- Simulator/Core/GPUModel.cpp | 5 +++-- Simulator/Utils/RNG/AsyncPhilox_d.cu | 3 ++- Simulator/Utils/RNG/AsyncPhilox_d.h | 3 +-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index b1563292d..ef4621f1f 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -104,7 +104,7 @@ void GPUModel::setupSim() // rng_mt_rng_count); //cout << "blocks, threads, nPerRng, rng_rng_count: " << rng_blocks << " " << rng_threads << " " << rng_nPerRng << " " << rng_mt_rng_count << endl; AsyncGenerator_.loadAsyncPhilox(Simulator::getInstance().getTotalVertices(), - Simulator::getInstance().getNoiseRngSeed()); + Simulator::getInstance().getNoiseRngSeed()); #ifdef PERFORMANCE_METRICS cudaEventCreate(&start); @@ -255,7 +255,8 @@ void GPUModel::updateConnections() // Update Connections data if (connections_->updateConnections(vertices)) { connections_->updateEdgesWeights(Simulator::getInstance().getTotalVertices(), vertices, edges, - allVerticesDevice_, allEdgesDevice_, getLayout(), simulationStream_); + allVerticesDevice_, allEdgesDevice_, getLayout(), + simulationStream_); // create edge index map connections_->createEdgeIndexMap(); // copy index map to the device memory diff --git a/Simulator/Utils/RNG/AsyncPhilox_d.cu b/Simulator/Utils/RNG/AsyncPhilox_d.cu index 485910da0..b11e9e4a6 100644 --- a/Simulator/Utils/RNG/AsyncPhilox_d.cu +++ b/Simulator/Utils/RNG/AsyncPhilox_d.cu @@ -178,5 +178,6 @@ float *AsyncPhilox_d::requestSegment() void AsyncPhilox_d::fillBuffer(int bufferIndex) { //LOG4CPLUS_TRACE(consoleLogger_, "filling buffer:"); - generatePhilox<<>>(spStates_d, buffers_d[bufferIndex], bufferSize_); + generatePhilox<<>>(spStates_d, buffers_d[bufferIndex], + bufferSize_); } diff --git a/Simulator/Utils/RNG/AsyncPhilox_d.h b/Simulator/Utils/RNG/AsyncPhilox_d.h index 030dd2ac1..91c2a5b4c 100644 --- a/Simulator/Utils/RNG/AsyncPhilox_d.h +++ b/Simulator/Utils/RNG/AsyncPhilox_d.h @@ -85,8 +85,7 @@ class AsyncPhilox_d { // FILE* logfile; // float* hostBuffer; /// Logger for printing to the console as well as the logging file - log4cplus::Logger - consoleLogger_; + log4cplus::Logger consoleLogger_; /// Internal helper to fill a specified buffer with random floats. /// @param bufferIndex Index (0 or 1) of the buffer to fill. From 66a97fa1ac206a9231c495c05310bc5e40916cd6 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 23 Jun 2025 09:10:47 -0700 Subject: [PATCH 33/37] Added documentation --- docs/Developer/AsyncPhilox.md | 40 +++++++++++++++++++++++++++++++++++ docs/Developer/index.md | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 docs/Developer/AsyncPhilox.md diff --git a/docs/Developer/AsyncPhilox.md b/docs/Developer/AsyncPhilox.md new file mode 100644 index 000000000..8117c8c97 --- /dev/null +++ b/docs/Developer/AsyncPhilox.md @@ -0,0 +1,40 @@ +# AsyncPhilox_d Class + +## Overview + +`AsyncPhilox_d` is a GPU-based random number generator class that uses NVIDIA's [CURAND](https://docs.nvidia.com/cuda/curand/index.html) library and the **Philox** counter-based RNG engine. It is designed for high-throughput simulations and supports asynchronous random number generation via an internal CUDA stream. + +The class provides a **double-buffered**, asynchronous mechanism to produce random floating-point numbers (normally distributed) directly on the GPU. This enables overlapping random number generation with compute or memory transfer tasks in other streams. + +--- + +## Purpose + +`AsyncPhilox_d` enables: + +- **Per-thread RNG state initialization** on the GPU +- **Segmented random number generation** using `curand_normal` +- **Double-buffering** for non-blocking buffer filling +- **Asynchronous execution** using a CUDA stream created and managed internally + +This design improves parallelism and simulation throughput by decoupling RNG generation from synchronous host and device execution. + +--- + +## CURAND Philox Generator + +Philox is a **counter-based** RNG suitable for parallel applications. It is: + +- **Stateless** across launches (state only encodes seed, counter, and thread ID) +- **Efficient** on GPUs due to its low register and instruction overhead +- **Deterministic**, producing reproducible sequences across threads + +NVIDIA’s CURAND provides the Philox generator via the `curandStatePhilox4_32_10_t` type, which is initialized on a per-thread basis using `curand_init`. + +You can find more details in the [CURAND Device API Overview](https://docs.nvidia.com/cuda/curand/device-api-overview.html). + + +## CUDA Documentation Links + +- [CURAND API Reference](https://docs.nvidia.com/cuda/curand/index.html) +- [Philox Generator in CURAND](https://docs.nvidia.com/cuda/curand/device-api-overview.html#bit-generation-3) diff --git a/docs/Developer/index.md b/docs/Developer/index.md index fba940935..70ff35500 100644 --- a/docs/Developer/index.md +++ b/docs/Developer/index.md @@ -46,7 +46,7 @@ Students, use this [quickstart guide](StudentSetup.md) to help setup, use, and d - [Neuro Implementation](NeuroImplementation.md) - [GraphManager and InputManager classes](GraphAndEventInputs.md) - [Configuration](../User/configuration.md) - +- [AsyncPhilox RNG class](AsyncPhilox.md) --------- [<< Go back to the Graphitti home page](../index.md) From 7aa77eba96202d9097d140758f4fda8ec2019c87 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 23 Jun 2025 09:20:06 -0700 Subject: [PATCH 34/37] fix nvtx variable names --- Simulator/Utils/RNG/AsyncPhilox_d.cu | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Simulator/Utils/RNG/AsyncPhilox_d.cu b/Simulator/Utils/RNG/AsyncPhilox_d.cu index b11e9e4a6..a9b61c2fb 100644 --- a/Simulator/Utils/RNG/AsyncPhilox_d.cu +++ b/Simulator/Utils/RNG/AsyncPhilox_d.cu @@ -81,8 +81,8 @@ void AsyncPhilox_d::loadAsyncPhilox(int samplesPerSegment, unsigned long seed) totalSegments_ = 10; #ifdef ENABLE_NVTX - nvtxMarker = 10000 / totalSegments; // make a marker every nvtxMarker buffer fills; - nvtxCurrentMarker = nvtxMarker; // count down to color flip + nvtxMarker_ = 10000 / totalSegments; // make a marker every nvtxMarker buffer fills; + nvtxCurrentMarker_ = nvtxMarker_; // count down to color flip #endif bufferSize_ = segmentSize_ * totalSegments_; numBlocks_ = 64; //placeholder num of blocks From ca319a9c2a361852d070fa5cdf29497d4cf73d2a Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 2 Jul 2025 10:29:11 -0700 Subject: [PATCH 35/37] Revert "Merge branch 'SharedDevelopment' into AndrewDevelopment" This reverts commit 5d7ee8c1ce9aaa32ca6c49f87969b6b4bb64ecda, reversing changes made to 7aa77eba96202d9097d140758f4fda8ec2019c87. --- Contributors.md | 2 - Simulator/Core/Core.cpp | 4 +- Simulator/Core/GPUModel.cpp | 171 +++++++++++------- Simulator/Core/GPUModel.h | 38 ++-- Simulator/Core/OperationManager.cpp | 4 +- Simulator/Core/Operations.h | 4 +- Simulator/Core/Serializer.cpp | 5 +- Simulator/Edges/AllEdges.cpp | 24 +-- Simulator/Edges/AllEdges.h | 16 +- Simulator/Edges/NG911/All911Edges.h | 10 +- Simulator/Edges/Neuro/AllDSSynapses.h | 15 +- Simulator/Edges/Neuro/AllDSSynapses_d.cpp | 29 +-- .../Edges/Neuro/AllDynamicSTDPSynapses.h | 17 +- .../Edges/Neuro/AllDynamicSTDPSynapses_d.cpp | 28 +-- Simulator/Edges/Neuro/AllSTDPSynapses.h | 16 +- Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp | 28 +-- Simulator/Edges/Neuro/AllSpikingSynapses.h | 15 +- .../Edges/Neuro/AllSpikingSynapses_d.cpp | 27 ++- Simulator/Utils/Global.cpp | 15 ++ Simulator/Utils/Global.h | 34 ++++ Simulator/Vertices/AllVertices.cpp | 41 ----- Simulator/Vertices/AllVertices.h | 19 +- Simulator/Vertices/NG911/All911Vertices.h | 10 +- Simulator/Vertices/Neuro/AllIFNeurons.h | 15 +- Simulator/Vertices/Neuro/AllIFNeurons_d.cpp | 24 ++- Simulator/Vertices/Neuro/AllIZHNeurons.h | 16 +- Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp | 31 ++-- Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp | 2 +- Simulator/Vertices/Neuro/AllSpikingNeurons.h | 7 +- .../Vertices/Neuro/AllSpikingNeurons_d.cpp | 12 +- docs/Developer/GHActions.md | 7 +- docs/Developer/index.md | 11 +- docs/Doxygen/Doxyfile | 2 +- docs/Doxygen/index.md | 19 -- docs/{Notes => }/Glossary.md | 0 docs/Notes/index.md | 32 ---- .../ConnectionsNotes.md | 0 docs/{Notes => RebuildNotes}/GeneralNotes.md | 0 docs/{Notes => RebuildNotes}/LayoutsNotes.md | 0 docs/{Notes => RebuildNotes}/NeuronsNotes.md | 0 .../{Notes => RebuildNotes}/RecordersNotes.md | 0 docs/{Notes => RebuildNotes}/SynapsesNotes.md | 0 docs/{Notes => }/Resources.md | 0 docs/Testing/index.md | 9 +- docs/User/installation.md | 42 ++--- docs/index.md | 64 +++++-- 46 files changed, 428 insertions(+), 437 deletions(-) delete mode 100644 docs/Doxygen/index.md rename docs/{Notes => }/Glossary.md (100%) delete mode 100644 docs/Notes/index.md rename docs/{Notes => RebuildNotes}/ConnectionsNotes.md (100%) rename docs/{Notes => RebuildNotes}/GeneralNotes.md (100%) rename docs/{Notes => RebuildNotes}/LayoutsNotes.md (100%) rename docs/{Notes => RebuildNotes}/NeuronsNotes.md (100%) rename docs/{Notes => RebuildNotes}/RecordersNotes.md (100%) rename docs/{Notes => RebuildNotes}/SynapsesNotes.md (100%) rename docs/{Notes => }/Resources.md (100%) diff --git a/Contributors.md b/Contributors.md index d2d338c78..b2ead0ce9 100644 --- a/Contributors.md +++ b/Contributors.md @@ -85,8 +85,6 @@ Andrew Madison Padmanabh Patil -Lawrence Scott - # Graduate diff --git a/Simulator/Core/Core.cpp b/Simulator/Core/Core.cpp index 6c206ed36..da1c59bce 100644 --- a/Simulator/Core/Core.cpp +++ b/Simulator/Core/Core.cpp @@ -206,7 +206,7 @@ int Core::runSimulation(string executableName, string cmdLineArguments) } // Helper function for recorder to register spike history variables for all neurons. - OperationManager::getInstance().executeOperation(Operations::registerHistoryVariables); + simulator.getModel().getLayout().getVertices().registerHistoryVariables(); // Run simulation LOG4CPLUS_TRACE(consoleLogger, "Starting Simulation"); @@ -247,4 +247,4 @@ int Core::runSimulation(string executableName, string cmdLineArguments) cout << "time elapsed: " << timeElapsed << endl; cout << "ssps (simulation seconds / real time seconds): " << ssps << endl; return 0; -} \ No newline at end of file +} diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index 62017b18a..ef4621f1f 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -12,8 +12,6 @@ #include "AllVertices.h" #include "Connections.h" #include "Global.h" -#include "OperationManager.h" - #ifdef VALIDATION_MODE #include "AllIFNeurons.h" #include "OperationManager.h" @@ -29,34 +27,31 @@ GPUModel::GPUModel() : Model::Model(), edgeIndexMapDevice_(nullptr), randNoise_d(nullptr), allVerticesDevice_(nullptr), allEdgesDevice_(nullptr) { - // Register allocNeuronDeviceStruct function as a allocateGPU operation in the OperationManager - function allocateGPU = bind(&GPUModel::allocDeviceStruct, this); - OperationManager::getInstance().registerOperation(Operations::allocateGPU, allocateGPU); - - // Register copyCPUtoGPU function as a copyCPUtoGPU operation in the OperationManager - function copyCPUtoGPU = bind(&GPUModel::copyCPUtoGPU, this); - OperationManager::getInstance().registerOperation(Operations::copyToGPU, copyCPUtoGPU); - - // Note: We do not register a corresponding copyFromGPU operation here because - // we are only copying the synapseIndexMap to the GPU. This map is a read-only lookup table - // that gets recreated from scratch on each update. As a result, we only need to allocate, - // copy to GPU, and deallocate — there is no meaningful data to copy back from the GPU. - - // Register deleteSynapseImap function as a deallocateGPUMemory operation in the OperationManager - function deallocateGPUMemory = bind(&GPUModel::deleteDeviceStruct, this); - OperationManager::getInstance().registerOperation(Operations::deallocateGPUMemory, - deallocateGPUMemory); } /// Allocates and initializes memories on CUDA device. -void GPUModel::allocDeviceStruct() +/// @param[out] allVerticesDevice Memory location of the pointer to the vertices list on device memory. +/// @param[out] allEdgesDevice Memory location of the pointer to the edges list on device memory. +void GPUModel::allocDeviceStruct(void **allVerticesDevice, void **allEdgesDevice) { + // Get vertices and edges + AllVertices &vertices = layout_->getVertices(); + AllEdges &edges = connections_->getEdges(); + + // Allocate vertices and edges structs on GPU device memory + vertices.allocVerticesDeviceStruct(allVerticesDevice); + edges.allocEdgeDeviceStruct(allEdgesDevice); + // Allocate memory for random noise array int numVertices = Simulator::getInstance().getTotalVertices(); // BGSIZE randNoise_d_size = numVertices * sizeof(float); // size of random noise array // HANDLE_ERROR(cudaMalloc((void **)&randNoise_d, randNoise_d_size)); - // Allocate synapse inverse map in device memory + // Copy host vertex and edge arrays into GPU device + vertices.copyToDevice(*allVerticesDevice); + edges.copyEdgeHostToDevice(*allEdgesDevice); + + // Allocate edge inverse map in device memory allocEdgeIndexMap(numVertices); // Create the CUDA stream used to launch synchronous GPU kernels during the simulation. @@ -67,8 +62,16 @@ void GPUModel::allocDeviceStruct() } /// Copies device memories to host memories and deallocates them. -void GPUModel::deleteDeviceStruct() +/// @param[out] allVerticesDevice Memory location of the pointer to the vertices list on device memory. +/// @param[out] allEdgesDevice Memory location of the pointer to the edges list on device memory. +void GPUModel::deleteDeviceStruct(void **allVerticesDevice, void **allEdgesDevice) { + // Get vertices and edges + AllVertices &vertices = layout_->getVertices(); + AllEdges &edges = connections_->getEdges(); + + // Copy device edge and vertex structs to host memory + vertices.copyFromDevice(*allVerticesDevice); // Deallocate device memory vertices.deleteVerticesDeviceStruct(*allVerticesDevice); // Copy device edge and vertex structs to host memory @@ -112,9 +115,13 @@ void GPUModel::setupSim() t_gpu_advanceSynapses = 0.0; t_gpu_calcSummation = 0.0; #endif // PERFORMANCE_METRICS - // Allocate and copy neuron/synapse data structures to GPU memory - OperationManager::getInstance().executeOperation(Operations::allocateGPU); - OperationManager::getInstance().executeOperation(Operations::copyToGPU); + + // allocates memories on CUDA device + allocDeviceStruct((void **)&allVerticesDevice_, (void **)&allEdgesDevice_); + + EdgeIndexMap &edgeIndexMap = connections_->getEdgeIndexMap(); + // copy inverse map to the device memory + copyEdgeIndexMapHostToDevice(edgeIndexMap, Simulator::getInstance().getTotalVertices()); AllEdges &edges = connections_->getEdges(); // set some parameters used for advanceVerticesDevice @@ -130,14 +137,11 @@ void GPUModel::setupSim() /// Performs any finalization tasks on network following a simulation. void GPUModel::finish() { - // copy device synapse and neuron structs to host memory - OperationManager::getInstance().executeOperation(Operations::copyFromGPU); // deallocates memories on CUDA device - // TODO: Resolve whether the deletion operations below that don't seem bundled - // with the OperationManager are necessary, should be moved, or if they can be removed. AsyncGenerator_.deleteDeviceStruct(); - OperationManager::getInstance().executeOperation(Operations::deallocateGPUMemory); + deleteDeviceStruct((void **)&allVerticesDevice_, (void **)&allEdgesDevice_); deleteEdgeIndexMap(); + #ifdef PERFORMANCE_METRICS cudaEventDestroy(start); cudaEventDestroy(stop); @@ -246,7 +250,7 @@ void GPUModel::updateConnections() AllVertices &vertices = layout_->getVertices(); AllEdges &edges = connections_->getEdges(); - vertices.copyFromDevice(); + vertices.copyFromDevice(allVerticesDevice_); // Update Connections data if (connections_->updateConnections(vertices)) { @@ -256,7 +260,8 @@ void GPUModel::updateConnections() // create edge index map connections_->createEdgeIndexMap(); // copy index map to the device memory - copyCPUtoGPU(); + copyEdgeIndexMapHostToDevice(connections_->getEdgeIndexMap(), + Simulator::getInstance().getTotalVertices()); } } @@ -293,53 +298,81 @@ void GPUModel::allocEdgeIndexMap(int count) cudaMemcpyHostToDevice)); } -/// Allocate and Copy CPU Synapse data to GPU. -void GPUModel::copyCPUtoGPU() +/// Deallocate device memory for edge inverse map. +void GPUModel::deleteEdgeIndexMap() { - EdgeIndexMap synapseIndexMapHost = connections_->getEdgeIndexMap(); - int numVertices = Simulator::getInstance().getTotalVertices(); - AllEdges &synapses = connections_->getEdges(); - int totalSynapseCount = dynamic_cast(synapses).totalEdgeCount_; - if (totalSynapseCount == 0) + EdgeIndexMapDevice edgeIndexMapDevice; + HANDLE_ERROR(cudaMemcpy(&edgeIndexMapDevice, edgeIndexMapDevice_, sizeof(EdgeIndexMapDevice), + cudaMemcpyDeviceToHost)); + HANDLE_ERROR(cudaFree(edgeIndexMapDevice.outgoingEdgeBegin_)); + HANDLE_ERROR(cudaFree(edgeIndexMapDevice.outgoingEdgeCount_)); + HANDLE_ERROR(cudaFree(edgeIndexMapDevice.outgoingEdgeIndexMap_)); + HANDLE_ERROR(cudaFree(edgeIndexMapDevice.incomingEdgeBegin_)); + HANDLE_ERROR(cudaFree(edgeIndexMapDevice.incomingEdgeCount_)); + HANDLE_ERROR(cudaFree(edgeIndexMapDevice.incomingEdgeIndexMap_)); + HANDLE_ERROR(cudaFree(edgeIndexMapDevice_)); +} + +/// Copy EdgeIndexMap in host memory to EdgeIndexMap in device memory. +/// @param edgeIndexMapHost Reference to the EdgeIndexMap in host memory. +void GPUModel::copyEdgeIndexMapHostToDevice(EdgeIndexMap &edgeIndexMapHost, int numVertices) +{ + AllEdges &edges = connections_->getEdges(); + int totalEdgeCount = edges.totalEdgeCount_; + if (totalEdgeCount == 0) return; - EdgeIndexMapDevice synapseIMapDevice; - HANDLE_ERROR(cudaMemcpy(&synapseIMapDevice, edgeIndexMapDevice_, sizeof(EdgeIndexMapDevice), + EdgeIndexMapDevice edgeIndexMapDevice; + HANDLE_ERROR(cudaMemcpy(&edgeIndexMapDevice, edgeIndexMapDevice_, sizeof(EdgeIndexMapDevice), cudaMemcpyDeviceToHost)); - HANDLE_ERROR(cudaMemcpy(synapseIMapDevice.outgoingEdgeBegin_, - synapseIndexMapHost.outgoingEdgeBegin_.data(), - numVertices * sizeof(BGSIZE), cudaMemcpyHostToDevice)); - HANDLE_ERROR(cudaMemcpy(synapseIMapDevice.outgoingEdgeCount_, - synapseIndexMapHost.outgoingEdgeCount_.data(), - numVertices * sizeof(BGSIZE), cudaMemcpyHostToDevice)); - if (synapseIMapDevice.outgoingEdgeIndexMap_ != nullptr) { - HANDLE_ERROR(cudaFree(synapseIMapDevice.outgoingEdgeIndexMap_)); + HANDLE_ERROR(cudaMemcpy(edgeIndexMapDevice.outgoingEdgeBegin_, + edgeIndexMapHost.outgoingEdgeBegin_.data(), numVertices * sizeof(BGSIZE), + cudaMemcpyHostToDevice)); + HANDLE_ERROR(cudaMemcpy(edgeIndexMapDevice.outgoingEdgeCount_, + edgeIndexMapHost.outgoingEdgeCount_.data(), numVertices * sizeof(BGSIZE), + cudaMemcpyHostToDevice)); + if (edgeIndexMapDevice.outgoingEdgeIndexMap_ != nullptr) { + HANDLE_ERROR(cudaFree(edgeIndexMapDevice.outgoingEdgeIndexMap_)); } - HANDLE_ERROR(cudaMalloc((void **)&synapseIMapDevice.outgoingEdgeIndexMap_, - totalSynapseCount * sizeof(BGSIZE))); - HANDLE_ERROR(cudaMemcpy(synapseIMapDevice.outgoingEdgeIndexMap_, - synapseIndexMapHost.outgoingEdgeIndexMap_.data(), - totalSynapseCount * sizeof(BGSIZE), cudaMemcpyHostToDevice)); + HANDLE_ERROR(cudaMalloc((void **)&edgeIndexMapDevice.outgoingEdgeIndexMap_, + totalEdgeCount * sizeof(BGSIZE))); + HANDLE_ERROR(cudaMemcpy(edgeIndexMapDevice.outgoingEdgeIndexMap_, + edgeIndexMapHost.outgoingEdgeIndexMap_.data(), + totalEdgeCount * sizeof(BGSIZE), cudaMemcpyHostToDevice)); // active synapse map - HANDLE_ERROR(cudaMemcpy(synapseIMapDevice.incomingEdgeBegin_, - synapseIndexMapHost.incomingEdgeBegin_.data(), - numVertices * sizeof(BGSIZE), cudaMemcpyHostToDevice)); - HANDLE_ERROR(cudaMemcpy(synapseIMapDevice.incomingEdgeCount_, - synapseIndexMapHost.incomingEdgeCount_.data(), - numVertices * sizeof(BGSIZE), cudaMemcpyHostToDevice)); + HANDLE_ERROR(cudaMemcpy(edgeIndexMapDevice.incomingEdgeBegin_, + edgeIndexMapHost.incomingEdgeBegin_.data(), numVertices * sizeof(BGSIZE), + cudaMemcpyHostToDevice)); + HANDLE_ERROR(cudaMemcpy(edgeIndexMapDevice.incomingEdgeCount_, + edgeIndexMapHost.incomingEdgeCount_.data(), numVertices * sizeof(BGSIZE), + cudaMemcpyHostToDevice)); // the number of synapses may change, so we reallocate the memory - if (synapseIMapDevice.incomingEdgeIndexMap_ != nullptr) { - HANDLE_ERROR(cudaFree(synapseIMapDevice.incomingEdgeIndexMap_)); - synapseIMapDevice.incomingEdgeIndexMap_ = nullptr; + if (edgeIndexMapDevice.incomingEdgeIndexMap_ != nullptr) { + HANDLE_ERROR(cudaFree(edgeIndexMapDevice.incomingEdgeIndexMap_)); + edgeIndexMapDevice.incomingEdgeIndexMap_ = nullptr; } - HANDLE_ERROR(cudaMalloc((void **)&synapseIMapDevice.incomingEdgeIndexMap_, - totalSynapseCount * sizeof(BGSIZE))); - HANDLE_ERROR(cudaMemcpy(synapseIMapDevice.incomingEdgeIndexMap_, - synapseIndexMapHost.incomingEdgeIndexMap_.data(), - totalSynapseCount * sizeof(BGSIZE), cudaMemcpyHostToDevice)); - HANDLE_ERROR(cudaMemcpy(edgeIndexMapDevice_, &synapseIMapDevice, sizeof(EdgeIndexMapDevice), + HANDLE_ERROR(cudaMalloc((void **)&edgeIndexMapDevice.incomingEdgeIndexMap_, + totalEdgeCount * sizeof(BGSIZE))); + HANDLE_ERROR(cudaMemcpy(edgeIndexMapDevice.incomingEdgeIndexMap_, + edgeIndexMapHost.incomingEdgeIndexMap_.data(), + totalEdgeCount * sizeof(BGSIZE), cudaMemcpyHostToDevice)); + HANDLE_ERROR(cudaMemcpy(edgeIndexMapDevice_, &edgeIndexMapDevice, sizeof(EdgeIndexMapDevice), cudaMemcpyHostToDevice)); } +/// Copy GPU edge data to CPU. +void GPUModel::copyGPUtoCPU() +{ + // copy device edge structs to host memory + connections_->getEdges().copyEdgeDeviceToHost(allEdgesDevice_); +} + +/// Copy CPU edge data to GPU. +void GPUModel::copyCPUtoGPU() +{ + // copy host edge structs to device memory + connections_->getEdges().copyEdgeHostToDevice(allEdgesDevice_); +} + /// Print out EdgeProps on the GPU. void GPUModel::printGPUEdgesPropsModel() const { diff --git a/Simulator/Core/GPUModel.h b/Simulator/Core/GPUModel.h index eae1aa9f8..7bfbf1de5 100644 --- a/Simulator/Core/GPUModel.h +++ b/Simulator/Core/GPUModel.h @@ -22,14 +22,11 @@ #pragma once #include "AllEdges.h" -#include "AllSpikingNeurons.h" -#include "AllSpikingSynapses.h" #include "AllVertices.h" #include "AsyncPhilox_d.h" #ifdef USE_GPU #include #endif -#include "OperationManager.h" #ifdef VALIDATION_MODE #include @@ -88,33 +85,25 @@ class GPUModel : public Model { /// over the past epoch. Should be called once every epoch. virtual void updateConnections() override; - /// Copies neuron and synapse data from CPU to GPU memory. - /// TODO: Refactor this. Currently, GPUModel handles low-level memory transfer for vertices and edges. - /// Consider moving this responsibility to a more appropriate class, such as a dedicated memory manager - /// or the OperationManager, to better separate concerns and keep the model focused on high-level coordination. - virtual void copyCPUtoGPU() override; + /// Copy GPU edge data to CPU. + virtual void copyGPUtoCPU() override; - // GPUModel itself does not have anything to be copied back, this function is a - // dummy function just to make GPUModel non virtual - virtual void copyGPUtoCPU() override - { - } + /// Copy CPU edge data to GPU. + virtual void copyCPUtoGPU() override; /// Print out EdgeProps on the GPU. void printGPUEdgesPropsModel() const; - /// Getter for edge (synapse) structures in device memory - AllEdgesDeviceProperties *&getAllEdgesDevice(); - - /// Getter for vertex (neuron) structures in device memory - AllVerticesDeviceProperties *&getAllVerticesDevice(); - protected: /// Allocates and initializes memories on CUDA device. - void allocDeviceStruct(); + /// @param[out] allVerticesDevice Memory location of the pointer to the vertices list on device memory. + /// @param[out] allEdgesDevice Memory location of the pointer to the edges list on device memory. + void allocDeviceStruct(void **allVerticesDevice, void **allEdgesDevice); - /// Deallocates device memories. - virtual void deleteDeviceStruct(); + /// Copies device memories to host memories and deallocates them. + /// @param[out] allVerticesDevice Memory location of the pointer to the vertices list on device memory. + /// @param[out] allEdgesDevice Memory location of the pointer to the edges list on device memory. + virtual void deleteDeviceStruct(void **allVerticesDevice, void **allEdgesDevice); /// Pointer to device random noise array. float *randNoise_d; @@ -142,6 +131,11 @@ class GPUModel : public Model { private: void allocEdgeIndexMap(int count); + void deleteEdgeIndexMap(); + +public: //2020/03/14 changed to public for accessing in Core + void copyEdgeIndexMapHostToDevice(EdgeIndexMap &edgeIndexMapHost, int numVertices); + private: void updateHistory(); diff --git a/Simulator/Core/OperationManager.cpp b/Simulator/Core/OperationManager.cpp index 7cd1222b4..dda9e411a 100644 --- a/Simulator/Core/OperationManager.cpp +++ b/Simulator/Core/OperationManager.cpp @@ -71,9 +71,7 @@ string OperationManager::operationToString(const Operations &operation) const return "copyToGPU"; case Operations::copyFromGPU: return "copyFromGPU"; - case Operations::allocateGPU: - return "allocateGPU"; default: return "Operation isn't in OperationManager::operationToString()"; } -} \ No newline at end of file +} diff --git a/Simulator/Core/Operations.h b/Simulator/Core/Operations.h index 27b5c4ff2..8cd21f3b6 100644 --- a/Simulator/Core/Operations.h +++ b/Simulator/Core/Operations.h @@ -20,7 +20,5 @@ enum class Operations { deallocateGPUMemory, // Make sure deallocate memory isn't called until all GPU memory is copied back. restoreToDefault, // Not sure what this refers to. copyToGPU, - copyFromGPU, - allocateGPU, - registerHistoryVariables + copyFromGPU }; \ No newline at end of file diff --git a/Simulator/Core/Serializer.cpp b/Simulator/Core/Serializer.cpp index d1b2c0f7f..21652f37e 100644 --- a/Simulator/Core/Serializer.cpp +++ b/Simulator/Core/Serializer.cpp @@ -69,7 +69,8 @@ bool Serializer::deserialize() #if defined(USE_GPU) GPUModel &gpuModel = static_cast(simulator.getModel()); - gpuModel.copyCPUtoGPU(); + gpuModel.copyEdgeIndexMapHostToDevice(simulator.getModel().getConnections().getEdgeIndexMap(), + simulator.getTotalVertices()); #endif // USE_GPU return true; @@ -109,4 +110,4 @@ template bool Serializer::processArchive(Archive &archive, Si return false; } return true; -} \ No newline at end of file +} diff --git a/Simulator/Edges/AllEdges.cpp b/Simulator/Edges/AllEdges.cpp index 193a5290c..8c570e24c 100644 --- a/Simulator/Edges/AllEdges.cpp +++ b/Simulator/Edges/AllEdges.cpp @@ -26,28 +26,6 @@ AllEdges::AllEdges() : totalEdgeCount_(0), maxEdgesPerVertex_(0), countVertices_ OperationManager::getInstance().registerOperation(Operations::printParameters, printParametersFunc); -#if defined(USE_GPU) - // Register allocNeuronDeviceStruct function as a allocateGPU operation in the OperationManager - function allocateGPU - = bind(static_cast(&AllEdges::allocEdgeDeviceStruct), this); - OperationManager::getInstance().registerOperation(Operations::allocateGPU, allocateGPU); - - // Register AllEdges::copyEdgeHostToDevice function as a copyToGPU operation in the OperationManager - function copyCPUtoGPU - = bind(static_cast(&AllEdges::copyEdgeHostToDevice), this); - OperationManager::getInstance().registerOperation(Operations::copyToGPU, copyCPUtoGPU); - - // Register copyFromGPU operation for transferring edge data from device to host - function copyFromGPU = bind(&AllEdges::copyEdgeDeviceToHost, this); - OperationManager::getInstance().registerOperation(Operations::copyFromGPU, copyFromGPU); - - // Register deleteEdgeDeviceStruct function as a deallocateGPUMemory operation in the OperationManager - function deallocateGPUMemory = bind(&AllEdges::deleteEdgeDeviceStruct, this); - OperationManager::getInstance().registerOperation(Operations::deallocateGPUMemory, - deallocateGPUMemory); - -#endif - fileLogger_ = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("file")); edgeLogger_ = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("edge")); } @@ -312,4 +290,4 @@ BGSIZE AllEdges::addEdge(edgeType type, int srcVertex, int destVertex, BGFLOAT d // create an edge createEdge(iEdg, srcVertex, destVertex, deltaT, type); return iEdg; -} \ No newline at end of file +} diff --git a/Simulator/Edges/AllEdges.h b/Simulator/Edges/AllEdges.h index cdf0c2b6f..d5592dc35 100644 --- a/Simulator/Edges/AllEdges.h +++ b/Simulator/Edges/AllEdges.h @@ -111,7 +111,9 @@ class AllEdges { /// Allocate GPU memories to store all edges' states, /// and copy them from host to GPU memory. - virtual void allocEdgeDeviceStruct() = 0; + /// + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void allocEdgeDeviceStruct(void **allEdgesDevice) = 0; /// Allocate GPU memories to store all edges' states, /// and copy them from host to GPU memory. @@ -124,10 +126,13 @@ class AllEdges { /// Delete GPU memories. /// - virtual void deleteEdgeDeviceStruct() = 0; + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void deleteEdgeDeviceStruct(void *allEdgesDevice) = 0; /// Copy all edges' data from host to device. - virtual void copyEdgeHostToDevice() = 0; + /// + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void copyEdgeHostToDevice(void *allEdgesDevice) = 0; /// Copy all edges' data from host to device. /// @@ -139,7 +144,8 @@ class AllEdges { /// Copy all edges' data from device to host. /// - virtual void copyEdgeDeviceToHost() = 0; + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void copyEdgeDeviceToHost(void *allEdgesDevice) = 0; /// Get edge_counts in AllEdges struct on device memory. /// @@ -280,4 +286,4 @@ template void AllEdges::serialize(Archive &archive) cereal::make_nvp("totalEdgeCount", totalEdgeCount_), cereal::make_nvp("maxEdgesPerVertex", maxEdgesPerVertex_), cereal::make_nvp("countVertices", countVertices_)); -} \ No newline at end of file +} diff --git a/Simulator/Edges/NG911/All911Edges.h b/Simulator/Edges/NG911/All911Edges.h index 505972266..6b1c288cf 100644 --- a/Simulator/Edges/NG911/All911Edges.h +++ b/Simulator/Edges/NG911/All911Edges.h @@ -65,14 +65,14 @@ class All911Edges : public AllEdges { // GPU functionality for 911 simulation is unimplemented. // These signatures are required to make the class non-abstract public: - virtual void allocEdgeDeviceStruct() {}; + virtual void allocEdgeDeviceStruct(void **allEdgesDevice) {}; virtual void allocEdgeDeviceStruct(void **allEdgesDevice, int numVertices, int maxEdgesPerVertex) {}; - virtual void deleteEdgeDeviceStruct() {}; - virtual void copyEdgeHostToDevice() {}; + virtual void deleteEdgeDeviceStruct(void *allEdgesDevice) {}; + virtual void copyEdgeHostToDevice(void *allEdgesDevice) {}; virtual void copyEdgeHostToDevice(void *allEdgesDevice, int numVertices, int maxEdgesPerVertex) { }; - virtual void copyEdgeDeviceToHost() {}; + virtual void copyEdgeDeviceToHost(void *allEdgesDevice) {}; virtual void copyDeviceEdgeCountsToHost(void *allEdgesDevice) {}; virtual void advanceEdges(void *allEdgesDevice, void *allVerticesDevice, void *edgeIndexMapDevice) {}; @@ -107,4 +107,4 @@ class All911Edges : public AllEdges { /// The call information per edge vector call_; -}; \ No newline at end of file +}; diff --git a/Simulator/Edges/Neuro/AllDSSynapses.h b/Simulator/Edges/Neuro/AllDSSynapses.h index baaa94b41..47427e679 100644 --- a/Simulator/Edges/Neuro/AllDSSynapses.h +++ b/Simulator/Edges/Neuro/AllDSSynapses.h @@ -121,7 +121,9 @@ class AllDSSynapses : public AllSpikingSynapses { public: /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. - virtual void allocEdgeDeviceStruct() override; + /// + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void allocEdgeDeviceStruct(void **allEdgesDevice) override; /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. @@ -134,11 +136,13 @@ class AllDSSynapses : public AllSpikingSynapses { /// Delete GPU memories. /// - virtual void deleteEdgeDeviceStruct() override; + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void deleteEdgeDeviceStruct(void *allEdgesDevice) override; /// Copy all synapses' data from host to device. /// - virtual void copyEdgeHostToDevice() override; + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void copyEdgeHostToDevice(void *allEdgesDevice) override; /// Copy all synapses' data from host to device. /// @@ -150,7 +154,8 @@ class AllDSSynapses : public AllSpikingSynapses { /// Copy all synapses' data from device to host. /// - virtual void copyEdgeDeviceToHost() override; + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void copyEdgeDeviceToHost(void *allEdgesDevice) override; /// Set synapse class ID defined by enumClassSynapses for the caller's Synapse class. /// The class ID will be set to classSynapses_d in device memory, @@ -258,4 +263,4 @@ template void AllDSSynapses::serialize(Archive &archive) archive(cereal::base_class(this), cereal::make_nvp("lastSpike", lastSpike_), cereal::make_nvp("r", r_), cereal::make_nvp("u", u_), cereal::make_nvp("D", D_), cereal::make_nvp("U", U_), cereal::make_nvp("F", F_)); -} \ No newline at end of file +} diff --git a/Simulator/Edges/Neuro/AllDSSynapses_d.cpp b/Simulator/Edges/Neuro/AllDSSynapses_d.cpp index c1939d882..d1783e969 100644 --- a/Simulator/Edges/Neuro/AllDSSynapses_d.cpp +++ b/Simulator/Edges/Neuro/AllDSSynapses_d.cpp @@ -14,10 +14,11 @@ /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. -void AllDSSynapses::allocEdgeDeviceStruct() +/// +/// @param allEdgesDevice GPU address of the AllDSSynapsesDeviceProperties struct +/// on device memory. +void AllDSSynapses::allocEdgeDeviceStruct(void **allEdgesDevice) { - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void **allEdgesDevice = reinterpret_cast(&(gpuModel->getAllEdgesDevice())); allocEdgeDeviceStruct(allEdgesDevice, Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getMaxEdgesPerVertex()); } @@ -66,11 +67,12 @@ void AllDSSynapses::allocDeviceStruct(AllDSSynapsesDeviceProperties &allEdges, i /// Delete GPU memories. /// -void AllDSSynapses::deleteEdgeDeviceStruct() +/// @param allEdgesDevice GPU address of the AllDSSynapsesDeviceProperties struct +/// on device memory. +void AllDSSynapses::deleteEdgeDeviceStruct(void *allEdgesDevice) { AllDSSynapsesDeviceProperties allEdges; - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); + HANDLE_ERROR(cudaMemcpy(&allEdges, allEdgesDevice, sizeof(AllDSSynapsesDeviceProperties), cudaMemcpyDeviceToHost)); @@ -98,10 +100,10 @@ void AllDSSynapses::deleteDeviceStruct(AllDSSynapsesDeviceProperties &allEdgesDe /// Copy all synapses' data from host to device. /// -void AllDSSynapses::copyEdgeHostToDevice() +/// @param allEdgesDevice GPU address of the AllDSSynapsesDeviceProperties struct +/// on device memory. +void AllDSSynapses::copyEdgeHostToDevice(void *allEdgesDevice) { // copy everything necessary - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); copyEdgeHostToDevice(allEdgesDevice, Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getMaxEdgesPerVertex()); } @@ -154,12 +156,13 @@ void AllDSSynapses::copyHostToDevice(void *allEdgesDevice, /// Copy all synapses' data from device to host. /// -void AllDSSynapses::copyEdgeDeviceToHost() +/// @param allEdgesDevice GPU address of the AllDSSynapsesDeviceProperties struct +/// on device memory. +void AllDSSynapses::copyEdgeDeviceToHost(void *allEdgesDevice) { // copy everything necessary AllDSSynapsesDeviceProperties allEdgesDeviceProps; - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); + HANDLE_ERROR(cudaMemcpy(&allEdgesDeviceProps, allEdgesDevice, sizeof(AllDSSynapsesDeviceProperties), cudaMemcpyDeviceToHost)); @@ -373,4 +376,4 @@ void AllDSSynapses::printGPUEdgesProps(void *allEdgesDeviceProps) const UPrint = nullptr; FPrint = nullptr; } -} \ No newline at end of file +} diff --git a/Simulator/Edges/Neuro/AllDynamicSTDPSynapses.h b/Simulator/Edges/Neuro/AllDynamicSTDPSynapses.h index c12b25b62..2d5c6e79d 100644 --- a/Simulator/Edges/Neuro/AllDynamicSTDPSynapses.h +++ b/Simulator/Edges/Neuro/AllDynamicSTDPSynapses.h @@ -124,8 +124,10 @@ class AllDynamicSTDPSynapses : public AllSTDPSynapses { #if defined(USE_GPU) public: /// Allocate GPU memories to store all synapses' states, - /// and copy them from host to GPU memory. memory. - virtual void allocEdgeDeviceStruct() override; + /// and copy them from host to GPU memory. + /// + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void allocEdgeDeviceStruct(void **allEdgesDevice) override; /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. @@ -137,11 +139,13 @@ class AllDynamicSTDPSynapses : public AllSTDPSynapses { /// Delete GPU memories. /// - virtual void deleteEdgeDeviceStruct() override; + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void deleteEdgeDeviceStruct(void *allEdgesDevice) override; /// Copy all synapses' data from host to device. /// - virtual void copyEdgeHostToDevice() override; + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void copyEdgeHostToDevice(void *allEdgesDevice) override; /// Copy all synapses' data from host to device. /// @@ -153,7 +157,8 @@ class AllDynamicSTDPSynapses : public AllSTDPSynapses { /// Copy all synapses' data from device to host. /// - virtual void copyEdgeDeviceToHost() override; + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void copyEdgeDeviceToHost(void *allEdgesDevice) override; /// Set synapse class ID defined by enumClassSynapses for the caller's Synapse class. /// The class ID will be set to classSynapses_d in device memory, @@ -264,4 +269,4 @@ template void AllDynamicSTDPSynapses::serialize(Archive &archive archive(cereal::base_class(this), cereal::make_nvp("lastSpike", lastSpike_), cereal::make_nvp("r", r_), cereal::make_nvp("u", u_), cereal::make_nvp("D", D_), cereal::make_nvp("U", U_), cereal::make_nvp("F", F_)); -} \ No newline at end of file +} diff --git a/Simulator/Edges/Neuro/AllDynamicSTDPSynapses_d.cpp b/Simulator/Edges/Neuro/AllDynamicSTDPSynapses_d.cpp index f4ef0e2c7..1c676b19e 100644 --- a/Simulator/Edges/Neuro/AllDynamicSTDPSynapses_d.cpp +++ b/Simulator/Edges/Neuro/AllDynamicSTDPSynapses_d.cpp @@ -9,15 +9,15 @@ #include "AllDynamicSTDPSynapses.h" #include "AllSynapsesDeviceFuncs.h" #include "Book.h" -#include "GPUModel.h" #include "Simulator.h" /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. -void AllDynamicSTDPSynapses::allocEdgeDeviceStruct() +/// +/// @param allEdgesDevice GPU address of the AllDynamicSTDPSynapsesDeviceProperties struct +/// on device memory. +void AllDynamicSTDPSynapses::allocEdgeDeviceStruct(void **allEdgesDevice) { - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void **allEdgesDevice = reinterpret_cast(&(gpuModel->getAllEdgesDevice())); allocEdgeDeviceStruct(allEdgesDevice, Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getMaxEdgesPerVertex()); } @@ -70,11 +70,10 @@ void AllDynamicSTDPSynapses::allocDeviceStruct( /// /// @param allEdgesDevice GPU address of the AllDynamicSTDPSynapsesDeviceProperties struct /// on device memory. -void AllDynamicSTDPSynapses::deleteEdgeDeviceStruct() +void AllDynamicSTDPSynapses::deleteEdgeDeviceStruct(void *allEdgesDevice) { AllDynamicSTDPSynapsesDeviceProperties allEdges; - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); + HANDLE_ERROR(cudaMemcpy(&allEdges, allEdgesDevice, sizeof(AllDynamicSTDPSynapsesDeviceProperties), cudaMemcpyDeviceToHost)); @@ -103,10 +102,10 @@ void AllDynamicSTDPSynapses::deleteDeviceStruct( /// Copy all synapses' data from host to device. /// -void AllDynamicSTDPSynapses::copyEdgeHostToDevice() +/// @param allEdgesDevice GPU address of the AllDynamicSTDPSynapsesDeviceProperties struct +/// on device memory. +void AllDynamicSTDPSynapses::copyEdgeHostToDevice(void *allEdgesDevice) { // copy everything necessary - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); copyEdgeHostToDevice(allEdgesDevice, Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getMaxEdgesPerVertex()); } @@ -160,12 +159,13 @@ void AllDynamicSTDPSynapses::copyHostToDevice( /// Copy all synapses' data from device to host. /// -void AllDynamicSTDPSynapses::copyEdgeDeviceToHost() +/// @param allEdgesDevice GPU address of the AllDynamicSTDPSynapsesDeviceProperties struct +/// on device memory. +void AllDynamicSTDPSynapses::copyEdgeDeviceToHost(void *allEdgesDevice) { // copy everything necessary AllDynamicSTDPSynapsesDeviceProperties allEdges; - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); + HANDLE_ERROR(cudaMemcpy(&allEdges, allEdgesDevice, sizeof(AllDynamicSTDPSynapsesDeviceProperties), cudaMemcpyDeviceToHost)); @@ -452,4 +452,4 @@ void AllDynamicSTDPSynapses::printGPUEdgesProps(void *allEdgesDeviceProps) const UPrint = nullptr; FPrint = nullptr; } -} \ No newline at end of file +} diff --git a/Simulator/Edges/Neuro/AllSTDPSynapses.h b/Simulator/Edges/Neuro/AllSTDPSynapses.h index 8598981e8..7d7ea0660 100644 --- a/Simulator/Edges/Neuro/AllSTDPSynapses.h +++ b/Simulator/Edges/Neuro/AllSTDPSynapses.h @@ -154,7 +154,9 @@ class AllSTDPSynapses : public AllSpikingSynapses { public: /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. - virtual void allocEdgeDeviceStruct() override; + /// + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void allocEdgeDeviceStruct(void **allEdgesDevice) override; /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. @@ -167,10 +169,13 @@ class AllSTDPSynapses : public AllSpikingSynapses { /// Delete GPU memories. /// - virtual void deleteEdgeDeviceStruct() override; + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void deleteEdgeDeviceStruct(void *allEdgesDevice) override; /// Copy all synapses' data from host to device. - virtual void copyEdgeHostToDevice() override; + /// + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void copyEdgeHostToDevice(void *allEdgesDevice) override; /// Copy all synapses' data from host to device. /// @@ -182,7 +187,8 @@ class AllSTDPSynapses : public AllSpikingSynapses { /// Copy all synapses' data from device to host. /// - virtual void copyEdgeDeviceToHost() override; + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void copyEdgeDeviceToHost(void *allEdgesDevice) override; /// Advance all the Synapses in the simulation. /// Update the state of all synapses for a time step. @@ -435,4 +441,4 @@ template void AllSTDPSynapses::serialize(Archive &archive) cereal::make_nvp("Wex_I", Wex_I_), cereal::make_nvp("Wex_E", Wex_E_), cereal::make_nvp("Aneg_I", Aneg_I_), cereal::make_nvp("Aneg_E", Aneg_E_), cereal::make_nvp("Apos_I", Apos_I_), cereal::make_nvp("Apos_E", Apos_E_)); -} \ No newline at end of file +} diff --git a/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp b/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp index a7c3804fc..f68b9add4 100644 --- a/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp +++ b/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp @@ -32,10 +32,11 @@ __global__ void advanceSTDPSynapsesDevice(int totalSynapseCount, int maxSpikes); /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. -void AllSTDPSynapses::allocEdgeDeviceStruct() +/// +/// @param allEdgesDevice GPU address of the AllSTDPSynapsesDeviceProperties struct +/// on device memory. +void AllSTDPSynapses::allocEdgeDeviceStruct(void **allEdgesDevice) { - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void **allEdgesDevice = reinterpret_cast(&(gpuModel->getAllEdgesDevice())); allocEdgeDeviceStruct(allEdgesDevice, Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getMaxEdgesPerVertex()); } @@ -92,11 +93,11 @@ void AllSTDPSynapses::allocDeviceStruct(AllSTDPSynapsesDeviceProperties &allEdge /// Delete GPU memories. /// -void AllSTDPSynapses::deleteEdgeDeviceStruct() +/// @param allEdgesDevice GPU address of the AllSTDPSynapsesDeviceProperties struct +/// on device memory. +void AllSTDPSynapses::deleteEdgeDeviceStruct(void *allEdgesDevice) { AllSTDPSynapsesDeviceProperties allEdgesDeviceProps; - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); HANDLE_ERROR(cudaMemcpy(&allEdgesDeviceProps, allEdgesDevice, sizeof(AllSTDPSynapsesDeviceProperties), cudaMemcpyDeviceToHost)); deleteDeviceStruct(allEdgesDeviceProps); @@ -128,10 +129,12 @@ void AllSTDPSynapses::deleteDeviceStruct(AllSTDPSynapsesDeviceProperties &allEdg /// Copy all synapses' data from host to device. /// -void AllSTDPSynapses::copyEdgeHostToDevice() +/// @param allEdgesDevice GPU address of the AllSTDPSynapsesDeviceProperties struct +/// on device memory. +/// @param numVertices Number of vertices. +/// @param maxEdgesPerVertex Maximum number of synapses per neuron. +void AllSTDPSynapses::copyEdgeHostToDevice(void *allEdgesDevice) { // copy everything necessary - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); copyEdgeHostToDevice(allEdgesDevice, Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getMaxEdgesPerVertex()); } @@ -197,12 +200,13 @@ void AllSTDPSynapses::copyHostToDevice(void *allEdgesDevice, /// Copy all synapses' data from device to host. /// -void AllSTDPSynapses::copyEdgeDeviceToHost() +/// @param allEdgesDevice GPU address of the AllSTDPSynapsesDeviceProperties struct +/// on device memory. +void AllSTDPSynapses::copyEdgeDeviceToHost(void *allEdgesDevice) { // copy everything necessary AllSTDPSynapsesDeviceProperties allEdgesDeviceProps; - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); + HANDLE_ERROR(cudaMemcpy(&allEdgesDeviceProps, allEdgesDevice, sizeof(AllSTDPSynapsesDeviceProperties), cudaMemcpyDeviceToHost)); copyDeviceToHost(allEdgesDeviceProps); diff --git a/Simulator/Edges/Neuro/AllSpikingSynapses.h b/Simulator/Edges/Neuro/AllSpikingSynapses.h index 534c86220..0cd04821b 100644 --- a/Simulator/Edges/Neuro/AllSpikingSynapses.h +++ b/Simulator/Edges/Neuro/AllSpikingSynapses.h @@ -122,7 +122,9 @@ class AllSpikingSynapses : public AllNeuroEdges { public: /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. - virtual void allocEdgeDeviceStruct() override; + /// + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void allocEdgeDeviceStruct(void **allEdgesDevice) override; /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. @@ -135,11 +137,13 @@ class AllSpikingSynapses : public AllNeuroEdges { /// Delete GPU memories. /// - virtual void deleteEdgeDeviceStruct() override; + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void deleteEdgeDeviceStruct(void *allEdgesDevice) override; /// Copy all synapses' data from host to device. /// - virtual void copyEdgeHostToDevice() override; + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void copyEdgeHostToDevice(void *allEdgesDevice) override; /// Copy all synapses' data from host to device. /// @@ -151,7 +155,8 @@ class AllSpikingSynapses : public AllNeuroEdges { /// Copy all synapses' data from device to host. /// - virtual void copyEdgeDeviceToHost() override; + /// @param allEdgesDevice GPU address of the allEdges struct on device memory. + virtual void copyEdgeDeviceToHost(void *allEdgesDevice) override; /// Get edge_counts in AllNeuroEdges struct on device memory. /// @@ -339,4 +344,4 @@ template void AllSpikingSynapses::serialize(Archive &archive) cereal::make_nvp("delay_EE", delay_EE_), cereal::make_nvp("totalDelay", totalDelay_), cereal::make_nvp("delayQueue", delayQueue_), cereal::make_nvp("delayIndex", delayIndex_), cereal::make_nvp("delayQueueLength", delayQueueLength_)); -} \ No newline at end of file +} diff --git a/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp b/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp index a6954cf82..011c3ef9f 100644 --- a/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp +++ b/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp @@ -10,8 +10,6 @@ #include "AllSpikingSynapses.h" #include "AllSynapsesDeviceFuncs.h" #include "Book.h" -#include "GPUModel.h" -#include "Simulator.h" #include /// CUDA code for advancing spiking synapses. @@ -30,10 +28,11 @@ __global__ void advanceSpikingSynapsesDevice(int totalSynapseCount, /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. -void AllSpikingSynapses::allocEdgeDeviceStruct() +/// +/// @param allEdgesDevice GPU address of the AllSpikingSynapsesDeviceProperties struct +/// on device memory. +void AllSpikingSynapses::allocEdgeDeviceStruct(void **allEdgesDevice) { - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void **allEdgesDevice = reinterpret_cast(&(gpuModel->getAllEdgesDevice())); allocEdgeDeviceStruct(allEdgesDevice, Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getMaxEdgesPerVertex()); } @@ -90,11 +89,11 @@ void AllSpikingSynapses::allocDeviceStruct(AllSpikingSynapsesDeviceProperties &a /// Delete GPU memories. /// -void AllSpikingSynapses::deleteEdgeDeviceStruct() +/// @param allEdgesDevice GPU address of the AllSpikingSynapsesDeviceProperties struct +/// on device memory. +void AllSpikingSynapses::deleteEdgeDeviceStruct(void *allEdgesDevice) { AllSpikingSynapsesDeviceProperties allEdges; - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); HANDLE_ERROR(cudaMemcpy(&allEdges, allEdgesDevice, sizeof(AllSpikingSynapsesDeviceProperties), cudaMemcpyDeviceToHost)); deleteDeviceStruct(allEdges); @@ -128,10 +127,10 @@ void AllSpikingSynapses::deleteDeviceStruct(AllSpikingSynapsesDeviceProperties & /// Copy all synapses' data from host to device. /// -void AllSpikingSynapses::copyEdgeHostToDevice() +/// @param allEdgesDevice GPU address of the AllSpikingSynapsesDeviceProperties struct +/// on device memory. +void AllSpikingSynapses::copyEdgeHostToDevice(void *allEdgesDevice) { // copy everything necessary - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); copyEdgeHostToDevice(allEdgesDevice, Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getMaxEdgesPerVertex()); } @@ -201,12 +200,12 @@ void AllSpikingSynapses::copyHostToDevice(void *allEdgesDevice, /// Copy all synapses' data from device to host. /// -void AllSpikingSynapses::copyEdgeDeviceToHost() +/// @param allEdgesDevice GPU address of the AllSpikingSynapsesDeviceProperties struct +/// on device memory. +void AllSpikingSynapses::copyEdgeDeviceToHost(void *allEdgesDevice) { // copy everything necessary AllSpikingSynapsesDeviceProperties allEdges; - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); HANDLE_ERROR(cudaMemcpy(&allEdges, allEdgesDevice, sizeof(AllSpikingSynapsesDeviceProperties), cudaMemcpyDeviceToHost)); copyDeviceToHost(allEdges); diff --git a/Simulator/Utils/Global.cpp b/Simulator/Utils/Global.cpp index 8b6b0db96..9f6572a0d 100644 --- a/Simulator/Utils/Global.cpp +++ b/Simulator/Utils/Global.cpp @@ -53,6 +53,21 @@ string coordToString(int x, int y, int z) return ss.str(); } +// MODEL INDEPENDENT FUNCTION NMV-BEGIN { +string neuronTypeToString(vertexType t) +{ + switch (t) { + case vertexType::INH: + return "INH"; + case vertexType::EXC: + return "EXC"; + default: + cerr << "ERROR->neuronTypeToString() failed, unknown type: " << t << endl; + assert(false); + return nullptr; // Must return a value -- this will probably cascade to another failure + } +} +// } NMV-END #if defined(USE_GPU) //! CUDA device ID int g_deviceId = 0; diff --git a/Simulator/Utils/Global.h b/Simulator/Utils/Global.h index db1678432..c44532140 100644 --- a/Simulator/Utils/Global.h +++ b/Simulator/Utils/Global.h @@ -91,6 +91,38 @@ extern uint64_t g_simulationStep; const int g_nMaxChunkSize = 100; +// NETWORK MODEL VARIABLES NMV-BEGIN { +// Vertex types. +// NEURO: +// INH - Inhibitory neuron +// EXC - Excitory neuron +// NG911: +// CALR: Caller radii +// PSAP: PSAP nodes +// EMS, FIRE, LAW: Responder nodes +/* +// Moved to Utils/VertexType.h +enum class vertexType { + // Neuro + INH = 1, + EXC = 2, + // NG911 + CALR = 3, + PSAP = 4, + EMS = 5, + FIRE = 6, + LAW = 7, + // UNDEF + VTYPE_UNDEF = 0 +}; +// Custom streaming operator<< for the enum class vertexType +inline std::ostream &operator<<(std::ostream &os, vertexType vT) +{ + os << static_cast(vT); + return os; +} +*/ + // Edge types. // NEURO: // II - Synapse from inhibitory neuron to inhibitory neuron. @@ -163,6 +195,8 @@ string index2dToString(int i, int width, int height); string coordToString(int x, int y); // Converts a 3-d coordinate into a string. string coordToString(int x, int y, int z); +// Converts a vertexType into a string. +string neuronTypeToString(vertexType t); template ostream &operator<<(ostream &os, const vector &v) { diff --git a/Simulator/Vertices/AllVertices.cpp b/Simulator/Vertices/AllVertices.cpp index 55b2ba160..6d804e2d5 100644 --- a/Simulator/Vertices/AllVertices.cpp +++ b/Simulator/Vertices/AllVertices.cpp @@ -9,23 +9,6 @@ #include "AllVertices.h" #include "OperationManager.h" -// Utility function to convert a vertexType into a string. -// MODEL INDEPENDENT FUNCTION NMV-BEGIN { -string vertexTypeToString(vertexType t) -{ - switch (t) { - case vertexType::INH: - return "INH"; - case vertexType::EXC: - return "EXC"; - default: - cerr << "ERROR->vertexTypeToString() failed, unknown type: " << t << endl; - assert(false); - return nullptr; // Must return a value -- this will probably cascade to another failure - } -} -// } NMV-END - // Default constructor AllVertices::AllVertices() : size_(0) { @@ -39,30 +22,6 @@ AllVertices::AllVertices() : size_(0) OperationManager::getInstance().registerOperation(Operations::printParameters, printParametersFunc); - // Register registerHistoryVariables function as a registerHistoryVariables operation in the OperationManager - function registerHistory = bind(&AllVertices::registerHistoryVariables, this); - OperationManager::getInstance().registerOperation(Operations::registerHistoryVariables, - registerHistory); - -#if defined(USE_GPU) - // Register allocNeuronDeviceStruct function as a allocateGPU operation in the OperationManager - function allocateGPU = bind(&AllVertices::allocVerticesDeviceStruct, this); - OperationManager::getInstance().registerOperation(Operations::allocateGPU, allocateGPU); - - // Register AllVertices::copyToDevice function as a copyToGPU operation in the OperationManager - function copyCPUtoGPU = bind(&AllVertices::copyToDevice, this); - OperationManager::getInstance().registerOperation(Operations::copyToGPU, copyCPUtoGPU); - - // Register copyFromGPU operation for transferring edge data from device to host - function copyFromGPU = bind(&AllVertices::copyFromDevice, this); - OperationManager::getInstance().registerOperation(Operations::copyFromGPU, copyFromGPU); - - // Register deleteNeuronDeviceStruct function as a deallocateGPUMemory operation in the OperationManager - function deallocateGPUMemory = bind(&AllVertices::deleteVerticesDeviceStruct, this); - OperationManager::getInstance().registerOperation(Operations::deallocateGPUMemory, - deallocateGPUMemory); -#endif - // Get a copy of the file and vertex logger to use log4cplus macros to print to debug files fileLogger_ = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("file")); vertexLogger_ = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("vertex")); diff --git a/Simulator/Vertices/AllVertices.h b/Simulator/Vertices/AllVertices.h index 7182adfa7..919bd8dce 100644 --- a/Simulator/Vertices/AllVertices.h +++ b/Simulator/Vertices/AllVertices.h @@ -36,9 +36,6 @@ using namespace std; #include #endif -// Utility function to convert a vertexType into a string. -string vertexTypeToString(vertexType t); - class Layout; class AllEdges; struct AllVerticesDeviceProperties; @@ -113,11 +110,14 @@ class AllVertices { /// Allocate GPU memories to store all vertices' states, /// and copy them from host to GPU memory. - virtual void allocVerticesDeviceStruct() = 0; + /// + /// @param allVerticesDevice GPU address of the allVertices struct on device memory. + virtual void allocVerticesDeviceStruct(void **allVerticesDevice) = 0; /// Delete GPU memories. /// - virtual void deleteVerticesDeviceStruct() = 0; + /// @param allVerticesDevice GPU address of the allVertices struct on device memory. + virtual void deleteVerticesDeviceStruct(void *allVerticesDevice) = 0; /// Clear the spike counts out of all vertices. // @@ -125,11 +125,14 @@ class AllVertices { virtual void clearVertexHistory(void *allVerticesDevice) = 0; /// Copy all vertices' data from host to device. - virtual void copyToDevice() = 0; + /// + /// @param allVerticesDevice GPU address of the allVertices struct on device memory. + virtual void copyToDevice(void *allVerticesDevice) = 0; /// Copy all vertices' data from device to host. /// - virtual void copyFromDevice() = 0; + /// @param allVerticesDevice GPU address of the allVertices struct on device memory. + virtual void copyFromDevice(void *allVerticesDevice) = 0; /// Update the state of all vertices for a time step /// Notify outgoing edges if vertex has fired. @@ -181,4 +184,4 @@ struct AllVerticesDeviceProperties {}; template void AllVertices::serialize(Archive &archive) { archive(cereal::make_nvp("size", size_)); -} \ No newline at end of file +} diff --git a/Simulator/Vertices/NG911/All911Vertices.h b/Simulator/Vertices/NG911/All911Vertices.h index b41f42e4a..1a537437a 100644 --- a/Simulator/Vertices/NG911/All911Vertices.h +++ b/Simulator/Vertices/NG911/All911Vertices.h @@ -225,10 +225,10 @@ class All911Vertices : public AllVertices { // GPU functionality for 911 simulation is unimplemented. // These signatures are required to make the class non-abstract public: - virtual void allocVerticesDeviceStruct() {}; - virtual void deleteVerticesDeviceStruct() {}; - virtual void copyToDevice() {}; - virtual void copyFromDevice() {}; + virtual void allocVerticesDeviceStruct(void **allVerticesDevice) {}; + virtual void deleteVerticesDeviceStruct(void *allVerticesDevice) {}; + virtual void copyToDevice(void *allVerticesDevice) {}; + virtual void copyFromDevice(void *allVerticesDevice) {}; virtual void advanceVertices(AllEdges &edges, void *allVerticesDevice, void *allEdgesDevice, float randNoise[], EdgeIndexMapDevice *edgeIndexMapDevice) {}; virtual void setAdvanceVerticesDeviceParams(AllEdges &edges) {}; @@ -260,4 +260,4 @@ class All911Vertices : public AllVertices { protected: #endif // defined(USE_GPU) -}; \ No newline at end of file +}; diff --git a/Simulator/Vertices/Neuro/AllIFNeurons.h b/Simulator/Vertices/Neuro/AllIFNeurons.h index bbe06c177..99f3b8140 100644 --- a/Simulator/Vertices/Neuro/AllIFNeurons.h +++ b/Simulator/Vertices/Neuro/AllIFNeurons.h @@ -95,20 +95,25 @@ class AllIFNeurons : public AllSpikingNeurons { /// Allocate GPU memories to store all neurons' states, /// and copy them from host to GPU memory. - virtual void allocVerticesDeviceStruct(); + /// + /// @param allVerticesDevice GPU address of the allNeurons struct on device memory. + virtual void allocVerticesDeviceStruct(void **allVerticesDevice); /// Delete GPU memories. /// - virtual void deleteVerticesDeviceStruct(); + /// @param allVerticesDevice GPU address of the allNeurons struct on device memory. + virtual void deleteVerticesDeviceStruct(void *allVerticesDevice); /// Clear the spike counts out of all neurons. // /// @param allVerticesDevice GPU address of the allNeurons struct on device memory. virtual void clearVertexHistory(void *allVerticesDevice) override; //Copy all neurons' data from device to host. - virtual void copyFromDevice() override; + //@param allVerticesDevice GPU address of the allNeurons struct on device memory. + virtual void copyFromDevice(void *deviceAddress) override; //Copy all neurons' data from host to device. - virtual void copyToDevice() override; + // @param allVerticesDevice GPU address of the allNeurons struct on device memory. + virtual void copyToDevice(void *deviceAddress) override; protected: /// Allocate GPU memories to store all neurons' states. @@ -267,4 +272,4 @@ template void AllIFNeurons::serialize(Archive &archive) cereal::make_nvp("Tau", Tau_.getHostVector())); //Private variables are intentionally excluded from serialization as they are populated from configuration files. -} \ No newline at end of file +} diff --git a/Simulator/Vertices/Neuro/AllIFNeurons_d.cpp b/Simulator/Vertices/Neuro/AllIFNeurons_d.cpp index f770d4c78..dd4bc2402 100644 --- a/Simulator/Vertices/Neuro/AllIFNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllIFNeurons_d.cpp @@ -9,18 +9,14 @@ #include "AllIFNeurons.h" #include "Book.h" #include "DeviceVector.h" -#include "GPUModel.h" -#include "Simulator.h" /// Allocate GPU memories to store all neurons' states, /// and copy them from host to GPU memory. /// /// @param allVerticesDevice GPU address of the AllIFNeuronsDeviceProperties struct on device memory. -void AllIFNeurons::allocVerticesDeviceStruct() +void AllIFNeurons::allocVerticesDeviceStruct(void **allVerticesDevice) { AllIFNeuronsDeviceProperties allNeurons; - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void **allVerticesDevice = reinterpret_cast(&(gpuModel->getAllVerticesDevice())); allocDeviceStruct(allNeurons); HANDLE_ERROR(cudaMalloc(allVerticesDevice, sizeof(AllIFNeuronsDeviceProperties))); HANDLE_ERROR(cudaMemcpy(*allVerticesDevice, &allNeurons, sizeof(AllIFNeuronsDeviceProperties), @@ -77,11 +73,10 @@ void AllIFNeurons::allocDeviceStruct(AllIFNeuronsDeviceProperties &allVerticesDe /// Delete GPU memories. /// -void AllIFNeurons::deleteVerticesDeviceStruct() +/// @param allVerticesDevice GPU address of the AllVerticesDeviceProperties struct on device memory. +void AllIFNeurons::deleteVerticesDeviceStruct(void *allVerticesDevice) { AllIFNeuronsDeviceProperties allVerticesDeviceProps; - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void *allVerticesDevice = static_cast(gpuModel->getAllVerticesDevice()); HANDLE_ERROR(cudaMemcpy(&allVerticesDeviceProps, allVerticesDevice, sizeof(AllIFNeuronsDeviceProperties), cudaMemcpyDeviceToHost)); deleteDeviceStruct(allVerticesDeviceProps); @@ -130,7 +125,9 @@ void AllIFNeurons::deleteDeviceStruct(AllIFNeuronsDeviceProperties &allVerticesD } /// Copy all neurons' data from host to device. -void AllIFNeurons::copyToDevice() +/// +/// @param allVerticesDevice GPU address of the AllIFNeuronsDeviceProperties struct on device memory. +void AllIFNeurons::copyToDevice(void *allVerticesDevice) { C1_.copyToDevice(); C2_.copyToDevice(); @@ -149,14 +146,15 @@ void AllIFNeurons::copyToDevice() Vreset_.copyToDevice(); numStepsInRefractoryPeriod_.copyToDevice(); - AllSpikingNeurons::copyToDevice(); + AllSpikingNeurons::copyToDevice(allVerticesDevice); } /// Copy all neurons' data from device to host. /// -void AllIFNeurons::copyFromDevice() +/// @param allVerticesDevice GPU address of the AllIFNeuronsDeviceProperties struct on device memory. +void AllIFNeurons::copyFromDevice(void *allVerticesDevice) { - AllSpikingNeurons::copyFromDevice(); + AllSpikingNeurons::copyFromDevice(allVerticesDevice); C1_.copyToHost(); C2_.copyToHost(); @@ -203,4 +201,4 @@ void AllIFNeurons::advanceVertices(AllEdges &synapses, void *allVerticesDevice, void *allEdgesDevice, float randNoise[], EdgeIndexMapDevice *edgeIndexMapDevice) { -} \ No newline at end of file +} diff --git a/Simulator/Vertices/Neuro/AllIZHNeurons.h b/Simulator/Vertices/Neuro/AllIZHNeurons.h index 09d69040a..349df0ee2 100644 --- a/Simulator/Vertices/Neuro/AllIZHNeurons.h +++ b/Simulator/Vertices/Neuro/AllIZHNeurons.h @@ -145,11 +145,14 @@ class AllIZHNeurons : public AllIFNeurons { /// Allocate GPU memories to store all neurons' states, /// and copy them from host to GPU memory. - virtual void allocVerticesDeviceStruct() override; + // + /// @param allVerticesDevice GPU address of the allNeurons struct on device memory. + virtual void allocVerticesDeviceStruct(void **allVerticesDevice) override; /// Delete GPU memories. // - virtual void deleteVerticesDeviceStruct() override; + /// @param allVerticesDevice GPU address of the allNeurons struct on device memory. + virtual void deleteVerticesDeviceStruct(void *allVerticesDevice) override; /// Copy spike history data stored in device memory to host. // @@ -168,10 +171,13 @@ class AllIZHNeurons : public AllIFNeurons { // Copy all neurons' data from device to host. // - virtual void copyFromDevice() override; + /// @param allVerticesDevice GPU address of the allNeurons struct on device memory. + virtual void copyFromDevice(void *deviceAddress) override; // Copy all neurons' data from host to device. - virtual void copyToDevice() override; + // + /// @param allVerticesDevice GPU address of the allNeurons struct on device memory. + virtual void copyToDevice(void *deviceAddress) override; protected: @@ -314,4 +320,4 @@ template void AllIZHNeurons::serialize(Archive &archive) cereal::make_nvp("u", u_.getHostVector()), cereal::make_nvp("C3", C3_.getHostVector())); //Private variables are intentionally excluded from serialization as they are populated from configuration files. -} \ No newline at end of file +} diff --git a/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp b/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp index f5325c54c..c3b7fa622 100644 --- a/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp @@ -11,8 +11,7 @@ #include "AllVerticesDeviceFuncs.h" #include "Book.h" #include "DeviceVector.h" -#include "GPUModel.h" -#include "Simulator.h" + /// CUDA code for advancing izhikevich neurons /// @@ -40,11 +39,12 @@ __global__ void advanceIZHNeuronsDevice( /// Allocate GPU memories to store all neurons' states, /// and copy them from host to GPU memory. /// -void AllIZHNeurons::allocVerticesDeviceStruct() +/// @param allVerticesDevice GPU address of the AllIZHNeuronsDeviceProperties struct +/// on device memory. +void AllIZHNeurons::allocVerticesDeviceStruct(void **allVerticesDevice) { AllIZHNeuronsDeviceProperties allVerticesDeviceProps; - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void **allVerticesDevice = reinterpret_cast(&(gpuModel->getAllVerticesDevice())); + allocDeviceStruct(allVerticesDeviceProps); HANDLE_ERROR(cudaMalloc(allVerticesDevice, sizeof(AllIZHNeuronsDeviceProperties))); @@ -70,11 +70,12 @@ void AllIZHNeurons::allocDeviceStruct(AllIZHNeuronsDeviceProperties &allVertices /// Delete GPU memories. /// -void AllIZHNeurons::deleteVerticesDeviceStruct() +/// @param allVerticesDevice GPU address of the AllVerticesDeviceProperties struct +/// on device memory. +void AllIZHNeurons::deleteVerticesDeviceStruct(void *allVerticesDevice) { AllIZHNeuronsDeviceProperties allVerticesDeviceProps; - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void *allVerticesDevice = static_cast(gpuModel->getAllVerticesDevice()); + HANDLE_ERROR(cudaMemcpy(&allVerticesDeviceProps, allVerticesDevice, sizeof(AllIZHNeuronsDeviceProperties), cudaMemcpyDeviceToHost)); @@ -101,9 +102,11 @@ void AllIZHNeurons::deleteDeviceStruct(AllIZHNeuronsDeviceProperties &allVertice /// Copy all neurons' data from host to device. /// -void AllIZHNeurons::copyToDevice() +/// @param allVerticesDevice GPU address of the AllIZHNeuronsDeviceProperties struct +/// on device memory. +void AllIZHNeurons::copyToDevice(void *allVerticesDevice) { - AllIFNeurons::copyToDevice(); + AllIFNeurons::copyToDevice(allVerticesDevice); Aconst_.copyToDevice(); Bconst_.copyToDevice(); @@ -115,9 +118,11 @@ void AllIZHNeurons::copyToDevice() /// Copy all neurons' data from device to host. /// -void AllIZHNeurons::copyFromDevice() +/// @param allVerticesDevice GPU address of the AllIZHNeuronsDeviceProperties struct +/// on device memory. +void AllIZHNeurons::copyFromDevice(void *allVerticesDevice) { - AllIFNeurons::copyFromDevice(); + AllIFNeurons::copyFromDevice(allVerticesDevice); Aconst_.copyToHost(); Bconst_.copyToHost(); @@ -296,4 +301,4 @@ __global__ void advanceIZHNeuronsDevice( // clear synaptic input for next time step sp = 0; } -///@} \ No newline at end of file +///@} diff --git a/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp b/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp index 62648ef16..bf46fbacc 100644 --- a/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp @@ -187,4 +187,4 @@ __global__ void advanceLIFNeuronsDevice( #endif // clear synaptic input for next time step sp = 0; -} \ No newline at end of file +} diff --git a/Simulator/Vertices/Neuro/AllSpikingNeurons.h b/Simulator/Vertices/Neuro/AllSpikingNeurons.h index 394f7091d..1d1b35726 100644 --- a/Simulator/Vertices/Neuro/AllSpikingNeurons.h +++ b/Simulator/Vertices/Neuro/AllSpikingNeurons.h @@ -63,8 +63,6 @@ class AllSpikingNeurons : public AllVertices { /// @param synapses Reference to the allEdges struct on host memory. virtual void setAdvanceVerticesDeviceParams(AllEdges &synapses); - virtual void copyFromDevice() override; - virtual void copyToDevice() override; /// Add psr of all incoming synapses to summation points. /// /// @param allVerticesDevice GPU address of the allVertices struct on device memory. @@ -73,6 +71,9 @@ class AllSpikingNeurons : public AllVertices { virtual void integrateVertexInputs(void *allVerticesDevice, EdgeIndexMapDevice *edgeIndexMapDevice, void *allEdgesDevice); + virtual void copyFromDevice(void *deviceAddress) override; + virtual void copyToDevice(void *deviceAddress) override; + protected: /// Clear the spike counts out of all neurons in device memory. /// (helper function of clearNeuronSpikeCounts) @@ -166,4 +167,4 @@ template void AllSpikingNeurons::serialize(Archive &archive) cereal::make_nvp("vertexEvents", vertexEvents_), cereal::make_nvp("summationPoints", summationPoints_.getHostVector()), cereal::make_nvp("fAllowBackPropagation", fAllowBackPropagation_)); -} \ No newline at end of file +} diff --git a/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp b/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp index 9b0af580e..c0a2f504d 100644 --- a/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp @@ -10,8 +10,6 @@ #include "AllSpikingSynapses.h" #include "Book.h" #include "DeviceVector.h" -#include "GPUModel.h" -#include "Simulator.h" /// CUDA kernel for adding psr of all incoming synapses to summation points. /// @@ -34,11 +32,9 @@ __global__ void calcSummationPointDevice(int totalVertices, BGFLOAT *summationPo EdgeIndexMapDevice *edgeIndexMapDevice, AllSpikingSynapsesDeviceProperties *allEdgesDevice); -void AllSpikingNeurons::copyToDevice() +void AllSpikingNeurons::copyToDevice(void *deviceAddress) { AllSpikingNeuronsDeviceProperties allVerticesDevice; - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void *deviceAddress = static_cast(gpuModel->getAllVerticesDevice()); HANDLE_ERROR(cudaMemcpy(&allVerticesDevice, deviceAddress, sizeof(AllSpikingNeuronsDeviceProperties), cudaMemcpyDeviceToHost)); @@ -89,10 +85,8 @@ void AllSpikingNeurons::copyToDevice() maxSpikes * sizeof(uint64_t), cudaMemcpyHostToDevice)); } } -void AllSpikingNeurons::copyFromDevice() +void AllSpikingNeurons::copyFromDevice(void *deviceAddress) { - GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); - void *deviceAddress = static_cast(gpuModel->getAllVerticesDevice()); int numVertices = Simulator::getInstance().getTotalVertices(); AllSpikingNeuronsDeviceProperties allVerticesDevice; @@ -254,4 +248,4 @@ __global__ void calcSummationPointDevice(int totalVertices, BGFLOAT *summationPo // Store summed PSR into this neuron's summation point summationPoints_[idx] = sum; } -} \ No newline at end of file +} diff --git a/docs/Developer/GHActions.md b/docs/Developer/GHActions.md index c60593fed..3c41a8c83 100644 --- a/docs/Developer/GHActions.md +++ b/docs/Developer/GHActions.md @@ -12,10 +12,5 @@ The manual GitHub Pages action is a feature that came from wanting to quickly pu ## PlantUML Action plantUML.yml -The plantUML action occurs anytime a plantUML file is modified or added during a pull request or a push to the master branch. These .puml files are supposed to be located in the UML folder within the Developer folder. This action starts by checking out the repository using [actions/checkout](https://github.com/actions/checkout) with a fetch depth of 0. The next step is to grab all of the .puml files that need to be turned into images. This is done by using a basic bash command to grab all .puml files which is then piped into an awk script to parse out the unnecessary files and construct an output string with all the necessary files. The output string will look like so: "file1.puml file2.puml file3.puml file4.puml\n". This output string is then confirmed by an echo command which prints out the string to the actions terminal. Next, the .png and .svg files are generated from the .puml files in the output string using a fork of [holowinski/plantuml-github-action]. These files are placed within the diagrams folder located within the UML folder. Lastly, the local changes are committed then pushed to the remote repository using [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action). +The plantUML action occurs anytime a plantUML file is modified or added during a pull request or a push to the master branch. These .puml files are supposed to be located in the UML folder within the Developer folder. This action starts by checking out the repository using [actions/checkout](https://github.com/actions/checkout) with a fetch depth of 0. The next step is to grab all of the .puml files that need to be turned into images. This is done by using a basic bash command to grab all .puml files which is then piped into an awk script to parse out the unnecessary files and construct an output string with all the necessary files. The output string will look like so: "file1.puml file2.puml file3.puml file4.puml\n". This output string is then confirmed by an echo command which prints out the string to the actions terminal. Next, the .png and .svg files are generated from the .puml files in the output string using [cloudbees/plantuml-github-action](https://github.com/cloudbees/plantuml-github-action). These files are placed within the diagrams folder located within the UML folder. Lastly, the local changes are committed then pushed to the remote repository using [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action). - -[//]: # (Moving URL links to the bottom of the document for ease of updating - LS) -[//]: # (Links to repo items which exist outside of the docs folder need an absolute link.) - -[holowinski/plantuml-github-action]: diff --git a/docs/Developer/index.md b/docs/Developer/index.md index a48dc6c7f..70ff35500 100644 --- a/docs/Developer/index.md +++ b/docs/Developer/index.md @@ -2,7 +2,7 @@ If you're developing Graphitti code, then here are your reference documents. -Writing new code? Then make sure to follow our [contributing guide] and *document your code here*. +Writing new code? Then make sure to follow our [contributing guide](../../CONTRIBUTING.md) and *document your code here*. Reading code that isn't obvious? When you figure out how it works, then *document it here* and *document it in comments in the code.* @@ -38,7 +38,7 @@ Students, use this [quickstart guide](StudentSetup.md) to help setup, use, and d - Doxygen - Documentation generated from source code - Doxygen provides web-based indices and hierarchical views of Graphitti's class and file structures - - [Visit Doxygen Generated Documentation] + - [Visit Doxygen Generated Documentation](https://uwb-biocomputing.github.io/Graphitti/Doxygen/html/index.html) - Document code in the `.h` file using the [Doxygen Style Guide](../Doxygen/DoxygenStyleGuide.md) format - [Doxygen Update Guide](../Doxygen/DoxygenUpdateGuide.md) - [Event buffering](eventBuffering.md) in vertex classes. @@ -50,10 +50,3 @@ Students, use this [quickstart guide](StudentSetup.md) to help setup, use, and d --------- [<< Go back to the Graphitti home page](../index.md) - -[//]: # (Moving URL links to the bottom of the document for ease of updating - LS) -[//]: # (Links to repo items which exist outside of the docs folder need an absolute link.) - -[contributing guide]: -[Visit Doxygen Generated Documentation]: - \ No newline at end of file diff --git a/docs/Doxygen/Doxyfile b/docs/Doxygen/Doxyfile index fc041908f..1d6498f5a 100644 --- a/docs/Doxygen/Doxyfile +++ b/docs/Doxygen/Doxyfile @@ -864,7 +864,7 @@ WARN_LOGFILE = # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = Simulator/ docs/Doxygen/index.md +INPUT = Simulator/ # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses diff --git a/docs/Doxygen/index.md b/docs/Doxygen/index.md deleted file mode 100644 index 4930a38c9..000000000 --- a/docs/Doxygen/index.md +++ /dev/null @@ -1,19 +0,0 @@ -# Graphitti Doxygen Documentation - -Welcome to the Graphitti Doxygen documentation! - -This documentation provides an overview of the Graphitti project's codebase and related resources. It includes detailed information about classes, functions, and structures. - -## Documentation Sections - -- [Classes](classes.md): Explore the detailed code reference documentation for Graphitti's classes, structs, unions and interfaces. -- [Files](files.md): Here is the documentation for Graphitti's documented files with descriptions and documentation. - -Feel free to navigate through the documentation using the top navigation bar. If you have any questions or need further assistance, please refer to the project's official repository or documentation. - -## Additional Resources - -- [Graphitti GitHub Repository](https://github.com/UWB-Biocomputing/Graphitti): Explore the source code, report issues, and contribute to the project. -- [Graphitti Project Website](https://uwb-biocomputing.github.io/Graphitti/): Visit the official website for more information about the Graphitti project. - -We hope you find this documentation useful and informative. Happy coding! \ No newline at end of file diff --git a/docs/Notes/Glossary.md b/docs/Glossary.md similarity index 100% rename from docs/Notes/Glossary.md rename to docs/Glossary.md diff --git a/docs/Notes/index.md b/docs/Notes/index.md deleted file mode 100644 index 6a088a1c0..000000000 --- a/docs/Notes/index.md +++ /dev/null @@ -1,32 +0,0 @@ -# 4. Glossary & Notes - -General notes for various parts of the Graphitti system. - -## 4.1 General Notes - - [General Notes](GeneralNotes.md) - - [Layout Notes](LayoutsNotes.md) - - [Connections Notes](ConnectionsNotes.md) - - [Neuron Notes](NeuronsNotes.md) - - [Synapses Notes](SynapsesNotes.md) - - [Recorder Notes](RecordersNotes.md) - -## 4.2 Glossary - - [Glossary](Glossary.md) - -## 4.3 Useful Resources - - [Recommended resources](Resources.md) to browse - -## Tools - -Here is documentation on the [GIS to GEXF](Tools/GIStoGraph.md) tool. This tool reads in Geographic Information Systems data, constructs a graph based on that data, and produces GEXF and GraphML formatted XML files that we can then pass into the Emergency Services Communication Systems simulation. - ---------- -[<< Go back to Graphitti home page](../index.md) \ No newline at end of file diff --git a/docs/Notes/ConnectionsNotes.md b/docs/RebuildNotes/ConnectionsNotes.md similarity index 100% rename from docs/Notes/ConnectionsNotes.md rename to docs/RebuildNotes/ConnectionsNotes.md diff --git a/docs/Notes/GeneralNotes.md b/docs/RebuildNotes/GeneralNotes.md similarity index 100% rename from docs/Notes/GeneralNotes.md rename to docs/RebuildNotes/GeneralNotes.md diff --git a/docs/Notes/LayoutsNotes.md b/docs/RebuildNotes/LayoutsNotes.md similarity index 100% rename from docs/Notes/LayoutsNotes.md rename to docs/RebuildNotes/LayoutsNotes.md diff --git a/docs/Notes/NeuronsNotes.md b/docs/RebuildNotes/NeuronsNotes.md similarity index 100% rename from docs/Notes/NeuronsNotes.md rename to docs/RebuildNotes/NeuronsNotes.md diff --git a/docs/Notes/RecordersNotes.md b/docs/RebuildNotes/RecordersNotes.md similarity index 100% rename from docs/Notes/RecordersNotes.md rename to docs/RebuildNotes/RecordersNotes.md diff --git a/docs/Notes/SynapsesNotes.md b/docs/RebuildNotes/SynapsesNotes.md similarity index 100% rename from docs/Notes/SynapsesNotes.md rename to docs/RebuildNotes/SynapsesNotes.md diff --git a/docs/Notes/Resources.md b/docs/Resources.md similarity index 100% rename from docs/Notes/Resources.md rename to docs/Resources.md diff --git a/docs/Testing/index.md b/docs/Testing/index.md index 19b553166..4903a29c7 100644 --- a/docs/Testing/index.md +++ b/docs/Testing/index.md @@ -4,14 +4,14 @@ Information on test config files for regression testing, and testing that has be ## 3.1 Unit Tests -We use [GoogleTest](../Developer/GoogleTestsTutorial.md) to develop our unit tests. +We use [GoogleTest](GoogleTestsTutorial.md) to develop our unit tests. To integrate your unit tests using GoogleTest in Graphitti you can follow these steps: 1. Open the CMakeLists.txt file in the root directory of Graphitti 2. Locate at the bottom of the file where the `tests` executable is defined and add your test file to the list of source files. 3. Build and run your tests using the Graphitti build system and use `./tests` to run the unit tests. -Please note that Graphitti follows the [singleton design pattern], and several of its classes, such as Simulator, ParameterManager, OperationManager, and GraphManager, are implemented as singletons. If your test scenario requires the instantiation of these classes, it may be necessary to create a separate executable specifically for your tests. +Please note that Graphitti follows the [singleton design pattern](https://en.wikipedia.org/wiki/Singleton_pattern), and several of its classes, such as Simulator, ParameterManager, OperationManager, and GraphManager, are implemented as singletons. If your test scenario requires the instantiation of these classes, it may be necessary to create a separate executable specifically for your tests. By creating a separate executable, you can ensure that the singleton instances used in the test environment are isolated from the main application's singleton instances. This approach helps maintain the desired behavior and avoid segmentation fault errors. @@ -77,8 +77,3 @@ generated during the CPU execution, causing the result files to be different to --------- [<< Go back to the Graphitti home page](../index.md) - -[//]: # (Moving URL links to the bottom of the document for ease of updating - LS) -[//]: # (Links to repo items which exist outside of the docs folder need an absolute link.) - -[singleton design pattern]: \ No newline at end of file diff --git a/docs/User/installation.md b/docs/User/installation.md index d995550df..9870085c1 100644 --- a/docs/User/installation.md +++ b/docs/User/installation.md @@ -1,9 +1,5 @@ # 1.2 Installation -For student installation and quickstart, see [student quickstart]. - -For all others, see below. - ## 1.2.1 Necessary Hardware/Software Graphitti is designed to be easy to use and fast to simulate with, but given its scope and flexibility, there are some tradeoffs. @@ -11,9 +7,9 @@ Graphitti is designed to be easy to use and fast to simulate with, but given its First, and perhaps most importantly, for the speedups that we desire, we found that **CUDA** was the most reasonable way to go. Hence,  if you want to use Graphitti for migrating your model to GPUs, you will need the following: - **Linux**: Currently, Graphitti only works on Linux. Any distro that supports **GNU-Make** and your chosen NVIDIA graphics card (if going the GPU route) should work. Make sure you have these packages: -- **NVIDIA GPU**: If you want your simulator to run on GPUs, you must use an NVIDIA GPU that is CUDA capable. Check NVIDIA's website for an up-to-date [list] of CUDA-compliant devices. -- [**CUDA**]: if you intend to use the GPU functionality for high performance. Graphitti has been tested running on CUDA Version 8.0.44. -- [HDF5]: HDF5 is a data model, library, and file format for storing and managing data. For example, Matlab has built-in functions that can easily manage, view, and analyze data in HDF5 format. To install HDF5, simply follow the website instructions. If you don't wish to use HDF5, you can use the XML format which is also supported. +- **NVIDIA GPU**: If you want your simulator to run on GPUs, you must use an NVIDIA GPU that is CUDA capable. Check NVIDIA's website for an up-to-date [list](https://developer.nvidia.com/cuda-gpus) of CUDA-compliant devices. +- [**CUDA**](https://developer.nvidia.com/cuda-downloads): if you intend to use the GPU functionality for high performance. Graphitti has been tested running on CUDA Version 8.0.44. +- [HDF5](https://support.hdfgroup.org/HDF5/): HDF5 is a data model, library, and file format for storing and managing data. For example, Matlab has built-in functions that can easily manage, view, and analyze data in HDF5 format. To install HDF5, simply follow the website instructions. If you don't wish to use HDF5, you can use the XML format which is also supported. To become a Graphitti user or collaborator, you might also need: @@ -23,11 +19,11 @@ To become a Graphitti user or collaborator, you might also need: Of course, Graphitti is totally open source. If you wanted, you could modify Graphitti and make an OpenCL version. ## 1.2.2 Download Graphitti -In order to get started with Graphitti, you will need to build it from scratch, which means getting its source codes. You can either download Graphitti source codes as a zip file of a stable release (See [the release page] or fork the development version from Graphitti GitHub repository (See [Fork and clone Graphitti](#1221-fork-and-clone-graphitti)). +In order to get started with Graphitti, you will need to build it from scratch, which means getting its source codes. You can either download Graphitti source codes as a zip file of a stable release (See [the release page](https://github.com/UWB-Biocomputing/Graphitti/releases)) or fork the development version from Graphitti GitHub repository (See [Fork and clone Graphitti](#1221-fork-and-clone-graphitti)). ### 1.2.2.1 Fork and clone Graphitti -If you are a Github user, you can simply fork and clone Graphitti. If you are new to Github, follow our Wiki page on [Contribute to Graphitti open source project]. You can also go over our [Git Crash Course] for some useful tips. +If you are a Github user, you can simply fork and clone Graphitti. If you are new to Github, follow our Wiki page on [Contribute to Graphitti open source project](https://github.com/UWB-Biocomputing/BrainGrid/wiki/Contribute-to-BrainGrid-open-source-project). You can also go over our [Git Crash Course](https://github.com/UWB-Biocomputing/BrainGrid/wiki/Git-Crash-Course) for some useful tips. ## 1.2.3 Install Graphitti -In order to compile and run Graphitti, you will need to set up a couple things in the [**CMakeLists.txt**] first. +In order to compile and run Graphitti, you will need to set up a couple things in the [**CMakeLists.txt**](https://github.com/UWB-Biocomputing/Graphitti/blob/master/CMakeLists.txt) first. 1. Change to Graphitti directory in your terminal @@ -43,10 +39,10 @@ In order to compile and run Graphitti, you will need to set up a couple things i - you might also need to add your CUDA home directory into the ```PATH``` environment variable 3. Graphitti is written in C++11 and CUDA C/C++. Make sure you have all these dependencies in order to compile Graphitti: - - [make] - - [g++] - - [h5c++]: compile script for HDF5 C++ programs - - [nvcc]: if you are using GPU for high performance, nvcc is the compiler by Nvidia for use with CUDA + - [make](https://www.gnu.org/software/make/) + - [g++](https://gcc.gnu.org/) + - [h5c++](https://support.hdfgroup.org/HDF5/Tutor/compile.html): compile script for HDF5 C++ programs + - [nvcc](http://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/#axzz4ftSRZe00): if you are using GPU for high performance, nvcc is the compiler by Nvidia for use with CUDA --------- [>> Next: 1.3 Quickstart](quickstart.md) @@ -55,20 +51,4 @@ In order to compile and run Graphitti, you will need to set up a couple things i [<< Go back to User Documentation page](index.md) --------- -[<< Go back to Graphitti home page](http://uwb-biocomputing.github.io/Graphitti/) - -[//]: # (Moving URL links to the bottom of the document for ease of updating - LS) -[//]: # (Links to repo items which exist outside of the docs folder need an absolute link.) - -[Contribute to Graphitti open source project]: -[**CMakeLists.txt**]: -[Git Crash Course]: -[the release page]: -[h5c++]: -[nvcc]: -[make]: -[g++]: -[**CUDA**]: -[HDF5]: -[list]: -[student quickstart]: (../Developer/StudentSetup.md) \ No newline at end of file +[<< Go back to Graphitti home page](http://uwb-biocomputing.github.io/Graphitti/) \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 3a4b860bf..3a720b891 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,37 +10,68 @@ 1.3 [Quickstart](User/quickstart.md) + 1.3.1 [UWB Student Quickstart](Developer/StudentSetup.md) + 1.4 [Configuration](User/configuration.md) 2. [Developer Documentation](Developer/index.md) - 2.1 [GitFlow Documentation](Developer/GitFlowDiagram.md) + 2.1 [Student Quick Start](Developer/StudentSetup.md) + + 2.2 [GitFlow Documentation](Developer/GitFlowDiagram.md) - 2.2 [Code Formatting Etiquettes](Developer/codingConventions.md) + 2.3 [Code Formatting Etiquettes](Developer/codingConventions.md) - 2.3 [C++ design and Coding standards](Developer/cppStyleGuide.md) + 2.4 [C++ design and Coding standards](Developer/cppStyleGuide.md) - 2.4 [Graphitti Repository Tools and Workflows](Developer/index.md) + 2.5 [Graphitti Repository Tools and Workflows](Developer/index.md) - 2.5 [Graphitti System Documentation](Developer/index.md) + 2.6 [Graphitti System Documentation](Developer/index.md) - 2.6 [Unit Tests](Developer/UnitTests.md) + 2.7 [Unit Tests](Developer/UnitTests.md) + + 2.8 [Serialization](Developer/Serialization.md) + +4. [Testing](Testing/index.md) + + 3.1 Array Performance Testing + + 3.2 Dynamic Cast Performance Testing + + 3.3 Test Config Files + +5. Notes + + 4.1 [General Notes](RebuildNotes/GeneralNotes.md) - 2.7 [Serialization](Developer/Serialization.md) + 4.2 [Layout Notes](RebuildNotes/LayoutsNotes.md) -3. [Testing](Testing/index.md) + 4.3 [Connections Notes](RebuildNotes/ConnectionsNotes.md) - 3.1 [Array Performance Testing](Testing/ArrayPerformance/ArrayPerformance.md) + 4.4 [Neuron Notes](RebuildNotes/NeuronsNotes.md) - 3.2 [Dynamic Cast Performance Testing](Testing/CastingTest/CastingTest.md) + 4.5 [Synapses Notes](RebuildNotes/SynapsesNotes.md) - 3.3 [Test Config Files](Testing/TestConfigFileParameters/testConfigFileParameters.md) + 4.6 [Recorder Notes](RebuildNotes/RecordersNotes.md) -4. [Glossary & Notes](Notes/index.md) +6. [Glossary](Glossary.md) -## [Code of Conduct] + 5.1 Graph Vocabulary -Our [code of conduct] by which Graphitti has been developed. + 5.2 Neuroscience Vocabulary + + +## Extra Resources + +Here are some [recommended resources](Resources.md) to browse + +## Tools + +Here is documentation on the [GIS to GEXF](Tools/GIStoGraph.md) tool. This tool reads in Geographic Information Systems data, constructs a graph based on that data, and produces GEXF and GraphML formatted XML files that we can then pass into the Emergency Services Communication Systems simulation. + +## Code of Conduct + +Our [code of conduct](../CODE_OF_CONDUCT.md) ## [Acknowledgements](acknowledgements.md) @@ -48,8 +79,3 @@ Those who have helped make Graphitti what it is and shaping what it will be. --------- [<< Go back to UWB Intelligent Networks Lab home page](http://uwb-biocomputing.github.io/) - -[//]: # (Moving URL links to the bottom of the document for ease of updating - LS) -[//]: # (Links to repo items which exist outside of the docs folder need a direct link.) - -[Code of Conduct]: \ No newline at end of file From 21f90633cdd14a68e8d70e1fdf955d29fa24f234 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 2 Jul 2025 11:59:12 -0700 Subject: [PATCH 36/37] Manually remerged SharedDevelopment into AndrewDevelopment by applying the diff and reviewing changes to resolve conflicts. --- Contributors.md | 2 + Simulator/Core/Core.cpp | 4 +- Simulator/Core/GPUModel.cpp | 196 ++++++++---------- Simulator/Core/GPUModel.h | 38 ++-- Simulator/Core/OperationManager.cpp | 4 +- Simulator/Core/Operations.h | 4 +- Simulator/Core/Serializer.cpp | 5 +- Simulator/Edges/AllEdges.cpp | 25 ++- Simulator/Edges/AllEdges.h | 16 +- Simulator/Edges/NG911/All911Edges.h | 10 +- Simulator/Edges/Neuro/AllDSSynapses.h | 15 +- Simulator/Edges/Neuro/AllDSSynapses_d.cpp | 29 ++- .../Edges/Neuro/AllDynamicSTDPSynapses.h | 17 +- .../Edges/Neuro/AllDynamicSTDPSynapses_d.cpp | 28 +-- Simulator/Edges/Neuro/AllSTDPSynapses.h | 16 +- Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp | 28 ++- Simulator/Edges/Neuro/AllSpikingSynapses.h | 15 +- .../Edges/Neuro/AllSpikingSynapses_d.cpp | 27 +-- Simulator/Utils/Global.cpp | 15 -- Simulator/Utils/Global.h | 34 --- Simulator/Vertices/AllVertices.cpp | 41 ++++ Simulator/Vertices/AllVertices.h | 19 +- Simulator/Vertices/NG911/All911Vertices.h | 10 +- Simulator/Vertices/Neuro/AllIFNeurons.h | 15 +- Simulator/Vertices/Neuro/AllIFNeurons_d.cpp | 24 ++- Simulator/Vertices/Neuro/AllIZHNeurons.h | 16 +- Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp | 31 ++- Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp | 2 +- Simulator/Vertices/Neuro/AllSpikingNeurons.h | 7 +- .../Vertices/Neuro/AllSpikingNeurons_d.cpp | 12 +- docs/Developer/GHActions.md | 7 +- docs/Developer/index.md | 11 +- docs/Doxygen/Doxyfile | 2 +- docs/Doxygen/index.md | 19 ++ .../ConnectionsNotes.md | 0 docs/{RebuildNotes => Notes}/GeneralNotes.md | 0 docs/{ => Notes}/Glossary.md | 0 docs/{RebuildNotes => Notes}/LayoutsNotes.md | 0 docs/{RebuildNotes => Notes}/NeuronsNotes.md | 0 .../{RebuildNotes => Notes}/RecordersNotes.md | 0 docs/{ => Notes}/Resources.md | 0 docs/{RebuildNotes => Notes}/SynapsesNotes.md | 0 docs/Notes/index.md | 32 +++ docs/Testing/index.md | 9 +- docs/User/installation.md | 42 +++- docs/index.md | 64 ++---- 46 files changed, 457 insertions(+), 434 deletions(-) create mode 100644 docs/Doxygen/index.md rename docs/{RebuildNotes => Notes}/ConnectionsNotes.md (100%) rename docs/{RebuildNotes => Notes}/GeneralNotes.md (100%) rename docs/{ => Notes}/Glossary.md (100%) rename docs/{RebuildNotes => Notes}/LayoutsNotes.md (100%) rename docs/{RebuildNotes => Notes}/NeuronsNotes.md (100%) rename docs/{RebuildNotes => Notes}/RecordersNotes.md (100%) rename docs/{ => Notes}/Resources.md (100%) rename docs/{RebuildNotes => Notes}/SynapsesNotes.md (100%) create mode 100644 docs/Notes/index.md diff --git a/Contributors.md b/Contributors.md index b2ead0ce9..d2d338c78 100644 --- a/Contributors.md +++ b/Contributors.md @@ -85,6 +85,8 @@ Andrew Madison Padmanabh Patil +Lawrence Scott + # Graduate diff --git a/Simulator/Core/Core.cpp b/Simulator/Core/Core.cpp index da1c59bce..6c206ed36 100644 --- a/Simulator/Core/Core.cpp +++ b/Simulator/Core/Core.cpp @@ -206,7 +206,7 @@ int Core::runSimulation(string executableName, string cmdLineArguments) } // Helper function for recorder to register spike history variables for all neurons. - simulator.getModel().getLayout().getVertices().registerHistoryVariables(); + OperationManager::getInstance().executeOperation(Operations::registerHistoryVariables); // Run simulation LOG4CPLUS_TRACE(consoleLogger, "Starting Simulation"); @@ -247,4 +247,4 @@ int Core::runSimulation(string executableName, string cmdLineArguments) cout << "time elapsed: " << timeElapsed << endl; cout << "ssps (simulation seconds / real time seconds): " << ssps << endl; return 0; -} +} \ No newline at end of file diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index ef4621f1f..68f2f6857 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -12,6 +12,8 @@ #include "AllVertices.h" #include "Connections.h" #include "Global.h" +#include "OperationManager.h" + #ifdef VALIDATION_MODE #include "AllIFNeurons.h" #include "OperationManager.h" @@ -27,31 +29,34 @@ GPUModel::GPUModel() : Model::Model(), edgeIndexMapDevice_(nullptr), randNoise_d(nullptr), allVerticesDevice_(nullptr), allEdgesDevice_(nullptr) { + // Register allocNeuronDeviceStruct function as a allocateGPU operation in the OperationManager + function allocateGPU = bind(&GPUModel::allocDeviceStruct, this); + OperationManager::getInstance().registerOperation(Operations::allocateGPU, allocateGPU); + + // Register copyCPUtoGPU function as a copyCPUtoGPU operation in the OperationManager + function copyCPUtoGPU = bind(&GPUModel::copyCPUtoGPU, this); + OperationManager::getInstance().registerOperation(Operations::copyToGPU, copyCPUtoGPU); + + // Note: We do not register a corresponding copyFromGPU operation here because + // we are only copying the synapseIndexMap to the GPU. This map is a read-only lookup table + // that gets recreated from scratch on each update. As a result, we only need to allocate, + // copy to GPU, and deallocate — there is no meaningful data to copy back from the GPU. + + // Register deleteSynapseImap function as a deallocateGPUMemory operation in the OperationManager + function deallocateGPUMemory = bind(&GPUModel::deleteDeviceStruct, this); + OperationManager::getInstance().registerOperation(Operations::deallocateGPUMemory, + deallocateGPUMemory); } /// Allocates and initializes memories on CUDA device. -/// @param[out] allVerticesDevice Memory location of the pointer to the vertices list on device memory. -/// @param[out] allEdgesDevice Memory location of the pointer to the edges list on device memory. -void GPUModel::allocDeviceStruct(void **allVerticesDevice, void **allEdgesDevice) +void GPUModel::allocDeviceStruct() { - // Get vertices and edges - AllVertices &vertices = layout_->getVertices(); - AllEdges &edges = connections_->getEdges(); - - // Allocate vertices and edges structs on GPU device memory - vertices.allocVerticesDeviceStruct(allVerticesDevice); - edges.allocEdgeDeviceStruct(allEdgesDevice); - // Allocate memory for random noise array int numVertices = Simulator::getInstance().getTotalVertices(); - // BGSIZE randNoise_d_size = numVertices * sizeof(float); // size of random noise array - // HANDLE_ERROR(cudaMalloc((void **)&randNoise_d, randNoise_d_size)); - - // Copy host vertex and edge arrays into GPU device - vertices.copyToDevice(*allVerticesDevice); - edges.copyEdgeHostToDevice(*allEdgesDevice); + BGSIZE randNoise_d_size = numVertices * sizeof(float); // size of random noise array + HANDLE_ERROR(cudaMalloc((void **)&randNoise_d, randNoise_d_size)); - // Allocate edge inverse map in device memory + // Allocate synapse inverse map in device memory allocEdgeIndexMap(numVertices); // Create the CUDA stream used to launch synchronous GPU kernels during the simulation. @@ -62,24 +67,19 @@ void GPUModel::allocDeviceStruct(void **allVerticesDevice, void **allEdgesDevice } /// Copies device memories to host memories and deallocates them. -/// @param[out] allVerticesDevice Memory location of the pointer to the vertices list on device memory. -/// @param[out] allEdgesDevice Memory location of the pointer to the edges list on device memory. -void GPUModel::deleteDeviceStruct(void **allVerticesDevice, void **allEdgesDevice) +void GPUModel::deleteDeviceStruct() { - // Get vertices and edges - AllVertices &vertices = layout_->getVertices(); - AllEdges &edges = connections_->getEdges(); - - // Copy device edge and vertex structs to host memory - vertices.copyFromDevice(*allVerticesDevice); - // Deallocate device memory - vertices.deleteVerticesDeviceStruct(*allVerticesDevice); - // Copy device edge and vertex structs to host memory - edges.copyEdgeDeviceToHost(*allEdgesDevice); // Deallocate device memory - edges.deleteEdgeDeviceStruct(*allEdgesDevice); - // HANDLE_ERROR(cudaFree(randNoise_d)); - // closeFileMT(); + EdgeIndexMapDevice synapseIMapDevice; + HANDLE_ERROR(cudaMemcpy(&synapseIMapDevice, edgeIndexMapDevice_, sizeof(EdgeIndexMapDevice), + cudaMemcpyDeviceToHost)); + HANDLE_ERROR(cudaFree(synapseIMapDevice.outgoingEdgeBegin_)); + HANDLE_ERROR(cudaFree(synapseIMapDevice.outgoingEdgeCount_)); + HANDLE_ERROR(cudaFree(synapseIMapDevice.outgoingEdgeIndexMap_)); + HANDLE_ERROR(cudaFree(synapseIMapDevice.incomingEdgeBegin_)); + HANDLE_ERROR(cudaFree(synapseIMapDevice.incomingEdgeCount_)); + HANDLE_ERROR(cudaFree(synapseIMapDevice.incomingEdgeIndexMap_)); + HANDLE_ERROR(cudaFree(edgeIndexMapDevice_)); HANDLE_ERROR(cudaStreamDestroy(simulationStream_)); } @@ -115,13 +115,9 @@ void GPUModel::setupSim() t_gpu_advanceSynapses = 0.0; t_gpu_calcSummation = 0.0; #endif // PERFORMANCE_METRICS - - // allocates memories on CUDA device - allocDeviceStruct((void **)&allVerticesDevice_, (void **)&allEdgesDevice_); - - EdgeIndexMap &edgeIndexMap = connections_->getEdgeIndexMap(); - // copy inverse map to the device memory - copyEdgeIndexMapHostToDevice(edgeIndexMap, Simulator::getInstance().getTotalVertices()); + // Allocate and copy neuron/synapse data structures to GPU memory + OperationManager::getInstance().executeOperation(Operations::allocateGPU); + OperationManager::getInstance().executeOperation(Operations::copyToGPU); AllEdges &edges = connections_->getEdges(); // set some parameters used for advanceVerticesDevice @@ -137,10 +133,11 @@ void GPUModel::setupSim() /// Performs any finalization tasks on network following a simulation. void GPUModel::finish() { + // copy device synapse and neuron structs to host memory + OperationManager::getInstance().executeOperation(Operations::copyFromGPU); // deallocates memories on CUDA device + OperationManager::getInstance().executeOperation(Operations::deallocateGPUMemory); AsyncGenerator_.deleteDeviceStruct(); - deleteDeviceStruct((void **)&allVerticesDevice_, (void **)&allEdgesDevice_); - deleteEdgeIndexMap(); #ifdef PERFORMANCE_METRICS cudaEventDestroy(start); @@ -250,7 +247,7 @@ void GPUModel::updateConnections() AllVertices &vertices = layout_->getVertices(); AllEdges &edges = connections_->getEdges(); - vertices.copyFromDevice(allVerticesDevice_); + vertices.copyFromDevice(); // Update Connections data if (connections_->updateConnections(vertices)) { @@ -260,8 +257,7 @@ void GPUModel::updateConnections() // create edge index map connections_->createEdgeIndexMap(); // copy index map to the device memory - copyEdgeIndexMapHostToDevice(connections_->getEdgeIndexMap(), - Simulator::getInstance().getTotalVertices()); + copyCPUtoGPU(); } } @@ -298,83 +294,67 @@ void GPUModel::allocEdgeIndexMap(int count) cudaMemcpyHostToDevice)); } -/// Deallocate device memory for edge inverse map. -void GPUModel::deleteEdgeIndexMap() -{ - EdgeIndexMapDevice edgeIndexMapDevice; - HANDLE_ERROR(cudaMemcpy(&edgeIndexMapDevice, edgeIndexMapDevice_, sizeof(EdgeIndexMapDevice), - cudaMemcpyDeviceToHost)); - HANDLE_ERROR(cudaFree(edgeIndexMapDevice.outgoingEdgeBegin_)); - HANDLE_ERROR(cudaFree(edgeIndexMapDevice.outgoingEdgeCount_)); - HANDLE_ERROR(cudaFree(edgeIndexMapDevice.outgoingEdgeIndexMap_)); - HANDLE_ERROR(cudaFree(edgeIndexMapDevice.incomingEdgeBegin_)); - HANDLE_ERROR(cudaFree(edgeIndexMapDevice.incomingEdgeCount_)); - HANDLE_ERROR(cudaFree(edgeIndexMapDevice.incomingEdgeIndexMap_)); - HANDLE_ERROR(cudaFree(edgeIndexMapDevice_)); -} - -/// Copy EdgeIndexMap in host memory to EdgeIndexMap in device memory. -/// @param edgeIndexMapHost Reference to the EdgeIndexMap in host memory. -void GPUModel::copyEdgeIndexMapHostToDevice(EdgeIndexMap &edgeIndexMapHost, int numVertices) +/// Allocate and Copy CPU Synapse data to GPU. +void GPUModel::copyCPUtoGPU() { - AllEdges &edges = connections_->getEdges(); - int totalEdgeCount = edges.totalEdgeCount_; - if (totalEdgeCount == 0) + EdgeIndexMap synapseIndexMapHost = connections_->getEdgeIndexMap(); + int numVertices = Simulator::getInstance().getTotalVertices(); + AllEdges &synapses = connections_->getEdges(); + int totalSynapseCount = dynamic_cast(synapses).totalEdgeCount_; + if (totalSynapseCount == 0) return; - EdgeIndexMapDevice edgeIndexMapDevice; - HANDLE_ERROR(cudaMemcpy(&edgeIndexMapDevice, edgeIndexMapDevice_, sizeof(EdgeIndexMapDevice), + EdgeIndexMapDevice synapseIMapDevice; + HANDLE_ERROR(cudaMemcpy(&synapseIMapDevice, edgeIndexMapDevice_, sizeof(EdgeIndexMapDevice), cudaMemcpyDeviceToHost)); - HANDLE_ERROR(cudaMemcpy(edgeIndexMapDevice.outgoingEdgeBegin_, - edgeIndexMapHost.outgoingEdgeBegin_.data(), numVertices * sizeof(BGSIZE), - cudaMemcpyHostToDevice)); - HANDLE_ERROR(cudaMemcpy(edgeIndexMapDevice.outgoingEdgeCount_, - edgeIndexMapHost.outgoingEdgeCount_.data(), numVertices * sizeof(BGSIZE), - cudaMemcpyHostToDevice)); - if (edgeIndexMapDevice.outgoingEdgeIndexMap_ != nullptr) { - HANDLE_ERROR(cudaFree(edgeIndexMapDevice.outgoingEdgeIndexMap_)); + HANDLE_ERROR(cudaMemcpy(synapseIMapDevice.outgoingEdgeBegin_, + synapseIndexMapHost.outgoingEdgeBegin_.data(), + numVertices * sizeof(BGSIZE), cudaMemcpyHostToDevice)); + HANDLE_ERROR(cudaMemcpy(synapseIMapDevice.outgoingEdgeCount_, + synapseIndexMapHost.outgoingEdgeCount_.data(), + numVertices * sizeof(BGSIZE), cudaMemcpyHostToDevice)); + if (synapseIMapDevice.outgoingEdgeIndexMap_ != nullptr) { + HANDLE_ERROR(cudaFree(synapseIMapDevice.outgoingEdgeIndexMap_)); } - HANDLE_ERROR(cudaMalloc((void **)&edgeIndexMapDevice.outgoingEdgeIndexMap_, - totalEdgeCount * sizeof(BGSIZE))); - HANDLE_ERROR(cudaMemcpy(edgeIndexMapDevice.outgoingEdgeIndexMap_, - edgeIndexMapHost.outgoingEdgeIndexMap_.data(), - totalEdgeCount * sizeof(BGSIZE), cudaMemcpyHostToDevice)); + HANDLE_ERROR(cudaMalloc((void **)&synapseIMapDevice.outgoingEdgeIndexMap_, + totalSynapseCount * sizeof(BGSIZE))); + HANDLE_ERROR(cudaMemcpy(synapseIMapDevice.outgoingEdgeIndexMap_, + synapseIndexMapHost.outgoingEdgeIndexMap_.data(), + totalSynapseCount * sizeof(BGSIZE), cudaMemcpyHostToDevice)); // active synapse map - HANDLE_ERROR(cudaMemcpy(edgeIndexMapDevice.incomingEdgeBegin_, - edgeIndexMapHost.incomingEdgeBegin_.data(), numVertices * sizeof(BGSIZE), - cudaMemcpyHostToDevice)); - HANDLE_ERROR(cudaMemcpy(edgeIndexMapDevice.incomingEdgeCount_, - edgeIndexMapHost.incomingEdgeCount_.data(), numVertices * sizeof(BGSIZE), - cudaMemcpyHostToDevice)); + HANDLE_ERROR(cudaMemcpy(synapseIMapDevice.incomingEdgeBegin_, + synapseIndexMapHost.incomingEdgeBegin_.data(), + numVertices * sizeof(BGSIZE), cudaMemcpyHostToDevice)); + HANDLE_ERROR(cudaMemcpy(synapseIMapDevice.incomingEdgeCount_, + synapseIndexMapHost.incomingEdgeCount_.data(), + numVertices * sizeof(BGSIZE), cudaMemcpyHostToDevice)); // the number of synapses may change, so we reallocate the memory - if (edgeIndexMapDevice.incomingEdgeIndexMap_ != nullptr) { - HANDLE_ERROR(cudaFree(edgeIndexMapDevice.incomingEdgeIndexMap_)); - edgeIndexMapDevice.incomingEdgeIndexMap_ = nullptr; + if (synapseIMapDevice.incomingEdgeIndexMap_ != nullptr) { + HANDLE_ERROR(cudaFree(synapseIMapDevice.incomingEdgeIndexMap_)); + synapseIMapDevice.incomingEdgeIndexMap_ = nullptr; } - HANDLE_ERROR(cudaMalloc((void **)&edgeIndexMapDevice.incomingEdgeIndexMap_, - totalEdgeCount * sizeof(BGSIZE))); - HANDLE_ERROR(cudaMemcpy(edgeIndexMapDevice.incomingEdgeIndexMap_, - edgeIndexMapHost.incomingEdgeIndexMap_.data(), - totalEdgeCount * sizeof(BGSIZE), cudaMemcpyHostToDevice)); - HANDLE_ERROR(cudaMemcpy(edgeIndexMapDevice_, &edgeIndexMapDevice, sizeof(EdgeIndexMapDevice), + HANDLE_ERROR(cudaMalloc((void **)&synapseIMapDevice.incomingEdgeIndexMap_, + totalSynapseCount * sizeof(BGSIZE))); + HANDLE_ERROR(cudaMemcpy(synapseIMapDevice.incomingEdgeIndexMap_, + synapseIndexMapHost.incomingEdgeIndexMap_.data(), + totalSynapseCount * sizeof(BGSIZE), cudaMemcpyHostToDevice)); + HANDLE_ERROR(cudaMemcpy(edgeIndexMapDevice_, &synapseIMapDevice, sizeof(EdgeIndexMapDevice), cudaMemcpyHostToDevice)); } -/// Copy GPU edge data to CPU. -void GPUModel::copyGPUtoCPU() +/// Print out EdgeProps on the GPU. +void GPUModel::printGPUEdgesPropsModel() const { - // copy device edge structs to host memory - connections_->getEdges().copyEdgeDeviceToHost(allEdgesDevice_); + connections_->getEdges().printGPUEdgesProps(allEdgesDevice_); } -/// Copy CPU edge data to GPU. -void GPUModel::copyCPUtoGPU() +/// Getter for neuron structure in device memory +AllVerticesDeviceProperties *&GPUModel::getAllVerticesDevice() { - // copy host edge structs to device memory - connections_->getEdges().copyEdgeHostToDevice(allEdgesDevice_); + return allVerticesDevice_; } -/// Print out EdgeProps on the GPU. -void GPUModel::printGPUEdgesPropsModel() const +/// Getter for synapse structures in device memory +AllEdgesDeviceProperties *&GPUModel::getAllEdgesDevice() { - connections_->getEdges().printGPUEdgesProps(allEdgesDevice_); + return allEdgesDevice_; } \ No newline at end of file diff --git a/Simulator/Core/GPUModel.h b/Simulator/Core/GPUModel.h index 7bfbf1de5..4dd20ca70 100644 --- a/Simulator/Core/GPUModel.h +++ b/Simulator/Core/GPUModel.h @@ -22,8 +22,11 @@ #pragma once #include "AllEdges.h" +#include "AllSpikingNeurons.h" +#include "AllSpikingSynapses.h" #include "AllVertices.h" #include "AsyncPhilox_d.h" +#include "OperationManager.h" #ifdef USE_GPU #include #endif @@ -85,25 +88,33 @@ class GPUModel : public Model { /// over the past epoch. Should be called once every epoch. virtual void updateConnections() override; - /// Copy GPU edge data to CPU. - virtual void copyGPUtoCPU() override; - - /// Copy CPU edge data to GPU. + /// Copies neuron and synapse data from CPU to GPU memory. + /// TODO: Refactor this. Currently, GPUModel handles low-level memory transfer for vertices and edges. + /// Consider moving this responsibility to a more appropriate class, such as a dedicated memory manager + /// or the OperationManager, to better separate concerns and keep the model focused on high-level coordination. virtual void copyCPUtoGPU() override; + // GPUModel itself does not have anything to be copied back, this function is a + // dummy function just to make GPUModel non virtual + virtual void copyGPUtoCPU() override + { + } + /// Print out EdgeProps on the GPU. void printGPUEdgesPropsModel() const; + /// Getter for edge (synapse) structures in device memory + AllEdgesDeviceProperties *&getAllEdgesDevice(); + + /// Getter for vertex (neuron) structures in device memory + AllVerticesDeviceProperties *&getAllVerticesDevice(); + protected: /// Allocates and initializes memories on CUDA device. - /// @param[out] allVerticesDevice Memory location of the pointer to the vertices list on device memory. - /// @param[out] allEdgesDevice Memory location of the pointer to the edges list on device memory. - void allocDeviceStruct(void **allVerticesDevice, void **allEdgesDevice); + void allocDeviceStruct(); - /// Copies device memories to host memories and deallocates them. - /// @param[out] allVerticesDevice Memory location of the pointer to the vertices list on device memory. - /// @param[out] allEdgesDevice Memory location of the pointer to the edges list on device memory. - virtual void deleteDeviceStruct(void **allVerticesDevice, void **allEdgesDevice); + /// Deallocates device memories. + virtual void deleteDeviceStruct(); /// Pointer to device random noise array. float *randNoise_d; @@ -131,11 +142,6 @@ class GPUModel : public Model { private: void allocEdgeIndexMap(int count); - void deleteEdgeIndexMap(); - -public: //2020/03/14 changed to public for accessing in Core - void copyEdgeIndexMapHostToDevice(EdgeIndexMap &edgeIndexMapHost, int numVertices); - private: void updateHistory(); diff --git a/Simulator/Core/OperationManager.cpp b/Simulator/Core/OperationManager.cpp index dda9e411a..7cd1222b4 100644 --- a/Simulator/Core/OperationManager.cpp +++ b/Simulator/Core/OperationManager.cpp @@ -71,7 +71,9 @@ string OperationManager::operationToString(const Operations &operation) const return "copyToGPU"; case Operations::copyFromGPU: return "copyFromGPU"; + case Operations::allocateGPU: + return "allocateGPU"; default: return "Operation isn't in OperationManager::operationToString()"; } -} +} \ No newline at end of file diff --git a/Simulator/Core/Operations.h b/Simulator/Core/Operations.h index 8cd21f3b6..27b5c4ff2 100644 --- a/Simulator/Core/Operations.h +++ b/Simulator/Core/Operations.h @@ -20,5 +20,7 @@ enum class Operations { deallocateGPUMemory, // Make sure deallocate memory isn't called until all GPU memory is copied back. restoreToDefault, // Not sure what this refers to. copyToGPU, - copyFromGPU + copyFromGPU, + allocateGPU, + registerHistoryVariables }; \ No newline at end of file diff --git a/Simulator/Core/Serializer.cpp b/Simulator/Core/Serializer.cpp index 21652f37e..d1b2c0f7f 100644 --- a/Simulator/Core/Serializer.cpp +++ b/Simulator/Core/Serializer.cpp @@ -69,8 +69,7 @@ bool Serializer::deserialize() #if defined(USE_GPU) GPUModel &gpuModel = static_cast(simulator.getModel()); - gpuModel.copyEdgeIndexMapHostToDevice(simulator.getModel().getConnections().getEdgeIndexMap(), - simulator.getTotalVertices()); + gpuModel.copyCPUtoGPU(); #endif // USE_GPU return true; @@ -110,4 +109,4 @@ template bool Serializer::processArchive(Archive &archive, Si return false; } return true; -} +} \ No newline at end of file diff --git a/Simulator/Edges/AllEdges.cpp b/Simulator/Edges/AllEdges.cpp index 8c570e24c..27dd694f7 100644 --- a/Simulator/Edges/AllEdges.cpp +++ b/Simulator/Edges/AllEdges.cpp @@ -26,6 +26,28 @@ AllEdges::AllEdges() : totalEdgeCount_(0), maxEdgesPerVertex_(0), countVertices_ OperationManager::getInstance().registerOperation(Operations::printParameters, printParametersFunc); +#if defined(USE_GPU) + // Register allocNeuronDeviceStruct function as a allocateGPU operation in the OperationManager + function allocateGPU + = bind(static_cast(&AllEdges::allocEdgeDeviceStruct), this); + OperationManager::getInstance().registerOperation(Operations::allocateGPU, allocateGPU); + + // Register AllEdges::copyEdgeHostToDevice function as a copyToGPU operation in the OperationManager + function copyCPUtoGPU + = bind(static_cast(&AllEdges::copyEdgeHostToDevice), this); + OperationManager::getInstance().registerOperation(Operations::copyToGPU, copyCPUtoGPU); + + // Register copyFromGPU operation for transferring edge data from device to host + function copyFromGPU = bind(&AllEdges::copyEdgeDeviceToHost, this); + OperationManager::getInstance().registerOperation(Operations::copyFromGPU, copyFromGPU); + + // Register deleteEdgeDeviceStruct function as a deallocateGPUMemory operation in the OperationManager + function deallocateGPUMemory = bind(&AllEdges::deleteEdgeDeviceStruct, this); + OperationManager::getInstance().registerOperation(Operations::deallocateGPUMemory, + deallocateGPUMemory); + +#endif + fileLogger_ = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("file")); edgeLogger_ = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("edge")); } @@ -233,6 +255,7 @@ void AllEdges::SetStream(cudaStream_t simulationStream) #endif #if !defined(USE_GPU) + /// Advance all the edges in the simulation. /// /// @param vertices The vertices. @@ -290,4 +313,4 @@ BGSIZE AllEdges::addEdge(edgeType type, int srcVertex, int destVertex, BGFLOAT d // create an edge createEdge(iEdg, srcVertex, destVertex, deltaT, type); return iEdg; -} +} \ No newline at end of file diff --git a/Simulator/Edges/AllEdges.h b/Simulator/Edges/AllEdges.h index d5592dc35..cdf0c2b6f 100644 --- a/Simulator/Edges/AllEdges.h +++ b/Simulator/Edges/AllEdges.h @@ -111,9 +111,7 @@ class AllEdges { /// Allocate GPU memories to store all edges' states, /// and copy them from host to GPU memory. - /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void allocEdgeDeviceStruct(void **allEdgesDevice) = 0; + virtual void allocEdgeDeviceStruct() = 0; /// Allocate GPU memories to store all edges' states, /// and copy them from host to GPU memory. @@ -126,13 +124,10 @@ class AllEdges { /// Delete GPU memories. /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void deleteEdgeDeviceStruct(void *allEdgesDevice) = 0; + virtual void deleteEdgeDeviceStruct() = 0; /// Copy all edges' data from host to device. - /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void copyEdgeHostToDevice(void *allEdgesDevice) = 0; + virtual void copyEdgeHostToDevice() = 0; /// Copy all edges' data from host to device. /// @@ -144,8 +139,7 @@ class AllEdges { /// Copy all edges' data from device to host. /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void copyEdgeDeviceToHost(void *allEdgesDevice) = 0; + virtual void copyEdgeDeviceToHost() = 0; /// Get edge_counts in AllEdges struct on device memory. /// @@ -286,4 +280,4 @@ template void AllEdges::serialize(Archive &archive) cereal::make_nvp("totalEdgeCount", totalEdgeCount_), cereal::make_nvp("maxEdgesPerVertex", maxEdgesPerVertex_), cereal::make_nvp("countVertices", countVertices_)); -} +} \ No newline at end of file diff --git a/Simulator/Edges/NG911/All911Edges.h b/Simulator/Edges/NG911/All911Edges.h index 6b1c288cf..505972266 100644 --- a/Simulator/Edges/NG911/All911Edges.h +++ b/Simulator/Edges/NG911/All911Edges.h @@ -65,14 +65,14 @@ class All911Edges : public AllEdges { // GPU functionality for 911 simulation is unimplemented. // These signatures are required to make the class non-abstract public: - virtual void allocEdgeDeviceStruct(void **allEdgesDevice) {}; + virtual void allocEdgeDeviceStruct() {}; virtual void allocEdgeDeviceStruct(void **allEdgesDevice, int numVertices, int maxEdgesPerVertex) {}; - virtual void deleteEdgeDeviceStruct(void *allEdgesDevice) {}; - virtual void copyEdgeHostToDevice(void *allEdgesDevice) {}; + virtual void deleteEdgeDeviceStruct() {}; + virtual void copyEdgeHostToDevice() {}; virtual void copyEdgeHostToDevice(void *allEdgesDevice, int numVertices, int maxEdgesPerVertex) { }; - virtual void copyEdgeDeviceToHost(void *allEdgesDevice) {}; + virtual void copyEdgeDeviceToHost() {}; virtual void copyDeviceEdgeCountsToHost(void *allEdgesDevice) {}; virtual void advanceEdges(void *allEdgesDevice, void *allVerticesDevice, void *edgeIndexMapDevice) {}; @@ -107,4 +107,4 @@ class All911Edges : public AllEdges { /// The call information per edge vector call_; -}; +}; \ No newline at end of file diff --git a/Simulator/Edges/Neuro/AllDSSynapses.h b/Simulator/Edges/Neuro/AllDSSynapses.h index 47427e679..baaa94b41 100644 --- a/Simulator/Edges/Neuro/AllDSSynapses.h +++ b/Simulator/Edges/Neuro/AllDSSynapses.h @@ -121,9 +121,7 @@ class AllDSSynapses : public AllSpikingSynapses { public: /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. - /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void allocEdgeDeviceStruct(void **allEdgesDevice) override; + virtual void allocEdgeDeviceStruct() override; /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. @@ -136,13 +134,11 @@ class AllDSSynapses : public AllSpikingSynapses { /// Delete GPU memories. /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void deleteEdgeDeviceStruct(void *allEdgesDevice) override; + virtual void deleteEdgeDeviceStruct() override; /// Copy all synapses' data from host to device. /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void copyEdgeHostToDevice(void *allEdgesDevice) override; + virtual void copyEdgeHostToDevice() override; /// Copy all synapses' data from host to device. /// @@ -154,8 +150,7 @@ class AllDSSynapses : public AllSpikingSynapses { /// Copy all synapses' data from device to host. /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void copyEdgeDeviceToHost(void *allEdgesDevice) override; + virtual void copyEdgeDeviceToHost() override; /// Set synapse class ID defined by enumClassSynapses for the caller's Synapse class. /// The class ID will be set to classSynapses_d in device memory, @@ -263,4 +258,4 @@ template void AllDSSynapses::serialize(Archive &archive) archive(cereal::base_class(this), cereal::make_nvp("lastSpike", lastSpike_), cereal::make_nvp("r", r_), cereal::make_nvp("u", u_), cereal::make_nvp("D", D_), cereal::make_nvp("U", U_), cereal::make_nvp("F", F_)); -} +} \ No newline at end of file diff --git a/Simulator/Edges/Neuro/AllDSSynapses_d.cpp b/Simulator/Edges/Neuro/AllDSSynapses_d.cpp index d1783e969..c1939d882 100644 --- a/Simulator/Edges/Neuro/AllDSSynapses_d.cpp +++ b/Simulator/Edges/Neuro/AllDSSynapses_d.cpp @@ -14,11 +14,10 @@ /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. -/// -/// @param allEdgesDevice GPU address of the AllDSSynapsesDeviceProperties struct -/// on device memory. -void AllDSSynapses::allocEdgeDeviceStruct(void **allEdgesDevice) +void AllDSSynapses::allocEdgeDeviceStruct() { + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void **allEdgesDevice = reinterpret_cast(&(gpuModel->getAllEdgesDevice())); allocEdgeDeviceStruct(allEdgesDevice, Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getMaxEdgesPerVertex()); } @@ -67,12 +66,11 @@ void AllDSSynapses::allocDeviceStruct(AllDSSynapsesDeviceProperties &allEdges, i /// Delete GPU memories. /// -/// @param allEdgesDevice GPU address of the AllDSSynapsesDeviceProperties struct -/// on device memory. -void AllDSSynapses::deleteEdgeDeviceStruct(void *allEdgesDevice) +void AllDSSynapses::deleteEdgeDeviceStruct() { AllDSSynapsesDeviceProperties allEdges; - + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); HANDLE_ERROR(cudaMemcpy(&allEdges, allEdgesDevice, sizeof(AllDSSynapsesDeviceProperties), cudaMemcpyDeviceToHost)); @@ -100,10 +98,10 @@ void AllDSSynapses::deleteDeviceStruct(AllDSSynapsesDeviceProperties &allEdgesDe /// Copy all synapses' data from host to device. /// -/// @param allEdgesDevice GPU address of the AllDSSynapsesDeviceProperties struct -/// on device memory. -void AllDSSynapses::copyEdgeHostToDevice(void *allEdgesDevice) +void AllDSSynapses::copyEdgeHostToDevice() { // copy everything necessary + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); copyEdgeHostToDevice(allEdgesDevice, Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getMaxEdgesPerVertex()); } @@ -156,13 +154,12 @@ void AllDSSynapses::copyHostToDevice(void *allEdgesDevice, /// Copy all synapses' data from device to host. /// -/// @param allEdgesDevice GPU address of the AllDSSynapsesDeviceProperties struct -/// on device memory. -void AllDSSynapses::copyEdgeDeviceToHost(void *allEdgesDevice) +void AllDSSynapses::copyEdgeDeviceToHost() { // copy everything necessary AllDSSynapsesDeviceProperties allEdgesDeviceProps; - + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); HANDLE_ERROR(cudaMemcpy(&allEdgesDeviceProps, allEdgesDevice, sizeof(AllDSSynapsesDeviceProperties), cudaMemcpyDeviceToHost)); @@ -376,4 +373,4 @@ void AllDSSynapses::printGPUEdgesProps(void *allEdgesDeviceProps) const UPrint = nullptr; FPrint = nullptr; } -} +} \ No newline at end of file diff --git a/Simulator/Edges/Neuro/AllDynamicSTDPSynapses.h b/Simulator/Edges/Neuro/AllDynamicSTDPSynapses.h index 2d5c6e79d..c12b25b62 100644 --- a/Simulator/Edges/Neuro/AllDynamicSTDPSynapses.h +++ b/Simulator/Edges/Neuro/AllDynamicSTDPSynapses.h @@ -124,10 +124,8 @@ class AllDynamicSTDPSynapses : public AllSTDPSynapses { #if defined(USE_GPU) public: /// Allocate GPU memories to store all synapses' states, - /// and copy them from host to GPU memory. - /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void allocEdgeDeviceStruct(void **allEdgesDevice) override; + /// and copy them from host to GPU memory. memory. + virtual void allocEdgeDeviceStruct() override; /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. @@ -139,13 +137,11 @@ class AllDynamicSTDPSynapses : public AllSTDPSynapses { /// Delete GPU memories. /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void deleteEdgeDeviceStruct(void *allEdgesDevice) override; + virtual void deleteEdgeDeviceStruct() override; /// Copy all synapses' data from host to device. /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void copyEdgeHostToDevice(void *allEdgesDevice) override; + virtual void copyEdgeHostToDevice() override; /// Copy all synapses' data from host to device. /// @@ -157,8 +153,7 @@ class AllDynamicSTDPSynapses : public AllSTDPSynapses { /// Copy all synapses' data from device to host. /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void copyEdgeDeviceToHost(void *allEdgesDevice) override; + virtual void copyEdgeDeviceToHost() override; /// Set synapse class ID defined by enumClassSynapses for the caller's Synapse class. /// The class ID will be set to classSynapses_d in device memory, @@ -269,4 +264,4 @@ template void AllDynamicSTDPSynapses::serialize(Archive &archive archive(cereal::base_class(this), cereal::make_nvp("lastSpike", lastSpike_), cereal::make_nvp("r", r_), cereal::make_nvp("u", u_), cereal::make_nvp("D", D_), cereal::make_nvp("U", U_), cereal::make_nvp("F", F_)); -} +} \ No newline at end of file diff --git a/Simulator/Edges/Neuro/AllDynamicSTDPSynapses_d.cpp b/Simulator/Edges/Neuro/AllDynamicSTDPSynapses_d.cpp index 1c676b19e..f4ef0e2c7 100644 --- a/Simulator/Edges/Neuro/AllDynamicSTDPSynapses_d.cpp +++ b/Simulator/Edges/Neuro/AllDynamicSTDPSynapses_d.cpp @@ -9,15 +9,15 @@ #include "AllDynamicSTDPSynapses.h" #include "AllSynapsesDeviceFuncs.h" #include "Book.h" +#include "GPUModel.h" #include "Simulator.h" /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. -/// -/// @param allEdgesDevice GPU address of the AllDynamicSTDPSynapsesDeviceProperties struct -/// on device memory. -void AllDynamicSTDPSynapses::allocEdgeDeviceStruct(void **allEdgesDevice) +void AllDynamicSTDPSynapses::allocEdgeDeviceStruct() { + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void **allEdgesDevice = reinterpret_cast(&(gpuModel->getAllEdgesDevice())); allocEdgeDeviceStruct(allEdgesDevice, Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getMaxEdgesPerVertex()); } @@ -70,10 +70,11 @@ void AllDynamicSTDPSynapses::allocDeviceStruct( /// /// @param allEdgesDevice GPU address of the AllDynamicSTDPSynapsesDeviceProperties struct /// on device memory. -void AllDynamicSTDPSynapses::deleteEdgeDeviceStruct(void *allEdgesDevice) +void AllDynamicSTDPSynapses::deleteEdgeDeviceStruct() { AllDynamicSTDPSynapsesDeviceProperties allEdges; - + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); HANDLE_ERROR(cudaMemcpy(&allEdges, allEdgesDevice, sizeof(AllDynamicSTDPSynapsesDeviceProperties), cudaMemcpyDeviceToHost)); @@ -102,10 +103,10 @@ void AllDynamicSTDPSynapses::deleteDeviceStruct( /// Copy all synapses' data from host to device. /// -/// @param allEdgesDevice GPU address of the AllDynamicSTDPSynapsesDeviceProperties struct -/// on device memory. -void AllDynamicSTDPSynapses::copyEdgeHostToDevice(void *allEdgesDevice) +void AllDynamicSTDPSynapses::copyEdgeHostToDevice() { // copy everything necessary + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); copyEdgeHostToDevice(allEdgesDevice, Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getMaxEdgesPerVertex()); } @@ -159,13 +160,12 @@ void AllDynamicSTDPSynapses::copyHostToDevice( /// Copy all synapses' data from device to host. /// -/// @param allEdgesDevice GPU address of the AllDynamicSTDPSynapsesDeviceProperties struct -/// on device memory. -void AllDynamicSTDPSynapses::copyEdgeDeviceToHost(void *allEdgesDevice) +void AllDynamicSTDPSynapses::copyEdgeDeviceToHost() { // copy everything necessary AllDynamicSTDPSynapsesDeviceProperties allEdges; - + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); HANDLE_ERROR(cudaMemcpy(&allEdges, allEdgesDevice, sizeof(AllDynamicSTDPSynapsesDeviceProperties), cudaMemcpyDeviceToHost)); @@ -452,4 +452,4 @@ void AllDynamicSTDPSynapses::printGPUEdgesProps(void *allEdgesDeviceProps) const UPrint = nullptr; FPrint = nullptr; } -} +} \ No newline at end of file diff --git a/Simulator/Edges/Neuro/AllSTDPSynapses.h b/Simulator/Edges/Neuro/AllSTDPSynapses.h index 7d7ea0660..8598981e8 100644 --- a/Simulator/Edges/Neuro/AllSTDPSynapses.h +++ b/Simulator/Edges/Neuro/AllSTDPSynapses.h @@ -154,9 +154,7 @@ class AllSTDPSynapses : public AllSpikingSynapses { public: /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. - /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void allocEdgeDeviceStruct(void **allEdgesDevice) override; + virtual void allocEdgeDeviceStruct() override; /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. @@ -169,13 +167,10 @@ class AllSTDPSynapses : public AllSpikingSynapses { /// Delete GPU memories. /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void deleteEdgeDeviceStruct(void *allEdgesDevice) override; + virtual void deleteEdgeDeviceStruct() override; /// Copy all synapses' data from host to device. - /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void copyEdgeHostToDevice(void *allEdgesDevice) override; + virtual void copyEdgeHostToDevice() override; /// Copy all synapses' data from host to device. /// @@ -187,8 +182,7 @@ class AllSTDPSynapses : public AllSpikingSynapses { /// Copy all synapses' data from device to host. /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void copyEdgeDeviceToHost(void *allEdgesDevice) override; + virtual void copyEdgeDeviceToHost() override; /// Advance all the Synapses in the simulation. /// Update the state of all synapses for a time step. @@ -441,4 +435,4 @@ template void AllSTDPSynapses::serialize(Archive &archive) cereal::make_nvp("Wex_I", Wex_I_), cereal::make_nvp("Wex_E", Wex_E_), cereal::make_nvp("Aneg_I", Aneg_I_), cereal::make_nvp("Aneg_E", Aneg_E_), cereal::make_nvp("Apos_I", Apos_I_), cereal::make_nvp("Apos_E", Apos_E_)); -} +} \ No newline at end of file diff --git a/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp b/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp index f68b9add4..a7c3804fc 100644 --- a/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp +++ b/Simulator/Edges/Neuro/AllSTDPSynapses_d.cpp @@ -32,11 +32,10 @@ __global__ void advanceSTDPSynapsesDevice(int totalSynapseCount, int maxSpikes); /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. -/// -/// @param allEdgesDevice GPU address of the AllSTDPSynapsesDeviceProperties struct -/// on device memory. -void AllSTDPSynapses::allocEdgeDeviceStruct(void **allEdgesDevice) +void AllSTDPSynapses::allocEdgeDeviceStruct() { + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void **allEdgesDevice = reinterpret_cast(&(gpuModel->getAllEdgesDevice())); allocEdgeDeviceStruct(allEdgesDevice, Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getMaxEdgesPerVertex()); } @@ -93,11 +92,11 @@ void AllSTDPSynapses::allocDeviceStruct(AllSTDPSynapsesDeviceProperties &allEdge /// Delete GPU memories. /// -/// @param allEdgesDevice GPU address of the AllSTDPSynapsesDeviceProperties struct -/// on device memory. -void AllSTDPSynapses::deleteEdgeDeviceStruct(void *allEdgesDevice) +void AllSTDPSynapses::deleteEdgeDeviceStruct() { AllSTDPSynapsesDeviceProperties allEdgesDeviceProps; + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); HANDLE_ERROR(cudaMemcpy(&allEdgesDeviceProps, allEdgesDevice, sizeof(AllSTDPSynapsesDeviceProperties), cudaMemcpyDeviceToHost)); deleteDeviceStruct(allEdgesDeviceProps); @@ -129,12 +128,10 @@ void AllSTDPSynapses::deleteDeviceStruct(AllSTDPSynapsesDeviceProperties &allEdg /// Copy all synapses' data from host to device. /// -/// @param allEdgesDevice GPU address of the AllSTDPSynapsesDeviceProperties struct -/// on device memory. -/// @param numVertices Number of vertices. -/// @param maxEdgesPerVertex Maximum number of synapses per neuron. -void AllSTDPSynapses::copyEdgeHostToDevice(void *allEdgesDevice) +void AllSTDPSynapses::copyEdgeHostToDevice() { // copy everything necessary + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); copyEdgeHostToDevice(allEdgesDevice, Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getMaxEdgesPerVertex()); } @@ -200,13 +197,12 @@ void AllSTDPSynapses::copyHostToDevice(void *allEdgesDevice, /// Copy all synapses' data from device to host. /// -/// @param allEdgesDevice GPU address of the AllSTDPSynapsesDeviceProperties struct -/// on device memory. -void AllSTDPSynapses::copyEdgeDeviceToHost(void *allEdgesDevice) +void AllSTDPSynapses::copyEdgeDeviceToHost() { // copy everything necessary AllSTDPSynapsesDeviceProperties allEdgesDeviceProps; - + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); HANDLE_ERROR(cudaMemcpy(&allEdgesDeviceProps, allEdgesDevice, sizeof(AllSTDPSynapsesDeviceProperties), cudaMemcpyDeviceToHost)); copyDeviceToHost(allEdgesDeviceProps); diff --git a/Simulator/Edges/Neuro/AllSpikingSynapses.h b/Simulator/Edges/Neuro/AllSpikingSynapses.h index 0cd04821b..534c86220 100644 --- a/Simulator/Edges/Neuro/AllSpikingSynapses.h +++ b/Simulator/Edges/Neuro/AllSpikingSynapses.h @@ -122,9 +122,7 @@ class AllSpikingSynapses : public AllNeuroEdges { public: /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. - /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void allocEdgeDeviceStruct(void **allEdgesDevice) override; + virtual void allocEdgeDeviceStruct() override; /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. @@ -137,13 +135,11 @@ class AllSpikingSynapses : public AllNeuroEdges { /// Delete GPU memories. /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void deleteEdgeDeviceStruct(void *allEdgesDevice) override; + virtual void deleteEdgeDeviceStruct() override; /// Copy all synapses' data from host to device. /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void copyEdgeHostToDevice(void *allEdgesDevice) override; + virtual void copyEdgeHostToDevice() override; /// Copy all synapses' data from host to device. /// @@ -155,8 +151,7 @@ class AllSpikingSynapses : public AllNeuroEdges { /// Copy all synapses' data from device to host. /// - /// @param allEdgesDevice GPU address of the allEdges struct on device memory. - virtual void copyEdgeDeviceToHost(void *allEdgesDevice) override; + virtual void copyEdgeDeviceToHost() override; /// Get edge_counts in AllNeuroEdges struct on device memory. /// @@ -344,4 +339,4 @@ template void AllSpikingSynapses::serialize(Archive &archive) cereal::make_nvp("delay_EE", delay_EE_), cereal::make_nvp("totalDelay", totalDelay_), cereal::make_nvp("delayQueue", delayQueue_), cereal::make_nvp("delayIndex", delayIndex_), cereal::make_nvp("delayQueueLength", delayQueueLength_)); -} +} \ No newline at end of file diff --git a/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp b/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp index 011c3ef9f..a6954cf82 100644 --- a/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp +++ b/Simulator/Edges/Neuro/AllSpikingSynapses_d.cpp @@ -10,6 +10,8 @@ #include "AllSpikingSynapses.h" #include "AllSynapsesDeviceFuncs.h" #include "Book.h" +#include "GPUModel.h" +#include "Simulator.h" #include /// CUDA code for advancing spiking synapses. @@ -28,11 +30,10 @@ __global__ void advanceSpikingSynapsesDevice(int totalSynapseCount, /// Allocate GPU memories to store all synapses' states, /// and copy them from host to GPU memory. -/// -/// @param allEdgesDevice GPU address of the AllSpikingSynapsesDeviceProperties struct -/// on device memory. -void AllSpikingSynapses::allocEdgeDeviceStruct(void **allEdgesDevice) +void AllSpikingSynapses::allocEdgeDeviceStruct() { + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void **allEdgesDevice = reinterpret_cast(&(gpuModel->getAllEdgesDevice())); allocEdgeDeviceStruct(allEdgesDevice, Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getMaxEdgesPerVertex()); } @@ -89,11 +90,11 @@ void AllSpikingSynapses::allocDeviceStruct(AllSpikingSynapsesDeviceProperties &a /// Delete GPU memories. /// -/// @param allEdgesDevice GPU address of the AllSpikingSynapsesDeviceProperties struct -/// on device memory. -void AllSpikingSynapses::deleteEdgeDeviceStruct(void *allEdgesDevice) +void AllSpikingSynapses::deleteEdgeDeviceStruct() { AllSpikingSynapsesDeviceProperties allEdges; + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); HANDLE_ERROR(cudaMemcpy(&allEdges, allEdgesDevice, sizeof(AllSpikingSynapsesDeviceProperties), cudaMemcpyDeviceToHost)); deleteDeviceStruct(allEdges); @@ -127,10 +128,10 @@ void AllSpikingSynapses::deleteDeviceStruct(AllSpikingSynapsesDeviceProperties & /// Copy all synapses' data from host to device. /// -/// @param allEdgesDevice GPU address of the AllSpikingSynapsesDeviceProperties struct -/// on device memory. -void AllSpikingSynapses::copyEdgeHostToDevice(void *allEdgesDevice) +void AllSpikingSynapses::copyEdgeHostToDevice() { // copy everything necessary + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); copyEdgeHostToDevice(allEdgesDevice, Simulator::getInstance().getTotalVertices(), Simulator::getInstance().getMaxEdgesPerVertex()); } @@ -200,12 +201,12 @@ void AllSpikingSynapses::copyHostToDevice(void *allEdgesDevice, /// Copy all synapses' data from device to host. /// -/// @param allEdgesDevice GPU address of the AllSpikingSynapsesDeviceProperties struct -/// on device memory. -void AllSpikingSynapses::copyEdgeDeviceToHost(void *allEdgesDevice) +void AllSpikingSynapses::copyEdgeDeviceToHost() { // copy everything necessary AllSpikingSynapsesDeviceProperties allEdges; + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void *allEdgesDevice = static_cast(gpuModel->getAllEdgesDevice()); HANDLE_ERROR(cudaMemcpy(&allEdges, allEdgesDevice, sizeof(AllSpikingSynapsesDeviceProperties), cudaMemcpyDeviceToHost)); copyDeviceToHost(allEdges); diff --git a/Simulator/Utils/Global.cpp b/Simulator/Utils/Global.cpp index 9f6572a0d..8b6b0db96 100644 --- a/Simulator/Utils/Global.cpp +++ b/Simulator/Utils/Global.cpp @@ -53,21 +53,6 @@ string coordToString(int x, int y, int z) return ss.str(); } -// MODEL INDEPENDENT FUNCTION NMV-BEGIN { -string neuronTypeToString(vertexType t) -{ - switch (t) { - case vertexType::INH: - return "INH"; - case vertexType::EXC: - return "EXC"; - default: - cerr << "ERROR->neuronTypeToString() failed, unknown type: " << t << endl; - assert(false); - return nullptr; // Must return a value -- this will probably cascade to another failure - } -} -// } NMV-END #if defined(USE_GPU) //! CUDA device ID int g_deviceId = 0; diff --git a/Simulator/Utils/Global.h b/Simulator/Utils/Global.h index c44532140..db1678432 100644 --- a/Simulator/Utils/Global.h +++ b/Simulator/Utils/Global.h @@ -91,38 +91,6 @@ extern uint64_t g_simulationStep; const int g_nMaxChunkSize = 100; -// NETWORK MODEL VARIABLES NMV-BEGIN { -// Vertex types. -// NEURO: -// INH - Inhibitory neuron -// EXC - Excitory neuron -// NG911: -// CALR: Caller radii -// PSAP: PSAP nodes -// EMS, FIRE, LAW: Responder nodes -/* -// Moved to Utils/VertexType.h -enum class vertexType { - // Neuro - INH = 1, - EXC = 2, - // NG911 - CALR = 3, - PSAP = 4, - EMS = 5, - FIRE = 6, - LAW = 7, - // UNDEF - VTYPE_UNDEF = 0 -}; -// Custom streaming operator<< for the enum class vertexType -inline std::ostream &operator<<(std::ostream &os, vertexType vT) -{ - os << static_cast(vT); - return os; -} -*/ - // Edge types. // NEURO: // II - Synapse from inhibitory neuron to inhibitory neuron. @@ -195,8 +163,6 @@ string index2dToString(int i, int width, int height); string coordToString(int x, int y); // Converts a 3-d coordinate into a string. string coordToString(int x, int y, int z); -// Converts a vertexType into a string. -string neuronTypeToString(vertexType t); template ostream &operator<<(ostream &os, const vector &v) { diff --git a/Simulator/Vertices/AllVertices.cpp b/Simulator/Vertices/AllVertices.cpp index 6d804e2d5..55b2ba160 100644 --- a/Simulator/Vertices/AllVertices.cpp +++ b/Simulator/Vertices/AllVertices.cpp @@ -9,6 +9,23 @@ #include "AllVertices.h" #include "OperationManager.h" +// Utility function to convert a vertexType into a string. +// MODEL INDEPENDENT FUNCTION NMV-BEGIN { +string vertexTypeToString(vertexType t) +{ + switch (t) { + case vertexType::INH: + return "INH"; + case vertexType::EXC: + return "EXC"; + default: + cerr << "ERROR->vertexTypeToString() failed, unknown type: " << t << endl; + assert(false); + return nullptr; // Must return a value -- this will probably cascade to another failure + } +} +// } NMV-END + // Default constructor AllVertices::AllVertices() : size_(0) { @@ -22,6 +39,30 @@ AllVertices::AllVertices() : size_(0) OperationManager::getInstance().registerOperation(Operations::printParameters, printParametersFunc); + // Register registerHistoryVariables function as a registerHistoryVariables operation in the OperationManager + function registerHistory = bind(&AllVertices::registerHistoryVariables, this); + OperationManager::getInstance().registerOperation(Operations::registerHistoryVariables, + registerHistory); + +#if defined(USE_GPU) + // Register allocNeuronDeviceStruct function as a allocateGPU operation in the OperationManager + function allocateGPU = bind(&AllVertices::allocVerticesDeviceStruct, this); + OperationManager::getInstance().registerOperation(Operations::allocateGPU, allocateGPU); + + // Register AllVertices::copyToDevice function as a copyToGPU operation in the OperationManager + function copyCPUtoGPU = bind(&AllVertices::copyToDevice, this); + OperationManager::getInstance().registerOperation(Operations::copyToGPU, copyCPUtoGPU); + + // Register copyFromGPU operation for transferring edge data from device to host + function copyFromGPU = bind(&AllVertices::copyFromDevice, this); + OperationManager::getInstance().registerOperation(Operations::copyFromGPU, copyFromGPU); + + // Register deleteNeuronDeviceStruct function as a deallocateGPUMemory operation in the OperationManager + function deallocateGPUMemory = bind(&AllVertices::deleteVerticesDeviceStruct, this); + OperationManager::getInstance().registerOperation(Operations::deallocateGPUMemory, + deallocateGPUMemory); +#endif + // Get a copy of the file and vertex logger to use log4cplus macros to print to debug files fileLogger_ = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("file")); vertexLogger_ = log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("vertex")); diff --git a/Simulator/Vertices/AllVertices.h b/Simulator/Vertices/AllVertices.h index 919bd8dce..7182adfa7 100644 --- a/Simulator/Vertices/AllVertices.h +++ b/Simulator/Vertices/AllVertices.h @@ -36,6 +36,9 @@ using namespace std; #include #endif +// Utility function to convert a vertexType into a string. +string vertexTypeToString(vertexType t); + class Layout; class AllEdges; struct AllVerticesDeviceProperties; @@ -110,14 +113,11 @@ class AllVertices { /// Allocate GPU memories to store all vertices' states, /// and copy them from host to GPU memory. - /// - /// @param allVerticesDevice GPU address of the allVertices struct on device memory. - virtual void allocVerticesDeviceStruct(void **allVerticesDevice) = 0; + virtual void allocVerticesDeviceStruct() = 0; /// Delete GPU memories. /// - /// @param allVerticesDevice GPU address of the allVertices struct on device memory. - virtual void deleteVerticesDeviceStruct(void *allVerticesDevice) = 0; + virtual void deleteVerticesDeviceStruct() = 0; /// Clear the spike counts out of all vertices. // @@ -125,14 +125,11 @@ class AllVertices { virtual void clearVertexHistory(void *allVerticesDevice) = 0; /// Copy all vertices' data from host to device. - /// - /// @param allVerticesDevice GPU address of the allVertices struct on device memory. - virtual void copyToDevice(void *allVerticesDevice) = 0; + virtual void copyToDevice() = 0; /// Copy all vertices' data from device to host. /// - /// @param allVerticesDevice GPU address of the allVertices struct on device memory. - virtual void copyFromDevice(void *allVerticesDevice) = 0; + virtual void copyFromDevice() = 0; /// Update the state of all vertices for a time step /// Notify outgoing edges if vertex has fired. @@ -184,4 +181,4 @@ struct AllVerticesDeviceProperties {}; template void AllVertices::serialize(Archive &archive) { archive(cereal::make_nvp("size", size_)); -} +} \ No newline at end of file diff --git a/Simulator/Vertices/NG911/All911Vertices.h b/Simulator/Vertices/NG911/All911Vertices.h index 1a537437a..b41f42e4a 100644 --- a/Simulator/Vertices/NG911/All911Vertices.h +++ b/Simulator/Vertices/NG911/All911Vertices.h @@ -225,10 +225,10 @@ class All911Vertices : public AllVertices { // GPU functionality for 911 simulation is unimplemented. // These signatures are required to make the class non-abstract public: - virtual void allocVerticesDeviceStruct(void **allVerticesDevice) {}; - virtual void deleteVerticesDeviceStruct(void *allVerticesDevice) {}; - virtual void copyToDevice(void *allVerticesDevice) {}; - virtual void copyFromDevice(void *allVerticesDevice) {}; + virtual void allocVerticesDeviceStruct() {}; + virtual void deleteVerticesDeviceStruct() {}; + virtual void copyToDevice() {}; + virtual void copyFromDevice() {}; virtual void advanceVertices(AllEdges &edges, void *allVerticesDevice, void *allEdgesDevice, float randNoise[], EdgeIndexMapDevice *edgeIndexMapDevice) {}; virtual void setAdvanceVerticesDeviceParams(AllEdges &edges) {}; @@ -260,4 +260,4 @@ class All911Vertices : public AllVertices { protected: #endif // defined(USE_GPU) -}; +}; \ No newline at end of file diff --git a/Simulator/Vertices/Neuro/AllIFNeurons.h b/Simulator/Vertices/Neuro/AllIFNeurons.h index 99f3b8140..bbe06c177 100644 --- a/Simulator/Vertices/Neuro/AllIFNeurons.h +++ b/Simulator/Vertices/Neuro/AllIFNeurons.h @@ -95,25 +95,20 @@ class AllIFNeurons : public AllSpikingNeurons { /// Allocate GPU memories to store all neurons' states, /// and copy them from host to GPU memory. - /// - /// @param allVerticesDevice GPU address of the allNeurons struct on device memory. - virtual void allocVerticesDeviceStruct(void **allVerticesDevice); + virtual void allocVerticesDeviceStruct(); /// Delete GPU memories. /// - /// @param allVerticesDevice GPU address of the allNeurons struct on device memory. - virtual void deleteVerticesDeviceStruct(void *allVerticesDevice); + virtual void deleteVerticesDeviceStruct(); /// Clear the spike counts out of all neurons. // /// @param allVerticesDevice GPU address of the allNeurons struct on device memory. virtual void clearVertexHistory(void *allVerticesDevice) override; //Copy all neurons' data from device to host. - //@param allVerticesDevice GPU address of the allNeurons struct on device memory. - virtual void copyFromDevice(void *deviceAddress) override; + virtual void copyFromDevice() override; //Copy all neurons' data from host to device. - // @param allVerticesDevice GPU address of the allNeurons struct on device memory. - virtual void copyToDevice(void *deviceAddress) override; + virtual void copyToDevice() override; protected: /// Allocate GPU memories to store all neurons' states. @@ -272,4 +267,4 @@ template void AllIFNeurons::serialize(Archive &archive) cereal::make_nvp("Tau", Tau_.getHostVector())); //Private variables are intentionally excluded from serialization as they are populated from configuration files. -} +} \ No newline at end of file diff --git a/Simulator/Vertices/Neuro/AllIFNeurons_d.cpp b/Simulator/Vertices/Neuro/AllIFNeurons_d.cpp index dd4bc2402..f770d4c78 100644 --- a/Simulator/Vertices/Neuro/AllIFNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllIFNeurons_d.cpp @@ -9,14 +9,18 @@ #include "AllIFNeurons.h" #include "Book.h" #include "DeviceVector.h" +#include "GPUModel.h" +#include "Simulator.h" /// Allocate GPU memories to store all neurons' states, /// and copy them from host to GPU memory. /// /// @param allVerticesDevice GPU address of the AllIFNeuronsDeviceProperties struct on device memory. -void AllIFNeurons::allocVerticesDeviceStruct(void **allVerticesDevice) +void AllIFNeurons::allocVerticesDeviceStruct() { AllIFNeuronsDeviceProperties allNeurons; + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void **allVerticesDevice = reinterpret_cast(&(gpuModel->getAllVerticesDevice())); allocDeviceStruct(allNeurons); HANDLE_ERROR(cudaMalloc(allVerticesDevice, sizeof(AllIFNeuronsDeviceProperties))); HANDLE_ERROR(cudaMemcpy(*allVerticesDevice, &allNeurons, sizeof(AllIFNeuronsDeviceProperties), @@ -73,10 +77,11 @@ void AllIFNeurons::allocDeviceStruct(AllIFNeuronsDeviceProperties &allVerticesDe /// Delete GPU memories. /// -/// @param allVerticesDevice GPU address of the AllVerticesDeviceProperties struct on device memory. -void AllIFNeurons::deleteVerticesDeviceStruct(void *allVerticesDevice) +void AllIFNeurons::deleteVerticesDeviceStruct() { AllIFNeuronsDeviceProperties allVerticesDeviceProps; + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void *allVerticesDevice = static_cast(gpuModel->getAllVerticesDevice()); HANDLE_ERROR(cudaMemcpy(&allVerticesDeviceProps, allVerticesDevice, sizeof(AllIFNeuronsDeviceProperties), cudaMemcpyDeviceToHost)); deleteDeviceStruct(allVerticesDeviceProps); @@ -125,9 +130,7 @@ void AllIFNeurons::deleteDeviceStruct(AllIFNeuronsDeviceProperties &allVerticesD } /// Copy all neurons' data from host to device. -/// -/// @param allVerticesDevice GPU address of the AllIFNeuronsDeviceProperties struct on device memory. -void AllIFNeurons::copyToDevice(void *allVerticesDevice) +void AllIFNeurons::copyToDevice() { C1_.copyToDevice(); C2_.copyToDevice(); @@ -146,15 +149,14 @@ void AllIFNeurons::copyToDevice(void *allVerticesDevice) Vreset_.copyToDevice(); numStepsInRefractoryPeriod_.copyToDevice(); - AllSpikingNeurons::copyToDevice(allVerticesDevice); + AllSpikingNeurons::copyToDevice(); } /// Copy all neurons' data from device to host. /// -/// @param allVerticesDevice GPU address of the AllIFNeuronsDeviceProperties struct on device memory. -void AllIFNeurons::copyFromDevice(void *allVerticesDevice) +void AllIFNeurons::copyFromDevice() { - AllSpikingNeurons::copyFromDevice(allVerticesDevice); + AllSpikingNeurons::copyFromDevice(); C1_.copyToHost(); C2_.copyToHost(); @@ -201,4 +203,4 @@ void AllIFNeurons::advanceVertices(AllEdges &synapses, void *allVerticesDevice, void *allEdgesDevice, float randNoise[], EdgeIndexMapDevice *edgeIndexMapDevice) { -} +} \ No newline at end of file diff --git a/Simulator/Vertices/Neuro/AllIZHNeurons.h b/Simulator/Vertices/Neuro/AllIZHNeurons.h index 349df0ee2..09d69040a 100644 --- a/Simulator/Vertices/Neuro/AllIZHNeurons.h +++ b/Simulator/Vertices/Neuro/AllIZHNeurons.h @@ -145,14 +145,11 @@ class AllIZHNeurons : public AllIFNeurons { /// Allocate GPU memories to store all neurons' states, /// and copy them from host to GPU memory. - // - /// @param allVerticesDevice GPU address of the allNeurons struct on device memory. - virtual void allocVerticesDeviceStruct(void **allVerticesDevice) override; + virtual void allocVerticesDeviceStruct() override; /// Delete GPU memories. // - /// @param allVerticesDevice GPU address of the allNeurons struct on device memory. - virtual void deleteVerticesDeviceStruct(void *allVerticesDevice) override; + virtual void deleteVerticesDeviceStruct() override; /// Copy spike history data stored in device memory to host. // @@ -171,13 +168,10 @@ class AllIZHNeurons : public AllIFNeurons { // Copy all neurons' data from device to host. // - /// @param allVerticesDevice GPU address of the allNeurons struct on device memory. - virtual void copyFromDevice(void *deviceAddress) override; + virtual void copyFromDevice() override; // Copy all neurons' data from host to device. - // - /// @param allVerticesDevice GPU address of the allNeurons struct on device memory. - virtual void copyToDevice(void *deviceAddress) override; + virtual void copyToDevice() override; protected: @@ -320,4 +314,4 @@ template void AllIZHNeurons::serialize(Archive &archive) cereal::make_nvp("u", u_.getHostVector()), cereal::make_nvp("C3", C3_.getHostVector())); //Private variables are intentionally excluded from serialization as they are populated from configuration files. -} +} \ No newline at end of file diff --git a/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp b/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp index c3b7fa622..f5325c54c 100644 --- a/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllIZHNeurons_d.cpp @@ -11,7 +11,8 @@ #include "AllVerticesDeviceFuncs.h" #include "Book.h" #include "DeviceVector.h" - +#include "GPUModel.h" +#include "Simulator.h" /// CUDA code for advancing izhikevich neurons /// @@ -39,12 +40,11 @@ __global__ void advanceIZHNeuronsDevice( /// Allocate GPU memories to store all neurons' states, /// and copy them from host to GPU memory. /// -/// @param allVerticesDevice GPU address of the AllIZHNeuronsDeviceProperties struct -/// on device memory. -void AllIZHNeurons::allocVerticesDeviceStruct(void **allVerticesDevice) +void AllIZHNeurons::allocVerticesDeviceStruct() { AllIZHNeuronsDeviceProperties allVerticesDeviceProps; - + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void **allVerticesDevice = reinterpret_cast(&(gpuModel->getAllVerticesDevice())); allocDeviceStruct(allVerticesDeviceProps); HANDLE_ERROR(cudaMalloc(allVerticesDevice, sizeof(AllIZHNeuronsDeviceProperties))); @@ -70,12 +70,11 @@ void AllIZHNeurons::allocDeviceStruct(AllIZHNeuronsDeviceProperties &allVertices /// Delete GPU memories. /// -/// @param allVerticesDevice GPU address of the AllVerticesDeviceProperties struct -/// on device memory. -void AllIZHNeurons::deleteVerticesDeviceStruct(void *allVerticesDevice) +void AllIZHNeurons::deleteVerticesDeviceStruct() { AllIZHNeuronsDeviceProperties allVerticesDeviceProps; - + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void *allVerticesDevice = static_cast(gpuModel->getAllVerticesDevice()); HANDLE_ERROR(cudaMemcpy(&allVerticesDeviceProps, allVerticesDevice, sizeof(AllIZHNeuronsDeviceProperties), cudaMemcpyDeviceToHost)); @@ -102,11 +101,9 @@ void AllIZHNeurons::deleteDeviceStruct(AllIZHNeuronsDeviceProperties &allVertice /// Copy all neurons' data from host to device. /// -/// @param allVerticesDevice GPU address of the AllIZHNeuronsDeviceProperties struct -/// on device memory. -void AllIZHNeurons::copyToDevice(void *allVerticesDevice) +void AllIZHNeurons::copyToDevice() { - AllIFNeurons::copyToDevice(allVerticesDevice); + AllIFNeurons::copyToDevice(); Aconst_.copyToDevice(); Bconst_.copyToDevice(); @@ -118,11 +115,9 @@ void AllIZHNeurons::copyToDevice(void *allVerticesDevice) /// Copy all neurons' data from device to host. /// -/// @param allVerticesDevice GPU address of the AllIZHNeuronsDeviceProperties struct -/// on device memory. -void AllIZHNeurons::copyFromDevice(void *allVerticesDevice) +void AllIZHNeurons::copyFromDevice() { - AllIFNeurons::copyFromDevice(allVerticesDevice); + AllIFNeurons::copyFromDevice(); Aconst_.copyToHost(); Bconst_.copyToHost(); @@ -301,4 +296,4 @@ __global__ void advanceIZHNeuronsDevice( // clear synaptic input for next time step sp = 0; } -///@} +///@} \ No newline at end of file diff --git a/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp b/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp index bf46fbacc..62648ef16 100644 --- a/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllLIFNeurons_d.cpp @@ -187,4 +187,4 @@ __global__ void advanceLIFNeuronsDevice( #endif // clear synaptic input for next time step sp = 0; -} +} \ No newline at end of file diff --git a/Simulator/Vertices/Neuro/AllSpikingNeurons.h b/Simulator/Vertices/Neuro/AllSpikingNeurons.h index 1d1b35726..394f7091d 100644 --- a/Simulator/Vertices/Neuro/AllSpikingNeurons.h +++ b/Simulator/Vertices/Neuro/AllSpikingNeurons.h @@ -63,6 +63,8 @@ class AllSpikingNeurons : public AllVertices { /// @param synapses Reference to the allEdges struct on host memory. virtual void setAdvanceVerticesDeviceParams(AllEdges &synapses); + virtual void copyFromDevice() override; + virtual void copyToDevice() override; /// Add psr of all incoming synapses to summation points. /// /// @param allVerticesDevice GPU address of the allVertices struct on device memory. @@ -71,9 +73,6 @@ class AllSpikingNeurons : public AllVertices { virtual void integrateVertexInputs(void *allVerticesDevice, EdgeIndexMapDevice *edgeIndexMapDevice, void *allEdgesDevice); - virtual void copyFromDevice(void *deviceAddress) override; - virtual void copyToDevice(void *deviceAddress) override; - protected: /// Clear the spike counts out of all neurons in device memory. /// (helper function of clearNeuronSpikeCounts) @@ -167,4 +166,4 @@ template void AllSpikingNeurons::serialize(Archive &archive) cereal::make_nvp("vertexEvents", vertexEvents_), cereal::make_nvp("summationPoints", summationPoints_.getHostVector()), cereal::make_nvp("fAllowBackPropagation", fAllowBackPropagation_)); -} +} \ No newline at end of file diff --git a/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp b/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp index c0a2f504d..9b0af580e 100644 --- a/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp +++ b/Simulator/Vertices/Neuro/AllSpikingNeurons_d.cpp @@ -10,6 +10,8 @@ #include "AllSpikingSynapses.h" #include "Book.h" #include "DeviceVector.h" +#include "GPUModel.h" +#include "Simulator.h" /// CUDA kernel for adding psr of all incoming synapses to summation points. /// @@ -32,9 +34,11 @@ __global__ void calcSummationPointDevice(int totalVertices, BGFLOAT *summationPo EdgeIndexMapDevice *edgeIndexMapDevice, AllSpikingSynapsesDeviceProperties *allEdgesDevice); -void AllSpikingNeurons::copyToDevice(void *deviceAddress) +void AllSpikingNeurons::copyToDevice() { AllSpikingNeuronsDeviceProperties allVerticesDevice; + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void *deviceAddress = static_cast(gpuModel->getAllVerticesDevice()); HANDLE_ERROR(cudaMemcpy(&allVerticesDevice, deviceAddress, sizeof(AllSpikingNeuronsDeviceProperties), cudaMemcpyDeviceToHost)); @@ -85,8 +89,10 @@ void AllSpikingNeurons::copyToDevice(void *deviceAddress) maxSpikes * sizeof(uint64_t), cudaMemcpyHostToDevice)); } } -void AllSpikingNeurons::copyFromDevice(void *deviceAddress) +void AllSpikingNeurons::copyFromDevice() { + GPUModel *gpuModel = static_cast(&Simulator::getInstance().getModel()); + void *deviceAddress = static_cast(gpuModel->getAllVerticesDevice()); int numVertices = Simulator::getInstance().getTotalVertices(); AllSpikingNeuronsDeviceProperties allVerticesDevice; @@ -248,4 +254,4 @@ __global__ void calcSummationPointDevice(int totalVertices, BGFLOAT *summationPo // Store summed PSR into this neuron's summation point summationPoints_[idx] = sum; } -} +} \ No newline at end of file diff --git a/docs/Developer/GHActions.md b/docs/Developer/GHActions.md index 3c41a8c83..c60593fed 100644 --- a/docs/Developer/GHActions.md +++ b/docs/Developer/GHActions.md @@ -12,5 +12,10 @@ The manual GitHub Pages action is a feature that came from wanting to quickly pu ## PlantUML Action plantUML.yml -The plantUML action occurs anytime a plantUML file is modified or added during a pull request or a push to the master branch. These .puml files are supposed to be located in the UML folder within the Developer folder. This action starts by checking out the repository using [actions/checkout](https://github.com/actions/checkout) with a fetch depth of 0. The next step is to grab all of the .puml files that need to be turned into images. This is done by using a basic bash command to grab all .puml files which is then piped into an awk script to parse out the unnecessary files and construct an output string with all the necessary files. The output string will look like so: "file1.puml file2.puml file3.puml file4.puml\n". This output string is then confirmed by an echo command which prints out the string to the actions terminal. Next, the .png and .svg files are generated from the .puml files in the output string using [cloudbees/plantuml-github-action](https://github.com/cloudbees/plantuml-github-action). These files are placed within the diagrams folder located within the UML folder. Lastly, the local changes are committed then pushed to the remote repository using [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action). +The plantUML action occurs anytime a plantUML file is modified or added during a pull request or a push to the master branch. These .puml files are supposed to be located in the UML folder within the Developer folder. This action starts by checking out the repository using [actions/checkout](https://github.com/actions/checkout) with a fetch depth of 0. The next step is to grab all of the .puml files that need to be turned into images. This is done by using a basic bash command to grab all .puml files which is then piped into an awk script to parse out the unnecessary files and construct an output string with all the necessary files. The output string will look like so: "file1.puml file2.puml file3.puml file4.puml\n". This output string is then confirmed by an echo command which prints out the string to the actions terminal. Next, the .png and .svg files are generated from the .puml files in the output string using a fork of [holowinski/plantuml-github-action]. These files are placed within the diagrams folder located within the UML folder. Lastly, the local changes are committed then pushed to the remote repository using [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action). + +[//]: # (Moving URL links to the bottom of the document for ease of updating - LS) +[//]: # (Links to repo items which exist outside of the docs folder need an absolute link.) + +[holowinski/plantuml-github-action]: diff --git a/docs/Developer/index.md b/docs/Developer/index.md index 70ff35500..a48dc6c7f 100644 --- a/docs/Developer/index.md +++ b/docs/Developer/index.md @@ -2,7 +2,7 @@ If you're developing Graphitti code, then here are your reference documents. -Writing new code? Then make sure to follow our [contributing guide](../../CONTRIBUTING.md) and *document your code here*. +Writing new code? Then make sure to follow our [contributing guide] and *document your code here*. Reading code that isn't obvious? When you figure out how it works, then *document it here* and *document it in comments in the code.* @@ -38,7 +38,7 @@ Students, use this [quickstart guide](StudentSetup.md) to help setup, use, and d - Doxygen - Documentation generated from source code - Doxygen provides web-based indices and hierarchical views of Graphitti's class and file structures - - [Visit Doxygen Generated Documentation](https://uwb-biocomputing.github.io/Graphitti/Doxygen/html/index.html) + - [Visit Doxygen Generated Documentation] - Document code in the `.h` file using the [Doxygen Style Guide](../Doxygen/DoxygenStyleGuide.md) format - [Doxygen Update Guide](../Doxygen/DoxygenUpdateGuide.md) - [Event buffering](eventBuffering.md) in vertex classes. @@ -50,3 +50,10 @@ Students, use this [quickstart guide](StudentSetup.md) to help setup, use, and d --------- [<< Go back to the Graphitti home page](../index.md) + +[//]: # (Moving URL links to the bottom of the document for ease of updating - LS) +[//]: # (Links to repo items which exist outside of the docs folder need an absolute link.) + +[contributing guide]: +[Visit Doxygen Generated Documentation]: + \ No newline at end of file diff --git a/docs/Doxygen/Doxyfile b/docs/Doxygen/Doxyfile index 1d6498f5a..fc041908f 100644 --- a/docs/Doxygen/Doxyfile +++ b/docs/Doxygen/Doxyfile @@ -864,7 +864,7 @@ WARN_LOGFILE = # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = Simulator/ +INPUT = Simulator/ docs/Doxygen/index.md # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses diff --git a/docs/Doxygen/index.md b/docs/Doxygen/index.md new file mode 100644 index 000000000..4930a38c9 --- /dev/null +++ b/docs/Doxygen/index.md @@ -0,0 +1,19 @@ +# Graphitti Doxygen Documentation + +Welcome to the Graphitti Doxygen documentation! + +This documentation provides an overview of the Graphitti project's codebase and related resources. It includes detailed information about classes, functions, and structures. + +## Documentation Sections + +- [Classes](classes.md): Explore the detailed code reference documentation for Graphitti's classes, structs, unions and interfaces. +- [Files](files.md): Here is the documentation for Graphitti's documented files with descriptions and documentation. + +Feel free to navigate through the documentation using the top navigation bar. If you have any questions or need further assistance, please refer to the project's official repository or documentation. + +## Additional Resources + +- [Graphitti GitHub Repository](https://github.com/UWB-Biocomputing/Graphitti): Explore the source code, report issues, and contribute to the project. +- [Graphitti Project Website](https://uwb-biocomputing.github.io/Graphitti/): Visit the official website for more information about the Graphitti project. + +We hope you find this documentation useful and informative. Happy coding! \ No newline at end of file diff --git a/docs/RebuildNotes/ConnectionsNotes.md b/docs/Notes/ConnectionsNotes.md similarity index 100% rename from docs/RebuildNotes/ConnectionsNotes.md rename to docs/Notes/ConnectionsNotes.md diff --git a/docs/RebuildNotes/GeneralNotes.md b/docs/Notes/GeneralNotes.md similarity index 100% rename from docs/RebuildNotes/GeneralNotes.md rename to docs/Notes/GeneralNotes.md diff --git a/docs/Glossary.md b/docs/Notes/Glossary.md similarity index 100% rename from docs/Glossary.md rename to docs/Notes/Glossary.md diff --git a/docs/RebuildNotes/LayoutsNotes.md b/docs/Notes/LayoutsNotes.md similarity index 100% rename from docs/RebuildNotes/LayoutsNotes.md rename to docs/Notes/LayoutsNotes.md diff --git a/docs/RebuildNotes/NeuronsNotes.md b/docs/Notes/NeuronsNotes.md similarity index 100% rename from docs/RebuildNotes/NeuronsNotes.md rename to docs/Notes/NeuronsNotes.md diff --git a/docs/RebuildNotes/RecordersNotes.md b/docs/Notes/RecordersNotes.md similarity index 100% rename from docs/RebuildNotes/RecordersNotes.md rename to docs/Notes/RecordersNotes.md diff --git a/docs/Resources.md b/docs/Notes/Resources.md similarity index 100% rename from docs/Resources.md rename to docs/Notes/Resources.md diff --git a/docs/RebuildNotes/SynapsesNotes.md b/docs/Notes/SynapsesNotes.md similarity index 100% rename from docs/RebuildNotes/SynapsesNotes.md rename to docs/Notes/SynapsesNotes.md diff --git a/docs/Notes/index.md b/docs/Notes/index.md new file mode 100644 index 000000000..6a088a1c0 --- /dev/null +++ b/docs/Notes/index.md @@ -0,0 +1,32 @@ +# 4. Glossary & Notes + +General notes for various parts of the Graphitti system. + +## 4.1 General Notes + + [General Notes](GeneralNotes.md) + + [Layout Notes](LayoutsNotes.md) + + [Connections Notes](ConnectionsNotes.md) + + [Neuron Notes](NeuronsNotes.md) + + [Synapses Notes](SynapsesNotes.md) + + [Recorder Notes](RecordersNotes.md) + +## 4.2 Glossary + + [Glossary](Glossary.md) + +## 4.3 Useful Resources + + [Recommended resources](Resources.md) to browse + +## Tools + +Here is documentation on the [GIS to GEXF](Tools/GIStoGraph.md) tool. This tool reads in Geographic Information Systems data, constructs a graph based on that data, and produces GEXF and GraphML formatted XML files that we can then pass into the Emergency Services Communication Systems simulation. + +--------- +[<< Go back to Graphitti home page](../index.md) \ No newline at end of file diff --git a/docs/Testing/index.md b/docs/Testing/index.md index 4903a29c7..19b553166 100644 --- a/docs/Testing/index.md +++ b/docs/Testing/index.md @@ -4,14 +4,14 @@ Information on test config files for regression testing, and testing that has be ## 3.1 Unit Tests -We use [GoogleTest](GoogleTestsTutorial.md) to develop our unit tests. +We use [GoogleTest](../Developer/GoogleTestsTutorial.md) to develop our unit tests. To integrate your unit tests using GoogleTest in Graphitti you can follow these steps: 1. Open the CMakeLists.txt file in the root directory of Graphitti 2. Locate at the bottom of the file where the `tests` executable is defined and add your test file to the list of source files. 3. Build and run your tests using the Graphitti build system and use `./tests` to run the unit tests. -Please note that Graphitti follows the [singleton design pattern](https://en.wikipedia.org/wiki/Singleton_pattern), and several of its classes, such as Simulator, ParameterManager, OperationManager, and GraphManager, are implemented as singletons. If your test scenario requires the instantiation of these classes, it may be necessary to create a separate executable specifically for your tests. +Please note that Graphitti follows the [singleton design pattern], and several of its classes, such as Simulator, ParameterManager, OperationManager, and GraphManager, are implemented as singletons. If your test scenario requires the instantiation of these classes, it may be necessary to create a separate executable specifically for your tests. By creating a separate executable, you can ensure that the singleton instances used in the test environment are isolated from the main application's singleton instances. This approach helps maintain the desired behavior and avoid segmentation fault errors. @@ -77,3 +77,8 @@ generated during the CPU execution, causing the result files to be different to --------- [<< Go back to the Graphitti home page](../index.md) + +[//]: # (Moving URL links to the bottom of the document for ease of updating - LS) +[//]: # (Links to repo items which exist outside of the docs folder need an absolute link.) + +[singleton design pattern]: \ No newline at end of file diff --git a/docs/User/installation.md b/docs/User/installation.md index 9870085c1..d995550df 100644 --- a/docs/User/installation.md +++ b/docs/User/installation.md @@ -1,5 +1,9 @@ # 1.2 Installation +For student installation and quickstart, see [student quickstart]. + +For all others, see below. + ## 1.2.1 Necessary Hardware/Software Graphitti is designed to be easy to use and fast to simulate with, but given its scope and flexibility, there are some tradeoffs. @@ -7,9 +11,9 @@ Graphitti is designed to be easy to use and fast to simulate with, but given its First, and perhaps most importantly, for the speedups that we desire, we found that **CUDA** was the most reasonable way to go. Hence,  if you want to use Graphitti for migrating your model to GPUs, you will need the following: - **Linux**: Currently, Graphitti only works on Linux. Any distro that supports **GNU-Make** and your chosen NVIDIA graphics card (if going the GPU route) should work. Make sure you have these packages: -- **NVIDIA GPU**: If you want your simulator to run on GPUs, you must use an NVIDIA GPU that is CUDA capable. Check NVIDIA's website for an up-to-date [list](https://developer.nvidia.com/cuda-gpus) of CUDA-compliant devices. -- [**CUDA**](https://developer.nvidia.com/cuda-downloads): if you intend to use the GPU functionality for high performance. Graphitti has been tested running on CUDA Version 8.0.44. -- [HDF5](https://support.hdfgroup.org/HDF5/): HDF5 is a data model, library, and file format for storing and managing data. For example, Matlab has built-in functions that can easily manage, view, and analyze data in HDF5 format. To install HDF5, simply follow the website instructions. If you don't wish to use HDF5, you can use the XML format which is also supported. +- **NVIDIA GPU**: If you want your simulator to run on GPUs, you must use an NVIDIA GPU that is CUDA capable. Check NVIDIA's website for an up-to-date [list] of CUDA-compliant devices. +- [**CUDA**]: if you intend to use the GPU functionality for high performance. Graphitti has been tested running on CUDA Version 8.0.44. +- [HDF5]: HDF5 is a data model, library, and file format for storing and managing data. For example, Matlab has built-in functions that can easily manage, view, and analyze data in HDF5 format. To install HDF5, simply follow the website instructions. If you don't wish to use HDF5, you can use the XML format which is also supported. To become a Graphitti user or collaborator, you might also need: @@ -19,11 +23,11 @@ To become a Graphitti user or collaborator, you might also need: Of course, Graphitti is totally open source. If you wanted, you could modify Graphitti and make an OpenCL version. ## 1.2.2 Download Graphitti -In order to get started with Graphitti, you will need to build it from scratch, which means getting its source codes. You can either download Graphitti source codes as a zip file of a stable release (See [the release page](https://github.com/UWB-Biocomputing/Graphitti/releases)) or fork the development version from Graphitti GitHub repository (See [Fork and clone Graphitti](#1221-fork-and-clone-graphitti)). +In order to get started with Graphitti, you will need to build it from scratch, which means getting its source codes. You can either download Graphitti source codes as a zip file of a stable release (See [the release page] or fork the development version from Graphitti GitHub repository (See [Fork and clone Graphitti](#1221-fork-and-clone-graphitti)). ### 1.2.2.1 Fork and clone Graphitti -If you are a Github user, you can simply fork and clone Graphitti. If you are new to Github, follow our Wiki page on [Contribute to Graphitti open source project](https://github.com/UWB-Biocomputing/BrainGrid/wiki/Contribute-to-BrainGrid-open-source-project). You can also go over our [Git Crash Course](https://github.com/UWB-Biocomputing/BrainGrid/wiki/Git-Crash-Course) for some useful tips. +If you are a Github user, you can simply fork and clone Graphitti. If you are new to Github, follow our Wiki page on [Contribute to Graphitti open source project]. You can also go over our [Git Crash Course] for some useful tips. ## 1.2.3 Install Graphitti -In order to compile and run Graphitti, you will need to set up a couple things in the [**CMakeLists.txt**](https://github.com/UWB-Biocomputing/Graphitti/blob/master/CMakeLists.txt) first. +In order to compile and run Graphitti, you will need to set up a couple things in the [**CMakeLists.txt**] first. 1. Change to Graphitti directory in your terminal @@ -39,10 +43,10 @@ In order to compile and run Graphitti, you will need to set up a couple things i - you might also need to add your CUDA home directory into the ```PATH``` environment variable 3. Graphitti is written in C++11 and CUDA C/C++. Make sure you have all these dependencies in order to compile Graphitti: - - [make](https://www.gnu.org/software/make/) - - [g++](https://gcc.gnu.org/) - - [h5c++](https://support.hdfgroup.org/HDF5/Tutor/compile.html): compile script for HDF5 C++ programs - - [nvcc](http://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/#axzz4ftSRZe00): if you are using GPU for high performance, nvcc is the compiler by Nvidia for use with CUDA + - [make] + - [g++] + - [h5c++]: compile script for HDF5 C++ programs + - [nvcc]: if you are using GPU for high performance, nvcc is the compiler by Nvidia for use with CUDA --------- [>> Next: 1.3 Quickstart](quickstart.md) @@ -51,4 +55,20 @@ In order to compile and run Graphitti, you will need to set up a couple things i [<< Go back to User Documentation page](index.md) --------- -[<< Go back to Graphitti home page](http://uwb-biocomputing.github.io/Graphitti/) \ No newline at end of file +[<< Go back to Graphitti home page](http://uwb-biocomputing.github.io/Graphitti/) + +[//]: # (Moving URL links to the bottom of the document for ease of updating - LS) +[//]: # (Links to repo items which exist outside of the docs folder need an absolute link.) + +[Contribute to Graphitti open source project]: +[**CMakeLists.txt**]: +[Git Crash Course]: +[the release page]: +[h5c++]: +[nvcc]: +[make]: +[g++]: +[**CUDA**]: +[HDF5]: +[list]: +[student quickstart]: (../Developer/StudentSetup.md) \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 3a720b891..3a4b860bf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,68 +10,37 @@ 1.3 [Quickstart](User/quickstart.md) - 1.3.1 [UWB Student Quickstart](Developer/StudentSetup.md) - 1.4 [Configuration](User/configuration.md) 2. [Developer Documentation](Developer/index.md) - 2.1 [Student Quick Start](Developer/StudentSetup.md) - - 2.2 [GitFlow Documentation](Developer/GitFlowDiagram.md) + 2.1 [GitFlow Documentation](Developer/GitFlowDiagram.md) - 2.3 [Code Formatting Etiquettes](Developer/codingConventions.md) + 2.2 [Code Formatting Etiquettes](Developer/codingConventions.md) - 2.4 [C++ design and Coding standards](Developer/cppStyleGuide.md) + 2.3 [C++ design and Coding standards](Developer/cppStyleGuide.md) - 2.5 [Graphitti Repository Tools and Workflows](Developer/index.md) + 2.4 [Graphitti Repository Tools and Workflows](Developer/index.md) - 2.6 [Graphitti System Documentation](Developer/index.md) + 2.5 [Graphitti System Documentation](Developer/index.md) - 2.7 [Unit Tests](Developer/UnitTests.md) - - 2.8 [Serialization](Developer/Serialization.md) - -4. [Testing](Testing/index.md) - - 3.1 Array Performance Testing - - 3.2 Dynamic Cast Performance Testing - - 3.3 Test Config Files - -5. Notes - - 4.1 [General Notes](RebuildNotes/GeneralNotes.md) + 2.6 [Unit Tests](Developer/UnitTests.md) - 4.2 [Layout Notes](RebuildNotes/LayoutsNotes.md) + 2.7 [Serialization](Developer/Serialization.md) - 4.3 [Connections Notes](RebuildNotes/ConnectionsNotes.md) +3. [Testing](Testing/index.md) - 4.4 [Neuron Notes](RebuildNotes/NeuronsNotes.md) + 3.1 [Array Performance Testing](Testing/ArrayPerformance/ArrayPerformance.md) - 4.5 [Synapses Notes](RebuildNotes/SynapsesNotes.md) + 3.2 [Dynamic Cast Performance Testing](Testing/CastingTest/CastingTest.md) - 4.6 [Recorder Notes](RebuildNotes/RecordersNotes.md) + 3.3 [Test Config Files](Testing/TestConfigFileParameters/testConfigFileParameters.md) -6. [Glossary](Glossary.md) +4. [Glossary & Notes](Notes/index.md) - 5.1 Graph Vocabulary +## [Code of Conduct] - 5.2 Neuroscience Vocabulary - - -## Extra Resources - -Here are some [recommended resources](Resources.md) to browse - -## Tools - -Here is documentation on the [GIS to GEXF](Tools/GIStoGraph.md) tool. This tool reads in Geographic Information Systems data, constructs a graph based on that data, and produces GEXF and GraphML formatted XML files that we can then pass into the Emergency Services Communication Systems simulation. - -## Code of Conduct - -Our [code of conduct](../CODE_OF_CONDUCT.md) +Our [code of conduct] by which Graphitti has been developed. ## [Acknowledgements](acknowledgements.md) @@ -79,3 +48,8 @@ Those who have helped make Graphitti what it is and shaping what it will be. --------- [<< Go back to UWB Intelligent Networks Lab home page](http://uwb-biocomputing.github.io/) + +[//]: # (Moving URL links to the bottom of the document for ease of updating - LS) +[//]: # (Links to repo items which exist outside of the docs folder need a direct link.) + +[Code of Conduct]: \ No newline at end of file From f269c67ecf331c76defd501c976a29d011420ad2 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 2 Jul 2025 12:31:26 -0700 Subject: [PATCH 37/37] moved deallocation of AsyncGenerator to deleteDeviceStruct --- Simulator/Core/GPUModel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Simulator/Core/GPUModel.cpp b/Simulator/Core/GPUModel.cpp index 68f2f6857..a421c0193 100644 --- a/Simulator/Core/GPUModel.cpp +++ b/Simulator/Core/GPUModel.cpp @@ -81,6 +81,7 @@ void GPUModel::deleteDeviceStruct() HANDLE_ERROR(cudaFree(synapseIMapDevice.incomingEdgeIndexMap_)); HANDLE_ERROR(cudaFree(edgeIndexMapDevice_)); HANDLE_ERROR(cudaStreamDestroy(simulationStream_)); + AsyncGenerator_.deleteDeviceStruct(); } /// Sets up the Simulation. @@ -137,7 +138,6 @@ void GPUModel::finish() OperationManager::getInstance().executeOperation(Operations::copyFromGPU); // deallocates memories on CUDA device OperationManager::getInstance().executeOperation(Operations::deallocateGPUMemory); - AsyncGenerator_.deleteDeviceStruct(); #ifdef PERFORMANCE_METRICS cudaEventDestroy(start);