Skip to content

Commit

Permalink
Document global variables (#1321)
Browse files Browse the repository at this point in the history
  • Loading branch information
1uc authored Jun 17, 2024
1 parent bf67f44 commit 5ff221d
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 0 deletions.
181 changes: 181 additions & 0 deletions docs/contents/globals.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
Global Variables
----------------

The following global variables exist:

* ``GLOBAL`` visible from HOC/Python.
* ``LOCAL`` at file scope, called top-locals.

GLOBAL variables
================


GLOBAL variables behave in one of three different ways:

* **read-only** if the variable is not written to from inside the MOD file,
is considered read-only. It behaves like there's a single instance that's
shared across all threads. Currently, they're implemented as a static
``double``, i.e. a regular C/C++ global variable. This MOD file is (still)
thread-safe.

* **read-write** Setting a value via ``PARAMETER`` is not considered a
write-access. While setting the value from within the MOD file in all other
contexts, including ``INITIAL``, counts as a write-access.

* **THREAD_SAFE:**, if the MOD file is marked thread-safe, then the
assumption is that it's safe to create multiple instances of the global
variable, e.g. one per thread. We call these *thread-variables*. This MOD
file is considered thread-safe.

* **Not THREAD_SAFE:** if the MOD file is not stated to be ``THREADSAFE``,
then the assumption is that the global variables must behave like a
single instance would. As a result, these MOD files are not thread-safe.

The visibility of GLOBAL variables it the following:

* **ASSIGNED:** any GLOBAL variable that appears in an ASSIGNED block
is not visible from HOC/Python.

* **PARAMETER:** any GLOBAL variable that appears in a PARAMETER block
is visible (read/write) from HOC/Python. Any PARAMETER that's not
explicitly made a RANGE variable is considered a GLOBAL.

* **undefined:** any GLOBAL variable that's not listed in either an ASSIGNED
or PARAMETER block is treated as if it were ASSIGNED, i.e. it's not visible
from HOC/Python.

Top-LOCAL variables
===================

Top-LOCAL variables are LOCAL variables at file-scope. They are never visible
from HOC/Python and always treated as top-local variables.

Since top-locals can't be assigned a value from HOC/Python and assignment in
INITIAL blocks counts as a write-access, the only way of assigning a value to a
read-only top-local would be in the PARAMETER block. However, variables
mentioned in PARAMETER are always RANGE or GLOBAL variables; and it's not
allowed to have two global variables with the same name. Therefore, read-only
top-locals aren't possible.

Note that ``nocmodl`` promotes all top-local variables to thread-variables, even
if the MOD file isn't marked with ``THREADSAFE``. Hence, top-local variables are
always thread-variables.

Thread Variables
================
Thread variables can be safely used as scratch-pad memory. Historically,
they've been advertised as an optimization technique that reduces the memory
footprint.

The canonical example is ``hh.mod``. The common pattern is that they're used as
return values from a PROCEDURE:

.. code-block::
DERIVATIVE states {
rates(v)
m' = (minf-m)/mtau
}
PROCEDURE rates(v(mV)) {
TABLE minf, mtau DEPEND celsius FROM -100 TO 100 WITH 200
minf = ...
mtau = ...
}
What we see is that for every instance we compute the value ``minf`` and
``mtau``, before we use them in ``states``. Technically there's no need for one
copy per instance of the mechanism. For example in Python one could write:

.. code-block::
minf, mtau = rates(v[i])
dm[i] = (minf-m[i])/mtau
Therefore, if the author doesn't need to record the value of ``minf`` and
``mtau``, then using RANGE variables might be considered wasting memory. Under
these circumstances, and before multi-threading existed, the solution was to use
a GLOBAL. When multi-core processors arrived, these MOD files were suddenly not
thread-safe. The solution was to introduce a keyword ``THREADSAFE`` and create
one copy of the global per thread.


Initial Values
~~~~~~~~~~~~~~
Note that thread-variables ignore the initial value set in the PARAMETER block
entirely.

For INITIAL blocks the requirement is that:

.. code-block::
INITIAL {
gbl = 2.0
}
guarantees that all copies of ``gbl`` are assigned the value ``2.0``.

HOC/Python Access
~~~~~~~~~~~~~~~~~

Note that there's no synchronization when setting or writing to
thread-variables. What happens is that is acts the (or a) value of thread 0.
The value on other threads is either left unchanged when writing or ignored
when reading the global variable.


Implementation Details for NEURON
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

NMODL distinguishes between top-local variables and GLOBAL variables at the
level of the AST. Then for code-generation we introduce the concept of
"thread-variables". All top-locals are considered thread-variables. GLOBAL
variables that are **read-write** and THREADSAFE can be converted to thread
variables.

Since thread-variables are permitted to have multiple copies per thread, we can
generalize this to be multiple copies per thread and SIMD lane; or one copy per
instance of the mechanism for GPUs (effectively a RANGE variable similar to
what CoreNEURON does).

Registering GLOBAL variables for access from HOC/Python happens via

.. code-block::
static DoubScal hoc_scdoub[] = {
{"g_w_shared_global", &g_w_shared_global},
{0, 0}
};
static DoubVec hoc_vdoub[] = {
{"g_arr_shared_global", g_arr_shared_global, 3},
{0, 0, 0}
};
hoc_register_var(hoc_scdoub, hoc_vdoub, hoc_intfunc);
which means for each global we register a stable address (e.g. the address of
some static variable) individually. The elements of ARRAY valued globals must
be stored contiguously.

The strategy is the following: each instance of the mechanism is associated
with a specific, not necessarily unique, copy of the thread-variable. For SIMD
this allows us to compute the copy of the thread-variable using modulo
arithmetic; on a GPU one could either assign a copy to each variable; or use
scratch pad memory (e.g. ``__shared__`` memory when using CUDA).

Quirks
~~~~~~

Collection of slightly surprising behaviour:

* Thread variables effectively can't be use in NET_RECEIVE blocks, because
the code ``nocmodl`` produces will cause a SEGFAULT.


What Does CoreNEURON support?
=============================
CoreNEURON only supports read-only GLOBAL variables. Anything else needs to be
converted to a RANGE variable manually.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ About NMODL
contents/ions
contents/pointers
contents/cable_equations
contents/globals

.. toctree::
:maxdepth: 3
Expand Down

0 comments on commit 5ff221d

Please sign in to comment.