Skip to content

Commit 8626237

Browse files
committed
0-copy, full history voltage plot
1 parent da10eac commit 8626237

5 files changed

Lines changed: 261 additions & 163 deletions

File tree

bindsnet/network/network.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,11 @@ def __init__(self, *args, **kwargs) -> None:
529529
# FULL spike history, (T, batch, n) one byte/spike. Populated lazily by a
530530
# raster widget via enable_spike_history(); see step() for the write path.
531531
self._spike_history = {}
532+
# Same idea for voltages: (T, batch, n) float32, written IN PLACE by the node
533+
# (LIFNodes.forward updates self.v in place), so the voltage a layer computes
534+
# lands straight in the GL buffer with no BindsNET->viz copy. Populated by a
535+
# voltage widget via enable_voltage_history(); see step() for the write path.
536+
self._voltage_history = {}
532537
self._step_t = 0 # internal timestep counter (used if step() called without t)
533538

534539
def migrate(self) -> None:
@@ -718,6 +723,93 @@ def _map_history(self, h: dict) -> torch.Tensor:
718723
h['view'] = view
719724
return view
720725

726+
def enable_voltage_history(self, layer_name: str, total_timesteps: int) -> dict:
727+
# language=rst
728+
"""
729+
Allocate a CUDA-registered GL buffer holding a layer's FULL voltage history
730+
and arrange for the node to write each timestep's voltage straight into it.
731+
732+
Unlike spikes (written via ``torch.ge(..., out=self.s)``), voltage is a
733+
recurrent state: ``v[t]`` is computed from ``v[t-1]``. So :meth:`step` seeds
734+
row ``t`` with row ``t-1`` (a buffer-internal device copy) and points
735+
``layer.v`` at that row; :class:`LIFNodes` then updates ``v`` *in place*
736+
(see ``nodes.py``), so the voltage it computes lands directly in the GL
737+
buffer -- no copy of the value out of BindsNET into a viz object. A
738+
full-history voltage visual reads it back via ``texelFetch``.
739+
740+
Layout is time-major ``(T, batch, n)`` float32. ``T`` is clamped so
741+
``batch * n * T`` fits ``GL_MAX_TEXTURE_BUFFER_SIZE``.
742+
743+
:param layer_name: Name of the layer to record (must already be added).
744+
:param total_timesteps: Desired history length (typically the full run).
745+
:return: ``{'vbo', 'T', 'n', 'row'}`` for the owning widget/visual.
746+
"""
747+
layer = self.layers[layer_name]
748+
n = int(layer.n)
749+
batch = int(self.batch_size)
750+
row = batch * n # floats (=texels) per timestep
751+
T = int(total_timesteps)
752+
753+
# Cap to the driver's texture-buffer limit (in texels; one float == one R32F
754+
# texel) so glTexBuffer can address it all.
755+
max_texels = int(gl.glGetIntegerv(gl.GL_MAX_TEXTURE_BUFFER_SIZE))
756+
if row * T > max_texels:
757+
T = max(1, max_texels // row)
758+
warnings.warn(
759+
f"Voltage history for '{layer_name}' capped to T={T} timesteps "
760+
f"({row}*{total_timesteps} floats exceeds GL_MAX_TEXTURE_BUFFER_SIZE="
761+
f"{max_texels}). History before the cap will not be retained."
762+
)
763+
764+
nbytes = row * T * 4 # float32
765+
766+
### Allocate a GL buffer, zero-initialised so unwritten rows read 0 ###
767+
vbo = int(gl.glGenBuffers(1))
768+
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbo)
769+
gl.glBufferData(gl.GL_ARRAY_BUFFER, nbytes,
770+
np.zeros(nbytes, dtype=np.uint8), gl.GL_DYNAMIC_DRAW)
771+
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
772+
if gl.glIsBuffer(vbo) == 0:
773+
raise RuntimeError("Failed to create voltage-history GL buffer")
774+
775+
### Register with CUDA (NONE flag: keep prior contents -- this is an
776+
### accumulating history, and rows carry voltage forward, not WRITE_DISCARD) ###
777+
self._ensure_cuda_context()
778+
err, res = driver.cuGraphicsGLRegisterBuffer(buffer=vbo, Flags=0)
779+
if err != 0:
780+
raise RuntimeError(f"cuGraphicsGLRegisterBuffer (voltage) failed: {err}")
781+
782+
self._voltage_history[layer_name] = {
783+
'vbo': vbo, 'res': res, 'T': T, 'n': n, 'batch': batch, 'row': row,
784+
'shape': tuple(layer.shape), # per-sample shape, e.g. (n,)
785+
# Scratch v used once t exceeds the (possibly capped) capacity, so the
786+
# sim's voltage recurrence keeps running -- it just stops recording.
787+
'scratch': torch.zeros(batch, *layer.shape, dtype=torch.float32,
788+
device=layer.v.device),
789+
}
790+
return {'vbo': vbo, 'T': T, 'n': n, 'row': row}
791+
792+
def _map_voltage_history(self, h: dict) -> torch.Tensor:
793+
# Map the GL buffer (CUDA takes ownership so the node can write it) and wrap
794+
# it as a (T, batch, *shape) float32 torch view. Caches the wrapped view and
795+
# rebuilds only when the mapped pointer actually moves (see _map_history).
796+
(err,) = driver.cuGraphicsMapResources(1, h['res'], 0)
797+
if err != 0:
798+
raise RuntimeError(f"map voltage history failed: {err}")
799+
err, ptr, size = driver.cuGraphicsResourceGetMappedPointer(h['res'])
800+
if err != 0:
801+
raise RuntimeError(f"get mapped pointer (voltage) failed: {err}")
802+
if h.get('ptr') == int(ptr) and h.get('view') is not None:
803+
return h['view']
804+
n_elems = h['row'] * h['T']
805+
cp_ptr = cp.cuda.MemoryPointer(
806+
cp.cuda.UnownedMemory(int(ptr), size, h['res']), 0)
807+
cp_arr = cp.ndarray(n_elems, dtype=cp.float32, memptr=cp_ptr)
808+
view = torch.as_tensor(cp_arr).view(h['T'], h['batch'], *h['shape'])
809+
h['ptr'] = int(ptr)
810+
h['view'] = view
811+
return view
812+
721813
def step(self, input: Dict[str, torch.Tensor], t: int = None) -> None:
722814
### Simulate network activity for one time step ###
723815
if t is None:
@@ -738,6 +830,12 @@ def step(self, input: Dict[str, torch.Tensor], t: int = None) -> None:
738830
else:
739831
self.layers[name].s = h['scratch']
740832

833+
# Map any voltage-history buffers so the layer's in-place voltage update can
834+
# land in the GL buffer (the actual repoint happens just before forward()).
835+
vviews = {}
836+
for name, h in self._voltage_history.items():
837+
vviews[name] = self._map_voltage_history(h)
838+
741839
current_inputs = {}
742840
current_inputs.update(self._get_inputs())
743841
for l in self.layers:
@@ -755,6 +853,22 @@ def step(self, input: Dict[str, torch.Tensor], t: int = None) -> None:
755853
h = self._spike_history[l]
756854
self.layers[l].s = views[l][t] if t < h['T'] else h['scratch']
757855

856+
# Point a recorded layer's `v` at THIS timestep's row, seeded with the
857+
# PREVIOUS timestep's voltage (recurrent state carried forward). The node
858+
# then updates `v` in place, so the computed voltage lands in the GL
859+
# buffer with no copy out of BindsNET. Past the capacity cap, fall back to
860+
# scratch (recording stopped) but keep the recurrence alive.
861+
if l in vviews:
862+
h = self._voltage_history[l]
863+
if t < h['T']:
864+
dst = vviews[l][t]
865+
dst.copy_(self.layers[l].v if t == 0 else vviews[l][t - 1])
866+
self.layers[l].v = dst
867+
else:
868+
if t == h['T']:
869+
h['scratch'].copy_(vviews[l][h['T'] - 1])
870+
self.layers[l].v = h['scratch']
871+
758872
if l in current_inputs:
759873
self.layers[l].forward(x=current_inputs[l])
760874
else:
@@ -773,6 +887,12 @@ def step(self, input: Dict[str, torch.Tensor], t: int = None) -> None:
773887
if err != 0:
774888
raise RuntimeError(f"unmap spike history failed: {err}")
775889

890+
# Same for voltage-history buffers (written in place by the node above).
891+
for name, h in self._voltage_history.items():
892+
(err,) = driver.cuGraphicsUnmapResources(1, h['res'], 0)
893+
if err != 0:
894+
raise RuntimeError(f"unmap voltage history failed: {err}")
895+
776896
self._step_t = t + 1
777897

778898
def run(self, inputs: Dict[str, torch.Tensor], time: int, **kwargs) -> None:

bindsnet/network/nodes.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -504,8 +504,12 @@ def forward(self, x: torch.Tensor) -> None:
504504
505505
:param x: Inputs to the layer.
506506
"""
507-
# Decay voltages.
508-
self.v = self.decay * (self.v - self.rest) + self.rest
507+
# Decay voltages -- IN PLACE so `self.v` keeps the same tensor object across
508+
# the step (mathematically identical to decay*(v - rest) + rest). The GUI's
509+
# zero-copy voltage history (GUINetwork.enable_voltage_history) points
510+
# `self.v` at a row of a CUDA/GL buffer before forward(); rebinding here would
511+
# discard that buffer and the computed voltage would never reach the plot.
512+
self.v.sub_(self.rest).mul_(self.decay).add_(self.rest)
509513

510514
# Integrate inputs.
511515
x.masked_fill_(self.refrac_count > 0, 0.0)

0 commit comments

Comments
 (0)