perforatedai.library_perforatedai

  1# Copyright (c) 2025 Perforated AI
  2
  3import math
  4import pdb
  5from itertools import chain
  6
  7import torch
  8import torch.nn as nn
  9import torch.nn.init as init
 10import torch.nn.functional as F
 11import torchvision.models.resnet as resnet_pt
 12from abc import ABC, abstractmethod
 13
 14from perforatedai import globals_perforatedai as GPA
 15
 16"""
 17Details on processors can be found in customization.md in the API directory.
 18
 19They exist to enable simplicity in adding dendrites to modules where
 20forward() is not one tensor in and one tensor out.
 21
 22The main module has one instance, which uses post_n1 and post_n2
 23and each new Dendrite node gets a unique instance to use pre_d and post_d.
 24"""
 25
 26
 27class PAIProcessor(ABC):
 28    """
 29    Abstract base class for processing neuron and dendrite operations.
 30
 31    Processors handle state management and data flow between neurons and
 32    dendrites, allowing for custom pre/post processing of modules which have
 33    multiple inputs and outputs, rather than the default single tensor input/output.
 34    Subclasses should implement the five core processing methods to handle
 35    their specific state management needs.
 36    """
 37
 38    @abstractmethod
 39    def post_n1(self, *args, **kwargs):
 40        """
 41        Post-process neuron output before dendrite processing.
 42
 43        Called immediately after the main module/neuron is executed and before
 44        any dendrite processing occurs. This method should extract and return
 45        only the tensor of the neuron output that should be seen by
 46        dendrite operations.
 47
 48        Parameters
 49        ----------
 50        *args : tuple
 51            Positional arguments, typically containing the neuron output.
 52        **kwargs : dict
 53            Keyword arguments from the neuron output.
 54
 55        Returns
 56        -------
 57        Any
 58            The filtered output to be passed to dendrite processing.
 59        """
 60        pass
 61
 62    @abstractmethod
 63    def post_n2(self, *args, **kwargs):
 64        """
 65        Post-process dendrite-modified output before final return.
 66
 67        Called after dendrite processing is complete and before passing the
 68        final value forward in the network. This method should combine the
 69        dendrite-modified output with any stored state to produce the complete
 70        output that matches the expected format of the main module.
 71
 72        Parameters
 73        ----------
 74        *args : tuple
 75            Positional arguments containing the dendrite-modified output.
 76        **kwargs : dict
 77            Keyword arguments from the processing chain.
 78
 79        Returns
 80        -------
 81        Any
 82            The complete output in the format expected by downstream components.
 83        """
 84        pass
 85
 86    @abstractmethod
 87    def pre_d(self, *args, **kwargs):
 88        """
 89        Pre-process input before dendrite operations.
 90
 91        Filters and prepares inputs for dendrite processing. This method handles
 92        special cases such as initial time steps vs. subsequent iterations,
 93        ensuring dendrites receive the appropriate inputs (e.g., external inputs
 94        vs. internal recurrent state).
 95
 96        Parameters
 97        ----------
 98        *args : tuple
 99            Positional arguments containing inputs to the PAI module.
100        **kwargs : dict
101            Keyword arguments containing inputs to the PAI module.
102
103        Returns
104        -------
105        tuple
106            A tuple of (processed_args, processed_kwargs) to pass to dendrite.
107        """
108        pass
109
110    @abstractmethod
111    def post_d(self, *args, **kwargs):
112        """
113        Post-process dendrite output and manage state.
114
115        Processes the output from dendrite operations, storing any state needed
116        for future iterations and returning only the portion that should be
117        combined with the neuron output. E.g. this is where recurrent state is
118        saved for the next time step.
119
120        Parameters
121        ----------
122        *args : tuple
123            Positional arguments containing the dendrite output.
124        **kwargs : dict
125            Keyword arguments from the dendrite output.
126
127        Returns
128        -------
129        Any
130            The filtered dendrite output to be added to the neuron output.
131        """
132        pass
133
134    @abstractmethod
135    def clear_processor(self):
136        """
137        Clear all internal processor state.
138
139        Resets the processor by removing all stored state variables. Must
140        be called before saving or safe_tensors will run into errors.
141        Implementations should safely check for attribute existence before
142        deletion to avoid errors.
143
144        Parameters
145        ----------
146        None
147
148        Returns
149        -------
150        None
151            This function does not return a value.
152        """
153        pass
154
155
156# General multi output processor for any number that ignores later ones
157class MultiOutputProcessor:
158    """Processor for handling multiple outputs, ignoring later ones."""
159
160    def post_n1(self, *args, **kwargs):
161        """Saves extra outputs and returns the first output.
162
163        Parameters
164        ----------
165        *args : tuple
166            Contains the modules output tuple.
167        **kwargs : dict
168            Unused keyword arguments.
169
170        Returns
171        -------
172        torch.Tensor
173            The first tensor of the tuple
174        """
175        out = args[0][0]
176        extra_out = args[0][1:]
177        self.extra_out = extra_out
178        return out
179
180    def post_n2(self, *args, **kwargs):
181        """Combine output with stored extra outputs.
182
183        Parameters
184        ----------
185        *args : torch.tensor
186            The first tensor combined with dendrite output.
187        **kwargs : dict
188            Unused keyword arguments.
189
190        Returns
191        -------
192        tuple
193            The recombined output tuple wth the new first output modified
194        """
195        out = args[0]
196        if isinstance(self.extra_out, tuple):
197            return (out,) + self.extra_out
198        else:
199            return (out,) + (self.extra_out,)
200
201    def pre_d(self, *args, **kwargs):
202        """Pass through arguments unchanged for dendrite preprocessing.
203
204        Parameters
205        ----------
206        *args : tuple
207            Positional arguments containing inputs to the PAI module.
208        **kwargs : dict
209            Keyword arguments containing inputs to the PAI module.
210
211        Returns
212        -------
213        args : tuple
214            Positional arguments containing inputs to the PAI module.
215        kwargs : dict
216            Keyword arguments containing inputs to the PAI module.
217        """
218        return args, kwargs
219
220    def post_d(self, *args, **kwargs):
221        """Extract first output for dendrite postprocessing.
222
223        Parameters
224        ----------
225        *args : tuple
226            Contains the dendrite modules output tuple.
227        **kwargs : dict
228            Unused keyword arguments.
229
230        Returns
231        -------
232        torch.Tensor
233            The first tensor of the tuple
234        """
235        out = args[0][0]
236        return out
237
238    def clear_processor(self):
239        """Clear stored processor state.
240
241        Parameters
242        ----------
243        None
244
245        Returns
246        -------
247        None
248            This function does not return a value.
249        """
250
251        if hasattr(self, "extra_out"):
252            delattr(self, "extra_out")
253
254class LSTMCellProcessor(PAIProcessor):
255    """Processor for LSTM cells to handle hidden and cell states."""
256
257    def post_n1(self, *args, **kwargs):
258        """
259        Extract hidden state from LSTM output for dendrite processing.
260
261        Separates the hidden state (h_t) from the cell state (c_t) in the
262        LSTM output tuple. Stores the cell state temporarily since only the
263        hidden state should be modified by dendrites.
264
265        Parameters
266        ----------
267        *args : tuple
268            Contains LSTM output tuple (h_t, c_t) as first element.
269        **kwargs : dict
270            Unused keyword arguments.
271
272        Returns
273        -------
274        torch.Tensor
275            Hidden state h_t to be passed to dendrite processing.
276        """
277        h_t = args[0][0]
278        c_t = args[0][1]
279        # Store the cell state temporarily and just use the hidden state
280        # to do Dendrite functions
281        self.c_t_n = c_t
282        return h_t
283
284    def post_n2(self, *args, **kwargs):
285        """
286        Recombine dendrite-modified hidden state with cell state.
287
288        Takes the hidden state that has been modified by dendrite operations
289        and combines it with the stored cell state to produce the complete
290        LSTM output tuple.
291
292        Parameters
293        ----------
294        *args : tuple
295            Contains the dendrite-modified hidden state h_t.
296        **kwargs : dict
297            Unused keyword arguments.
298
299        Returns
300        -------
301        tuple
302            Complete LSTM output (h_t, c_t) where h_t has been modified.
303        """
304        h_t = args[0]
305        return h_t, self.c_t_n
306
307    def pre_d(self, *args, **kwargs):
308        """
309        Filter LSTMCell input for dendrite based on initialization state.
310
311        Checks if this is the first time step (all zeros in h_t) or a
312        subsequent step. For the first step, passes through the original
313        inputs. For subsequent steps, replaces the neuron's hidden state
314        with the dendrite's own internal state from the previous iteration.
315
316        Parameters
317        ----------
318        *args : tuple
319            Contains (input, (h_t, c_t)) where input is the external input
320            and (h_t, c_t) is the neuron's recurrent state.
321        **kwargs : dict
322            Keyword arguments to pass through.
323
324        Returns
325        -------
326        tuple
327            ((processed_input, processed_state), kwargs) for dendrite call.
328        """
329        h_t = args[1][0]
330        # If its the initial step then just use the normal input and zeros
331        if h_t.sum() == 0:
332            return args, kwargs
333        # If its not the first one then return the input it got with its own
334        # h_t and c_t to replace neurons
335        else:
336            return (args[0], (self.h_t_d, self.c_t_d)), kwargs
337
338    def post_d(self, *args, **kwargs):
339        """
340        Extract and store dendrite's LSTM state for next iteration.
341
342        Separates the dendrite's hidden and cell states from its output tuple,
343        stores both for use in the next time step, and returns only the hidden
344        state to be combined with the neuron's output.
345
346        Parameters
347        ----------
348        *args : tuple
349            Contains dendrite LSTM output tuple (h_t, c_t).
350        **kwargs : dict
351            Unused keyword arguments.
352
353        Returns
354        -------
355        torch.Tensor
356            Hidden state h_t to be added to the neuron output.
357        """
358        h_t = args[0][0]
359        c_t = args[0][1]
360        self.h_t_d = h_t
361        self.c_t_d = c_t
362        return h_t
363
364    def clear_processor(self):
365        """
366        Clear all stored LSTM states.
367
368        Removes dendrite hidden state (h_t_d), dendrite cell state (c_t_d),
369        and temporarily stored neuron cell state (c_t_n). Safe to call even
370        if attributes don't exist.
371
372        Parameters
373        ----------
374        None
375
376        Returns
377        -------
378        None
379            This function does not return a value.
380        """
381        if hasattr(self, "h_t_d"):
382            delattr(self, "h_t_d")
383        if hasattr(self, "c_t_d"):
384            delattr(self, "c_t_d")
385        if hasattr(self, "c_t_n"):
386            delattr(self, "c_t_n")
387
388
389
390class LSTMProcessor(PAIProcessor):
391    """Processor for LSTM to handle hidden and output states."""
392
393    def post_n1(self, *args, **kwargs):
394        """
395        Extract hidden state from LSTM output for dendrite processing.
396
397        Separates the hidden state from the output in the
398        LSTM output tuple. Stores the hidden state temporarily since only the
399        output state should be modified by dendrites.
400
401        Parameters
402        ----------
403        *args : tuple
404            Contains LSTM output tuple (output, hidden) as first element.
405        **kwargs : dict
406            Unused keyword arguments.
407
408        Returns
409        -------
410        torch.Tensor
411            Output state to be passed to dendrite processing.
412        """
413        output = args[0][0]
414        hidden = args[0][1]
415        # Store the hidden state temporarily and just use the output state
416        # to do Dendrite functions
417        self.hidden_n = hidden
418        return output
419
420    def post_n2(self, *args, **kwargs):
421        """
422        Recombine dendrite-modified output with hidden tuple.
423
424        Takes the output state that has been modified by dendrite operations
425        and combines it with the stored hidden state to produce the complete
426        LSTM output tuple.
427
428        Parameters
429        ----------
430        *args : tuple
431            Contains the dendrite-modified output state.
432        **kwargs : dict
433            Unused keyword arguments.
434
435        Returns
436        -------
437        tuple
438            Complete LSTM output (output, hidden) where output has been modified.
439        """
440        output = args[0]
441        return output, self.hidden_n
442
443    def pre_d(self, *args, **kwargs):
444        """
445        LSTM input is just the tensor which also goes to the dendrite
446
447        Parameters
448        ----------
449        *args : 
450            Input tensor
451        **kwargs : dict
452            Empty
453
454        Returns
455        -------
456        tuple
457            (output, hidden)
458        """
459        return args, kwargs
460        
461    def post_d(self, *args, **kwargs):
462        """
463        Extract dendrite's output to combine.
464
465        Parameters
466        ----------
467        *args : tuple
468            Contains dendrite LSTM output tuple (output, hidden).
469        **kwargs : dict
470            Unused keyword arguments.
471
472        Returns
473        -------
474        torch.Tensor
475            Output state to be added to the neuron output.
476        """
477        output = args[0][0]
478        hidden = args[0][1]
479        return output
480
481    def clear_processor(self):
482        """
483        Clear all stored LSTM states.
484
485
486        Parameters
487        ----------
488        None
489
490        Returns
491        -------
492        None
493            This function does not return a value.
494        """
495        if hasattr(self, "hidden_n"):
496            delattr(self, "hidden_n")
497
498
499class LSTMProcessorLastHidden(PAIProcessor):
500    """Processor for LSTM to forward the last hidden."""
501
502    def post_n1(self, *args, **kwargs):
503        """
504        Extract the last hidden to combine with dendrites
505
506        Parameters
507        ----------
508        *args : tuple
509            Contains LSTM output tuple (output, hidden) as first element.
510        **kwargs : dict
511            Unused keyword arguments.
512
513        Returns
514        -------
515        torch.Tensor
516            Output state to be passed to dendrite processing.
517        """
518        ignored_output = args[0][0]
519        last_hidden = args[0][1][-1]
520
521        return last_hidden
522
523    def post_n2(self, *args, **kwargs):
524        """
525        Recombine dendrite-modified last hidden, and append None just to maintain output format
526
527        Parameters
528        ----------
529        *args : tuple
530            Contains the dendrite-modified output state.
531        **kwargs : dict
532            Unused keyword arguments.
533
534        Returns
535        -------
536        tuple
537            Complete LSTM output (output, hidden) where output has been modified.
538        """
539        combined_last_hidden = args[0]
540        return None, combined_last_hidden
541
542    def pre_d(self, *args, **kwargs):
543        """
544        LSTM input is just the tensor which also goes to the dendrite
545
546        Parameters
547        ----------
548        *args : 
549            Input tensor
550        **kwargs : dict
551            Empty
552
553        Returns
554        -------
555        tuple
556            (output, hidden)
557        """
558        return args, kwargs
559        
560    def post_d(self, *args, **kwargs):
561        """
562        Extract extract the dendrites last hidden to combine with neurons.
563
564        Parameters
565        ----------
566        *args : tuple
567            Contains dendrite LSTM output tuple (output, hidden).
568        **kwargs : dict
569            Unused keyword arguments.
570
571        Returns
572        -------
573        torch.Tensor
574            Output state to be added to the neuron output.
575        """
576        ignored_output = args[0][0]
577        last_hidden = args[0][1][-1]
578        return last_hidden
579
580    def clear_processor(self):
581        """Clear processor state.
582
583        Notes
584        -----
585        This processor keeps no persistent internal state.
586
587        Parameters
588        ----------
589        None
590
591        Returns
592        -------
593        None
594            This function does not return a value.
595        """
596        # Nothing is stored
597        pass
598
599class ResNetPAI(nn.Module):
600    """PB-compatible ResNet wrapper.
601
602    All normalization layers should be wrapped in a PAISequential, or other
603    wrapped module. When working with a predefined model the following shows
604    an example of how to create a module for modules_to_replace.
605    """
606
607    def __init__(self, other_resnet):
608        """Initialize ResNetPAI from existing ResNet model.
609
610        Parameters
611        ----------
612        *args : other_resnet : torchvision.models.resnet.ResNet
613            An existing ResNet model to convert to PAI-compatible format.
614        """
615        super(ResNetPAI, self).__init__()
616
617        # For the most part, just copy the exact values from the original module
618        self._norm_layer = other_resnet._norm_layer
619        self.inplanes = other_resnet.inplanes
620        self.dilation = other_resnet.dilation
621        self.groups = other_resnet.groups
622        self.base_width = other_resnet.base_width
623
624        # For the component to be changed, define a PAISequential with the old
625        # modules included
626        self.b1 = GPA.PAISequential([other_resnet.conv1, other_resnet.bn1])
627
628        self.relu = other_resnet.relu
629        self.maxpool = other_resnet.maxpool
630
631        for i in range(1, 5):
632            layer_name = "layer" + str(i)
633            original_layer = getattr(other_resnet, layer_name)
634            pb_layer = self._make_layer_pb(original_layer, other_resnet, i)
635            setattr(self, layer_name, pb_layer)
636
637        self.avgpool = other_resnet.avgpool
638        self.fc = other_resnet.fc
639
640    def _make_layer_pb(self, other_block_set, other_resnet, block_id):
641        """Convert ResNet layer blocks to PB-compatible format.
642
643        Parameters
644        ----------
645        other_block_set : torch.vision.models.resnet.any_block
646            A set of blocks from the original ResNet model.
647        other_resnet : torchvision.models.resnet.ResNet
648            The original ResNet model.
649        block_id : int
650            The layer number being converted.
651        Returns
652        -------
653        nn.Sequential
654            A sequential container with the converted blocks.
655        """
656        layers = []
657        for i in range(len(other_block_set)):
658            block_type = type(other_block_set[i])
659            if block_type == resnet_pt.BasicBlock:
660                layers.append(other_block_set[i])
661            elif block_type == resnet_pt.Bottleneck:
662                layers.append(other_block_set[i])
663            else:
664                print(
665                    "Your resnet uses a block type that has not been "
666                    "accounted for. Customization might be required."
667                )
668                layer_name = "layer" + str(block_id)
669                print(type(getattr(other_resnet, layer_name)))
670                pdb.set_trace()
671        return nn.Sequential(*layers)
672
673    def _forward_impl(self, x):
674        """Implementation of the forward pass.
675
676        Parameters
677        ----------
678        x : torch.Tensor
679            Input tensor to the network.
680
681        Returns
682        -------
683        torch.Tensor
684            Output tensor from the network.
685        """
686        # Modified b1 rather than conv1 and bn1
687        x = self.b1(x)
688        # Rest of forward remains the same
689        x = F.relu(x)
690        x = self.maxpool(x)
691
692        x = self.layer1(x)
693        x = self.layer2(x)
694        x = self.layer3(x)
695        x = self.layer4(x)
696
697        x = self.avgpool(x)
698        x = torch.flatten(x, 1)
699        x = self.fc(x)
700
701        return x
702
703    def forward(self, x):
704        """Forward pass through the network.
705
706        Parameters
707        ----------
708        x : torch.Tensor
709            Input tensor to the network.
710
711        Returns
712        -------
713        torch.Tensor
714            Output tensor from the network.
715        """
716        return self._forward_impl(x)
717
718
719class ResNetPAIPreFC(nn.Module):
720    """PB-compatible ResNet wrapper.
721
722    All normalization layers should be wrapped in a PAISequential, or other
723    wrapped module. When working with a predefined model the following shows
724    an example of how to create a module for modules_to_replace.
725    """
726
727    def __init__(self, other_resnet):
728        """Initialize ResNetPAI from existing ResNet model.
729
730        Parameters
731        ----------
732        *args : other_resnet : torchvision.models.resnet.ResNet
733            An existing ResNet model to convert to PAI-compatible format.
734        """
735        super(ResNetPAIPreFC, self).__init__()
736
737        # For the most part, just copy the exact values from the original module
738        self._norm_layer = other_resnet._norm_layer
739        self.inplanes = other_resnet.inplanes
740        self.dilation = other_resnet.dilation
741        self.groups = other_resnet.groups
742        self.base_width = other_resnet.base_width
743
744        # For the component to be changed, define a PAISequential with the old
745        # modules included
746        self.conv1 = other_resnet.conv1
747        self.bn1 = other_resnet.bn1
748
749        self.relu = other_resnet.relu
750        self.maxpool = other_resnet.maxpool
751
752        for i in range(1, 5):
753            layer_name = "layer" + str(i)
754            original_layer = getattr(other_resnet, layer_name)
755            pb_layer = self._make_layer_pb(original_layer, other_resnet, i)
756            setattr(self, layer_name, pb_layer)
757
758        self.avgpool = other_resnet.avgpool
759        
760        # Create pre_fc layer with dimensions matching layer4 output (same as fc input)
761        fc_in_features = other_resnet.fc.in_features
762        self.pre_fc = nn.Linear(fc_in_features, fc_in_features)
763        
764        self.fc = other_resnet.fc
765
766    def _make_layer_pb(self, other_block_set, other_resnet, block_id):
767        """Convert ResNet layer blocks to PB-compatible format.
768
769        Parameters
770        ----------
771        other_block_set : torch.vision.models.resnet.any_block
772            A set of blocks from the original ResNet model.
773        other_resnet : torchvision.models.resnet.ResNet
774            The original ResNet model.
775        block_id : int
776            The layer number being converted.
777        Returns
778        -------
779        nn.Sequential
780            A sequential container with the converted blocks.
781        """
782        layers = []
783        for i in range(len(other_block_set)):
784            block_type = type(other_block_set[i])
785            if block_type == resnet_pt.BasicBlock:
786                layers.append(other_block_set[i])
787            elif block_type == resnet_pt.Bottleneck:
788                layers.append(other_block_set[i])
789            else:
790                print(
791                    "Your resnet uses a block type that has not been "
792                    "accounted for. Customization might be required."
793                )
794                layer_name = "layer" + str(block_id)
795                print(type(getattr(other_resnet, layer_name)))
796                pdb.set_trace()
797        return nn.Sequential(*layers)
798
799    def _forward_impl(self, x):
800        """Implementation of the forward pass.
801
802        Parameters
803        ----------
804        x : torch.Tensor
805            Input tensor to the network.
806
807        Returns
808        -------
809        torch.Tensor
810            Output tensor from the network.
811        """
812        # Modified b1 rather than conv1 and bn1
813        x = self.conv1(x)
814        x = self.bn1(x)
815        # Rest of forward remains the same
816        x = F.relu(x)
817        x = self.maxpool(x)
818
819        x = self.layer1(x)
820        x = self.layer2(x)
821        x = self.layer3(x)
822        x = self.layer4(x)
823
824        x = self.avgpool(x)
825        x = torch.flatten(x, 1)
826        x = self.pre_fc(x)
827        x = F.relu(x)
828        x = self.fc(x)
829
830        return x
831
832    def forward(self, x):
833        """Forward pass through the network.
834
835        Parameters
836        ----------
837        x : torch.Tensor
838            Input tensor to the network.
839
840        Returns
841        -------
842        torch.Tensor
843            Output tensor from the network.
844        """
845        return self._forward_impl(x)
class PAIProcessor(abc.ABC):
 28class PAIProcessor(ABC):
 29    """
 30    Abstract base class for processing neuron and dendrite operations.
 31
 32    Processors handle state management and data flow between neurons and
 33    dendrites, allowing for custom pre/post processing of modules which have
 34    multiple inputs and outputs, rather than the default single tensor input/output.
 35    Subclasses should implement the five core processing methods to handle
 36    their specific state management needs.
 37    """
 38
 39    @abstractmethod
 40    def post_n1(self, *args, **kwargs):
 41        """
 42        Post-process neuron output before dendrite processing.
 43
 44        Called immediately after the main module/neuron is executed and before
 45        any dendrite processing occurs. This method should extract and return
 46        only the tensor of the neuron output that should be seen by
 47        dendrite operations.
 48
 49        Parameters
 50        ----------
 51        *args : tuple
 52            Positional arguments, typically containing the neuron output.
 53        **kwargs : dict
 54            Keyword arguments from the neuron output.
 55
 56        Returns
 57        -------
 58        Any
 59            The filtered output to be passed to dendrite processing.
 60        """
 61        pass
 62
 63    @abstractmethod
 64    def post_n2(self, *args, **kwargs):
 65        """
 66        Post-process dendrite-modified output before final return.
 67
 68        Called after dendrite processing is complete and before passing the
 69        final value forward in the network. This method should combine the
 70        dendrite-modified output with any stored state to produce the complete
 71        output that matches the expected format of the main module.
 72
 73        Parameters
 74        ----------
 75        *args : tuple
 76            Positional arguments containing the dendrite-modified output.
 77        **kwargs : dict
 78            Keyword arguments from the processing chain.
 79
 80        Returns
 81        -------
 82        Any
 83            The complete output in the format expected by downstream components.
 84        """
 85        pass
 86
 87    @abstractmethod
 88    def pre_d(self, *args, **kwargs):
 89        """
 90        Pre-process input before dendrite operations.
 91
 92        Filters and prepares inputs for dendrite processing. This method handles
 93        special cases such as initial time steps vs. subsequent iterations,
 94        ensuring dendrites receive the appropriate inputs (e.g., external inputs
 95        vs. internal recurrent state).
 96
 97        Parameters
 98        ----------
 99        *args : tuple
100            Positional arguments containing inputs to the PAI module.
101        **kwargs : dict
102            Keyword arguments containing inputs to the PAI module.
103
104        Returns
105        -------
106        tuple
107            A tuple of (processed_args, processed_kwargs) to pass to dendrite.
108        """
109        pass
110
111    @abstractmethod
112    def post_d(self, *args, **kwargs):
113        """
114        Post-process dendrite output and manage state.
115
116        Processes the output from dendrite operations, storing any state needed
117        for future iterations and returning only the portion that should be
118        combined with the neuron output. E.g. this is where recurrent state is
119        saved for the next time step.
120
121        Parameters
122        ----------
123        *args : tuple
124            Positional arguments containing the dendrite output.
125        **kwargs : dict
126            Keyword arguments from the dendrite output.
127
128        Returns
129        -------
130        Any
131            The filtered dendrite output to be added to the neuron output.
132        """
133        pass
134
135    @abstractmethod
136    def clear_processor(self):
137        """
138        Clear all internal processor state.
139
140        Resets the processor by removing all stored state variables. Must
141        be called before saving or safe_tensors will run into errors.
142        Implementations should safely check for attribute existence before
143        deletion to avoid errors.
144
145        Parameters
146        ----------
147        None
148
149        Returns
150        -------
151        None
152            This function does not return a value.
153        """
154        pass

Abstract base class for processing neuron and dendrite operations.

Processors handle state management and data flow between neurons and dendrites, allowing for custom pre/post processing of modules which have multiple inputs and outputs, rather than the default single tensor input/output. Subclasses should implement the five core processing methods to handle their specific state management needs.

@abstractmethod
def post_n1(self, *args, **kwargs):
39    @abstractmethod
40    def post_n1(self, *args, **kwargs):
41        """
42        Post-process neuron output before dendrite processing.
43
44        Called immediately after the main module/neuron is executed and before
45        any dendrite processing occurs. This method should extract and return
46        only the tensor of the neuron output that should be seen by
47        dendrite operations.
48
49        Parameters
50        ----------
51        *args : tuple
52            Positional arguments, typically containing the neuron output.
53        **kwargs : dict
54            Keyword arguments from the neuron output.
55
56        Returns
57        -------
58        Any
59            The filtered output to be passed to dendrite processing.
60        """
61        pass

Post-process neuron output before dendrite processing.

Called immediately after the main module/neuron is executed and before any dendrite processing occurs. This method should extract and return only the tensor of the neuron output that should be seen by dendrite operations.

Parameters
  • *args (tuple): Positional arguments, typically containing the neuron output.
  • **kwargs (dict): Keyword arguments from the neuron output.
Returns
  • Any: The filtered output to be passed to dendrite processing.
@abstractmethod
def post_n2(self, *args, **kwargs):
63    @abstractmethod
64    def post_n2(self, *args, **kwargs):
65        """
66        Post-process dendrite-modified output before final return.
67
68        Called after dendrite processing is complete and before passing the
69        final value forward in the network. This method should combine the
70        dendrite-modified output with any stored state to produce the complete
71        output that matches the expected format of the main module.
72
73        Parameters
74        ----------
75        *args : tuple
76            Positional arguments containing the dendrite-modified output.
77        **kwargs : dict
78            Keyword arguments from the processing chain.
79
80        Returns
81        -------
82        Any
83            The complete output in the format expected by downstream components.
84        """
85        pass

Post-process dendrite-modified output before final return.

Called after dendrite processing is complete and before passing the final value forward in the network. This method should combine the dendrite-modified output with any stored state to produce the complete output that matches the expected format of the main module.

Parameters
  • *args (tuple): Positional arguments containing the dendrite-modified output.
  • **kwargs (dict): Keyword arguments from the processing chain.
Returns
  • Any: The complete output in the format expected by downstream components.
@abstractmethod
def pre_d(self, *args, **kwargs):
 87    @abstractmethod
 88    def pre_d(self, *args, **kwargs):
 89        """
 90        Pre-process input before dendrite operations.
 91
 92        Filters and prepares inputs for dendrite processing. This method handles
 93        special cases such as initial time steps vs. subsequent iterations,
 94        ensuring dendrites receive the appropriate inputs (e.g., external inputs
 95        vs. internal recurrent state).
 96
 97        Parameters
 98        ----------
 99        *args : tuple
100            Positional arguments containing inputs to the PAI module.
101        **kwargs : dict
102            Keyword arguments containing inputs to the PAI module.
103
104        Returns
105        -------
106        tuple
107            A tuple of (processed_args, processed_kwargs) to pass to dendrite.
108        """
109        pass

Pre-process input before dendrite operations.

Filters and prepares inputs for dendrite processing. This method handles special cases such as initial time steps vs. subsequent iterations, ensuring dendrites receive the appropriate inputs (e.g., external inputs vs. internal recurrent state).

Parameters
  • *args (tuple): Positional arguments containing inputs to the PAI module.
  • **kwargs (dict): Keyword arguments containing inputs to the PAI module.
Returns
  • tuple: A tuple of (processed_args, processed_kwargs) to pass to dendrite.
@abstractmethod
def post_d(self, *args, **kwargs):
111    @abstractmethod
112    def post_d(self, *args, **kwargs):
113        """
114        Post-process dendrite output and manage state.
115
116        Processes the output from dendrite operations, storing any state needed
117        for future iterations and returning only the portion that should be
118        combined with the neuron output. E.g. this is where recurrent state is
119        saved for the next time step.
120
121        Parameters
122        ----------
123        *args : tuple
124            Positional arguments containing the dendrite output.
125        **kwargs : dict
126            Keyword arguments from the dendrite output.
127
128        Returns
129        -------
130        Any
131            The filtered dendrite output to be added to the neuron output.
132        """
133        pass

Post-process dendrite output and manage state.

Processes the output from dendrite operations, storing any state needed for future iterations and returning only the portion that should be combined with the neuron output. E.g. this is where recurrent state is saved for the next time step.

Parameters
  • *args (tuple): Positional arguments containing the dendrite output.
  • **kwargs (dict): Keyword arguments from the dendrite output.
Returns
  • Any: The filtered dendrite output to be added to the neuron output.
@abstractmethod
def clear_processor(self):
135    @abstractmethod
136    def clear_processor(self):
137        """
138        Clear all internal processor state.
139
140        Resets the processor by removing all stored state variables. Must
141        be called before saving or safe_tensors will run into errors.
142        Implementations should safely check for attribute existence before
143        deletion to avoid errors.
144
145        Parameters
146        ----------
147        None
148
149        Returns
150        -------
151        None
152            This function does not return a value.
153        """
154        pass

Clear all internal processor state.

Resets the processor by removing all stored state variables. Must be called before saving or safe_tensors will run into errors. Implementations should safely check for attribute existence before deletion to avoid errors.

Parameters
  • None
Returns
  • None: This function does not return a value.
class MultiOutputProcessor:
158class MultiOutputProcessor:
159    """Processor for handling multiple outputs, ignoring later ones."""
160
161    def post_n1(self, *args, **kwargs):
162        """Saves extra outputs and returns the first output.
163
164        Parameters
165        ----------
166        *args : tuple
167            Contains the modules output tuple.
168        **kwargs : dict
169            Unused keyword arguments.
170
171        Returns
172        -------
173        torch.Tensor
174            The first tensor of the tuple
175        """
176        out = args[0][0]
177        extra_out = args[0][1:]
178        self.extra_out = extra_out
179        return out
180
181    def post_n2(self, *args, **kwargs):
182        """Combine output with stored extra outputs.
183
184        Parameters
185        ----------
186        *args : torch.tensor
187            The first tensor combined with dendrite output.
188        **kwargs : dict
189            Unused keyword arguments.
190
191        Returns
192        -------
193        tuple
194            The recombined output tuple wth the new first output modified
195        """
196        out = args[0]
197        if isinstance(self.extra_out, tuple):
198            return (out,) + self.extra_out
199        else:
200            return (out,) + (self.extra_out,)
201
202    def pre_d(self, *args, **kwargs):
203        """Pass through arguments unchanged for dendrite preprocessing.
204
205        Parameters
206        ----------
207        *args : tuple
208            Positional arguments containing inputs to the PAI module.
209        **kwargs : dict
210            Keyword arguments containing inputs to the PAI module.
211
212        Returns
213        -------
214        args : tuple
215            Positional arguments containing inputs to the PAI module.
216        kwargs : dict
217            Keyword arguments containing inputs to the PAI module.
218        """
219        return args, kwargs
220
221    def post_d(self, *args, **kwargs):
222        """Extract first output for dendrite postprocessing.
223
224        Parameters
225        ----------
226        *args : tuple
227            Contains the dendrite modules output tuple.
228        **kwargs : dict
229            Unused keyword arguments.
230
231        Returns
232        -------
233        torch.Tensor
234            The first tensor of the tuple
235        """
236        out = args[0][0]
237        return out
238
239    def clear_processor(self):
240        """Clear stored processor state.
241
242        Parameters
243        ----------
244        None
245
246        Returns
247        -------
248        None
249            This function does not return a value.
250        """
251
252        if hasattr(self, "extra_out"):
253            delattr(self, "extra_out")

Processor for handling multiple outputs, ignoring later ones.

def post_n1(self, *args, **kwargs):
161    def post_n1(self, *args, **kwargs):
162        """Saves extra outputs and returns the first output.
163
164        Parameters
165        ----------
166        *args : tuple
167            Contains the modules output tuple.
168        **kwargs : dict
169            Unused keyword arguments.
170
171        Returns
172        -------
173        torch.Tensor
174            The first tensor of the tuple
175        """
176        out = args[0][0]
177        extra_out = args[0][1:]
178        self.extra_out = extra_out
179        return out

Saves extra outputs and returns the first output.

Parameters
  • *args (tuple): Contains the modules output tuple.
  • **kwargs (dict): Unused keyword arguments.
Returns
  • torch.Tensor: The first tensor of the tuple
def post_n2(self, *args, **kwargs):
181    def post_n2(self, *args, **kwargs):
182        """Combine output with stored extra outputs.
183
184        Parameters
185        ----------
186        *args : torch.tensor
187            The first tensor combined with dendrite output.
188        **kwargs : dict
189            Unused keyword arguments.
190
191        Returns
192        -------
193        tuple
194            The recombined output tuple wth the new first output modified
195        """
196        out = args[0]
197        if isinstance(self.extra_out, tuple):
198            return (out,) + self.extra_out
199        else:
200            return (out,) + (self.extra_out,)

Combine output with stored extra outputs.

Parameters
  • *args (torch.tensor): The first tensor combined with dendrite output.
  • **kwargs (dict): Unused keyword arguments.
Returns
  • tuple: The recombined output tuple wth the new first output modified
def pre_d(self, *args, **kwargs):
202    def pre_d(self, *args, **kwargs):
203        """Pass through arguments unchanged for dendrite preprocessing.
204
205        Parameters
206        ----------
207        *args : tuple
208            Positional arguments containing inputs to the PAI module.
209        **kwargs : dict
210            Keyword arguments containing inputs to the PAI module.
211
212        Returns
213        -------
214        args : tuple
215            Positional arguments containing inputs to the PAI module.
216        kwargs : dict
217            Keyword arguments containing inputs to the PAI module.
218        """
219        return args, kwargs

Pass through arguments unchanged for dendrite preprocessing.

Parameters
  • *args (tuple): Positional arguments containing inputs to the PAI module.
  • **kwargs (dict): Keyword arguments containing inputs to the PAI module.
Returns
  • args (tuple): Positional arguments containing inputs to the PAI module.
  • kwargs (dict): Keyword arguments containing inputs to the PAI module.
def post_d(self, *args, **kwargs):
221    def post_d(self, *args, **kwargs):
222        """Extract first output for dendrite postprocessing.
223
224        Parameters
225        ----------
226        *args : tuple
227            Contains the dendrite modules output tuple.
228        **kwargs : dict
229            Unused keyword arguments.
230
231        Returns
232        -------
233        torch.Tensor
234            The first tensor of the tuple
235        """
236        out = args[0][0]
237        return out

Extract first output for dendrite postprocessing.

Parameters
  • *args (tuple): Contains the dendrite modules output tuple.
  • **kwargs (dict): Unused keyword arguments.
Returns
  • torch.Tensor: The first tensor of the tuple
def clear_processor(self):
239    def clear_processor(self):
240        """Clear stored processor state.
241
242        Parameters
243        ----------
244        None
245
246        Returns
247        -------
248        None
249            This function does not return a value.
250        """
251
252        if hasattr(self, "extra_out"):
253            delattr(self, "extra_out")

Clear stored processor state.

Parameters
  • None
Returns
  • None: This function does not return a value.
class LSTMCellProcessor(PAIProcessor):
255class LSTMCellProcessor(PAIProcessor):
256    """Processor for LSTM cells to handle hidden and cell states."""
257
258    def post_n1(self, *args, **kwargs):
259        """
260        Extract hidden state from LSTM output for dendrite processing.
261
262        Separates the hidden state (h_t) from the cell state (c_t) in the
263        LSTM output tuple. Stores the cell state temporarily since only the
264        hidden state should be modified by dendrites.
265
266        Parameters
267        ----------
268        *args : tuple
269            Contains LSTM output tuple (h_t, c_t) as first element.
270        **kwargs : dict
271            Unused keyword arguments.
272
273        Returns
274        -------
275        torch.Tensor
276            Hidden state h_t to be passed to dendrite processing.
277        """
278        h_t = args[0][0]
279        c_t = args[0][1]
280        # Store the cell state temporarily and just use the hidden state
281        # to do Dendrite functions
282        self.c_t_n = c_t
283        return h_t
284
285    def post_n2(self, *args, **kwargs):
286        """
287        Recombine dendrite-modified hidden state with cell state.
288
289        Takes the hidden state that has been modified by dendrite operations
290        and combines it with the stored cell state to produce the complete
291        LSTM output tuple.
292
293        Parameters
294        ----------
295        *args : tuple
296            Contains the dendrite-modified hidden state h_t.
297        **kwargs : dict
298            Unused keyword arguments.
299
300        Returns
301        -------
302        tuple
303            Complete LSTM output (h_t, c_t) where h_t has been modified.
304        """
305        h_t = args[0]
306        return h_t, self.c_t_n
307
308    def pre_d(self, *args, **kwargs):
309        """
310        Filter LSTMCell input for dendrite based on initialization state.
311
312        Checks if this is the first time step (all zeros in h_t) or a
313        subsequent step. For the first step, passes through the original
314        inputs. For subsequent steps, replaces the neuron's hidden state
315        with the dendrite's own internal state from the previous iteration.
316
317        Parameters
318        ----------
319        *args : tuple
320            Contains (input, (h_t, c_t)) where input is the external input
321            and (h_t, c_t) is the neuron's recurrent state.
322        **kwargs : dict
323            Keyword arguments to pass through.
324
325        Returns
326        -------
327        tuple
328            ((processed_input, processed_state), kwargs) for dendrite call.
329        """
330        h_t = args[1][0]
331        # If its the initial step then just use the normal input and zeros
332        if h_t.sum() == 0:
333            return args, kwargs
334        # If its not the first one then return the input it got with its own
335        # h_t and c_t to replace neurons
336        else:
337            return (args[0], (self.h_t_d, self.c_t_d)), kwargs
338
339    def post_d(self, *args, **kwargs):
340        """
341        Extract and store dendrite's LSTM state for next iteration.
342
343        Separates the dendrite's hidden and cell states from its output tuple,
344        stores both for use in the next time step, and returns only the hidden
345        state to be combined with the neuron's output.
346
347        Parameters
348        ----------
349        *args : tuple
350            Contains dendrite LSTM output tuple (h_t, c_t).
351        **kwargs : dict
352            Unused keyword arguments.
353
354        Returns
355        -------
356        torch.Tensor
357            Hidden state h_t to be added to the neuron output.
358        """
359        h_t = args[0][0]
360        c_t = args[0][1]
361        self.h_t_d = h_t
362        self.c_t_d = c_t
363        return h_t
364
365    def clear_processor(self):
366        """
367        Clear all stored LSTM states.
368
369        Removes dendrite hidden state (h_t_d), dendrite cell state (c_t_d),
370        and temporarily stored neuron cell state (c_t_n). Safe to call even
371        if attributes don't exist.
372
373        Parameters
374        ----------
375        None
376
377        Returns
378        -------
379        None
380            This function does not return a value.
381        """
382        if hasattr(self, "h_t_d"):
383            delattr(self, "h_t_d")
384        if hasattr(self, "c_t_d"):
385            delattr(self, "c_t_d")
386        if hasattr(self, "c_t_n"):
387            delattr(self, "c_t_n")

Processor for LSTM cells to handle hidden and cell states.

def post_n1(self, *args, **kwargs):
258    def post_n1(self, *args, **kwargs):
259        """
260        Extract hidden state from LSTM output for dendrite processing.
261
262        Separates the hidden state (h_t) from the cell state (c_t) in the
263        LSTM output tuple. Stores the cell state temporarily since only the
264        hidden state should be modified by dendrites.
265
266        Parameters
267        ----------
268        *args : tuple
269            Contains LSTM output tuple (h_t, c_t) as first element.
270        **kwargs : dict
271            Unused keyword arguments.
272
273        Returns
274        -------
275        torch.Tensor
276            Hidden state h_t to be passed to dendrite processing.
277        """
278        h_t = args[0][0]
279        c_t = args[0][1]
280        # Store the cell state temporarily and just use the hidden state
281        # to do Dendrite functions
282        self.c_t_n = c_t
283        return h_t

Extract hidden state from LSTM output for dendrite processing.

Separates the hidden state (h_t) from the cell state (c_t) in the LSTM output tuple. Stores the cell state temporarily since only the hidden state should be modified by dendrites.

Parameters
  • *args (tuple): Contains LSTM output tuple (h_t, c_t) as first element.
  • **kwargs (dict): Unused keyword arguments.
Returns
  • torch.Tensor: Hidden state h_t to be passed to dendrite processing.
def post_n2(self, *args, **kwargs):
285    def post_n2(self, *args, **kwargs):
286        """
287        Recombine dendrite-modified hidden state with cell state.
288
289        Takes the hidden state that has been modified by dendrite operations
290        and combines it with the stored cell state to produce the complete
291        LSTM output tuple.
292
293        Parameters
294        ----------
295        *args : tuple
296            Contains the dendrite-modified hidden state h_t.
297        **kwargs : dict
298            Unused keyword arguments.
299
300        Returns
301        -------
302        tuple
303            Complete LSTM output (h_t, c_t) where h_t has been modified.
304        """
305        h_t = args[0]
306        return h_t, self.c_t_n

Recombine dendrite-modified hidden state with cell state.

Takes the hidden state that has been modified by dendrite operations and combines it with the stored cell state to produce the complete LSTM output tuple.

Parameters
  • *args (tuple): Contains the dendrite-modified hidden state h_t.
  • **kwargs (dict): Unused keyword arguments.
Returns
  • tuple: Complete LSTM output (h_t, c_t) where h_t has been modified.
def pre_d(self, *args, **kwargs):
308    def pre_d(self, *args, **kwargs):
309        """
310        Filter LSTMCell input for dendrite based on initialization state.
311
312        Checks if this is the first time step (all zeros in h_t) or a
313        subsequent step. For the first step, passes through the original
314        inputs. For subsequent steps, replaces the neuron's hidden state
315        with the dendrite's own internal state from the previous iteration.
316
317        Parameters
318        ----------
319        *args : tuple
320            Contains (input, (h_t, c_t)) where input is the external input
321            and (h_t, c_t) is the neuron's recurrent state.
322        **kwargs : dict
323            Keyword arguments to pass through.
324
325        Returns
326        -------
327        tuple
328            ((processed_input, processed_state), kwargs) for dendrite call.
329        """
330        h_t = args[1][0]
331        # If its the initial step then just use the normal input and zeros
332        if h_t.sum() == 0:
333            return args, kwargs
334        # If its not the first one then return the input it got with its own
335        # h_t and c_t to replace neurons
336        else:
337            return (args[0], (self.h_t_d, self.c_t_d)), kwargs

Filter LSTMCell input for dendrite based on initialization state.

Checks if this is the first time step (all zeros in h_t) or a subsequent step. For the first step, passes through the original inputs. For subsequent steps, replaces the neuron's hidden state with the dendrite's own internal state from the previous iteration.

Parameters
  • *args (tuple): Contains (input, (h_t, c_t)) where input is the external input and (h_t, c_t) is the neuron's recurrent state.
  • **kwargs (dict): Keyword arguments to pass through.
Returns
  • tuple: ((processed_input, processed_state), kwargs) for dendrite call.
def post_d(self, *args, **kwargs):
339    def post_d(self, *args, **kwargs):
340        """
341        Extract and store dendrite's LSTM state for next iteration.
342
343        Separates the dendrite's hidden and cell states from its output tuple,
344        stores both for use in the next time step, and returns only the hidden
345        state to be combined with the neuron's output.
346
347        Parameters
348        ----------
349        *args : tuple
350            Contains dendrite LSTM output tuple (h_t, c_t).
351        **kwargs : dict
352            Unused keyword arguments.
353
354        Returns
355        -------
356        torch.Tensor
357            Hidden state h_t to be added to the neuron output.
358        """
359        h_t = args[0][0]
360        c_t = args[0][1]
361        self.h_t_d = h_t
362        self.c_t_d = c_t
363        return h_t

Extract and store dendrite's LSTM state for next iteration.

Separates the dendrite's hidden and cell states from its output tuple, stores both for use in the next time step, and returns only the hidden state to be combined with the neuron's output.

Parameters
  • *args (tuple): Contains dendrite LSTM output tuple (h_t, c_t).
  • **kwargs (dict): Unused keyword arguments.
Returns
  • torch.Tensor: Hidden state h_t to be added to the neuron output.
def clear_processor(self):
365    def clear_processor(self):
366        """
367        Clear all stored LSTM states.
368
369        Removes dendrite hidden state (h_t_d), dendrite cell state (c_t_d),
370        and temporarily stored neuron cell state (c_t_n). Safe to call even
371        if attributes don't exist.
372
373        Parameters
374        ----------
375        None
376
377        Returns
378        -------
379        None
380            This function does not return a value.
381        """
382        if hasattr(self, "h_t_d"):
383            delattr(self, "h_t_d")
384        if hasattr(self, "c_t_d"):
385            delattr(self, "c_t_d")
386        if hasattr(self, "c_t_n"):
387            delattr(self, "c_t_n")

Clear all stored LSTM states.

Removes dendrite hidden state (h_t_d), dendrite cell state (c_t_d), and temporarily stored neuron cell state (c_t_n). Safe to call even if attributes don't exist.

Parameters
  • None
Returns
  • None: This function does not return a value.
class LSTMProcessor(PAIProcessor):
391class LSTMProcessor(PAIProcessor):
392    """Processor for LSTM to handle hidden and output states."""
393
394    def post_n1(self, *args, **kwargs):
395        """
396        Extract hidden state from LSTM output for dendrite processing.
397
398        Separates the hidden state from the output in the
399        LSTM output tuple. Stores the hidden state temporarily since only the
400        output state should be modified by dendrites.
401
402        Parameters
403        ----------
404        *args : tuple
405            Contains LSTM output tuple (output, hidden) as first element.
406        **kwargs : dict
407            Unused keyword arguments.
408
409        Returns
410        -------
411        torch.Tensor
412            Output state to be passed to dendrite processing.
413        """
414        output = args[0][0]
415        hidden = args[0][1]
416        # Store the hidden state temporarily and just use the output state
417        # to do Dendrite functions
418        self.hidden_n = hidden
419        return output
420
421    def post_n2(self, *args, **kwargs):
422        """
423        Recombine dendrite-modified output with hidden tuple.
424
425        Takes the output state that has been modified by dendrite operations
426        and combines it with the stored hidden state to produce the complete
427        LSTM output tuple.
428
429        Parameters
430        ----------
431        *args : tuple
432            Contains the dendrite-modified output state.
433        **kwargs : dict
434            Unused keyword arguments.
435
436        Returns
437        -------
438        tuple
439            Complete LSTM output (output, hidden) where output has been modified.
440        """
441        output = args[0]
442        return output, self.hidden_n
443
444    def pre_d(self, *args, **kwargs):
445        """
446        LSTM input is just the tensor which also goes to the dendrite
447
448        Parameters
449        ----------
450        *args : 
451            Input tensor
452        **kwargs : dict
453            Empty
454
455        Returns
456        -------
457        tuple
458            (output, hidden)
459        """
460        return args, kwargs
461        
462    def post_d(self, *args, **kwargs):
463        """
464        Extract dendrite's output to combine.
465
466        Parameters
467        ----------
468        *args : tuple
469            Contains dendrite LSTM output tuple (output, hidden).
470        **kwargs : dict
471            Unused keyword arguments.
472
473        Returns
474        -------
475        torch.Tensor
476            Output state to be added to the neuron output.
477        """
478        output = args[0][0]
479        hidden = args[0][1]
480        return output
481
482    def clear_processor(self):
483        """
484        Clear all stored LSTM states.
485
486
487        Parameters
488        ----------
489        None
490
491        Returns
492        -------
493        None
494            This function does not return a value.
495        """
496        if hasattr(self, "hidden_n"):
497            delattr(self, "hidden_n")

Processor for LSTM to handle hidden and output states.

def post_n1(self, *args, **kwargs):
394    def post_n1(self, *args, **kwargs):
395        """
396        Extract hidden state from LSTM output for dendrite processing.
397
398        Separates the hidden state from the output in the
399        LSTM output tuple. Stores the hidden state temporarily since only the
400        output state should be modified by dendrites.
401
402        Parameters
403        ----------
404        *args : tuple
405            Contains LSTM output tuple (output, hidden) as first element.
406        **kwargs : dict
407            Unused keyword arguments.
408
409        Returns
410        -------
411        torch.Tensor
412            Output state to be passed to dendrite processing.
413        """
414        output = args[0][0]
415        hidden = args[0][1]
416        # Store the hidden state temporarily and just use the output state
417        # to do Dendrite functions
418        self.hidden_n = hidden
419        return output

Extract hidden state from LSTM output for dendrite processing.

Separates the hidden state from the output in the LSTM output tuple. Stores the hidden state temporarily since only the output state should be modified by dendrites.

Parameters
  • *args (tuple): Contains LSTM output tuple (output, hidden) as first element.
  • **kwargs (dict): Unused keyword arguments.
Returns
  • torch.Tensor: Output state to be passed to dendrite processing.
def post_n2(self, *args, **kwargs):
421    def post_n2(self, *args, **kwargs):
422        """
423        Recombine dendrite-modified output with hidden tuple.
424
425        Takes the output state that has been modified by dendrite operations
426        and combines it with the stored hidden state to produce the complete
427        LSTM output tuple.
428
429        Parameters
430        ----------
431        *args : tuple
432            Contains the dendrite-modified output state.
433        **kwargs : dict
434            Unused keyword arguments.
435
436        Returns
437        -------
438        tuple
439            Complete LSTM output (output, hidden) where output has been modified.
440        """
441        output = args[0]
442        return output, self.hidden_n

Recombine dendrite-modified output with hidden tuple.

Takes the output state that has been modified by dendrite operations and combines it with the stored hidden state to produce the complete LSTM output tuple.

Parameters
  • *args (tuple): Contains the dendrite-modified output state.
  • **kwargs (dict): Unused keyword arguments.
Returns
  • tuple: Complete LSTM output (output, hidden) where output has been modified.
def pre_d(self, *args, **kwargs):
444    def pre_d(self, *args, **kwargs):
445        """
446        LSTM input is just the tensor which also goes to the dendrite
447
448        Parameters
449        ----------
450        *args : 
451            Input tensor
452        **kwargs : dict
453            Empty
454
455        Returns
456        -------
457        tuple
458            (output, hidden)
459        """
460        return args, kwargs

LSTM input is just the tensor which also goes to the dendrite

Parameters
  • *args (): Input tensor
  • **kwargs (dict): Empty
Returns
  • tuple: (output, hidden)
def post_d(self, *args, **kwargs):
462    def post_d(self, *args, **kwargs):
463        """
464        Extract dendrite's output to combine.
465
466        Parameters
467        ----------
468        *args : tuple
469            Contains dendrite LSTM output tuple (output, hidden).
470        **kwargs : dict
471            Unused keyword arguments.
472
473        Returns
474        -------
475        torch.Tensor
476            Output state to be added to the neuron output.
477        """
478        output = args[0][0]
479        hidden = args[0][1]
480        return output

Extract dendrite's output to combine.

Parameters
  • *args (tuple): Contains dendrite LSTM output tuple (output, hidden).
  • **kwargs (dict): Unused keyword arguments.
Returns
  • torch.Tensor: Output state to be added to the neuron output.
def clear_processor(self):
482    def clear_processor(self):
483        """
484        Clear all stored LSTM states.
485
486
487        Parameters
488        ----------
489        None
490
491        Returns
492        -------
493        None
494            This function does not return a value.
495        """
496        if hasattr(self, "hidden_n"):
497            delattr(self, "hidden_n")

Clear all stored LSTM states.

Parameters
  • None
Returns
  • None: This function does not return a value.
class LSTMProcessorLastHidden(PAIProcessor):
500class LSTMProcessorLastHidden(PAIProcessor):
501    """Processor for LSTM to forward the last hidden."""
502
503    def post_n1(self, *args, **kwargs):
504        """
505        Extract the last hidden to combine with dendrites
506
507        Parameters
508        ----------
509        *args : tuple
510            Contains LSTM output tuple (output, hidden) as first element.
511        **kwargs : dict
512            Unused keyword arguments.
513
514        Returns
515        -------
516        torch.Tensor
517            Output state to be passed to dendrite processing.
518        """
519        ignored_output = args[0][0]
520        last_hidden = args[0][1][-1]
521
522        return last_hidden
523
524    def post_n2(self, *args, **kwargs):
525        """
526        Recombine dendrite-modified last hidden, and append None just to maintain output format
527
528        Parameters
529        ----------
530        *args : tuple
531            Contains the dendrite-modified output state.
532        **kwargs : dict
533            Unused keyword arguments.
534
535        Returns
536        -------
537        tuple
538            Complete LSTM output (output, hidden) where output has been modified.
539        """
540        combined_last_hidden = args[0]
541        return None, combined_last_hidden
542
543    def pre_d(self, *args, **kwargs):
544        """
545        LSTM input is just the tensor which also goes to the dendrite
546
547        Parameters
548        ----------
549        *args : 
550            Input tensor
551        **kwargs : dict
552            Empty
553
554        Returns
555        -------
556        tuple
557            (output, hidden)
558        """
559        return args, kwargs
560        
561    def post_d(self, *args, **kwargs):
562        """
563        Extract extract the dendrites last hidden to combine with neurons.
564
565        Parameters
566        ----------
567        *args : tuple
568            Contains dendrite LSTM output tuple (output, hidden).
569        **kwargs : dict
570            Unused keyword arguments.
571
572        Returns
573        -------
574        torch.Tensor
575            Output state to be added to the neuron output.
576        """
577        ignored_output = args[0][0]
578        last_hidden = args[0][1][-1]
579        return last_hidden
580
581    def clear_processor(self):
582        """Clear processor state.
583
584        Notes
585        -----
586        This processor keeps no persistent internal state.
587
588        Parameters
589        ----------
590        None
591
592        Returns
593        -------
594        None
595            This function does not return a value.
596        """
597        # Nothing is stored
598        pass

Processor for LSTM to forward the last hidden.

def post_n1(self, *args, **kwargs):
503    def post_n1(self, *args, **kwargs):
504        """
505        Extract the last hidden to combine with dendrites
506
507        Parameters
508        ----------
509        *args : tuple
510            Contains LSTM output tuple (output, hidden) as first element.
511        **kwargs : dict
512            Unused keyword arguments.
513
514        Returns
515        -------
516        torch.Tensor
517            Output state to be passed to dendrite processing.
518        """
519        ignored_output = args[0][0]
520        last_hidden = args[0][1][-1]
521
522        return last_hidden

Extract the last hidden to combine with dendrites

Parameters
  • *args (tuple): Contains LSTM output tuple (output, hidden) as first element.
  • **kwargs (dict): Unused keyword arguments.
Returns
  • torch.Tensor: Output state to be passed to dendrite processing.
def post_n2(self, *args, **kwargs):
524    def post_n2(self, *args, **kwargs):
525        """
526        Recombine dendrite-modified last hidden, and append None just to maintain output format
527
528        Parameters
529        ----------
530        *args : tuple
531            Contains the dendrite-modified output state.
532        **kwargs : dict
533            Unused keyword arguments.
534
535        Returns
536        -------
537        tuple
538            Complete LSTM output (output, hidden) where output has been modified.
539        """
540        combined_last_hidden = args[0]
541        return None, combined_last_hidden

Recombine dendrite-modified last hidden, and append None just to maintain output format

Parameters
  • *args (tuple): Contains the dendrite-modified output state.
  • **kwargs (dict): Unused keyword arguments.
Returns
  • tuple: Complete LSTM output (output, hidden) where output has been modified.
def pre_d(self, *args, **kwargs):
543    def pre_d(self, *args, **kwargs):
544        """
545        LSTM input is just the tensor which also goes to the dendrite
546
547        Parameters
548        ----------
549        *args : 
550            Input tensor
551        **kwargs : dict
552            Empty
553
554        Returns
555        -------
556        tuple
557            (output, hidden)
558        """
559        return args, kwargs

LSTM input is just the tensor which also goes to the dendrite

Parameters
  • *args (): Input tensor
  • **kwargs (dict): Empty
Returns
  • tuple: (output, hidden)
def post_d(self, *args, **kwargs):
561    def post_d(self, *args, **kwargs):
562        """
563        Extract extract the dendrites last hidden to combine with neurons.
564
565        Parameters
566        ----------
567        *args : tuple
568            Contains dendrite LSTM output tuple (output, hidden).
569        **kwargs : dict
570            Unused keyword arguments.
571
572        Returns
573        -------
574        torch.Tensor
575            Output state to be added to the neuron output.
576        """
577        ignored_output = args[0][0]
578        last_hidden = args[0][1][-1]
579        return last_hidden

Extract extract the dendrites last hidden to combine with neurons.

Parameters
  • *args (tuple): Contains dendrite LSTM output tuple (output, hidden).
  • **kwargs (dict): Unused keyword arguments.
Returns
  • torch.Tensor: Output state to be added to the neuron output.
def clear_processor(self):
581    def clear_processor(self):
582        """Clear processor state.
583
584        Notes
585        -----
586        This processor keeps no persistent internal state.
587
588        Parameters
589        ----------
590        None
591
592        Returns
593        -------
594        None
595            This function does not return a value.
596        """
597        # Nothing is stored
598        pass

Clear processor state.

Notes

This processor keeps no persistent internal state.

Parameters
  • None
Returns
  • None: This function does not return a value.
class ResNetPAI(torch.nn.modules.module.Module):
600class ResNetPAI(nn.Module):
601    """PB-compatible ResNet wrapper.
602
603    All normalization layers should be wrapped in a PAISequential, or other
604    wrapped module. When working with a predefined model the following shows
605    an example of how to create a module for modules_to_replace.
606    """
607
608    def __init__(self, other_resnet):
609        """Initialize ResNetPAI from existing ResNet model.
610
611        Parameters
612        ----------
613        *args : other_resnet : torchvision.models.resnet.ResNet
614            An existing ResNet model to convert to PAI-compatible format.
615        """
616        super(ResNetPAI, self).__init__()
617
618        # For the most part, just copy the exact values from the original module
619        self._norm_layer = other_resnet._norm_layer
620        self.inplanes = other_resnet.inplanes
621        self.dilation = other_resnet.dilation
622        self.groups = other_resnet.groups
623        self.base_width = other_resnet.base_width
624
625        # For the component to be changed, define a PAISequential with the old
626        # modules included
627        self.b1 = GPA.PAISequential([other_resnet.conv1, other_resnet.bn1])
628
629        self.relu = other_resnet.relu
630        self.maxpool = other_resnet.maxpool
631
632        for i in range(1, 5):
633            layer_name = "layer" + str(i)
634            original_layer = getattr(other_resnet, layer_name)
635            pb_layer = self._make_layer_pb(original_layer, other_resnet, i)
636            setattr(self, layer_name, pb_layer)
637
638        self.avgpool = other_resnet.avgpool
639        self.fc = other_resnet.fc
640
641    def _make_layer_pb(self, other_block_set, other_resnet, block_id):
642        """Convert ResNet layer blocks to PB-compatible format.
643
644        Parameters
645        ----------
646        other_block_set : torch.vision.models.resnet.any_block
647            A set of blocks from the original ResNet model.
648        other_resnet : torchvision.models.resnet.ResNet
649            The original ResNet model.
650        block_id : int
651            The layer number being converted.
652        Returns
653        -------
654        nn.Sequential
655            A sequential container with the converted blocks.
656        """
657        layers = []
658        for i in range(len(other_block_set)):
659            block_type = type(other_block_set[i])
660            if block_type == resnet_pt.BasicBlock:
661                layers.append(other_block_set[i])
662            elif block_type == resnet_pt.Bottleneck:
663                layers.append(other_block_set[i])
664            else:
665                print(
666                    "Your resnet uses a block type that has not been "
667                    "accounted for. Customization might be required."
668                )
669                layer_name = "layer" + str(block_id)
670                print(type(getattr(other_resnet, layer_name)))
671                pdb.set_trace()
672        return nn.Sequential(*layers)
673
674    def _forward_impl(self, x):
675        """Implementation of the forward pass.
676
677        Parameters
678        ----------
679        x : torch.Tensor
680            Input tensor to the network.
681
682        Returns
683        -------
684        torch.Tensor
685            Output tensor from the network.
686        """
687        # Modified b1 rather than conv1 and bn1
688        x = self.b1(x)
689        # Rest of forward remains the same
690        x = F.relu(x)
691        x = self.maxpool(x)
692
693        x = self.layer1(x)
694        x = self.layer2(x)
695        x = self.layer3(x)
696        x = self.layer4(x)
697
698        x = self.avgpool(x)
699        x = torch.flatten(x, 1)
700        x = self.fc(x)
701
702        return x
703
704    def forward(self, x):
705        """Forward pass through the network.
706
707        Parameters
708        ----------
709        x : torch.Tensor
710            Input tensor to the network.
711
712        Returns
713        -------
714        torch.Tensor
715            Output tensor from the network.
716        """
717        return self._forward_impl(x)

PB-compatible ResNet wrapper.

All normalization layers should be wrapped in a PAISequential, or other wrapped module. When working with a predefined model the following shows an example of how to create a module for modules_to_replace.

ResNetPAI(other_resnet)
608    def __init__(self, other_resnet):
609        """Initialize ResNetPAI from existing ResNet model.
610
611        Parameters
612        ----------
613        *args : other_resnet : torchvision.models.resnet.ResNet
614            An existing ResNet model to convert to PAI-compatible format.
615        """
616        super(ResNetPAI, self).__init__()
617
618        # For the most part, just copy the exact values from the original module
619        self._norm_layer = other_resnet._norm_layer
620        self.inplanes = other_resnet.inplanes
621        self.dilation = other_resnet.dilation
622        self.groups = other_resnet.groups
623        self.base_width = other_resnet.base_width
624
625        # For the component to be changed, define a PAISequential with the old
626        # modules included
627        self.b1 = GPA.PAISequential([other_resnet.conv1, other_resnet.bn1])
628
629        self.relu = other_resnet.relu
630        self.maxpool = other_resnet.maxpool
631
632        for i in range(1, 5):
633            layer_name = "layer" + str(i)
634            original_layer = getattr(other_resnet, layer_name)
635            pb_layer = self._make_layer_pb(original_layer, other_resnet, i)
636            setattr(self, layer_name, pb_layer)
637
638        self.avgpool = other_resnet.avgpool
639        self.fc = other_resnet.fc

Initialize ResNetPAI from existing ResNet model.

Parameters
  • *args : other_resnet (torchvision.models.resnet.ResNet): An existing ResNet model to convert to PAI-compatible format.
inplanes
dilation
groups
base_width
b1
relu
maxpool
avgpool
fc
def forward(self, x):
704    def forward(self, x):
705        """Forward pass through the network.
706
707        Parameters
708        ----------
709        x : torch.Tensor
710            Input tensor to the network.
711
712        Returns
713        -------
714        torch.Tensor
715            Output tensor from the network.
716        """
717        return self._forward_impl(x)

Forward pass through the network.

Parameters
  • x (torch.Tensor): Input tensor to the network.
Returns
  • torch.Tensor: Output tensor from the network.
class ResNetPAIPreFC(torch.nn.modules.module.Module):
720class ResNetPAIPreFC(nn.Module):
721    """PB-compatible ResNet wrapper.
722
723    All normalization layers should be wrapped in a PAISequential, or other
724    wrapped module. When working with a predefined model the following shows
725    an example of how to create a module for modules_to_replace.
726    """
727
728    def __init__(self, other_resnet):
729        """Initialize ResNetPAI from existing ResNet model.
730
731        Parameters
732        ----------
733        *args : other_resnet : torchvision.models.resnet.ResNet
734            An existing ResNet model to convert to PAI-compatible format.
735        """
736        super(ResNetPAIPreFC, self).__init__()
737
738        # For the most part, just copy the exact values from the original module
739        self._norm_layer = other_resnet._norm_layer
740        self.inplanes = other_resnet.inplanes
741        self.dilation = other_resnet.dilation
742        self.groups = other_resnet.groups
743        self.base_width = other_resnet.base_width
744
745        # For the component to be changed, define a PAISequential with the old
746        # modules included
747        self.conv1 = other_resnet.conv1
748        self.bn1 = other_resnet.bn1
749
750        self.relu = other_resnet.relu
751        self.maxpool = other_resnet.maxpool
752
753        for i in range(1, 5):
754            layer_name = "layer" + str(i)
755            original_layer = getattr(other_resnet, layer_name)
756            pb_layer = self._make_layer_pb(original_layer, other_resnet, i)
757            setattr(self, layer_name, pb_layer)
758
759        self.avgpool = other_resnet.avgpool
760        
761        # Create pre_fc layer with dimensions matching layer4 output (same as fc input)
762        fc_in_features = other_resnet.fc.in_features
763        self.pre_fc = nn.Linear(fc_in_features, fc_in_features)
764        
765        self.fc = other_resnet.fc
766
767    def _make_layer_pb(self, other_block_set, other_resnet, block_id):
768        """Convert ResNet layer blocks to PB-compatible format.
769
770        Parameters
771        ----------
772        other_block_set : torch.vision.models.resnet.any_block
773            A set of blocks from the original ResNet model.
774        other_resnet : torchvision.models.resnet.ResNet
775            The original ResNet model.
776        block_id : int
777            The layer number being converted.
778        Returns
779        -------
780        nn.Sequential
781            A sequential container with the converted blocks.
782        """
783        layers = []
784        for i in range(len(other_block_set)):
785            block_type = type(other_block_set[i])
786            if block_type == resnet_pt.BasicBlock:
787                layers.append(other_block_set[i])
788            elif block_type == resnet_pt.Bottleneck:
789                layers.append(other_block_set[i])
790            else:
791                print(
792                    "Your resnet uses a block type that has not been "
793                    "accounted for. Customization might be required."
794                )
795                layer_name = "layer" + str(block_id)
796                print(type(getattr(other_resnet, layer_name)))
797                pdb.set_trace()
798        return nn.Sequential(*layers)
799
800    def _forward_impl(self, x):
801        """Implementation of the forward pass.
802
803        Parameters
804        ----------
805        x : torch.Tensor
806            Input tensor to the network.
807
808        Returns
809        -------
810        torch.Tensor
811            Output tensor from the network.
812        """
813        # Modified b1 rather than conv1 and bn1
814        x = self.conv1(x)
815        x = self.bn1(x)
816        # Rest of forward remains the same
817        x = F.relu(x)
818        x = self.maxpool(x)
819
820        x = self.layer1(x)
821        x = self.layer2(x)
822        x = self.layer3(x)
823        x = self.layer4(x)
824
825        x = self.avgpool(x)
826        x = torch.flatten(x, 1)
827        x = self.pre_fc(x)
828        x = F.relu(x)
829        x = self.fc(x)
830
831        return x
832
833    def forward(self, x):
834        """Forward pass through the network.
835
836        Parameters
837        ----------
838        x : torch.Tensor
839            Input tensor to the network.
840
841        Returns
842        -------
843        torch.Tensor
844            Output tensor from the network.
845        """
846        return self._forward_impl(x)

PB-compatible ResNet wrapper.

All normalization layers should be wrapped in a PAISequential, or other wrapped module. When working with a predefined model the following shows an example of how to create a module for modules_to_replace.

ResNetPAIPreFC(other_resnet)
728    def __init__(self, other_resnet):
729        """Initialize ResNetPAI from existing ResNet model.
730
731        Parameters
732        ----------
733        *args : other_resnet : torchvision.models.resnet.ResNet
734            An existing ResNet model to convert to PAI-compatible format.
735        """
736        super(ResNetPAIPreFC, self).__init__()
737
738        # For the most part, just copy the exact values from the original module
739        self._norm_layer = other_resnet._norm_layer
740        self.inplanes = other_resnet.inplanes
741        self.dilation = other_resnet.dilation
742        self.groups = other_resnet.groups
743        self.base_width = other_resnet.base_width
744
745        # For the component to be changed, define a PAISequential with the old
746        # modules included
747        self.conv1 = other_resnet.conv1
748        self.bn1 = other_resnet.bn1
749
750        self.relu = other_resnet.relu
751        self.maxpool = other_resnet.maxpool
752
753        for i in range(1, 5):
754            layer_name = "layer" + str(i)
755            original_layer = getattr(other_resnet, layer_name)
756            pb_layer = self._make_layer_pb(original_layer, other_resnet, i)
757            setattr(self, layer_name, pb_layer)
758
759        self.avgpool = other_resnet.avgpool
760        
761        # Create pre_fc layer with dimensions matching layer4 output (same as fc input)
762        fc_in_features = other_resnet.fc.in_features
763        self.pre_fc = nn.Linear(fc_in_features, fc_in_features)
764        
765        self.fc = other_resnet.fc

Initialize ResNetPAI from existing ResNet model.

Parameters
  • *args : other_resnet (torchvision.models.resnet.ResNet): An existing ResNet model to convert to PAI-compatible format.
inplanes
dilation
groups
base_width
conv1
bn1
relu
maxpool
avgpool
pre_fc
fc
def forward(self, x):
833    def forward(self, x):
834        """Forward pass through the network.
835
836        Parameters
837        ----------
838        x : torch.Tensor
839            Input tensor to the network.
840
841        Returns
842        -------
843        torch.Tensor
844            Output tensor from the network.
845        """
846        return self._forward_impl(x)

Forward pass through the network.

Parameters
  • x (torch.Tensor): Input tensor to the network.
Returns
  • torch.Tensor: Output tensor from the network.