@@ -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 :
0 commit comments