Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Time-driven pre and postsynaptic traces #377

Merged
merged 25 commits into from
Nov 25, 2020

Conversation

neworderofjamie
Copy link
Contributor

@neworderofjamie neworderofjamie commented Nov 24, 2020

#152 added support for pre and postsynaptic weight update model variables to allow better encapsulation of learning vs neuron dynamics. However, these were purely event-driven dynamics which, retrospectively:

  1. Even with "classic" event-driven STDP, is an inefficient approach for GPUs as loads of threads are going to waiting while a small subset are calculating exponentials - having all threads performing a much cheaper multiply every timestep is much more GPU-friendly (clearly, back in the heady days of 2018, optimising for SpiNNaker was too recent a memory)
  2. Delays are annoying as you need to copy the event-driven traces between delay buffers every timestep anyway
  3. Event-based traces are extra inefficient with continuous learning as you would need to calculate exponentials every timestep
  4. Purely event-driven dynamics don't let you use traces to capture e.g. low-pass filtered neuron voltage

In this P.R. we introduce new syntax for time-driven pre and postsynaptic traces:

Python

For example, the following simple additive STDP rule:

stdp_additive = genn_model.create_custom_weight_update_class(
    "STDPAdditive",
    param_names=["tauPlus", "tauMinus", "aPlus", "aMinus", "wMin", "wMax"],
    var_name_types=[("g", "scalar")],
    pre_var_name_types=[("preTrace", "scalar")],
    post_var_name_types=[("postTrace", "scalar")],
    derived_params=[("aPlusScaled", genn_model.create_dpf_class(lambda pars, dt: pars[2] * (pars[5] - pars[4]))()),
                    ("aMinusScaled", genn_model.create_dpf_class(lambda pars, dt: pars[3] * (pars[5] - pars[4]))())],

    sim_code="""
        $(addToInSyn, $(g));
        const scalar dt = $(t) - $(sT_post);
        if(dt > 0) {
            const scalar timing = exp(-dt / $(tauMinus));
            const scalar newWeight = $(g) - ($(aMinusScaled) * $(postTrace) * timing);
            $(g) = fmin($(wMax), fmax($(wMin), newWeight));
        }
        """,

    learn_post_code="""        
        const scalar dt = $(t) - $(sT_pre);
        if(dt > 0) {
            const scalar timing = exp(-dt / $(tauPlus));
            const scalar newWeight = $(g) + ($(aPlusScaled) * $(preTrace) * timing);
            $(g) = fmin($(wMax), fmax($(wMin), newWeight));
        }
        """,

    pre_spike_code="""
        const scalar dt = $(t) - $(sT_pre);
        $(preTrace) = $(preTrace) * exp(-dt / $(tauPlus)) + 1.0;
        """,

    post_spike_code="""
        const scalar dt = $(t) - $(sT_post);
        $(postTrace) = $(postTrace) * exp(-dt / $(tauMinus)) + 1.0;
        """,

    is_pre_spike_time_required=True,
    is_post_spike_time_required=True)

can now be implemented more efficiently as:

stdp_additive = genn_model.create_custom_weight_update_class(
    "STDPAdditive",
    param_names=["tauPlus", "tauMinus", "aPlus", "aMinus", "wMin", "wMax"],
    var_name_types=[("g", "scalar")],
    pre_var_name_types=[("preTrace", "scalar")],
    post_var_name_types=[("postTrace", "scalar")],
    derived_params=[("aPlusScaled", genn_model.create_dpf_class(lambda pars, dt: pars[2] * (pars[5] - pars[4]))()),
                    ("aMinusScaled", genn_model.create_dpf_class(lambda pars, dt: pars[3] * (pars[5] - pars[4]))()),
                    ("tauPlusDecay", genn_model.create_dpf_class(lambda pars, dt: np.exp(-dt / pars[0]))()),
                    ("tauMinusDecay", genn_model.create_dpf_class(lambda pars, dt: np.exp(-dt / pars[1]))())],

    sim_code="""
        $(addToInSyn, $(g));
        const scalar dt = $(t) - $(sT_post);
        if(dt > 0) {
            const scalar newWeight = $(g) - ($(aMinusScaled) * $(postTrace));
            $(g) = fmin($(wMax), fmax($(wMin), newWeight));
        }
        """,

    learn_post_code="""
        const scalar dt = $(t) - $(sT_pre);
        if(dt > 0) {
            const scalar newWeight = $(g) + ($(aPlusScaled) * $(preTrace));
            $(g) = fmin($(wMax), fmax($(wMin), newWeight));
        }
        """,

    pre_spike_code="""
        $(preTrace) += 1.0;
        """,
    pre_dynamics_code="""
        $(preTrace) *= $(tauPlusDecay);
        """,
        
    post_spike_code="""
        $(postTrace) += 1.0;
        """,
    post_dynamics_code="""
        $(postTrace) *= $(tauMinusDecay);
        """,
    
    is_pre_spike_time_required=True,
    is_post_spike_time_required=True)

C++

In C++, the following macros have the same effect:

SET_PRE_DYNAMICS_CODE("$(preTrace) *= $(tauPlusDecay);\n");
SET_POST_DYNAMICS_CODE("$(postTrace) *= $(tauMinusDecay);\n");

Syntactically, this works nicely but, the resultant generated code is a bit of a mess with lots of duplication of pre and postsynaptic traces and code in a neuron update kernel. A similar approach to #201 is required for traces, but I think that's one to fix in a future PR.

neworderofjamie and others added 22 commits October 2, 2020 10:46
…ateModel::Base::getPostDynamicsCode`` virtuals
…NeuronGroup::getInSynWithPostCode`` and ``NeuronGroup::getOutSynWithPreCode``
…o be manually copied between delay slots in the absence of a spike
# Conflicts:
#	src/genn/genn/code_generator/groupMerged.cc
… structure for tracking merged postsynaptic models can be much simplified
@neworderofjamie neworderofjamie linked an issue Nov 24, 2020 that may be closed by this pull request
@neworderofjamie neworderofjamie added this to the GeNN 4.4.0 milestone Nov 24, 2020
@codecov
Copy link

codecov bot commented Nov 24, 2020

Codecov Report

Merging #377 (99eac56) into master (5cbaa61) will increase coverage by 0.18%.
The diff coverage is 88.60%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #377      +/-   ##
==========================================
+ Coverage   86.46%   86.65%   +0.18%     
==========================================
  Files          70       70              
  Lines       12331    12407      +76     
==========================================
+ Hits        10662    10751      +89     
+ Misses       1669     1656      -13     
Impacted Files Coverage Δ
include/genn/genn/code_generator/groupMerged.h 91.21% <ø> (ø)
src/genn/genn/code_generator/groupMerged.cc 85.44% <72.50%> (+0.48%) ⬆️
src/genn/genn/neuronGroup.cc 92.71% <93.54%> (-0.22%) ⬇️
...c/genn/genn/code_generator/generateNeuronUpdate.cc 96.11% <97.00%> (+0.84%) ⬆️
include/genn/genn/code_generator/codeGenUtils.h 97.56% <100.00%> (+0.19%) ⬆️
include/genn/genn/neuronGroup.h 74.00% <100.00%> (ø)
include/genn/genn/weightUpdateModels.h 88.88% <100.00%> (+1.38%) ⬆️
src/genn/genn/code_generator/generateInit.cc 98.15% <100.00%> (+1.31%) ⬆️
src/genn/genn/code_generator/generateRunner.cc 96.58% <100.00%> (-0.01%) ⬇️
.../genn/genn/code_generator/generateSynapseUpdate.cc 97.89% <100.00%> (+0.84%) ⬆️
... and 5 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 5cbaa61...99eac56. Read the comment docs.

@neworderofjamie neworderofjamie marked this pull request as ready for review November 24, 2020 14:05
Copy link
Member

@tnowotny tnowotny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for being late with this observation but how is the code set by SET_PRE_SPIKE_CODE treated any differently from that set with SET_SIM_CODE.
Similarly, how is the code from SET_POST_SPIKE_CODE different from the code set in SET_POST_LEARN ?

For the new time-driven code snippets, I don't quite see how either of them (PRE or POST) are to be treated differently from the code set generally through SET_SYNAPSE_DYNAMICS_CODE. What am I missing? Or do you feel it benefits the user to have these options to further mark up the roles of each code snippet?

@neworderofjamie
Copy link
Contributor Author

The main difference is that SET_XX_SPIKE_CODE and SET_XX_DYNAMICS_CODE are run per-neuron (infact in the neuron kernel)

@tnowotny
Copy link
Member

oh - I see ... so access to variables should be constrained to pre or post-synaptic variables for pre or post-synaptic code ...?

@neworderofjamie
Copy link
Contributor Author

exactly - you can access parameters, pre/postsynaptic weight update model variables and pre/postsynaptic neuron variables as appropriate

Copy link
Member

@tnowotny tnowotny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah ... makes sense now. One typo in a comment.

@neworderofjamie neworderofjamie merged commit 20de634 into master Nov 25, 2020
@neworderofjamie neworderofjamie deleted the pre_post_spike_dynamics branch November 25, 2020 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Weight update model traces with continuous dynamics
2 participants