perforatedai.modules_perforatedai

   1# Copyright (c) 2025 Perforated AI
   2
   3import copy
   4import math
   5import os
   6import pdb
   7import sys
   8import time
   9from datetime import datetime
  10
  11import numpy as np
  12import torch
  13import torch.nn as nn
  14import traceback
  15
  16from perforatedai import globals_perforatedai as GPA
  17from perforatedai import utils_perforatedai as UPA
  18
  19try:
  20    from perforatedbp import modules_pbp as MPB
  21except ModuleNotFoundError as e:
  22    # Only pass if perforatedbp package itself is missing
  23    if e.name == "perforatedbp":
  24        pass
  25    else:
  26        # perforatedbp exists but is missing a dependency
  27        raise
  28
  29
  30# Values for Dendrite training, minimally used in open source version
  31_DENDRITE_TENSOR_VALUES_BASE = [
  32    "shape"
  33]  # Shape is tensor of same shape as total neurons in module
  34_DENDRITE_SINGLE_VALUES_BASE = []
  35
  36DENDRITE_INIT_VALUES = ["initialized", "current_d_init"]
  37
  38_VALUE_TRACKER_ARRAYS_BASE = ["dendrite_outs"]
  39
  40# Cached values to avoid recomputation (each tracks its own state)
  41_cached_dendrite_tensor_values = None
  42_cached_dendrite_tensor_pb_state = None
  43_cached_dendrite_single_values = None
  44_cached_dendrite_single_pb_state = None
  45_cached_value_tracker_arrays = None
  46_cached_value_tracker_pb_state = None
  47
  48
  49def get_DENDRITE_TENSOR_VALUES():
  50    """Get DENDRITE_TENSOR_VALUES, updating from MPB if perforated_backpropagation is enabled.
  51
  52    Parameters
  53    ----------
  54    None
  55
  56    Returns
  57    -------
  58    list[str]
  59        Names of tensor attributes used for dendrite state handling.
  60    """
  61    global _cached_dendrite_tensor_values, _cached_dendrite_tensor_pb_state
  62    current_pb_state = GPA.pc.get_perforated_backpropagation()
  63
  64    if (
  65        _cached_dendrite_tensor_values is None
  66        or _cached_dendrite_tensor_pb_state != current_pb_state
  67    ):
  68        _cached_dendrite_tensor_pb_state = current_pb_state
  69        if current_pb_state:
  70            _cached_dendrite_tensor_values = MPB.update_dendrite_tensor_values(
  71                _DENDRITE_TENSOR_VALUES_BASE.copy()
  72            )
  73        else:
  74            _cached_dendrite_tensor_values = _DENDRITE_TENSOR_VALUES_BASE.copy()
  75        if current_pb_state:
  76            _cached_dendrite_tensor_values = _cached_dendrite_tensor_values + MPB._variant_tensor_values
  77
  78    return _cached_dendrite_tensor_values
  79
  80
  81def get_DENDRITE_SINGLE_VALUES():
  82    """Get DENDRITE_SINGLE_VALUES, updating from MPB if perforated_backpropagation is enabled.
  83
  84    Parameters
  85    ----------
  86    None
  87
  88    Returns
  89    -------
  90    list[str]
  91        Names of scalar attributes used for dendrite state handling.
  92    """
  93    global _cached_dendrite_single_values, _cached_dendrite_single_pb_state
  94    current_pb_state = GPA.pc.get_perforated_backpropagation()
  95
  96    if (
  97        _cached_dendrite_single_values is None
  98        or _cached_dendrite_single_pb_state != current_pb_state
  99    ):
 100        _cached_dendrite_single_pb_state = current_pb_state
 101        if current_pb_state:
 102            _cached_dendrite_single_values = MPB.update_dendrite_single_values(
 103                _DENDRITE_SINGLE_VALUES_BASE.copy()
 104            )
 105        else:
 106            _cached_dendrite_single_values = _DENDRITE_SINGLE_VALUES_BASE.copy()
 107        if current_pb_state:
 108            _cached_dendrite_single_values = _cached_dendrite_single_values + MPB._variant_single_values
 109
 110    return _cached_dendrite_single_values
 111
 112
 113def get_VALUE_TRACKER_ARRAYS():
 114    """Get VALUE_TRACKER_ARRAYS, updating from MPB if perforated_backpropagation is enabled.
 115
 116    Parameters
 117    ----------
 118    None
 119
 120    Returns
 121    -------
 122    list[str]
 123        Names of array-valued fields tracked in ``DendriteValueTracker``.
 124    """
 125    global _cached_value_tracker_arrays, _cached_value_tracker_pb_state
 126    current_pb_state = GPA.pc.get_perforated_backpropagation()
 127
 128    if (
 129        _cached_value_tracker_arrays is None
 130        or _cached_value_tracker_pb_state != current_pb_state
 131    ):
 132        _cached_value_tracker_pb_state = current_pb_state
 133        if current_pb_state:
 134            _cached_value_tracker_arrays = MPB.update_value_tracker_arrays(
 135                _VALUE_TRACKER_ARRAYS_BASE.copy()
 136            )
 137        else:
 138            _cached_value_tracker_arrays = _VALUE_TRACKER_ARRAYS_BASE.copy()
 139
 140    return _cached_value_tracker_arrays
 141
 142
 143def get_DENDRITE_REINIT_VALUES():
 144    """Get DENDRITE_REINIT_VALUES.
 145
 146    Parameters
 147    ----------
 148    None
 149
 150    Returns
 151    -------
 152    list[str]
 153        Combined list of attribute names that must be reinitialized.
 154    """
 155    return get_DENDRITE_TENSOR_VALUES() + get_DENDRITE_SINGLE_VALUES()
 156
 157
 158def get_DENDRITE_SAVE_VALUES():
 159    """Get DENDRITE_SAVE_VALUES.
 160
 161    Parameters
 162    ----------
 163    None
 164
 165    Returns
 166    -------
 167    list[str]
 168        Combined list of attribute names persisted for save/load.
 169    """
 170    return (
 171        get_DENDRITE_TENSOR_VALUES()
 172        + get_DENDRITE_SINGLE_VALUES()
 173        + DENDRITE_INIT_VALUES
 174    )
 175
 176
 177def filter_backward(grad_out, values, module=None):
 178    """Filter backward pass for gradient processing.
 179
 180    This function processes gradients during the backward pass,
 181    ensuring correct input dimensions,and applying perforated backpropagation if enabled.
 182
 183    Parameters
 184    ----------
 185    grad_out : torch.Tensor
 186        The gradient output tensor from the backward pass.
 187    values : DendriteValueTracker
 188        A DendriteValueTracker instance containing values associated with the module being processed.
 189    module : PAINeuronModule, optional
 190        The owning PAINeuronModule instance. When provided and PBP is disabled,
 191        the hook deregisters itself after the one-time initialization completes,
 192        eliminating per-batch Python overhead on all subsequent backward passes.
 193
 194    Returns
 195    -------
 196    None
 197    """
 198    if GPA.pc.get_extra_verbose():
 199        print(f"{values[0].layer_name} calling backward")
 200
 201    with torch.no_grad():
 202        val = grad_out.detach()
 203        # Fast path: Python bool set after first backward in this process.
 204        # Fallback: current_d_init is a saved buffer — survives checkpoint reload
 205        # even when _fb_init_done resets to False in a fresh process.
 206        already_init = (module is not None and module._fb_init_done) or \
 207                       values[0].current_d_init.item()
 208        if not already_init:
 209            # If input dimensions and gradient don't have same shape trigger error and quit
 210            if len(values[0].this_output_dimensions) != len(grad_out.shape):
 211                print(
 212                    "The following module has not properly set this_output_dimensions"
 213                )
 214                print(values[0].layer_name)
 215                print("it is expecting:")
 216                print(values[0].this_output_dimensions)
 217                print("but received")
 218                print(grad_out.shape)
 219                print(
 220                    "to check these all at once set GPA.pc.set_debugging_output_dimensions(1)"
 221                )
 222                print(
 223                    f"Call MODEL_VARIABLE{values[0].layer_name}.set_this_output_dimensions([...]) on this module after perforate_model"
 224                )
 225                print(
 226                    "where the ... is replaced with the correct vector as described in section 4 of customization.md"
 227                )
 228                if not GPA.pc.get_debugging_output_dimensions():
 229                    sys.exit(0)
 230                else:
 231                    GPA.pc.set_debugging_output_dimensions(2)
 232                    return
 233            # Make sure that the input dimensions are correct
 234            for i in range(len(values[0].this_output_dimensions)):
 235                if values[0].this_output_dimensions[i] == 0:
 236                    continue
 237                # Make sure all input dimensions are either -1 (reduce), 1 (retain), or exact values (old format)
 238                if (
 239                    not (grad_out.shape[i] == values[0].this_output_dimensions[i])
 240                    and not values[0].this_output_dimensions[i] == -1
 241                    and not values[0].this_output_dimensions[i] == 1
 242                ):
 243                    print(
 244                        "The following module has not properly set this_output_dimensions with this incorrect shape"
 245                    )
 246                    print(values[0].layer_name)
 247                    print("it is expecting:")
 248                    print(values[0].this_output_dimensions)
 249                    print("but received")
 250                    print(grad_out.shape)
 251                    print(
 252                        "to check these all at once set GPA.pc.set_debugging_output_dimensions(1)"
 253                    )
 254                    if not GPA.pc.get_debugging_output_dimensions():
 255                        sys.exit(0)
 256                    else:
 257                        GPA.pc.set_debugging_output_dimensions(2)
 258                        return
 259            # Setup the arrays with the now known shape
 260            with torch.no_grad():
 261                if GPA.pc.get_verbose():
 262                    print("setting d shape for")
 263                    print(values[0].layer_name)
 264                    print(val.size())
 265
 266                values[0].set_out_channels(val.size())
 267                ndim = len(values[0].this_output_dimensions)
 268                storage_shape = [1] * ndim
 269                for _i in range(ndim):
 270                    if values[0].this_output_dimensions[_i] == 1:
 271                        storage_shape[_i] = val.shape[_i]
 272                storage_shape[values[0].this_node_index.item()] = values[0].out_channels
 273                values[0].setup_arrays(storage_shape)
 274            # Flag that it has been setup (both the GPU tensor and the fast Python bool)
 275            values[0].current_d_init[0] = 1
 276            # If fixed_input_sizes is enabled, populate the tuple caches now
 277            # that val.shape is known. get_tuples_and_mult will read these on
 278            # every subsequent call instead of recomputing.
 279            if GPA.pc.get_perforated_backpropagation() and GPA.pc.get_fixed_input_sizes():
 280                from perforatedbp import modules_pbp as _MPB
 281                math_tuple, view_tuple, full_mult = _MPB.get_tuples_and_mult(val, values[0])
 282                ndim = len(val.shape)
 283                # math_tuple can be shorter than ndim (excludes this_node_index and
 284                # retained dims). Pad with -1 sentinel to fill the ndim-length buffer.
 285                padded_math = math_tuple + [-1] * (ndim - len(math_tuple))
 286                values[0].math_tuple_cache.copy_(
 287                    torch.tensor(padded_math, dtype=torch.long, device=val.device)
 288                )
 289                values[0].view_tuple_cache.copy_(
 290                    torch.tensor(view_tuple, dtype=torch.long, device=val.device)
 291                )
 292                values[0].full_mult_cache[0] = full_mult
 293            if module is not None:
 294                module._fb_init_done = True
 295                # When PBP is disabled this hook has no further work to do.
 296                # Deregister it so it never fires again, eliminating per-batch
 297                # Python overhead on all subsequent backward passes.
 298                if not GPA.pc.get_perforated_backpropagation() and module._fb_hook_handle is not None:
 299                    module._fb_hook_handle.remove()
 300                    module._fb_hook_handle = None
 301        if GPA.pc.get_perforated_backpropagation():
 302            MPB.filter_backward_pb(val, values)
 303
 304
 305def set_wrapped_params(model):
 306    """Set parameters as wrapped with dendrites.
 307
 308    Parameters
 309    ----------
 310    model : torch.nn.Module
 311        The model whose parameters are to be marked as wrapped.
 312
 313    Returns
 314    -------
 315    None
 316
 317    """
 318    for param in model.parameters():
 319        param.wrapped = True
 320
 321
 322def set_tracked_params(model):
 323    """Set parameters as tracked without dendrites.
 324
 325    Parameters
 326    ----------
 327    model : torch.nn.Module
 328        The model whose parameters are to be marked as tracked.
 329
 330    Returns
 331    -------
 332    None
 333    """
 334    for param in model.parameters():
 335        param.tracked = True
 336
 337
 338class PAINeuronModule(nn.Module):
 339    """Wrapper to set a module as one that will have dendritic copies."""
 340
 341    def __init__(self, start_module, name):
 342        """Initialize PAINeuronModule.
 343
 344        This function sets up the neuron module to wrap the start_module
 345        and manage its dendritic connections.
 346
 347        Parameters
 348        ----------
 349        start_module : nn.Module
 350            The module to wrap.
 351        name : str
 352            The name of the neuron module.
 353        """
 354        super(PAINeuronModule, self).__init__()
 355
 356        if isinstance(start_module, nn.Module):
 357            self.main_module = start_module
 358        else:
 359            print("start_module must be nn.Module: %s" % name)
 360            print(type(start_module))
 361            print(start_module)
 362            sys.exit(-1)
 363        self.name = name
 364        # Per-module config: loads custom settings from {save_name}_config.json if present.
 365        # Passes both the instance name (id) and the module type so load_config can
 366        # fall back to type-level settings when no name-specific entry exists.
 367        _module_type_name = type(start_module).__name__
 368        self.module_config = GPA.PAIConfig(
 369            module_name=self.name, module_type=_module_type_name
 370        )
 371
 372        set_wrapped_params(self.main_module)
 373        if self.module_config.get_verbose():
 374            print(
 375                f"initing a module {self.name} with main type {type(self.main_module)}"
 376            )
 377            print(start_module)
 378
 379        # If this main_module is one that requires processing set the processor
 380        if type(self.main_module) in self.module_config.get_modules_with_processing():
 381            module_index = self.module_config.get_modules_with_processing().index(
 382                type(self.main_module)
 383            )
 384            self.processor = self.module_config.get_modules_processing_classes()[
 385                module_index
 386            ]()
 387            if self.module_config.get_verbose():
 388                print("with processor")
 389                print(self.processor)
 390        elif (
 391            type(self.main_module).__name__
 392            in self.module_config.get_module_names_with_processing()
 393        ):
 394            module_index = self.module_config.get_module_names_with_processing().index(
 395                type(self.main_module).__name__
 396            )
 397            self.processor = self.module_config.get_module_by_name_processing_classes()[
 398                module_index
 399            ]()
 400            if self.module_config.get_verbose():
 401                print("with processor")
 402                print(self.processor)
 403        else:
 404            self.processor = None
 405
 406        # Field that can be filled in if your activation function requires a parameter
 407        self.activation_function_value = -1
 408        self.type = "neuron_module"
 409
 410        self.register_buffer(
 411            "this_output_dimensions",
 412            (torch.tensor(self.module_config.get_output_dimensions())),
 413        )
 414        if (self.this_output_dimensions == 0).sum() != 1:
 415            print(f"5 Need exactly one 0 in the input dimensions: {self.name}")
 416            print(self.this_output_dimensions)
 417            sys.exit(-1)
 418        self.register_buffer(
 419            "this_node_index",
 420            torch.tensor(self.module_config.get_output_dimensions().index(0)),
 421        )
 422        self.dendrite_modules_added = 0
 423
 424        # Values for dendrite to neuron weights
 425        self.dendrites_to_top = nn.ParameterList()
 426        self.register_parameter("newest_dendrite_to_top", None)
 427        self.candidate_to_top = nn.ParameterList()
 428        self.register_parameter("current_candidate_to_top", None)
 429        # Create the dendrite module
 430        self.dendrite_module = PAIDendriteModule(
 431            self.main_module,
 432            activation_function_value=self.activation_function_value,
 433            name=self.name,
 434            output_dimensions=self.this_output_dimensions,
 435        )
 436        # If it is linear and default has convolutional dimensions, automatically set to just be batch size and neuron indexes
 437        if (
 438            issubclass(type(start_module), nn.Linear)
 439            or (
 440                issubclass(type(start_module), GPA.PAISequential)
 441                and issubclass(type(start_module.model[0]), nn.Linear)
 442            )
 443        ) and (
 444            np.array(self.this_output_dimensions)[2:] == -1
 445        ).all():  # Everything past 2 is a negative 1
 446            self.set_this_output_dimensions(self.this_output_dimensions[0:2])
 447        if (
 448            issubclass(type(start_module), nn.Conv1d)
 449            or (
 450                issubclass(type(start_module), GPA.PAISequential)
 451                and issubclass(type(start_module.model[0]), nn.Conv1d)
 452            )
 453        ) and (
 454            np.array(self.this_output_dimensions)[3:] == -1
 455        ).all():  # Everything past 2 is a negative 1
 456            self.set_this_output_dimensions(self.this_output_dimensions[0:3])
 457        # Apply per-module output_dimensions override from config if present
 458        _custom_dims = self.module_config.__dict__.get("_output_dimensions")
 459        if _custom_dims is not None:
 460            self.set_this_output_dimensions(torch.tensor(_custom_dims))
 461        GPA.pai_tracker.add_pai_neuron_module(self)
 462        if self.module_config.get_perforated_backpropagation():
 463            MPB.set_neuron_parameters(self.main_module)
 464
 465        # Track filter_backward initialization state as a
 466        # plain Python bool (avoids a GPU .item() sync on every backward pass).
 467        # _fb_hook_handle stores the registered hook handle so it can be
 468        # removed after initialization completes when PBP is disabled.
 469        self._fb_init_done = False
 470        self._fb_hook_handle = None
 471
 472    def __getattr__(self, name):
 473        """Get member variables from the main module.
 474
 475        Parameters
 476        ----------
 477        name : str
 478            The name of the variable to retrieve.
 479        Returns
 480        -------
 481        The requested variable.
 482
 483        Notes
 484        -----
 485        This method first attempts to retrieve the attribute from the PAINeuronModule instance.
 486        If it fails, it tries to get the attribute from the wrapped main_module.
 487        This allows seamless access to the main module's attributes without modifying original code.
 488        """
 489        try:
 490            return super().__getattr__(name)
 491        except AttributeError:
 492            return getattr(self.main_module, name)
 493
 494    def __getitem__(self, index):
 495        """Support indexing operations on the main module.
 496
 497        Parameters
 498        ----------
 499        index : int or slice
 500            The index or slice to retrieve.
 501
 502        Returns
 503        -------
 504        The indexed item from the main module.
 505        """
 506        return self.main_module[index]
 507
 508    def apply_pb_grads(self):
 509        """Apply perforated backpropagation gradients if enabled.
 510
 511        Parameters
 512        ----------
 513        None
 514
 515        Returns
 516        -------
 517        None
 518            This function does not return a value.
 519        """
 520        self.dendrite_module.apply_pb_grads()
 521
 522    def apply_pb_zero(self):
 523        """Clear leftover saved tensors if there are any.
 524
 525        Parameters
 526        ----------
 527        None
 528
 529        Returns
 530        -------
 531        None
 532            This function does not return a value.
 533        """
 534        self.dendrite_module.apply_pb_zero()
 535
 536    def clear_processors(self):
 537        """Clear processors if they save values for DeepCopy and save.
 538
 539        Parameters
 540        ----------
 541        None
 542
 543        Returns
 544        -------
 545        None
 546        """
 547
 548        if not self.processor:
 549            return
 550        else:
 551            self.processor.clear_processor()
 552            self.dendrite_module.clear_processors()
 553
 554    def clear_dendrites(self):
 555        """Clear and reset dendrites before loading from a state dict.
 556
 557        Parameters
 558        ----------
 559        None
 560
 561        Returns
 562        -------
 563        None
 564
 565        """
 566        # Loading a saved state reconstructs PAIDendriteModule before simulating
 567        # its saved cycles. Preserve a registered variant factory so candidate
 568        # creation does not silently fall back to deep-copying the parent module.
 569        create_dendrite_fn = self.dendrite_module._create_dendrite_fn
 570        self.dendrite_modules_added = 0
 571        self.dendrites_to_top = nn.ParameterList()
 572        self.candidate_to_top = nn.ParameterList()
 573        self.dendrite_module = PAIDendriteModule(
 574            self.main_module,
 575            activation_function_value=self.activation_function_value,
 576            name=self.name,
 577            output_dimensions=self.this_output_dimensions,
 578        )
 579        if create_dendrite_fn is not None:
 580            self.dendrite_module.set_create_dendrite(create_dendrite_fn)
 581
 582    def __str__(self):
 583        """String representation of the module.
 584
 585        Parameters
 586        ----------
 587        None
 588
 589        Returns
 590        -------
 591        str
 592            String representation of the module.
 593
 594        Notes
 595        -----
 596        Setting for verbose changes level of details in the string output.
 597        """
 598        # If verbose print the whole module otherwise just print the module type as a PAIModule
 599        if self.module_config.get_verbose():
 600            total_string = self.main_module.__str__()
 601            total_string = "PAIModule(" + total_string + ")"
 602            return total_string + self.dendrite_module.__str__()
 603        else:
 604            total_string = self.main_module.__str__()
 605            total_string = "PAIModule(" + total_string + ")"
 606            return total_string
 607
 608    def __repr__(self):
 609        """Representation of the module."""
 610        return self.__str__()
 611
 612    def set_this_output_dimensions(self, new_output_dimensions):
 613        """Set the input dimensions for the neuron and dendrite blocks.
 614
 615        Signals to this NeuronModule that its input dimensions are different
 616        than the global default.
 617
 618        Parameters
 619        ----------
 620        new_output_dimensions : list
 621            A list or tensor specifying the new input dimensions.
 622        Returns
 623        -------
 624        None
 625
 626        """
 627        if type(new_output_dimensions) is list:
 628            new_output_dimensions = torch.tensor(new_output_dimensions)
 629        delattr(self, "this_output_dimensions")
 630        self.register_buffer(
 631            "this_output_dimensions", new_output_dimensions.detach().clone()
 632        )
 633        if (new_output_dimensions == 0).sum() != 1:
 634            print(f"6 need exactly one 0 in the input dimensions: {self.name}")
 635            print(new_output_dimensions)
 636        self.this_node_index.copy_(
 637            (new_output_dimensions == 0).nonzero(as_tuple=True)[0][0]
 638        )
 639        self.dendrite_module.set_this_output_dimensions(new_output_dimensions)
 640
 641    def set_create_dendrite(self, fn):
 642        """Set a custom function for creating dendrite modules.
 643
 644        Override how dendrite copies are created from the parent module.  By default
 645        the parent module is deep-copied.  Pass any callable with the signature
 646        ``fn(parent_module) -> nn.Module`` to replace that behaviour.
 647
 648        Parameters
 649        ----------
 650        fn : callable
 651            A function with signature ``fn(parent_module) -> nn.Module``.
 652
 653        Returns
 654        -------
 655        None
 656        """
 657        self.dendrite_module.set_create_dendrite(fn)
 658
 659
 660    def set_mode(self, mode):
 661        """Switch between neuron training and dendrite training.
 662
 663        Parameters
 664        ----------
 665        mode : str
 666            The mode to set. Either "n" for neuron training or "p" for pai-dendrite training.
 667
 668        Returns
 669        -------
 670        bool
 671            True if mode was set successfully, False otherwise.
 672
 673        Notes
 674        -----
 675        If False is returned, the mode was not changed due to an error.
 676        This is a problem that should not be ignored, but it can be ignored
 677        by calling PGA.pc.set_checked_skipped_modules(True)
 678        """
 679
 680        if self.module_config.get_verbose():
 681            print(f"{self.name} calling set mode {mode}")
 682        # If returning to neuron training
 683        if mode == "n":
 684            self.dendrite_module.set_mode(mode)
 685            # Initialize the dendrite to neuron connections
 686            if self.dendrite_modules_added > 0:
 687                if self.module_config.get_learn_dendrites_live():
 688                    values = torch.cat(
 689                        (
 690                            self.dendrites_to_top[self.dendrite_modules_added - 1],
 691                            nn.Parameter(
 692                                self.candidate_to_top.detach()
 693                                .clone()
 694                                .to(dtype=self.module_config.get_d_type())
 695                            ),
 696                        ),
 697                        0,
 698                    )
 699                else:
 700                    values = torch.cat(
 701                        (
 702                            self.dendrites_to_top[self.dendrite_modules_added - 1],
 703                            nn.Parameter(
 704                                torch.zeros(
 705                                    (1, self.out_channels),
 706                                    device=self.dendrites_to_top[
 707                                        self.dendrite_modules_added - 1
 708                                    ].device,
 709                                    dtype=self.module_config.get_d_type(),
 710                                )
 711                            ),
 712                        ),
 713                        0,
 714                    )
 715                # Freeze the previous dendrites_to_top entry
 716                # so the optimizer no longer updates stale entries. Only the
 717                # most recently appended entry is ever used in the forward pass.
 718                if len(self.dendrites_to_top) > 0:
 719                    self.dendrites_to_top[-1].requires_grad_(False)
 720                self.dendrites_to_top.append(
 721                    nn.Parameter(
 722                        values.detach()
 723                        .clone()
 724                        .to(
 725                            device=self.module_config.get_device(),
 726                            dtype=self.module_config.get_d_type(),
 727                        ),
 728                        requires_grad=True,
 729                    )
 730                )
 731            else:
 732                if self.module_config.get_learn_dendrites_live():
 733                    # Freeze previous entry before appending new one.
 734                    if len(self.dendrites_to_top) > 0:
 735                        self.dendrites_to_top[-1].requires_grad_(False)
 736                    self.dendrites_to_top.append(
 737                        nn.Parameter(
 738                            self.candidate_to_top.detach()
 739                            .clone()
 740                            .to(dtype=self.module_config.get_d_type()),
 741                            requires_grad=True,
 742                        )
 743                    )
 744                else:
 745                    # Freeze previous entry before appending new one.
 746                    if len(self.dendrites_to_top) > 0:
 747                        self.dendrites_to_top[-1].requires_grad_(False)
 748                    self.dendrites_to_top.append(
 749                        nn.Parameter(
 750                            torch.zeros(
 751                                (1, self.out_channels),
 752                                device=self.module_config.get_device(),
 753                                dtype=self.module_config.get_d_type(),
 754                            )
 755                            .detach()
 756                            .clone(),
 757                            requires_grad=True,
 758                        )
 759                    )
 760            self.dendrite_modules_added += 1
 761            if self.module_config.get_perforated_backpropagation():
 762                MPB.set_module_n_pb(self)
 763                MPB.set_neuron_parameters(self.dendrites_to_top)
 764
 765        # If starting dendrite training
 766        else:
 767            try:
 768                # Save the values that were calculated in filter_backward
 769                self.out_channels = self.dendrite_module.dendrite_values[0].out_channels
 770                self.dendrite_module.out_channels = (
 771                    self.dendrite_module.dendrite_values[0].out_channels
 772                )
 773            except Exception as e:
 774                print(e)
 775                print(
 776                    f"this occurred in module: {self.dendrite_module.dendrite_values[0].layer_name}"
 777                )
 778                print(
 779                    "Module should be added to module_names_to_track so it doesn't have dendrites added"
 780                )
 781                print("If you are getting here but out_channels has not been set")
 782                print(
 783                    "A common reason is that this module never had gradients flow through it."
 784                )
 785                print("I have seen this happen because:")
 786                print("-The weights were frozen (requires_grad = False)")
 787                print(
 788                    "-A model is added but not used so it was converted to a perforated module initialized"
 789                )
 790                print(
 791                    "-A module was converted that doesn't have weights that get modified so backward doesn't flow through it"
 792                )
 793                print(
 794                    "If this is normal behavior set GPA.pc.set_checked_skipped_modules(True) in the main to ignore"
 795                )
 796                print(
 797                    "You can also set right now in this pdb terminal to have this not happen more after checking all modules this cycle."
 798                )
 799                if not self.module_config.get_checked_skipped_modules():
 800                    pdb.set_trace()
 801                return False
 802            # Only change mode if it makes it past the above exception
 803            self.dendrite_module.set_mode(mode)
 804            if self.module_config.get_perforated_backpropagation():
 805                MPB.set_module_p_pb(self)
 806        return True
 807
 808    def create_new_dendrite_module(self):
 809        """Add an additional dendrite module.
 810
 811        Parameters
 812        ----------
 813        None
 814
 815        Returns
 816        -------
 817        None
 818        """
 819        self.dendrite_module.create_new_dendrite_module(self.main_module)
 820
 821    def forward(self, *args, **kwargs):
 822        """Forward pass through the neuron module.
 823
 824        Parameters
 825        ----------
 826        *args : tuple
 827            Positional arguments for the forward pass.
 828        **kwargs : dict
 829            Keyword arguments for the forward pass.
 830
 831        Returns
 832        -------
 833        Any
 834            The output of the module after processing through the neuron and dendrite modules.
 835
 836        Notes
 837        -----
 838            The output of this forward function will have the same format as the output
 839            of the original module
 840        """
 841
 842        # If debugging all input dimensions, quit program on first forward call
 843        if self.module_config.get_debugging_output_dimensions() == 2:
 844            print("all input dim problems now printed")
 845            sys.exit(0)
 846        if self.module_config.get_extra_verbose():
 847            print(f"{self.name} calling forward")
 848        # Call the main modules forward
 849        out = self.main_module(*args, **kwargs)
 850        # Filter with the processor if required
 851        if self.processor is not None:
 852            try:
 853                out = self.processor.post_n1(out)
 854            except Exception as e:
 855                traceback.print_exc(limit=None, chain=True)
 856                print(f"Your post_n1 processor for {self.name} caused this error")
 857                print(
 858                    f"You must check how this is defined and ensure that it is properly"
 859                )
 860                print(f"accepting outputs from the neuron module and returning the")
 861                print(f"single tensor to be combined with the dendrites output tensor")
 862                sys.exit()
 863        # Call the forwards for all of the Dendrites
 864        (
 865            dendrite_outs,
 866            candidate_outs,
 867            candidate_nonlinear_outs,
 868            candidate_outs_non_zeroed,
 869        ) = self.dendrite_module(*args, **kwargs)
 870        # If there are dendrites add all of their outputs to the neurons output
 871        if self.dendrite_modules_added > 0:
 872            for i in range(0, self.dendrite_modules_added):
 873                to_top = self.dendrites_to_top[self.dendrite_modules_added - 1][i, :]
 874                for dim in range(len(dendrite_outs[i].shape)):
 875                    if dim == self.this_node_index:
 876                        continue
 877                    to_top = to_top.unsqueeze(dim)
 878                if self.module_config.get_confirm_correct_sizes():
 879                    to_top = to_top.expand(
 880                        list(dendrite_outs[i].size())[0 : self.this_node_index]
 881                        + [self.out_channels]
 882                        + list(dendrite_outs[i].size())[self.this_node_index + 1 :]
 883                    )
 884                out = out + (dendrite_outs[i].to(out.device) * to_top.to(out.device))
 885
 886        # If learning live, add the candidate's output to the neuron's output via the live weight
 887        if self.module_config.get_perforated_backpropagation():
 888            out = MPB.apply_live_candidate_to_output(
 889                self, out, candidate_nonlinear_outs
 890            )
 891
 892        # Catch if processors are required
 893        if type(out) is tuple:
 894            print(self)
 895            print(
 896                f"The output of the above module {self.name} is a tuple when it must be a single tensor"
 897            )
 898            print(
 899                "This must be fixed to enable the dendrite and neuron output to be combined"
 900            )
 901            print(
 902                "Look in the API customization.md at section 2.2 regarding processors to fix this."
 903            )
 904            pdb.set_trace()
 905
 906        # Call filter backward to ensure the neuron index is setup correctly.
 907        # Register only when not yet initialized (PBP=False) or always when PBP
 908        # is enabled. Store the handle so the hook can deregister itself after
 909        # the one-time initialization completes (PBP=False path).
 910        if out.requires_grad and (not self._fb_init_done or GPA.pc.get_perforated_backpropagation()):
 911            self._fb_hook_handle = out.register_hook(
 912                lambda grad: filter_backward(grad, self.dendrite_module.dendrite_values, self)
 913            )
 914
 915        # If there is a processor apply the second neuron stage
 916        if self.processor is not None:
 917            try:
 918                out = self.processor.post_n2(out)
 919            except Exception as e:
 920                traceback.print_exc(limit=None, chain=True)
 921                print(f"Your post_n2 processor for {self.name} caused this error")
 922                print(
 923                    f"You must check how this is defined and ensure that it is properly"
 924                )
 925                print(
 926                    f"accepting the output tensor after combining the neuron's output "
 927                )
 928                print(f"with the dendrite's output and returning something that is the")
 929                print(f"same format as your original module's return")
 930                sys.exit()
 931        return out
 932
 933
 934class TrackedNeuronModule(nn.Module):
 935    """Wrapper for modules you don't want to add dendrites to. Ensures all modules are accounted for."""
 936
 937    def __init__(self, start_module, name):
 938        """Initialize TrackedNeuronModule.
 939
 940        This function sets up the tracked neuron module to wrap the start_module
 941        without adding dendrites.
 942
 943        Parameters
 944        ----------
 945        start_module : nn.Module
 946            The module to wrap.
 947        name : str
 948            The name of the neuron module.
 949        """
 950        super(TrackedNeuronModule, self).__init__()
 951
 952        if isinstance(start_module, nn.Module):
 953            self.main_module = start_module
 954        else:
 955            print("start_module must be nn.Module: %s" % name)
 956            print(type(start_module))
 957            print(start_module)
 958            sys.exit(-1)
 959        self.name = name
 960
 961        self.type = "tracked_module"
 962        set_tracked_params(self.main_module)
 963        if GPA.pc.get_verbose():
 964            print(
 965                f"tracking a module {self.name} with main type {type(self.main_module)}"
 966            )
 967            print(start_module)
 968        GPA.pai_tracker.add_tracked_neuron_module(self)
 969        if GPA.pc.get_perforated_backpropagation():
 970            MPB.set_neuron_parameters(self.main_module)
 971
 972    def __getattr__(self, name):
 973        """Get member variables from the main module.
 974
 975        Parameters
 976        ----------
 977        name : str
 978            The name of the variable to retrieve.
 979        Returns
 980        -------
 981        The requested variable.
 982
 983        Notes
 984        -----
 985        This method first attempts to retrieve the attribute from the PAINeuronModule instance.
 986        If it fails, it tries to get the attribute from the wrapped main_module.
 987        This allows seamless access to the main module's attributes without modifying original code.
 988        """
 989        try:
 990            return super().__getattr__(name)
 991        except AttributeError:
 992            return getattr(self.main_module, name)
 993
 994    def __getitem__(self, index):
 995        """Support indexing operations on the main module.
 996
 997        Parameters
 998        ----------
 999        index : int or slice
1000            The index or slice to retrieve.
1001
1002        Returns
1003        -------
1004        The indexed item from the main module.
1005        """
1006        return self.main_module[index]
1007
1008    def set_mode(self, mode):
1009        """Set mode for tracked module.
1010
1011        Parameters
1012        ----------
1013        mode : str
1014            The mode to set. Either "n" for neuron training or "p" for pai-dendrite training.
1015
1016        Returns
1017        -------
1018        bool
1019            True.
1020
1021        Notes
1022        -----
1023        This function does not change any behavior since this is a tracked module.
1024        """
1025
1026        if GPA.pc.get_verbose():
1027            print(f"{self.name} calling set mode {mode}")
1028        return True
1029
1030    def forward(self, *args, **kwargs):
1031        """Forward pass for tracked module.
1032
1033        Parameters
1034        ----------
1035        *args : tuple
1036            Positional arguments for the forward pass.
1037        **kwargs : dict
1038            Keyword arguments for the forward pass.
1039
1040        Returns
1041        -------
1042        Any
1043            The output of the module
1044
1045        Notes
1046        -----
1047            The output of this forward function will have the same format as the output
1048            of the original module
1049        """
1050        return self.main_module(*args, **kwargs)
1051
1052    def __str__(self):
1053        """String representation of the module.
1054
1055        Parameters
1056        ----------
1057        None
1058
1059        Returns
1060        -------
1061        str
1062            String representation of the module.
1063
1064        Notes
1065        -----
1066        Setting for verbose changes level of details in the string output.
1067        """
1068
1069        if GPA.pc.get_verbose():
1070            total_string = self.main_module.__str__()
1071            total_string = "PAITrackedModule(" + total_string + ")"
1072            return total_string
1073        else:
1074            total_string = self.main_module.__str__()
1075            total_string = "PAITrackedModule(" + total_string + ")"
1076            return total_string
1077
1078    def __repr__(self):
1079        """Representation of the module."""
1080        return self.__str__()
1081
1082
1083def init_params(module, neuron_main_module):
1084    """Randomize weights after duplicating the main module for the next set of dendrites.
1085
1086    Parameters
1087    ----------
1088    module : nn.Module
1089        The new dendrite module to initialize.
1090    neuron_main_module : nn.Module
1091        The main module of the neuron for potential weight scaling.
1092
1093
1094    Returns
1095    -------
1096    None
1097        This function does not return a value.
1098    """
1099    for param in module.parameters():
1100        if param.dtype == torch.uint8:
1101            param.data = torch.randint(0, 256, param.size(), dtype=torch.uint8)
1102        else:
1103            # If factoring in the main modules weights multiply the randn()
1104            #  by the average abs value of the main modules weights
1105            if GPA.pc.get_candidate_weight_init_by_main():
1106                main_module_abs = 0
1107                total_main_params = 0
1108                for main_param in neuron_main_module.parameters():
1109                    main_module_abs += main_param.abs().sum().item()
1110                    total_main_params += main_param.numel()
1111                if total_main_params > 0:
1112                    main_module_abs /= total_main_params
1113                else:
1114                    main_module_abs = 1.0
1115                multiplier = main_module_abs
1116            else:
1117                multiplier = 1.0
1118            param.data = (
1119                torch.randn(param.size(), dtype=param.dtype)
1120                * GPA.pc.get_candidate_weight_initialization_multiplier()
1121                * multiplier
1122            )
1123
1124
1125class PAIDendriteModule(nn.Module):
1126    """Module containing all dendrites modules added to the neuron module."""
1127
1128    def __init__(
1129        self,
1130        initial_module,
1131        activation_function_value=0.3,
1132        name="no_name_given",
1133        output_dimensions=None,
1134    ):
1135        """Initialize PAINeuronModule.
1136
1137        This function sets up the dendrite module to create candidate and permanent
1138        dendrite modules based on the initial_module provided.
1139
1140        Parameters
1141        ----------
1142        initial_module : nn.Module
1143            The module to copy.
1144        activation_function_value : float, optional
1145            A value associated with the activation function, by default 0.3.
1146        name : str
1147            The name of the neuron module.
1148        output_dimensions : vector, optional
1149            The dimensions of the input vector
1150        """
1151        super(PAIDendriteModule, self).__init__()
1152
1153        if output_dimensions is None:
1154            output_dimensions = []
1155
1156        self.layers = nn.ModuleList([])
1157        self.processors = []
1158        self.candidate_processors = []
1159        self.num_dendrites = 0
1160        self._create_dendrite_fn = None
1161        # Number of dendrite cycles performed
1162        self.register_buffer(
1163            "num_cycles",
1164            torch.zeros(1, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()),
1165        )
1166        self.mode = "n"
1167        self.name = name
1168        # Create a copy of the parent module so you don't have a pointer to the real one which causes save errors
1169        self.parent_module = UPA.deep_copy_pai(initial_module)
1170        if GPA.pc.get_perforated_backpropagation():
1171            MPB.set_ignored_parameters(self.parent_module)
1172        # Setup the input dimensions and node index for combining dendrite outputs
1173        if GPA.pc.get_perforated_backpropagation():
1174            MPB.create_extra_tensors(self)
1175        if output_dimensions == []:
1176            self.register_buffer(
1177                "this_output_dimensions", torch.tensor(GPA.pc.get_output_dimensions())
1178            )
1179        else:
1180            self.register_buffer(
1181                "this_output_dimensions", output_dimensions.detach().clone()
1182            )
1183        if (self.this_output_dimensions == 0).sum() != 1:
1184            print(f"1 need exactly one 0 in the input dimensions: {self.name}")
1185            print(self.this_output_dimensions)
1186            sys.exit(-1)
1187        self.register_buffer(
1188            "this_node_index", torch.tensor(GPA.pc.get_output_dimensions().index(0))
1189        )
1190
1191        # Initialize dendrite to dendrite connections
1192        self.dendrites_to_candidates = nn.ParameterList()
1193        self.dendrites_to_dendrites = nn.ParameterList()
1194
1195        # Store an activation function value if required
1196        self.activation_function_value = activation_function_value
1197        self.dendrite_values = nn.ModuleList([])
1198        for j in range(0, GPA.pc.get_global_candidates()):
1199            if GPA.pc.get_verbose():
1200                print(f"creating dendrite Values for {self.name}")
1201            self.dendrite_values.append(
1202                DendriteValueTracker(
1203                    False,
1204                    self.activation_function_value,
1205                    self.name,
1206                    self.this_output_dimensions,
1207                )
1208            )
1209        if GPA.pc.get_perforated_backpropagation():
1210            self.apply_pb_grads = MPB.apply_pb_grads.__get__(self, type(self))
1211            self.apply_pb_zero = MPB.apply_pb_zero.__get__(self, type(self))
1212
1213    def __getstate__(self):
1214        """Tell pickle what to save when this object is serialized (e.g. torch.save).
1215
1216        apply_pb_grads and apply_pb_zero are bound methods of functions defined
1217        in modules_pbp and cannot be pickled.  Strip them out; __setstate__ will
1218        re-attach them after loading.
1219        """
1220        import types
1221
1222        pickle_safe_state = {}
1223        for attr_name, attr_value in self.__dict__.items():
1224            if not isinstance(attr_value, types.MethodType):
1225                pickle_safe_state[attr_name] = attr_value
1226
1227        return pickle_safe_state
1228
1229    def __setstate__(self, saved_state):
1230        """Restore this object from a pickled state (e.g. torch.load).
1231
1232        Restores all normal attributes, then re-attaches apply_pb_grads and
1233        apply_pb_zero if perforated backpropagation is enabled.
1234        """
1235        self.__dict__.update(saved_state)
1236
1237        # Re-attach the PBP bound methods that were stripped by __getstate__.
1238        # dendrite_loss_fn being present on the saved state means PBP was active
1239        # when the checkpoint was saved.
1240        if "dendrite_loss_fn" in saved_state:
1241            import perforatedbp.modules_pbp as MPB
1242            self.apply_pb_grads = MPB.apply_pb_grads.__get__(self, type(self))
1243            self.apply_pb_zero = MPB.apply_pb_zero.__get__(self, type(self))
1244
1245    def create_dendrite(self, parent_module):
1246        """Create a dendrite module from the parent module.
1247
1248        Override this function via set_create_dendrite to control how the dendrite
1249        module is created (e.g. to avoid a deep copy).
1250
1251        Parameters
1252        ----------
1253        parent_module : nn.Module
1254            The module to create a dendrite from.
1255
1256        Returns
1257        -------
1258        nn.Module
1259            The new dendrite module.
1260        """
1261        if self._create_dendrite_fn is not None:
1262            return self._create_dendrite_fn(parent_module)
1263        return UPA.deep_copy_pai(parent_module)
1264
1265    def set_create_dendrite(self, fn):
1266        """Set a custom function for creating dendrite modules.
1267
1268        Call this on a PAIDendriteModule instance to override how dendrites are
1269        created from the parent module. The function receives the parent module
1270        and must return a new nn.Module.
1271
1272        Parameters
1273        ----------
1274        fn : callable
1275            A function with signature ``fn(parent_module) -> nn.Module``.
1276
1277        Returns
1278        -------
1279        None
1280        """
1281        self._create_dendrite_fn = fn
1282
1283
1284    def set_this_output_dimensions(self, new_output_dimensions):
1285        """Set input dimensions for dendrite module.
1286
1287        Signals to this DendriteModule that its input dimensions are different
1288        than the global default.
1289
1290        Parameters
1291        ----------
1292        new_output_dimensions : list
1293            A list or tensor specifying the new input dimensions.
1294        Returns
1295        -------
1296        None
1297
1298        """
1299
1300        if type(new_output_dimensions) is list:
1301            new_output_dimensions = torch.tensor(new_output_dimensions)
1302        delattr(self, "this_output_dimensions")
1303        self.register_buffer(
1304            "this_output_dimensions", new_output_dimensions.detach().clone()
1305        )
1306        if (new_output_dimensions == 0).sum() != 1:
1307            print(f"2 Need exactly one 0 in the input dimensions: {self.name}")
1308            print(new_output_dimensions)
1309            sys.exit(-1)
1310        self.this_node_index.copy_(
1311            (new_output_dimensions == 0).nonzero(as_tuple=True)[0][0]
1312        )
1313        for j in range(0, GPA.pc.get_global_candidates()):
1314            self.dendrite_values[j].set_this_output_dimensions(new_output_dimensions)
1315
1316    def create_new_dendrite_module(self, neuron_main_module):
1317        """Add a new set of dendrites.
1318
1319        Parameters
1320        ----------
1321        neuron_main_module : Any
1322            PyTorch module to be used for dendritic learning.
1323                Typically a copy of the original neuron module.
1324
1325        Returns
1326        -------
1327        None
1328            This function does not return a value.
1329        """
1330        # Candidate module
1331        self.candidate_module = nn.ModuleList([])
1332        # Copy that is unused for open source version
1333        self.best_candidate_module = nn.ModuleList([])
1334        if GPA.pc.get_verbose():
1335            print(self.name)
1336            print("Setting candidate processors")
1337        self.candidate_processors = []
1338        with torch.no_grad():
1339            for i in range(0, GPA.pc.get_global_candidates()):
1340
1341                new_module = self.create_dendrite(self.parent_module)
1342                init_params(new_module, neuron_main_module)
1343                self.candidate_module.append(new_module)
1344                self.best_candidate_module.append(self.create_dendrite(new_module))
1345                if type(self.parent_module) in GPA.pc.get_modules_with_processing():
1346                    module_index = GPA.pc.get_modules_with_processing().index(
1347                        type(self.parent_module)
1348                    )
1349                    self.candidate_processors.append(
1350                        GPA.pc.get_modules_processing_classes()[module_index]()
1351                    )
1352                elif (
1353                    type(self.parent_module).__name__
1354                    in GPA.pc.get_module_names_with_processing()
1355                ):
1356                    module_index = GPA.pc.get_module_names_with_processing().index(
1357                        type(self.parent_module).__name__
1358                    )
1359                    self.candidate_processors.append(
1360                        GPA.pc.get_module_by_name_processing_classes()[module_index]()
1361                    )
1362                if GPA.pc.get_perforated_backpropagation():
1363                    MPB.set_candidate_parameters(self.candidate_module[i])
1364                    MPB.set_ignored_parameters(self.best_candidate_module[i])
1365
1366        for i in range(0, GPA.pc.get_global_candidates()):
1367            self.candidate_module[i].to(GPA.pc.get_device())
1368            self.best_candidate_module[i].to(GPA.pc.get_device())
1369
1370        # Reset the dendrite_values objects
1371        for j in range(0, GPA.pc.get_global_candidates()):
1372            self.dendrite_values[j].reinitialize_for_pai()
1373
1374        # If there are already dendrites initialize the dendrite to dendrite connections
1375        if self.num_dendrites > 0:
1376            self.dendrites_to_candidates = nn.ParameterList()
1377            for j in range(0, GPA.pc.get_global_candidates()):
1378                self.dendrites_to_candidates.append(
1379                    nn.Parameter(
1380                        torch.zeros(
1381                            (self.num_dendrites, self.out_channels),
1382                            device=GPA.pc.get_device(),
1383                            dtype=GPA.pc.get_d_type(),
1384                        ),
1385                        requires_grad=True,
1386                    )
1387                )
1388                if GPA.pc.get_perforated_backpropagation():
1389                    MPB.init_candidates(self, j)
1390            if GPA.pc.get_perforated_backpropagation():
1391                MPB.set_candidate_parameters(self.dendrites_to_candidates)
1392            # Initialize best_dendrites_to_candidates_saved to snapshot peak-correlation weights at epoch boundaries
1393            self.best_dendrites_to_candidates_saved = []
1394            for j in range(0, GPA.pc.get_global_candidates()):
1395                self.best_dendrites_to_candidates_saved.append(
1396                    torch.zeros(
1397                        (self.num_dendrites, self.out_channels),
1398                        device=GPA.pc.get_device(),
1399                        dtype=GPA.pc.get_d_type(),
1400                    )
1401                )
1402
1403    def clear_processors(self):
1404        """Clear processors.
1405
1406        Parameters
1407        ----------
1408        None
1409
1410        Returns
1411        -------
1412        None
1413            This function does not return a value.
1414        """
1415        for processor in self.processors:
1416            if not processor:
1417                continue
1418            else:
1419                processor.clear_processor()
1420        for processor in self.candidate_processors:
1421            if not processor:
1422                continue
1423            else:
1424                processor.clear_processor()
1425
1426    def set_mode(self, mode):
1427        """Perform actions when switching between neuron and dendrite training.
1428
1429        Parameters
1430        ----------
1431        mode : str
1432            The mode to set. Either "n" for neuron training or "p" for pai-dendrite training.
1433
1434        Returns
1435        -------
1436        None
1437        """
1438
1439        self.mode = mode
1440        self.num_cycles += 1
1441        if GPA.pc.get_verbose():
1442            print(f"PAI calling set mode {mode} : {self.num_cycles}")
1443        if not GPA.pc.get_silent():
1444            print(f"Module {self.name} calling set mode {mode} : {self.num_cycles}")
1445        # When switching back to neuron training mode convert candidates modules into accepted modules
1446        if mode == "n":
1447            if GPA.pc.get_verbose():
1448                print("So calling all the things to add to modules")
1449            # Copy weights/bias from correct candidates
1450            if self.num_dendrites == 1:
1451                self.dendrites_to_dendrites = nn.ParameterList()
1452                self.dendrites_to_dendrites.append(torch.tensor([]))
1453            if self.num_dendrites >= 1:
1454                self.dendrites_to_dendrites.append(
1455                    torch.nn.Parameter(
1456                        torch.zeros(
1457                            [self.num_dendrites, self.out_channels],
1458                            device=GPA.pc.get_device(),
1459                            dtype=GPA.pc.get_d_type(),
1460                        ),
1461                        # Grad is true if not pb or if pb and dendrite_update_mode is true
1462                        requires_grad=(not GPA.pc.get_perforated_backpropagation())
1463                        or GPA.pc.get_dendrite_update_mode(),
1464                    )
1465                )
1466            with torch.no_grad():
1467                if GPA.pc.get_global_candidates() > 1:
1468                    print(
1469                        "This was a flag that will be needed if using multiple candidates. "
1470                        "It's not set up yet but nice work finding it."
1471                    )
1472                    print(
1473                        "Note: with multiple candidates, best-score ranking in new_best() uses "
1474                        "unnormalized covariance (prev_dendrite_candidate_correlation) rather than "
1475                        "the normalized correlation coefficient. Candidates with larger output "
1476                        "magnitude will be favored regardless of true correlation quality. "
1477                        "Fix by tracking running sigma_V and sigma_E and dividing in new_best()."
1478                    )
1479                    pdb.set_trace()
1480                plane_max_index = 0
1481                self.layers.append(
1482                    UPA.deep_copy_pai(self.best_candidate_module[plane_max_index])
1483                )
1484                self.layers[self.num_dendrites].to(GPA.pc.get_device())
1485                if self.num_dendrites > 0:
1486                    self.dendrites_to_dendrites[self.num_dendrites].copy_(
1487                        self.best_dendrites_to_candidates_saved[plane_max_index]
1488                    )
1489                if type(self.parent_module) in GPA.pc.get_modules_with_processing():
1490                    self.processors.append(self.candidate_processors[plane_max_index])
1491                if (
1492                    type(self.parent_module).__name__
1493                    in GPA.pc.get_module_names_with_processing()
1494                ):
1495                    self.processors.append(self.candidate_processors[plane_max_index])
1496            if GPA.pc.get_perforated_backpropagation():
1497                MPB.set_pb_mode(self, mode)
1498            del self.candidate_module, self.best_candidate_module
1499
1500            self.num_dendrites += 1
1501            if GPA.pc.get_perforated_backpropagation():
1502                MPB.set_dendrite_parameters(self.dendrites_to_dendrites)
1503                MPB.set_dendrite_parameters(self.layers)
1504
1505    def forward(self, *args, **kwargs):
1506        """Forward pass for dendrite module.
1507
1508        Parameters
1509        ----------
1510        *args : tuple
1511            Positional arguments for the forward pass.
1512        **kwargs : dict
1513            Keyword arguments for the forward pass.
1514
1515        Returns
1516        -------
1517        Any
1518            The output of the module after processing through the neuron and dendrite modules.
1519        Any
1520            Remaining outputs are only used for Perforated Backpropagation.
1521        Any
1522            Remaining outputs are only used for Perforated Backpropagation.
1523        Any
1524            Remaining outputs are only used for Perforated Backpropagation.
1525
1526        Notes
1527        -----
1528        If using Perforated Backpropagation, the additional outputs will be moved around in
1529        this code but left unused and only passed into separate PB functions.
1530        """
1531
1532        outs = {}
1533
1534        # For all modules apply processors, call the modules, then apply post processors
1535        args2, kwargs2 = args, kwargs
1536        for c in range(0, self.num_dendrites):
1537            if GPA.pc.get_perforated_backpropagation():
1538                args2, kwargs2 = MPB.preprocess_pb(*args, **kwargs)
1539            if self.processors != []:
1540                try:
1541                    args2, kwargs2 = self.processors[c].pre_d(*args2, **kwargs2)
1542                except Exception as e:
1543                    traceback.print_exc(limit=None, chain=True)
1544                    print(f"Your pre_d processor for {self.name} caused this error")
1545                    print(
1546                        f"You must check how this is defined and ensure that it is properly"
1547                    )
1548                    print(
1549                        f"accepting inputs to the PAIModule and returning what will then be"
1550                    )
1551                    print(f"the input to the dendrite module")
1552                    sys.exit()
1553            out_values = self.layers[c](*args2, **kwargs2)
1554            if self.processors != []:
1555                try:
1556                    outs[c] = self.processors[c].post_d(out_values)
1557                except Exception as e:
1558                    traceback.print_exc(limit=None, chain=True)
1559                    print(f"Your post_d processor for {self.name} caused this error")
1560                    print(
1561                        f"You must check how this is defined and ensure that it is properly"
1562                    )
1563                    print(
1564                        f"accepting outputs from the dendrite module and returning the"
1565                    )
1566                    print(
1567                        f"single tensor to be combined with the neurons output tensor"
1568                    )
1569                    sys.exit()
1570            else:
1571                outs[c] = out_values
1572
1573        # Create dendrite outputs
1574        # Each dendrite has input from previously created dendrites
1575        # So activation is added before the nonlinearity is called
1576        view_tuple = []
1577        for out_index in range(0, self.num_dendrites):
1578            current_out = outs[out_index]
1579            view_tuple = []
1580            for dim in range(len(current_out.shape)):
1581                if dim == self.this_node_index:
1582                    view_tuple.append(-1)
1583                    continue
1584                view_tuple.append(1)
1585
1586            for in_index in range(0, out_index):
1587                if view_tuple == [
1588                    1
1589                ]:  # This is only the case when passing a single datapoint rather than a batch
1590                    current_out = (
1591                        current_out
1592                        + self.dendrites_to_dendrites[out_index][in_index, :].to(
1593                            current_out.device
1594                        )
1595                        * outs[in_index]
1596                    )
1597                else:
1598                    current_out = (
1599                        current_out
1600                        + self.dendrites_to_dendrites[out_index][in_index, :]
1601                        .view(view_tuple)
1602                        .to(current_out.device)
1603                        * outs[in_index]
1604                    )
1605            outs[out_index] = GPA.pc.get_pai_forward_function()(current_out)
1606        # Return a dict which has all dendritic outputs after the activation functions were called
1607        if GPA.pc.get_perforated_backpropagation():
1608            candidate_outs, candidate_nonlinear_outs, candidate_non_zeroed = (
1609                MPB.forward_candidates(self, view_tuple, outs, *args2, **kwargs2)
1610            )
1611        else:
1612            candidate_outs, candidate_nonlinear_outs, candidate_non_zeroed = (
1613                {},
1614                {},
1615                {},
1616            )
1617        return outs, candidate_outs, candidate_nonlinear_outs, candidate_non_zeroed
1618
1619
1620class DendriteValueTracker(nn.Module):
1621    """Tracker object that maintains certain values for each set of dendrites."""
1622
1623    def __init__(
1624        self,
1625        initialized,
1626        activation_function_value,
1627        name,
1628        output_dimensions,
1629        out_channels=-1,
1630    ):
1631        """Initialize DendriteValueTracker.
1632
1633        This function sets up the value tracker to maintain statistics and values
1634        for each set of dendrites.
1635
1636        Parameters
1637        ----------
1638        initialized : int
1639            Whether the dendrite has been initialized (1) or not (0).
1640        activation_function_value : float
1641            A value associated with the activation function.
1642        name : str
1643            The name of the associated neuron module.
1644        output_dimensions : vector
1645            The dimensions of the input vector.
1646        out_channels : int
1647            The number of output channels
1648        """
1649        super(DendriteValueTracker, self).__init__()
1650
1651        self.layer_name = name
1652        for val_name in DENDRITE_INIT_VALUES:
1653            self.register_buffer(
1654                val_name,
1655                torch.zeros(1, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()),
1656            )
1657        self.initialized[0] = initialized
1658        self.activation_function_value = activation_function_value
1659        self.register_buffer(
1660            "this_output_dimensions", output_dimensions.clone().detach()
1661        )
1662        if (self.this_output_dimensions == 0).sum() != 1:
1663            print(f"3 need exactly one 0 in the input dimensions: {self.layer_name}")
1664            print(self.this_output_dimensions)
1665            sys.exit(-1)
1666        self.register_buffer(
1667            "this_node_index", (output_dimensions == 0).nonzero(as_tuple=True)[0]
1668        )
1669        if out_channels != -1:
1670            ndim = len(output_dimensions)
1671            init_shape = [1] * ndim
1672            init_shape[(output_dimensions == 0).nonzero(as_tuple=True)[0].item()] = out_channels
1673            self.setup_arrays(init_shape)
1674        else:
1675            self.out_channels = -1
1676
1677    def print(self):
1678        """Print value tracker information.
1679
1680        Parameters
1681        ----------
1682        None
1683
1684        Returns
1685        -------
1686        None
1687            This function does not return a value.
1688        """
1689        total_string = "Value Tracker:"
1690        for val_name in DENDRITE_INIT_VALUES:
1691            total_string += f"\t{val_name}:\n\t\t"
1692            total_string += getattr(self, val_name).__repr__()
1693            total_string += "\n"
1694        for val_name in get_DENDRITE_TENSOR_VALUES():
1695            if getattr(self, val_name, None) is not None:
1696                total_string += f"\t{val_name}:\n\t\t"
1697                total_string += getattr(self, val_name).__repr__()
1698                total_string += "\n"
1699        print(total_string)
1700
1701    def set_this_output_dimensions(self, new_output_dimensions):
1702        """Set input dimensions for value tracker
1703
1704        Signals to this DendriteValueTracker that its input dimensions are different
1705        than the global default.
1706
1707        Parameters
1708        ----------
1709        new_output_dimensions : list
1710            A list or tensor specifying the new input dimensions.
1711        Returns
1712        -------
1713        None
1714
1715        """
1716        if type(new_output_dimensions) is list:
1717            new_output_dimensions = torch.tensor(new_output_dimensions)
1718        delattr(self, "this_output_dimensions")
1719        self.register_buffer(
1720            "this_output_dimensions", new_output_dimensions.detach().clone()
1721        )
1722        if (new_output_dimensions == 0).sum() != 1:
1723            print(f"4 need exactly one 0 in the input dimensions: {self.layer_name}")
1724            print(new_output_dimensions)
1725            sys.exit(-1)
1726        self.this_node_index.copy_(
1727            (new_output_dimensions == 0).nonzero(as_tuple=True)[0][0]
1728        )
1729
1730    def set_out_channels(self, shape_values):
1731        """Set output channels based on shape values and saved node index
1732
1733        Parameters
1734        ----------
1735        shape_values : list or torch.Size
1736            A list or tensor specifying the shape values.
1737
1738        Returns
1739        -------
1740        None
1741        """
1742        if type(shape_values) == torch.Size:
1743            self.out_channels = int(shape_values[self.this_node_index])
1744        else:
1745            self.out_channels = int(shape_values[self.this_node_index].item())
1746
1747    def setup_arrays(self, storage_shape):
1748        """Setup arrays for value tracker.
1749
1750        Parameters
1751        ----------
1752        storage_shape : list
1753            Shape for the tracking tensors: 1 at every dim except the channel
1754            dim (this_node_index), which holds out_channels.  E.g. [1, 5] for
1755            a linear layer with 5 outputs.
1756        Returns
1757        -------
1758        None
1759
1760        """
1761        # storage_shape is a list with 1 at every dim except the channel dim.
1762        # Passed in directly from filter_backward so it is always derived from
1763        # the live gradient — not from out_channels, which is not saved/loaded.
1764        self.out_channels = storage_shape[self.this_node_index.item()]
1765        self.register_buffer(
1766            "dendrite_storage_shape",
1767            torch.tensor(storage_shape, dtype=torch.long, device=GPA.pc.get_device()),
1768        )
1769        for val_name in get_DENDRITE_TENSOR_VALUES():
1770            self.register_buffer(
1771                val_name,
1772                torch.zeros(
1773                    storage_shape, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()
1774                ),
1775            )
1776
1777        for name in get_VALUE_TRACKER_ARRAYS():
1778            setattr(self, name, {})
1779            count = 1
1780            if torch.cuda.device_count() > count:
1781                count = torch.cuda.device_count()
1782            for i in range(count):
1783                getattr(self, name)[i] = []
1784        for val_name in get_DENDRITE_SINGLE_VALUES():
1785            self.register_buffer(
1786                val_name,
1787                torch.zeros(1, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()),
1788            )
1789
1790        # If fixed_input_sizes is enabled, register cache buffers now that the
1791        # final ndim is known (output_dimensions may have been corrected after
1792        # __init__ for Linear layers). Mirrors the pattern of this_output_dimensions
1793        # — registered as buffers so they are saved and loaded automatically.
1794        if GPA.pc.get_perforated_backpropagation() and GPA.pc.get_fixed_input_sizes():
1795            ndim = len(storage_shape)
1796            if not hasattr(self, 'math_tuple_cache'):
1797                self.register_buffer(
1798                    "math_tuple_cache",
1799                    torch.zeros(ndim, dtype=torch.long, device=GPA.pc.get_device()),
1800                )
1801                self.register_buffer(
1802                    "view_tuple_cache",
1803                    torch.zeros(ndim, dtype=torch.long, device=GPA.pc.get_device()),
1804                )
1805                self.register_buffer(
1806                    "full_mult_cache",
1807                    torch.zeros(1, dtype=torch.long, device=GPA.pc.get_device()),
1808            )
1809
1810    def reinitialize_for_pai(self):
1811        """Reinitialize value tracker to add the next set of dendrites
1812
1813        Parameters
1814        ----------
1815        None
1816
1817        Returns
1818        -------
1819        None
1820            This function does not return a value.
1821        """
1822
1823        if self.out_channels == -1:
1824            print("You have a perforated module that was never initialized")
1825            print("This likely means it is not being added to the autograd graph")
1826            print("Check your forward function that it is actually being used")
1827            print("If its not you should really delete it, but you can also add")
1828            print(self.layer_name)
1829            print("with:")
1830            print("GPA.pc.append_module_ids_to_track(['" + self.layer_name + "'])")
1831            print("This can also happen while testing_dendrite_capacity if you")
1832            print(
1833                "run a validation cycle and try to add Dendrites before doing any training.\n"
1834            )
1835            pdb.set_trace()
1836
1837        self.initialized[0] = 0
1838        if GPA.pc.get_perforated_backpropagation():
1839            MPB.reinitialize_for_pb(self)
1840        else:
1841            for val_name in get_DENDRITE_REINIT_VALUES():
1842                setattr(self, val_name, getattr(self, val_name) * 0)
DENDRITE_INIT_VALUES = ['initialized', 'current_d_init']
def get_DENDRITE_TENSOR_VALUES():
50def get_DENDRITE_TENSOR_VALUES():
51    """Get DENDRITE_TENSOR_VALUES, updating from MPB if perforated_backpropagation is enabled.
52
53    Parameters
54    ----------
55    None
56
57    Returns
58    -------
59    list[str]
60        Names of tensor attributes used for dendrite state handling.
61    """
62    global _cached_dendrite_tensor_values, _cached_dendrite_tensor_pb_state
63    current_pb_state = GPA.pc.get_perforated_backpropagation()
64
65    if (
66        _cached_dendrite_tensor_values is None
67        or _cached_dendrite_tensor_pb_state != current_pb_state
68    ):
69        _cached_dendrite_tensor_pb_state = current_pb_state
70        if current_pb_state:
71            _cached_dendrite_tensor_values = MPB.update_dendrite_tensor_values(
72                _DENDRITE_TENSOR_VALUES_BASE.copy()
73            )
74        else:
75            _cached_dendrite_tensor_values = _DENDRITE_TENSOR_VALUES_BASE.copy()
76        if current_pb_state:
77            _cached_dendrite_tensor_values = _cached_dendrite_tensor_values + MPB._variant_tensor_values
78
79    return _cached_dendrite_tensor_values

Get DENDRITE_TENSOR_VALUES, updating from MPB if perforated_backpropagation is enabled.

Parameters
  • None
Returns
  • list[str]: Names of tensor attributes used for dendrite state handling.
def get_DENDRITE_SINGLE_VALUES():
 82def get_DENDRITE_SINGLE_VALUES():
 83    """Get DENDRITE_SINGLE_VALUES, updating from MPB if perforated_backpropagation is enabled.
 84
 85    Parameters
 86    ----------
 87    None
 88
 89    Returns
 90    -------
 91    list[str]
 92        Names of scalar attributes used for dendrite state handling.
 93    """
 94    global _cached_dendrite_single_values, _cached_dendrite_single_pb_state
 95    current_pb_state = GPA.pc.get_perforated_backpropagation()
 96
 97    if (
 98        _cached_dendrite_single_values is None
 99        or _cached_dendrite_single_pb_state != current_pb_state
100    ):
101        _cached_dendrite_single_pb_state = current_pb_state
102        if current_pb_state:
103            _cached_dendrite_single_values = MPB.update_dendrite_single_values(
104                _DENDRITE_SINGLE_VALUES_BASE.copy()
105            )
106        else:
107            _cached_dendrite_single_values = _DENDRITE_SINGLE_VALUES_BASE.copy()
108        if current_pb_state:
109            _cached_dendrite_single_values = _cached_dendrite_single_values + MPB._variant_single_values
110
111    return _cached_dendrite_single_values

Get DENDRITE_SINGLE_VALUES, updating from MPB if perforated_backpropagation is enabled.

Parameters
  • None
Returns
  • list[str]: Names of scalar attributes used for dendrite state handling.
def get_VALUE_TRACKER_ARRAYS():
114def get_VALUE_TRACKER_ARRAYS():
115    """Get VALUE_TRACKER_ARRAYS, updating from MPB if perforated_backpropagation is enabled.
116
117    Parameters
118    ----------
119    None
120
121    Returns
122    -------
123    list[str]
124        Names of array-valued fields tracked in ``DendriteValueTracker``.
125    """
126    global _cached_value_tracker_arrays, _cached_value_tracker_pb_state
127    current_pb_state = GPA.pc.get_perforated_backpropagation()
128
129    if (
130        _cached_value_tracker_arrays is None
131        or _cached_value_tracker_pb_state != current_pb_state
132    ):
133        _cached_value_tracker_pb_state = current_pb_state
134        if current_pb_state:
135            _cached_value_tracker_arrays = MPB.update_value_tracker_arrays(
136                _VALUE_TRACKER_ARRAYS_BASE.copy()
137            )
138        else:
139            _cached_value_tracker_arrays = _VALUE_TRACKER_ARRAYS_BASE.copy()
140
141    return _cached_value_tracker_arrays

Get VALUE_TRACKER_ARRAYS, updating from MPB if perforated_backpropagation is enabled.

Parameters
  • None
Returns
def get_DENDRITE_REINIT_VALUES():
144def get_DENDRITE_REINIT_VALUES():
145    """Get DENDRITE_REINIT_VALUES.
146
147    Parameters
148    ----------
149    None
150
151    Returns
152    -------
153    list[str]
154        Combined list of attribute names that must be reinitialized.
155    """
156    return get_DENDRITE_TENSOR_VALUES() + get_DENDRITE_SINGLE_VALUES()

Get DENDRITE_REINIT_VALUES.

Parameters
  • None
Returns
  • list[str]: Combined list of attribute names that must be reinitialized.
def get_DENDRITE_SAVE_VALUES():
159def get_DENDRITE_SAVE_VALUES():
160    """Get DENDRITE_SAVE_VALUES.
161
162    Parameters
163    ----------
164    None
165
166    Returns
167    -------
168    list[str]
169        Combined list of attribute names persisted for save/load.
170    """
171    return (
172        get_DENDRITE_TENSOR_VALUES()
173        + get_DENDRITE_SINGLE_VALUES()
174        + DENDRITE_INIT_VALUES
175    )

Get DENDRITE_SAVE_VALUES.

Parameters
  • None
Returns
  • list[str]: Combined list of attribute names persisted for save/load.
def filter_backward(grad_out, values, module=None):
178def filter_backward(grad_out, values, module=None):
179    """Filter backward pass for gradient processing.
180
181    This function processes gradients during the backward pass,
182    ensuring correct input dimensions,and applying perforated backpropagation if enabled.
183
184    Parameters
185    ----------
186    grad_out : torch.Tensor
187        The gradient output tensor from the backward pass.
188    values : DendriteValueTracker
189        A DendriteValueTracker instance containing values associated with the module being processed.
190    module : PAINeuronModule, optional
191        The owning PAINeuronModule instance. When provided and PBP is disabled,
192        the hook deregisters itself after the one-time initialization completes,
193        eliminating per-batch Python overhead on all subsequent backward passes.
194
195    Returns
196    -------
197    None
198    """
199    if GPA.pc.get_extra_verbose():
200        print(f"{values[0].layer_name} calling backward")
201
202    with torch.no_grad():
203        val = grad_out.detach()
204        # Fast path: Python bool set after first backward in this process.
205        # Fallback: current_d_init is a saved buffer — survives checkpoint reload
206        # even when _fb_init_done resets to False in a fresh process.
207        already_init = (module is not None and module._fb_init_done) or \
208                       values[0].current_d_init.item()
209        if not already_init:
210            # If input dimensions and gradient don't have same shape trigger error and quit
211            if len(values[0].this_output_dimensions) != len(grad_out.shape):
212                print(
213                    "The following module has not properly set this_output_dimensions"
214                )
215                print(values[0].layer_name)
216                print("it is expecting:")
217                print(values[0].this_output_dimensions)
218                print("but received")
219                print(grad_out.shape)
220                print(
221                    "to check these all at once set GPA.pc.set_debugging_output_dimensions(1)"
222                )
223                print(
224                    f"Call MODEL_VARIABLE{values[0].layer_name}.set_this_output_dimensions([...]) on this module after perforate_model"
225                )
226                print(
227                    "where the ... is replaced with the correct vector as described in section 4 of customization.md"
228                )
229                if not GPA.pc.get_debugging_output_dimensions():
230                    sys.exit(0)
231                else:
232                    GPA.pc.set_debugging_output_dimensions(2)
233                    return
234            # Make sure that the input dimensions are correct
235            for i in range(len(values[0].this_output_dimensions)):
236                if values[0].this_output_dimensions[i] == 0:
237                    continue
238                # Make sure all input dimensions are either -1 (reduce), 1 (retain), or exact values (old format)
239                if (
240                    not (grad_out.shape[i] == values[0].this_output_dimensions[i])
241                    and not values[0].this_output_dimensions[i] == -1
242                    and not values[0].this_output_dimensions[i] == 1
243                ):
244                    print(
245                        "The following module has not properly set this_output_dimensions with this incorrect shape"
246                    )
247                    print(values[0].layer_name)
248                    print("it is expecting:")
249                    print(values[0].this_output_dimensions)
250                    print("but received")
251                    print(grad_out.shape)
252                    print(
253                        "to check these all at once set GPA.pc.set_debugging_output_dimensions(1)"
254                    )
255                    if not GPA.pc.get_debugging_output_dimensions():
256                        sys.exit(0)
257                    else:
258                        GPA.pc.set_debugging_output_dimensions(2)
259                        return
260            # Setup the arrays with the now known shape
261            with torch.no_grad():
262                if GPA.pc.get_verbose():
263                    print("setting d shape for")
264                    print(values[0].layer_name)
265                    print(val.size())
266
267                values[0].set_out_channels(val.size())
268                ndim = len(values[0].this_output_dimensions)
269                storage_shape = [1] * ndim
270                for _i in range(ndim):
271                    if values[0].this_output_dimensions[_i] == 1:
272                        storage_shape[_i] = val.shape[_i]
273                storage_shape[values[0].this_node_index.item()] = values[0].out_channels
274                values[0].setup_arrays(storage_shape)
275            # Flag that it has been setup (both the GPU tensor and the fast Python bool)
276            values[0].current_d_init[0] = 1
277            # If fixed_input_sizes is enabled, populate the tuple caches now
278            # that val.shape is known. get_tuples_and_mult will read these on
279            # every subsequent call instead of recomputing.
280            if GPA.pc.get_perforated_backpropagation() and GPA.pc.get_fixed_input_sizes():
281                from perforatedbp import modules_pbp as _MPB
282                math_tuple, view_tuple, full_mult = _MPB.get_tuples_and_mult(val, values[0])
283                ndim = len(val.shape)
284                # math_tuple can be shorter than ndim (excludes this_node_index and
285                # retained dims). Pad with -1 sentinel to fill the ndim-length buffer.
286                padded_math = math_tuple + [-1] * (ndim - len(math_tuple))
287                values[0].math_tuple_cache.copy_(
288                    torch.tensor(padded_math, dtype=torch.long, device=val.device)
289                )
290                values[0].view_tuple_cache.copy_(
291                    torch.tensor(view_tuple, dtype=torch.long, device=val.device)
292                )
293                values[0].full_mult_cache[0] = full_mult
294            if module is not None:
295                module._fb_init_done = True
296                # When PBP is disabled this hook has no further work to do.
297                # Deregister it so it never fires again, eliminating per-batch
298                # Python overhead on all subsequent backward passes.
299                if not GPA.pc.get_perforated_backpropagation() and module._fb_hook_handle is not None:
300                    module._fb_hook_handle.remove()
301                    module._fb_hook_handle = None
302        if GPA.pc.get_perforated_backpropagation():
303            MPB.filter_backward_pb(val, values)

Filter backward pass for gradient processing.

This function processes gradients during the backward pass, ensuring correct input dimensions,and applying perforated backpropagation if enabled.

Parameters
  • grad_out (torch.Tensor): The gradient output tensor from the backward pass.
  • values (DendriteValueTracker): A DendriteValueTracker instance containing values associated with the module being processed.
  • module (PAINeuronModule, optional): The owning PAINeuronModule instance. When provided and PBP is disabled, the hook deregisters itself after the one-time initialization completes, eliminating per-batch Python overhead on all subsequent backward passes.
Returns
  • None
def set_wrapped_params(model):
306def set_wrapped_params(model):
307    """Set parameters as wrapped with dendrites.
308
309    Parameters
310    ----------
311    model : torch.nn.Module
312        The model whose parameters are to be marked as wrapped.
313
314    Returns
315    -------
316    None
317
318    """
319    for param in model.parameters():
320        param.wrapped = True

Set parameters as wrapped with dendrites.

Parameters
  • model (torch.nn.Module): The model whose parameters are to be marked as wrapped.
Returns
  • None
def set_tracked_params(model):
323def set_tracked_params(model):
324    """Set parameters as tracked without dendrites.
325
326    Parameters
327    ----------
328    model : torch.nn.Module
329        The model whose parameters are to be marked as tracked.
330
331    Returns
332    -------
333    None
334    """
335    for param in model.parameters():
336        param.tracked = True

Set parameters as tracked without dendrites.

Parameters
  • model (torch.nn.Module): The model whose parameters are to be marked as tracked.
Returns
  • None
class PAINeuronModule(torch.nn.modules.module.Module):
339class PAINeuronModule(nn.Module):
340    """Wrapper to set a module as one that will have dendritic copies."""
341
342    def __init__(self, start_module, name):
343        """Initialize PAINeuronModule.
344
345        This function sets up the neuron module to wrap the start_module
346        and manage its dendritic connections.
347
348        Parameters
349        ----------
350        start_module : nn.Module
351            The module to wrap.
352        name : str
353            The name of the neuron module.
354        """
355        super(PAINeuronModule, self).__init__()
356
357        if isinstance(start_module, nn.Module):
358            self.main_module = start_module
359        else:
360            print("start_module must be nn.Module: %s" % name)
361            print(type(start_module))
362            print(start_module)
363            sys.exit(-1)
364        self.name = name
365        # Per-module config: loads custom settings from {save_name}_config.json if present.
366        # Passes both the instance name (id) and the module type so load_config can
367        # fall back to type-level settings when no name-specific entry exists.
368        _module_type_name = type(start_module).__name__
369        self.module_config = GPA.PAIConfig(
370            module_name=self.name, module_type=_module_type_name
371        )
372
373        set_wrapped_params(self.main_module)
374        if self.module_config.get_verbose():
375            print(
376                f"initing a module {self.name} with main type {type(self.main_module)}"
377            )
378            print(start_module)
379
380        # If this main_module is one that requires processing set the processor
381        if type(self.main_module) in self.module_config.get_modules_with_processing():
382            module_index = self.module_config.get_modules_with_processing().index(
383                type(self.main_module)
384            )
385            self.processor = self.module_config.get_modules_processing_classes()[
386                module_index
387            ]()
388            if self.module_config.get_verbose():
389                print("with processor")
390                print(self.processor)
391        elif (
392            type(self.main_module).__name__
393            in self.module_config.get_module_names_with_processing()
394        ):
395            module_index = self.module_config.get_module_names_with_processing().index(
396                type(self.main_module).__name__
397            )
398            self.processor = self.module_config.get_module_by_name_processing_classes()[
399                module_index
400            ]()
401            if self.module_config.get_verbose():
402                print("with processor")
403                print(self.processor)
404        else:
405            self.processor = None
406
407        # Field that can be filled in if your activation function requires a parameter
408        self.activation_function_value = -1
409        self.type = "neuron_module"
410
411        self.register_buffer(
412            "this_output_dimensions",
413            (torch.tensor(self.module_config.get_output_dimensions())),
414        )
415        if (self.this_output_dimensions == 0).sum() != 1:
416            print(f"5 Need exactly one 0 in the input dimensions: {self.name}")
417            print(self.this_output_dimensions)
418            sys.exit(-1)
419        self.register_buffer(
420            "this_node_index",
421            torch.tensor(self.module_config.get_output_dimensions().index(0)),
422        )
423        self.dendrite_modules_added = 0
424
425        # Values for dendrite to neuron weights
426        self.dendrites_to_top = nn.ParameterList()
427        self.register_parameter("newest_dendrite_to_top", None)
428        self.candidate_to_top = nn.ParameterList()
429        self.register_parameter("current_candidate_to_top", None)
430        # Create the dendrite module
431        self.dendrite_module = PAIDendriteModule(
432            self.main_module,
433            activation_function_value=self.activation_function_value,
434            name=self.name,
435            output_dimensions=self.this_output_dimensions,
436        )
437        # If it is linear and default has convolutional dimensions, automatically set to just be batch size and neuron indexes
438        if (
439            issubclass(type(start_module), nn.Linear)
440            or (
441                issubclass(type(start_module), GPA.PAISequential)
442                and issubclass(type(start_module.model[0]), nn.Linear)
443            )
444        ) and (
445            np.array(self.this_output_dimensions)[2:] == -1
446        ).all():  # Everything past 2 is a negative 1
447            self.set_this_output_dimensions(self.this_output_dimensions[0:2])
448        if (
449            issubclass(type(start_module), nn.Conv1d)
450            or (
451                issubclass(type(start_module), GPA.PAISequential)
452                and issubclass(type(start_module.model[0]), nn.Conv1d)
453            )
454        ) and (
455            np.array(self.this_output_dimensions)[3:] == -1
456        ).all():  # Everything past 2 is a negative 1
457            self.set_this_output_dimensions(self.this_output_dimensions[0:3])
458        # Apply per-module output_dimensions override from config if present
459        _custom_dims = self.module_config.__dict__.get("_output_dimensions")
460        if _custom_dims is not None:
461            self.set_this_output_dimensions(torch.tensor(_custom_dims))
462        GPA.pai_tracker.add_pai_neuron_module(self)
463        if self.module_config.get_perforated_backpropagation():
464            MPB.set_neuron_parameters(self.main_module)
465
466        # Track filter_backward initialization state as a
467        # plain Python bool (avoids a GPU .item() sync on every backward pass).
468        # _fb_hook_handle stores the registered hook handle so it can be
469        # removed after initialization completes when PBP is disabled.
470        self._fb_init_done = False
471        self._fb_hook_handle = None
472
473    def __getattr__(self, name):
474        """Get member variables from the main module.
475
476        Parameters
477        ----------
478        name : str
479            The name of the variable to retrieve.
480        Returns
481        -------
482        The requested variable.
483
484        Notes
485        -----
486        This method first attempts to retrieve the attribute from the PAINeuronModule instance.
487        If it fails, it tries to get the attribute from the wrapped main_module.
488        This allows seamless access to the main module's attributes without modifying original code.
489        """
490        try:
491            return super().__getattr__(name)
492        except AttributeError:
493            return getattr(self.main_module, name)
494
495    def __getitem__(self, index):
496        """Support indexing operations on the main module.
497
498        Parameters
499        ----------
500        index : int or slice
501            The index or slice to retrieve.
502
503        Returns
504        -------
505        The indexed item from the main module.
506        """
507        return self.main_module[index]
508
509    def apply_pb_grads(self):
510        """Apply perforated backpropagation gradients if enabled.
511
512        Parameters
513        ----------
514        None
515
516        Returns
517        -------
518        None
519            This function does not return a value.
520        """
521        self.dendrite_module.apply_pb_grads()
522
523    def apply_pb_zero(self):
524        """Clear leftover saved tensors if there are any.
525
526        Parameters
527        ----------
528        None
529
530        Returns
531        -------
532        None
533            This function does not return a value.
534        """
535        self.dendrite_module.apply_pb_zero()
536
537    def clear_processors(self):
538        """Clear processors if they save values for DeepCopy and save.
539
540        Parameters
541        ----------
542        None
543
544        Returns
545        -------
546        None
547        """
548
549        if not self.processor:
550            return
551        else:
552            self.processor.clear_processor()
553            self.dendrite_module.clear_processors()
554
555    def clear_dendrites(self):
556        """Clear and reset dendrites before loading from a state dict.
557
558        Parameters
559        ----------
560        None
561
562        Returns
563        -------
564        None
565
566        """
567        # Loading a saved state reconstructs PAIDendriteModule before simulating
568        # its saved cycles. Preserve a registered variant factory so candidate
569        # creation does not silently fall back to deep-copying the parent module.
570        create_dendrite_fn = self.dendrite_module._create_dendrite_fn
571        self.dendrite_modules_added = 0
572        self.dendrites_to_top = nn.ParameterList()
573        self.candidate_to_top = nn.ParameterList()
574        self.dendrite_module = PAIDendriteModule(
575            self.main_module,
576            activation_function_value=self.activation_function_value,
577            name=self.name,
578            output_dimensions=self.this_output_dimensions,
579        )
580        if create_dendrite_fn is not None:
581            self.dendrite_module.set_create_dendrite(create_dendrite_fn)
582
583    def __str__(self):
584        """String representation of the module.
585
586        Parameters
587        ----------
588        None
589
590        Returns
591        -------
592        str
593            String representation of the module.
594
595        Notes
596        -----
597        Setting for verbose changes level of details in the string output.
598        """
599        # If verbose print the whole module otherwise just print the module type as a PAIModule
600        if self.module_config.get_verbose():
601            total_string = self.main_module.__str__()
602            total_string = "PAIModule(" + total_string + ")"
603            return total_string + self.dendrite_module.__str__()
604        else:
605            total_string = self.main_module.__str__()
606            total_string = "PAIModule(" + total_string + ")"
607            return total_string
608
609    def __repr__(self):
610        """Representation of the module."""
611        return self.__str__()
612
613    def set_this_output_dimensions(self, new_output_dimensions):
614        """Set the input dimensions for the neuron and dendrite blocks.
615
616        Signals to this NeuronModule that its input dimensions are different
617        than the global default.
618
619        Parameters
620        ----------
621        new_output_dimensions : list
622            A list or tensor specifying the new input dimensions.
623        Returns
624        -------
625        None
626
627        """
628        if type(new_output_dimensions) is list:
629            new_output_dimensions = torch.tensor(new_output_dimensions)
630        delattr(self, "this_output_dimensions")
631        self.register_buffer(
632            "this_output_dimensions", new_output_dimensions.detach().clone()
633        )
634        if (new_output_dimensions == 0).sum() != 1:
635            print(f"6 need exactly one 0 in the input dimensions: {self.name}")
636            print(new_output_dimensions)
637        self.this_node_index.copy_(
638            (new_output_dimensions == 0).nonzero(as_tuple=True)[0][0]
639        )
640        self.dendrite_module.set_this_output_dimensions(new_output_dimensions)
641
642    def set_create_dendrite(self, fn):
643        """Set a custom function for creating dendrite modules.
644
645        Override how dendrite copies are created from the parent module.  By default
646        the parent module is deep-copied.  Pass any callable with the signature
647        ``fn(parent_module) -> nn.Module`` to replace that behaviour.
648
649        Parameters
650        ----------
651        fn : callable
652            A function with signature ``fn(parent_module) -> nn.Module``.
653
654        Returns
655        -------
656        None
657        """
658        self.dendrite_module.set_create_dendrite(fn)
659
660
661    def set_mode(self, mode):
662        """Switch between neuron training and dendrite training.
663
664        Parameters
665        ----------
666        mode : str
667            The mode to set. Either "n" for neuron training or "p" for pai-dendrite training.
668
669        Returns
670        -------
671        bool
672            True if mode was set successfully, False otherwise.
673
674        Notes
675        -----
676        If False is returned, the mode was not changed due to an error.
677        This is a problem that should not be ignored, but it can be ignored
678        by calling PGA.pc.set_checked_skipped_modules(True)
679        """
680
681        if self.module_config.get_verbose():
682            print(f"{self.name} calling set mode {mode}")
683        # If returning to neuron training
684        if mode == "n":
685            self.dendrite_module.set_mode(mode)
686            # Initialize the dendrite to neuron connections
687            if self.dendrite_modules_added > 0:
688                if self.module_config.get_learn_dendrites_live():
689                    values = torch.cat(
690                        (
691                            self.dendrites_to_top[self.dendrite_modules_added - 1],
692                            nn.Parameter(
693                                self.candidate_to_top.detach()
694                                .clone()
695                                .to(dtype=self.module_config.get_d_type())
696                            ),
697                        ),
698                        0,
699                    )
700                else:
701                    values = torch.cat(
702                        (
703                            self.dendrites_to_top[self.dendrite_modules_added - 1],
704                            nn.Parameter(
705                                torch.zeros(
706                                    (1, self.out_channels),
707                                    device=self.dendrites_to_top[
708                                        self.dendrite_modules_added - 1
709                                    ].device,
710                                    dtype=self.module_config.get_d_type(),
711                                )
712                            ),
713                        ),
714                        0,
715                    )
716                # Freeze the previous dendrites_to_top entry
717                # so the optimizer no longer updates stale entries. Only the
718                # most recently appended entry is ever used in the forward pass.
719                if len(self.dendrites_to_top) > 0:
720                    self.dendrites_to_top[-1].requires_grad_(False)
721                self.dendrites_to_top.append(
722                    nn.Parameter(
723                        values.detach()
724                        .clone()
725                        .to(
726                            device=self.module_config.get_device(),
727                            dtype=self.module_config.get_d_type(),
728                        ),
729                        requires_grad=True,
730                    )
731                )
732            else:
733                if self.module_config.get_learn_dendrites_live():
734                    # Freeze previous entry before appending new one.
735                    if len(self.dendrites_to_top) > 0:
736                        self.dendrites_to_top[-1].requires_grad_(False)
737                    self.dendrites_to_top.append(
738                        nn.Parameter(
739                            self.candidate_to_top.detach()
740                            .clone()
741                            .to(dtype=self.module_config.get_d_type()),
742                            requires_grad=True,
743                        )
744                    )
745                else:
746                    # Freeze previous entry before appending new one.
747                    if len(self.dendrites_to_top) > 0:
748                        self.dendrites_to_top[-1].requires_grad_(False)
749                    self.dendrites_to_top.append(
750                        nn.Parameter(
751                            torch.zeros(
752                                (1, self.out_channels),
753                                device=self.module_config.get_device(),
754                                dtype=self.module_config.get_d_type(),
755                            )
756                            .detach()
757                            .clone(),
758                            requires_grad=True,
759                        )
760                    )
761            self.dendrite_modules_added += 1
762            if self.module_config.get_perforated_backpropagation():
763                MPB.set_module_n_pb(self)
764                MPB.set_neuron_parameters(self.dendrites_to_top)
765
766        # If starting dendrite training
767        else:
768            try:
769                # Save the values that were calculated in filter_backward
770                self.out_channels = self.dendrite_module.dendrite_values[0].out_channels
771                self.dendrite_module.out_channels = (
772                    self.dendrite_module.dendrite_values[0].out_channels
773                )
774            except Exception as e:
775                print(e)
776                print(
777                    f"this occurred in module: {self.dendrite_module.dendrite_values[0].layer_name}"
778                )
779                print(
780                    "Module should be added to module_names_to_track so it doesn't have dendrites added"
781                )
782                print("If you are getting here but out_channels has not been set")
783                print(
784                    "A common reason is that this module never had gradients flow through it."
785                )
786                print("I have seen this happen because:")
787                print("-The weights were frozen (requires_grad = False)")
788                print(
789                    "-A model is added but not used so it was converted to a perforated module initialized"
790                )
791                print(
792                    "-A module was converted that doesn't have weights that get modified so backward doesn't flow through it"
793                )
794                print(
795                    "If this is normal behavior set GPA.pc.set_checked_skipped_modules(True) in the main to ignore"
796                )
797                print(
798                    "You can also set right now in this pdb terminal to have this not happen more after checking all modules this cycle."
799                )
800                if not self.module_config.get_checked_skipped_modules():
801                    pdb.set_trace()
802                return False
803            # Only change mode if it makes it past the above exception
804            self.dendrite_module.set_mode(mode)
805            if self.module_config.get_perforated_backpropagation():
806                MPB.set_module_p_pb(self)
807        return True
808
809    def create_new_dendrite_module(self):
810        """Add an additional dendrite module.
811
812        Parameters
813        ----------
814        None
815
816        Returns
817        -------
818        None
819        """
820        self.dendrite_module.create_new_dendrite_module(self.main_module)
821
822    def forward(self, *args, **kwargs):
823        """Forward pass through the neuron module.
824
825        Parameters
826        ----------
827        *args : tuple
828            Positional arguments for the forward pass.
829        **kwargs : dict
830            Keyword arguments for the forward pass.
831
832        Returns
833        -------
834        Any
835            The output of the module after processing through the neuron and dendrite modules.
836
837        Notes
838        -----
839            The output of this forward function will have the same format as the output
840            of the original module
841        """
842
843        # If debugging all input dimensions, quit program on first forward call
844        if self.module_config.get_debugging_output_dimensions() == 2:
845            print("all input dim problems now printed")
846            sys.exit(0)
847        if self.module_config.get_extra_verbose():
848            print(f"{self.name} calling forward")
849        # Call the main modules forward
850        out = self.main_module(*args, **kwargs)
851        # Filter with the processor if required
852        if self.processor is not None:
853            try:
854                out = self.processor.post_n1(out)
855            except Exception as e:
856                traceback.print_exc(limit=None, chain=True)
857                print(f"Your post_n1 processor for {self.name} caused this error")
858                print(
859                    f"You must check how this is defined and ensure that it is properly"
860                )
861                print(f"accepting outputs from the neuron module and returning the")
862                print(f"single tensor to be combined with the dendrites output tensor")
863                sys.exit()
864        # Call the forwards for all of the Dendrites
865        (
866            dendrite_outs,
867            candidate_outs,
868            candidate_nonlinear_outs,
869            candidate_outs_non_zeroed,
870        ) = self.dendrite_module(*args, **kwargs)
871        # If there are dendrites add all of their outputs to the neurons output
872        if self.dendrite_modules_added > 0:
873            for i in range(0, self.dendrite_modules_added):
874                to_top = self.dendrites_to_top[self.dendrite_modules_added - 1][i, :]
875                for dim in range(len(dendrite_outs[i].shape)):
876                    if dim == self.this_node_index:
877                        continue
878                    to_top = to_top.unsqueeze(dim)
879                if self.module_config.get_confirm_correct_sizes():
880                    to_top = to_top.expand(
881                        list(dendrite_outs[i].size())[0 : self.this_node_index]
882                        + [self.out_channels]
883                        + list(dendrite_outs[i].size())[self.this_node_index + 1 :]
884                    )
885                out = out + (dendrite_outs[i].to(out.device) * to_top.to(out.device))
886
887        # If learning live, add the candidate's output to the neuron's output via the live weight
888        if self.module_config.get_perforated_backpropagation():
889            out = MPB.apply_live_candidate_to_output(
890                self, out, candidate_nonlinear_outs
891            )
892
893        # Catch if processors are required
894        if type(out) is tuple:
895            print(self)
896            print(
897                f"The output of the above module {self.name} is a tuple when it must be a single tensor"
898            )
899            print(
900                "This must be fixed to enable the dendrite and neuron output to be combined"
901            )
902            print(
903                "Look in the API customization.md at section 2.2 regarding processors to fix this."
904            )
905            pdb.set_trace()
906
907        # Call filter backward to ensure the neuron index is setup correctly.
908        # Register only when not yet initialized (PBP=False) or always when PBP
909        # is enabled. Store the handle so the hook can deregister itself after
910        # the one-time initialization completes (PBP=False path).
911        if out.requires_grad and (not self._fb_init_done or GPA.pc.get_perforated_backpropagation()):
912            self._fb_hook_handle = out.register_hook(
913                lambda grad: filter_backward(grad, self.dendrite_module.dendrite_values, self)
914            )
915
916        # If there is a processor apply the second neuron stage
917        if self.processor is not None:
918            try:
919                out = self.processor.post_n2(out)
920            except Exception as e:
921                traceback.print_exc(limit=None, chain=True)
922                print(f"Your post_n2 processor for {self.name} caused this error")
923                print(
924                    f"You must check how this is defined and ensure that it is properly"
925                )
926                print(
927                    f"accepting the output tensor after combining the neuron's output "
928                )
929                print(f"with the dendrite's output and returning something that is the")
930                print(f"same format as your original module's return")
931                sys.exit()
932        return out

Wrapper to set a module as one that will have dendritic copies.

PAINeuronModule(start_module, name)
342    def __init__(self, start_module, name):
343        """Initialize PAINeuronModule.
344
345        This function sets up the neuron module to wrap the start_module
346        and manage its dendritic connections.
347
348        Parameters
349        ----------
350        start_module : nn.Module
351            The module to wrap.
352        name : str
353            The name of the neuron module.
354        """
355        super(PAINeuronModule, self).__init__()
356
357        if isinstance(start_module, nn.Module):
358            self.main_module = start_module
359        else:
360            print("start_module must be nn.Module: %s" % name)
361            print(type(start_module))
362            print(start_module)
363            sys.exit(-1)
364        self.name = name
365        # Per-module config: loads custom settings from {save_name}_config.json if present.
366        # Passes both the instance name (id) and the module type so load_config can
367        # fall back to type-level settings when no name-specific entry exists.
368        _module_type_name = type(start_module).__name__
369        self.module_config = GPA.PAIConfig(
370            module_name=self.name, module_type=_module_type_name
371        )
372
373        set_wrapped_params(self.main_module)
374        if self.module_config.get_verbose():
375            print(
376                f"initing a module {self.name} with main type {type(self.main_module)}"
377            )
378            print(start_module)
379
380        # If this main_module is one that requires processing set the processor
381        if type(self.main_module) in self.module_config.get_modules_with_processing():
382            module_index = self.module_config.get_modules_with_processing().index(
383                type(self.main_module)
384            )
385            self.processor = self.module_config.get_modules_processing_classes()[
386                module_index
387            ]()
388            if self.module_config.get_verbose():
389                print("with processor")
390                print(self.processor)
391        elif (
392            type(self.main_module).__name__
393            in self.module_config.get_module_names_with_processing()
394        ):
395            module_index = self.module_config.get_module_names_with_processing().index(
396                type(self.main_module).__name__
397            )
398            self.processor = self.module_config.get_module_by_name_processing_classes()[
399                module_index
400            ]()
401            if self.module_config.get_verbose():
402                print("with processor")
403                print(self.processor)
404        else:
405            self.processor = None
406
407        # Field that can be filled in if your activation function requires a parameter
408        self.activation_function_value = -1
409        self.type = "neuron_module"
410
411        self.register_buffer(
412            "this_output_dimensions",
413            (torch.tensor(self.module_config.get_output_dimensions())),
414        )
415        if (self.this_output_dimensions == 0).sum() != 1:
416            print(f"5 Need exactly one 0 in the input dimensions: {self.name}")
417            print(self.this_output_dimensions)
418            sys.exit(-1)
419        self.register_buffer(
420            "this_node_index",
421            torch.tensor(self.module_config.get_output_dimensions().index(0)),
422        )
423        self.dendrite_modules_added = 0
424
425        # Values for dendrite to neuron weights
426        self.dendrites_to_top = nn.ParameterList()
427        self.register_parameter("newest_dendrite_to_top", None)
428        self.candidate_to_top = nn.ParameterList()
429        self.register_parameter("current_candidate_to_top", None)
430        # Create the dendrite module
431        self.dendrite_module = PAIDendriteModule(
432            self.main_module,
433            activation_function_value=self.activation_function_value,
434            name=self.name,
435            output_dimensions=self.this_output_dimensions,
436        )
437        # If it is linear and default has convolutional dimensions, automatically set to just be batch size and neuron indexes
438        if (
439            issubclass(type(start_module), nn.Linear)
440            or (
441                issubclass(type(start_module), GPA.PAISequential)
442                and issubclass(type(start_module.model[0]), nn.Linear)
443            )
444        ) and (
445            np.array(self.this_output_dimensions)[2:] == -1
446        ).all():  # Everything past 2 is a negative 1
447            self.set_this_output_dimensions(self.this_output_dimensions[0:2])
448        if (
449            issubclass(type(start_module), nn.Conv1d)
450            or (
451                issubclass(type(start_module), GPA.PAISequential)
452                and issubclass(type(start_module.model[0]), nn.Conv1d)
453            )
454        ) and (
455            np.array(self.this_output_dimensions)[3:] == -1
456        ).all():  # Everything past 2 is a negative 1
457            self.set_this_output_dimensions(self.this_output_dimensions[0:3])
458        # Apply per-module output_dimensions override from config if present
459        _custom_dims = self.module_config.__dict__.get("_output_dimensions")
460        if _custom_dims is not None:
461            self.set_this_output_dimensions(torch.tensor(_custom_dims))
462        GPA.pai_tracker.add_pai_neuron_module(self)
463        if self.module_config.get_perforated_backpropagation():
464            MPB.set_neuron_parameters(self.main_module)
465
466        # Track filter_backward initialization state as a
467        # plain Python bool (avoids a GPU .item() sync on every backward pass).
468        # _fb_hook_handle stores the registered hook handle so it can be
469        # removed after initialization completes when PBP is disabled.
470        self._fb_init_done = False
471        self._fb_hook_handle = None

Initialize PAINeuronModule.

This function sets up the neuron module to wrap the start_module and manage its dendritic connections.

Parameters
  • start_module (nn.Module): The module to wrap.
  • name (str): The name of the neuron module.
name
module_config
activation_function_value
def type(self, dst_type: torch.dtype | str) -> Self:
1169    def type(self, dst_type: dtype | str) -> Self:
1170        r"""Casts all parameters and buffers to :attr:`dst_type`.
1171
1172        .. note::
1173            This method modifies the module in-place.
1174
1175        Args:
1176            dst_type (type or string): the desired type
1177
1178        Returns:
1179            Module: self
1180        """
1181        return self._apply(lambda t: t.type(dst_type))

Casts all parameters and buffers to dst_type.

This method modifies the module in-place.

Args: dst_type (type or string): the desired type

Returns: Module: self

dendrite_modules_added
dendrites_to_top
candidate_to_top
dendrite_module
def apply_pb_grads(self):
509    def apply_pb_grads(self):
510        """Apply perforated backpropagation gradients if enabled.
511
512        Parameters
513        ----------
514        None
515
516        Returns
517        -------
518        None
519            This function does not return a value.
520        """
521        self.dendrite_module.apply_pb_grads()

Apply perforated backpropagation gradients if enabled.

Parameters
  • None
Returns
  • None: This function does not return a value.
def apply_pb_zero(self):
523    def apply_pb_zero(self):
524        """Clear leftover saved tensors if there are any.
525
526        Parameters
527        ----------
528        None
529
530        Returns
531        -------
532        None
533            This function does not return a value.
534        """
535        self.dendrite_module.apply_pb_zero()

Clear leftover saved tensors if there are any.

Parameters
  • None
Returns
  • None: This function does not return a value.
def clear_processors(self):
537    def clear_processors(self):
538        """Clear processors if they save values for DeepCopy and save.
539
540        Parameters
541        ----------
542        None
543
544        Returns
545        -------
546        None
547        """
548
549        if not self.processor:
550            return
551        else:
552            self.processor.clear_processor()
553            self.dendrite_module.clear_processors()

Clear processors if they save values for DeepCopy and save.

Parameters
  • None
Returns
  • None
def clear_dendrites(self):
555    def clear_dendrites(self):
556        """Clear and reset dendrites before loading from a state dict.
557
558        Parameters
559        ----------
560        None
561
562        Returns
563        -------
564        None
565
566        """
567        # Loading a saved state reconstructs PAIDendriteModule before simulating
568        # its saved cycles. Preserve a registered variant factory so candidate
569        # creation does not silently fall back to deep-copying the parent module.
570        create_dendrite_fn = self.dendrite_module._create_dendrite_fn
571        self.dendrite_modules_added = 0
572        self.dendrites_to_top = nn.ParameterList()
573        self.candidate_to_top = nn.ParameterList()
574        self.dendrite_module = PAIDendriteModule(
575            self.main_module,
576            activation_function_value=self.activation_function_value,
577            name=self.name,
578            output_dimensions=self.this_output_dimensions,
579        )
580        if create_dendrite_fn is not None:
581            self.dendrite_module.set_create_dendrite(create_dendrite_fn)

Clear and reset dendrites before loading from a state dict.

Parameters
  • None
Returns
  • None
def set_this_output_dimensions(self, new_output_dimensions):
613    def set_this_output_dimensions(self, new_output_dimensions):
614        """Set the input dimensions for the neuron and dendrite blocks.
615
616        Signals to this NeuronModule that its input dimensions are different
617        than the global default.
618
619        Parameters
620        ----------
621        new_output_dimensions : list
622            A list or tensor specifying the new input dimensions.
623        Returns
624        -------
625        None
626
627        """
628        if type(new_output_dimensions) is list:
629            new_output_dimensions = torch.tensor(new_output_dimensions)
630        delattr(self, "this_output_dimensions")
631        self.register_buffer(
632            "this_output_dimensions", new_output_dimensions.detach().clone()
633        )
634        if (new_output_dimensions == 0).sum() != 1:
635            print(f"6 need exactly one 0 in the input dimensions: {self.name}")
636            print(new_output_dimensions)
637        self.this_node_index.copy_(
638            (new_output_dimensions == 0).nonzero(as_tuple=True)[0][0]
639        )
640        self.dendrite_module.set_this_output_dimensions(new_output_dimensions)

Set the input dimensions for the neuron and dendrite blocks.

Signals to this NeuronModule that its input dimensions are different than the global default.

Parameters
  • new_output_dimensions (list): A list or tensor specifying the new input dimensions.
Returns
  • None
def set_create_dendrite(self, fn):
642    def set_create_dendrite(self, fn):
643        """Set a custom function for creating dendrite modules.
644
645        Override how dendrite copies are created from the parent module.  By default
646        the parent module is deep-copied.  Pass any callable with the signature
647        ``fn(parent_module) -> nn.Module`` to replace that behaviour.
648
649        Parameters
650        ----------
651        fn : callable
652            A function with signature ``fn(parent_module) -> nn.Module``.
653
654        Returns
655        -------
656        None
657        """
658        self.dendrite_module.set_create_dendrite(fn)

Set a custom function for creating dendrite modules.

Override how dendrite copies are created from the parent module. By default the parent module is deep-copied. Pass any callable with the signature fn(parent_module) -> nn.Module to replace that behaviour.

Parameters
  • fn (callable): A function with signature fn(parent_module) -> nn.Module.
Returns
  • None
def set_mode(self, mode):
661    def set_mode(self, mode):
662        """Switch between neuron training and dendrite training.
663
664        Parameters
665        ----------
666        mode : str
667            The mode to set. Either "n" for neuron training or "p" for pai-dendrite training.
668
669        Returns
670        -------
671        bool
672            True if mode was set successfully, False otherwise.
673
674        Notes
675        -----
676        If False is returned, the mode was not changed due to an error.
677        This is a problem that should not be ignored, but it can be ignored
678        by calling PGA.pc.set_checked_skipped_modules(True)
679        """
680
681        if self.module_config.get_verbose():
682            print(f"{self.name} calling set mode {mode}")
683        # If returning to neuron training
684        if mode == "n":
685            self.dendrite_module.set_mode(mode)
686            # Initialize the dendrite to neuron connections
687            if self.dendrite_modules_added > 0:
688                if self.module_config.get_learn_dendrites_live():
689                    values = torch.cat(
690                        (
691                            self.dendrites_to_top[self.dendrite_modules_added - 1],
692                            nn.Parameter(
693                                self.candidate_to_top.detach()
694                                .clone()
695                                .to(dtype=self.module_config.get_d_type())
696                            ),
697                        ),
698                        0,
699                    )
700                else:
701                    values = torch.cat(
702                        (
703                            self.dendrites_to_top[self.dendrite_modules_added - 1],
704                            nn.Parameter(
705                                torch.zeros(
706                                    (1, self.out_channels),
707                                    device=self.dendrites_to_top[
708                                        self.dendrite_modules_added - 1
709                                    ].device,
710                                    dtype=self.module_config.get_d_type(),
711                                )
712                            ),
713                        ),
714                        0,
715                    )
716                # Freeze the previous dendrites_to_top entry
717                # so the optimizer no longer updates stale entries. Only the
718                # most recently appended entry is ever used in the forward pass.
719                if len(self.dendrites_to_top) > 0:
720                    self.dendrites_to_top[-1].requires_grad_(False)
721                self.dendrites_to_top.append(
722                    nn.Parameter(
723                        values.detach()
724                        .clone()
725                        .to(
726                            device=self.module_config.get_device(),
727                            dtype=self.module_config.get_d_type(),
728                        ),
729                        requires_grad=True,
730                    )
731                )
732            else:
733                if self.module_config.get_learn_dendrites_live():
734                    # Freeze previous entry before appending new one.
735                    if len(self.dendrites_to_top) > 0:
736                        self.dendrites_to_top[-1].requires_grad_(False)
737                    self.dendrites_to_top.append(
738                        nn.Parameter(
739                            self.candidate_to_top.detach()
740                            .clone()
741                            .to(dtype=self.module_config.get_d_type()),
742                            requires_grad=True,
743                        )
744                    )
745                else:
746                    # Freeze previous entry before appending new one.
747                    if len(self.dendrites_to_top) > 0:
748                        self.dendrites_to_top[-1].requires_grad_(False)
749                    self.dendrites_to_top.append(
750                        nn.Parameter(
751                            torch.zeros(
752                                (1, self.out_channels),
753                                device=self.module_config.get_device(),
754                                dtype=self.module_config.get_d_type(),
755                            )
756                            .detach()
757                            .clone(),
758                            requires_grad=True,
759                        )
760                    )
761            self.dendrite_modules_added += 1
762            if self.module_config.get_perforated_backpropagation():
763                MPB.set_module_n_pb(self)
764                MPB.set_neuron_parameters(self.dendrites_to_top)
765
766        # If starting dendrite training
767        else:
768            try:
769                # Save the values that were calculated in filter_backward
770                self.out_channels = self.dendrite_module.dendrite_values[0].out_channels
771                self.dendrite_module.out_channels = (
772                    self.dendrite_module.dendrite_values[0].out_channels
773                )
774            except Exception as e:
775                print(e)
776                print(
777                    f"this occurred in module: {self.dendrite_module.dendrite_values[0].layer_name}"
778                )
779                print(
780                    "Module should be added to module_names_to_track so it doesn't have dendrites added"
781                )
782                print("If you are getting here but out_channels has not been set")
783                print(
784                    "A common reason is that this module never had gradients flow through it."
785                )
786                print("I have seen this happen because:")
787                print("-The weights were frozen (requires_grad = False)")
788                print(
789                    "-A model is added but not used so it was converted to a perforated module initialized"
790                )
791                print(
792                    "-A module was converted that doesn't have weights that get modified so backward doesn't flow through it"
793                )
794                print(
795                    "If this is normal behavior set GPA.pc.set_checked_skipped_modules(True) in the main to ignore"
796                )
797                print(
798                    "You can also set right now in this pdb terminal to have this not happen more after checking all modules this cycle."
799                )
800                if not self.module_config.get_checked_skipped_modules():
801                    pdb.set_trace()
802                return False
803            # Only change mode if it makes it past the above exception
804            self.dendrite_module.set_mode(mode)
805            if self.module_config.get_perforated_backpropagation():
806                MPB.set_module_p_pb(self)
807        return True

Switch between neuron training and dendrite training.

Parameters
  • mode (str): The mode to set. Either "n" for neuron training or "p" for pai-dendrite training.
Returns
  • bool: True if mode was set successfully, False otherwise.
Notes

If False is returned, the mode was not changed due to an error. This is a problem that should not be ignored, but it can be ignored by calling PGA.pc.set_checked_skipped_modules(True)

def create_new_dendrite_module(self):
809    def create_new_dendrite_module(self):
810        """Add an additional dendrite module.
811
812        Parameters
813        ----------
814        None
815
816        Returns
817        -------
818        None
819        """
820        self.dendrite_module.create_new_dendrite_module(self.main_module)

Add an additional dendrite module.

Parameters
  • None
Returns
  • None
def forward(self, *args, **kwargs):
822    def forward(self, *args, **kwargs):
823        """Forward pass through the neuron module.
824
825        Parameters
826        ----------
827        *args : tuple
828            Positional arguments for the forward pass.
829        **kwargs : dict
830            Keyword arguments for the forward pass.
831
832        Returns
833        -------
834        Any
835            The output of the module after processing through the neuron and dendrite modules.
836
837        Notes
838        -----
839            The output of this forward function will have the same format as the output
840            of the original module
841        """
842
843        # If debugging all input dimensions, quit program on first forward call
844        if self.module_config.get_debugging_output_dimensions() == 2:
845            print("all input dim problems now printed")
846            sys.exit(0)
847        if self.module_config.get_extra_verbose():
848            print(f"{self.name} calling forward")
849        # Call the main modules forward
850        out = self.main_module(*args, **kwargs)
851        # Filter with the processor if required
852        if self.processor is not None:
853            try:
854                out = self.processor.post_n1(out)
855            except Exception as e:
856                traceback.print_exc(limit=None, chain=True)
857                print(f"Your post_n1 processor for {self.name} caused this error")
858                print(
859                    f"You must check how this is defined and ensure that it is properly"
860                )
861                print(f"accepting outputs from the neuron module and returning the")
862                print(f"single tensor to be combined with the dendrites output tensor")
863                sys.exit()
864        # Call the forwards for all of the Dendrites
865        (
866            dendrite_outs,
867            candidate_outs,
868            candidate_nonlinear_outs,
869            candidate_outs_non_zeroed,
870        ) = self.dendrite_module(*args, **kwargs)
871        # If there are dendrites add all of their outputs to the neurons output
872        if self.dendrite_modules_added > 0:
873            for i in range(0, self.dendrite_modules_added):
874                to_top = self.dendrites_to_top[self.dendrite_modules_added - 1][i, :]
875                for dim in range(len(dendrite_outs[i].shape)):
876                    if dim == self.this_node_index:
877                        continue
878                    to_top = to_top.unsqueeze(dim)
879                if self.module_config.get_confirm_correct_sizes():
880                    to_top = to_top.expand(
881                        list(dendrite_outs[i].size())[0 : self.this_node_index]
882                        + [self.out_channels]
883                        + list(dendrite_outs[i].size())[self.this_node_index + 1 :]
884                    )
885                out = out + (dendrite_outs[i].to(out.device) * to_top.to(out.device))
886
887        # If learning live, add the candidate's output to the neuron's output via the live weight
888        if self.module_config.get_perforated_backpropagation():
889            out = MPB.apply_live_candidate_to_output(
890                self, out, candidate_nonlinear_outs
891            )
892
893        # Catch if processors are required
894        if type(out) is tuple:
895            print(self)
896            print(
897                f"The output of the above module {self.name} is a tuple when it must be a single tensor"
898            )
899            print(
900                "This must be fixed to enable the dendrite and neuron output to be combined"
901            )
902            print(
903                "Look in the API customization.md at section 2.2 regarding processors to fix this."
904            )
905            pdb.set_trace()
906
907        # Call filter backward to ensure the neuron index is setup correctly.
908        # Register only when not yet initialized (PBP=False) or always when PBP
909        # is enabled. Store the handle so the hook can deregister itself after
910        # the one-time initialization completes (PBP=False path).
911        if out.requires_grad and (not self._fb_init_done or GPA.pc.get_perforated_backpropagation()):
912            self._fb_hook_handle = out.register_hook(
913                lambda grad: filter_backward(grad, self.dendrite_module.dendrite_values, self)
914            )
915
916        # If there is a processor apply the second neuron stage
917        if self.processor is not None:
918            try:
919                out = self.processor.post_n2(out)
920            except Exception as e:
921                traceback.print_exc(limit=None, chain=True)
922                print(f"Your post_n2 processor for {self.name} caused this error")
923                print(
924                    f"You must check how this is defined and ensure that it is properly"
925                )
926                print(
927                    f"accepting the output tensor after combining the neuron's output "
928                )
929                print(f"with the dendrite's output and returning something that is the")
930                print(f"same format as your original module's return")
931                sys.exit()
932        return out

Forward pass through the neuron module.

Parameters
  • *args (tuple): Positional arguments for the forward pass.
  • **kwargs (dict): Keyword arguments for the forward pass.
Returns
  • Any: The output of the module after processing through the neuron and dendrite modules.
Notes

The output of this forward function will have the same format as the output of the original module

class TrackedNeuronModule(torch.nn.modules.module.Module):
 935class TrackedNeuronModule(nn.Module):
 936    """Wrapper for modules you don't want to add dendrites to. Ensures all modules are accounted for."""
 937
 938    def __init__(self, start_module, name):
 939        """Initialize TrackedNeuronModule.
 940
 941        This function sets up the tracked neuron module to wrap the start_module
 942        without adding dendrites.
 943
 944        Parameters
 945        ----------
 946        start_module : nn.Module
 947            The module to wrap.
 948        name : str
 949            The name of the neuron module.
 950        """
 951        super(TrackedNeuronModule, self).__init__()
 952
 953        if isinstance(start_module, nn.Module):
 954            self.main_module = start_module
 955        else:
 956            print("start_module must be nn.Module: %s" % name)
 957            print(type(start_module))
 958            print(start_module)
 959            sys.exit(-1)
 960        self.name = name
 961
 962        self.type = "tracked_module"
 963        set_tracked_params(self.main_module)
 964        if GPA.pc.get_verbose():
 965            print(
 966                f"tracking a module {self.name} with main type {type(self.main_module)}"
 967            )
 968            print(start_module)
 969        GPA.pai_tracker.add_tracked_neuron_module(self)
 970        if GPA.pc.get_perforated_backpropagation():
 971            MPB.set_neuron_parameters(self.main_module)
 972
 973    def __getattr__(self, name):
 974        """Get member variables from the main module.
 975
 976        Parameters
 977        ----------
 978        name : str
 979            The name of the variable to retrieve.
 980        Returns
 981        -------
 982        The requested variable.
 983
 984        Notes
 985        -----
 986        This method first attempts to retrieve the attribute from the PAINeuronModule instance.
 987        If it fails, it tries to get the attribute from the wrapped main_module.
 988        This allows seamless access to the main module's attributes without modifying original code.
 989        """
 990        try:
 991            return super().__getattr__(name)
 992        except AttributeError:
 993            return getattr(self.main_module, name)
 994
 995    def __getitem__(self, index):
 996        """Support indexing operations on the main module.
 997
 998        Parameters
 999        ----------
1000        index : int or slice
1001            The index or slice to retrieve.
1002
1003        Returns
1004        -------
1005        The indexed item from the main module.
1006        """
1007        return self.main_module[index]
1008
1009    def set_mode(self, mode):
1010        """Set mode for tracked module.
1011
1012        Parameters
1013        ----------
1014        mode : str
1015            The mode to set. Either "n" for neuron training or "p" for pai-dendrite training.
1016
1017        Returns
1018        -------
1019        bool
1020            True.
1021
1022        Notes
1023        -----
1024        This function does not change any behavior since this is a tracked module.
1025        """
1026
1027        if GPA.pc.get_verbose():
1028            print(f"{self.name} calling set mode {mode}")
1029        return True
1030
1031    def forward(self, *args, **kwargs):
1032        """Forward pass for tracked module.
1033
1034        Parameters
1035        ----------
1036        *args : tuple
1037            Positional arguments for the forward pass.
1038        **kwargs : dict
1039            Keyword arguments for the forward pass.
1040
1041        Returns
1042        -------
1043        Any
1044            The output of the module
1045
1046        Notes
1047        -----
1048            The output of this forward function will have the same format as the output
1049            of the original module
1050        """
1051        return self.main_module(*args, **kwargs)
1052
1053    def __str__(self):
1054        """String representation of the module.
1055
1056        Parameters
1057        ----------
1058        None
1059
1060        Returns
1061        -------
1062        str
1063            String representation of the module.
1064
1065        Notes
1066        -----
1067        Setting for verbose changes level of details in the string output.
1068        """
1069
1070        if GPA.pc.get_verbose():
1071            total_string = self.main_module.__str__()
1072            total_string = "PAITrackedModule(" + total_string + ")"
1073            return total_string
1074        else:
1075            total_string = self.main_module.__str__()
1076            total_string = "PAITrackedModule(" + total_string + ")"
1077            return total_string
1078
1079    def __repr__(self):
1080        """Representation of the module."""
1081        return self.__str__()

Wrapper for modules you don't want to add dendrites to. Ensures all modules are accounted for.

TrackedNeuronModule(start_module, name)
938    def __init__(self, start_module, name):
939        """Initialize TrackedNeuronModule.
940
941        This function sets up the tracked neuron module to wrap the start_module
942        without adding dendrites.
943
944        Parameters
945        ----------
946        start_module : nn.Module
947            The module to wrap.
948        name : str
949            The name of the neuron module.
950        """
951        super(TrackedNeuronModule, self).__init__()
952
953        if isinstance(start_module, nn.Module):
954            self.main_module = start_module
955        else:
956            print("start_module must be nn.Module: %s" % name)
957            print(type(start_module))
958            print(start_module)
959            sys.exit(-1)
960        self.name = name
961
962        self.type = "tracked_module"
963        set_tracked_params(self.main_module)
964        if GPA.pc.get_verbose():
965            print(
966                f"tracking a module {self.name} with main type {type(self.main_module)}"
967            )
968            print(start_module)
969        GPA.pai_tracker.add_tracked_neuron_module(self)
970        if GPA.pc.get_perforated_backpropagation():
971            MPB.set_neuron_parameters(self.main_module)

Initialize TrackedNeuronModule.

This function sets up the tracked neuron module to wrap the start_module without adding dendrites.

Parameters
  • start_module (nn.Module): The module to wrap.
  • name (str): The name of the neuron module.
name
def type(self, dst_type: torch.dtype | str) -> Self:
1169    def type(self, dst_type: dtype | str) -> Self:
1170        r"""Casts all parameters and buffers to :attr:`dst_type`.
1171
1172        .. note::
1173            This method modifies the module in-place.
1174
1175        Args:
1176            dst_type (type or string): the desired type
1177
1178        Returns:
1179            Module: self
1180        """
1181        return self._apply(lambda t: t.type(dst_type))

Casts all parameters and buffers to dst_type.

This method modifies the module in-place.

Args: dst_type (type or string): the desired type

Returns: Module: self

def set_mode(self, mode):
1009    def set_mode(self, mode):
1010        """Set mode for tracked module.
1011
1012        Parameters
1013        ----------
1014        mode : str
1015            The mode to set. Either "n" for neuron training or "p" for pai-dendrite training.
1016
1017        Returns
1018        -------
1019        bool
1020            True.
1021
1022        Notes
1023        -----
1024        This function does not change any behavior since this is a tracked module.
1025        """
1026
1027        if GPA.pc.get_verbose():
1028            print(f"{self.name} calling set mode {mode}")
1029        return True

Set mode for tracked module.

Parameters
  • mode (str): The mode to set. Either "n" for neuron training or "p" for pai-dendrite training.
Returns
  • bool: True.
Notes

This function does not change any behavior since this is a tracked module.

def forward(self, *args, **kwargs):
1031    def forward(self, *args, **kwargs):
1032        """Forward pass for tracked module.
1033
1034        Parameters
1035        ----------
1036        *args : tuple
1037            Positional arguments for the forward pass.
1038        **kwargs : dict
1039            Keyword arguments for the forward pass.
1040
1041        Returns
1042        -------
1043        Any
1044            The output of the module
1045
1046        Notes
1047        -----
1048            The output of this forward function will have the same format as the output
1049            of the original module
1050        """
1051        return self.main_module(*args, **kwargs)

Forward pass for tracked module.

Parameters
  • *args (tuple): Positional arguments for the forward pass.
  • **kwargs (dict): Keyword arguments for the forward pass.
Returns
  • Any: The output of the module
Notes

The output of this forward function will have the same format as the output of the original module

def init_params(module, neuron_main_module):
1084def init_params(module, neuron_main_module):
1085    """Randomize weights after duplicating the main module for the next set of dendrites.
1086
1087    Parameters
1088    ----------
1089    module : nn.Module
1090        The new dendrite module to initialize.
1091    neuron_main_module : nn.Module
1092        The main module of the neuron for potential weight scaling.
1093
1094
1095    Returns
1096    -------
1097    None
1098        This function does not return a value.
1099    """
1100    for param in module.parameters():
1101        if param.dtype == torch.uint8:
1102            param.data = torch.randint(0, 256, param.size(), dtype=torch.uint8)
1103        else:
1104            # If factoring in the main modules weights multiply the randn()
1105            #  by the average abs value of the main modules weights
1106            if GPA.pc.get_candidate_weight_init_by_main():
1107                main_module_abs = 0
1108                total_main_params = 0
1109                for main_param in neuron_main_module.parameters():
1110                    main_module_abs += main_param.abs().sum().item()
1111                    total_main_params += main_param.numel()
1112                if total_main_params > 0:
1113                    main_module_abs /= total_main_params
1114                else:
1115                    main_module_abs = 1.0
1116                multiplier = main_module_abs
1117            else:
1118                multiplier = 1.0
1119            param.data = (
1120                torch.randn(param.size(), dtype=param.dtype)
1121                * GPA.pc.get_candidate_weight_initialization_multiplier()
1122                * multiplier
1123            )

Randomize weights after duplicating the main module for the next set of dendrites.

Parameters
  • module (nn.Module): The new dendrite module to initialize.
  • neuron_main_module (nn.Module): The main module of the neuron for potential weight scaling.
Returns
  • None: This function does not return a value.
class PAIDendriteModule(torch.nn.modules.module.Module):
1126class PAIDendriteModule(nn.Module):
1127    """Module containing all dendrites modules added to the neuron module."""
1128
1129    def __init__(
1130        self,
1131        initial_module,
1132        activation_function_value=0.3,
1133        name="no_name_given",
1134        output_dimensions=None,
1135    ):
1136        """Initialize PAINeuronModule.
1137
1138        This function sets up the dendrite module to create candidate and permanent
1139        dendrite modules based on the initial_module provided.
1140
1141        Parameters
1142        ----------
1143        initial_module : nn.Module
1144            The module to copy.
1145        activation_function_value : float, optional
1146            A value associated with the activation function, by default 0.3.
1147        name : str
1148            The name of the neuron module.
1149        output_dimensions : vector, optional
1150            The dimensions of the input vector
1151        """
1152        super(PAIDendriteModule, self).__init__()
1153
1154        if output_dimensions is None:
1155            output_dimensions = []
1156
1157        self.layers = nn.ModuleList([])
1158        self.processors = []
1159        self.candidate_processors = []
1160        self.num_dendrites = 0
1161        self._create_dendrite_fn = None
1162        # Number of dendrite cycles performed
1163        self.register_buffer(
1164            "num_cycles",
1165            torch.zeros(1, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()),
1166        )
1167        self.mode = "n"
1168        self.name = name
1169        # Create a copy of the parent module so you don't have a pointer to the real one which causes save errors
1170        self.parent_module = UPA.deep_copy_pai(initial_module)
1171        if GPA.pc.get_perforated_backpropagation():
1172            MPB.set_ignored_parameters(self.parent_module)
1173        # Setup the input dimensions and node index for combining dendrite outputs
1174        if GPA.pc.get_perforated_backpropagation():
1175            MPB.create_extra_tensors(self)
1176        if output_dimensions == []:
1177            self.register_buffer(
1178                "this_output_dimensions", torch.tensor(GPA.pc.get_output_dimensions())
1179            )
1180        else:
1181            self.register_buffer(
1182                "this_output_dimensions", output_dimensions.detach().clone()
1183            )
1184        if (self.this_output_dimensions == 0).sum() != 1:
1185            print(f"1 need exactly one 0 in the input dimensions: {self.name}")
1186            print(self.this_output_dimensions)
1187            sys.exit(-1)
1188        self.register_buffer(
1189            "this_node_index", torch.tensor(GPA.pc.get_output_dimensions().index(0))
1190        )
1191
1192        # Initialize dendrite to dendrite connections
1193        self.dendrites_to_candidates = nn.ParameterList()
1194        self.dendrites_to_dendrites = nn.ParameterList()
1195
1196        # Store an activation function value if required
1197        self.activation_function_value = activation_function_value
1198        self.dendrite_values = nn.ModuleList([])
1199        for j in range(0, GPA.pc.get_global_candidates()):
1200            if GPA.pc.get_verbose():
1201                print(f"creating dendrite Values for {self.name}")
1202            self.dendrite_values.append(
1203                DendriteValueTracker(
1204                    False,
1205                    self.activation_function_value,
1206                    self.name,
1207                    self.this_output_dimensions,
1208                )
1209            )
1210        if GPA.pc.get_perforated_backpropagation():
1211            self.apply_pb_grads = MPB.apply_pb_grads.__get__(self, type(self))
1212            self.apply_pb_zero = MPB.apply_pb_zero.__get__(self, type(self))
1213
1214    def __getstate__(self):
1215        """Tell pickle what to save when this object is serialized (e.g. torch.save).
1216
1217        apply_pb_grads and apply_pb_zero are bound methods of functions defined
1218        in modules_pbp and cannot be pickled.  Strip them out; __setstate__ will
1219        re-attach them after loading.
1220        """
1221        import types
1222
1223        pickle_safe_state = {}
1224        for attr_name, attr_value in self.__dict__.items():
1225            if not isinstance(attr_value, types.MethodType):
1226                pickle_safe_state[attr_name] = attr_value
1227
1228        return pickle_safe_state
1229
1230    def __setstate__(self, saved_state):
1231        """Restore this object from a pickled state (e.g. torch.load).
1232
1233        Restores all normal attributes, then re-attaches apply_pb_grads and
1234        apply_pb_zero if perforated backpropagation is enabled.
1235        """
1236        self.__dict__.update(saved_state)
1237
1238        # Re-attach the PBP bound methods that were stripped by __getstate__.
1239        # dendrite_loss_fn being present on the saved state means PBP was active
1240        # when the checkpoint was saved.
1241        if "dendrite_loss_fn" in saved_state:
1242            import perforatedbp.modules_pbp as MPB
1243            self.apply_pb_grads = MPB.apply_pb_grads.__get__(self, type(self))
1244            self.apply_pb_zero = MPB.apply_pb_zero.__get__(self, type(self))
1245
1246    def create_dendrite(self, parent_module):
1247        """Create a dendrite module from the parent module.
1248
1249        Override this function via set_create_dendrite to control how the dendrite
1250        module is created (e.g. to avoid a deep copy).
1251
1252        Parameters
1253        ----------
1254        parent_module : nn.Module
1255            The module to create a dendrite from.
1256
1257        Returns
1258        -------
1259        nn.Module
1260            The new dendrite module.
1261        """
1262        if self._create_dendrite_fn is not None:
1263            return self._create_dendrite_fn(parent_module)
1264        return UPA.deep_copy_pai(parent_module)
1265
1266    def set_create_dendrite(self, fn):
1267        """Set a custom function for creating dendrite modules.
1268
1269        Call this on a PAIDendriteModule instance to override how dendrites are
1270        created from the parent module. The function receives the parent module
1271        and must return a new nn.Module.
1272
1273        Parameters
1274        ----------
1275        fn : callable
1276            A function with signature ``fn(parent_module) -> nn.Module``.
1277
1278        Returns
1279        -------
1280        None
1281        """
1282        self._create_dendrite_fn = fn
1283
1284
1285    def set_this_output_dimensions(self, new_output_dimensions):
1286        """Set input dimensions for dendrite module.
1287
1288        Signals to this DendriteModule that its input dimensions are different
1289        than the global default.
1290
1291        Parameters
1292        ----------
1293        new_output_dimensions : list
1294            A list or tensor specifying the new input dimensions.
1295        Returns
1296        -------
1297        None
1298
1299        """
1300
1301        if type(new_output_dimensions) is list:
1302            new_output_dimensions = torch.tensor(new_output_dimensions)
1303        delattr(self, "this_output_dimensions")
1304        self.register_buffer(
1305            "this_output_dimensions", new_output_dimensions.detach().clone()
1306        )
1307        if (new_output_dimensions == 0).sum() != 1:
1308            print(f"2 Need exactly one 0 in the input dimensions: {self.name}")
1309            print(new_output_dimensions)
1310            sys.exit(-1)
1311        self.this_node_index.copy_(
1312            (new_output_dimensions == 0).nonzero(as_tuple=True)[0][0]
1313        )
1314        for j in range(0, GPA.pc.get_global_candidates()):
1315            self.dendrite_values[j].set_this_output_dimensions(new_output_dimensions)
1316
1317    def create_new_dendrite_module(self, neuron_main_module):
1318        """Add a new set of dendrites.
1319
1320        Parameters
1321        ----------
1322        neuron_main_module : Any
1323            PyTorch module to be used for dendritic learning.
1324                Typically a copy of the original neuron module.
1325
1326        Returns
1327        -------
1328        None
1329            This function does not return a value.
1330        """
1331        # Candidate module
1332        self.candidate_module = nn.ModuleList([])
1333        # Copy that is unused for open source version
1334        self.best_candidate_module = nn.ModuleList([])
1335        if GPA.pc.get_verbose():
1336            print(self.name)
1337            print("Setting candidate processors")
1338        self.candidate_processors = []
1339        with torch.no_grad():
1340            for i in range(0, GPA.pc.get_global_candidates()):
1341
1342                new_module = self.create_dendrite(self.parent_module)
1343                init_params(new_module, neuron_main_module)
1344                self.candidate_module.append(new_module)
1345                self.best_candidate_module.append(self.create_dendrite(new_module))
1346                if type(self.parent_module) in GPA.pc.get_modules_with_processing():
1347                    module_index = GPA.pc.get_modules_with_processing().index(
1348                        type(self.parent_module)
1349                    )
1350                    self.candidate_processors.append(
1351                        GPA.pc.get_modules_processing_classes()[module_index]()
1352                    )
1353                elif (
1354                    type(self.parent_module).__name__
1355                    in GPA.pc.get_module_names_with_processing()
1356                ):
1357                    module_index = GPA.pc.get_module_names_with_processing().index(
1358                        type(self.parent_module).__name__
1359                    )
1360                    self.candidate_processors.append(
1361                        GPA.pc.get_module_by_name_processing_classes()[module_index]()
1362                    )
1363                if GPA.pc.get_perforated_backpropagation():
1364                    MPB.set_candidate_parameters(self.candidate_module[i])
1365                    MPB.set_ignored_parameters(self.best_candidate_module[i])
1366
1367        for i in range(0, GPA.pc.get_global_candidates()):
1368            self.candidate_module[i].to(GPA.pc.get_device())
1369            self.best_candidate_module[i].to(GPA.pc.get_device())
1370
1371        # Reset the dendrite_values objects
1372        for j in range(0, GPA.pc.get_global_candidates()):
1373            self.dendrite_values[j].reinitialize_for_pai()
1374
1375        # If there are already dendrites initialize the dendrite to dendrite connections
1376        if self.num_dendrites > 0:
1377            self.dendrites_to_candidates = nn.ParameterList()
1378            for j in range(0, GPA.pc.get_global_candidates()):
1379                self.dendrites_to_candidates.append(
1380                    nn.Parameter(
1381                        torch.zeros(
1382                            (self.num_dendrites, self.out_channels),
1383                            device=GPA.pc.get_device(),
1384                            dtype=GPA.pc.get_d_type(),
1385                        ),
1386                        requires_grad=True,
1387                    )
1388                )
1389                if GPA.pc.get_perforated_backpropagation():
1390                    MPB.init_candidates(self, j)
1391            if GPA.pc.get_perforated_backpropagation():
1392                MPB.set_candidate_parameters(self.dendrites_to_candidates)
1393            # Initialize best_dendrites_to_candidates_saved to snapshot peak-correlation weights at epoch boundaries
1394            self.best_dendrites_to_candidates_saved = []
1395            for j in range(0, GPA.pc.get_global_candidates()):
1396                self.best_dendrites_to_candidates_saved.append(
1397                    torch.zeros(
1398                        (self.num_dendrites, self.out_channels),
1399                        device=GPA.pc.get_device(),
1400                        dtype=GPA.pc.get_d_type(),
1401                    )
1402                )
1403
1404    def clear_processors(self):
1405        """Clear processors.
1406
1407        Parameters
1408        ----------
1409        None
1410
1411        Returns
1412        -------
1413        None
1414            This function does not return a value.
1415        """
1416        for processor in self.processors:
1417            if not processor:
1418                continue
1419            else:
1420                processor.clear_processor()
1421        for processor in self.candidate_processors:
1422            if not processor:
1423                continue
1424            else:
1425                processor.clear_processor()
1426
1427    def set_mode(self, mode):
1428        """Perform actions when switching between neuron and dendrite training.
1429
1430        Parameters
1431        ----------
1432        mode : str
1433            The mode to set. Either "n" for neuron training or "p" for pai-dendrite training.
1434
1435        Returns
1436        -------
1437        None
1438        """
1439
1440        self.mode = mode
1441        self.num_cycles += 1
1442        if GPA.pc.get_verbose():
1443            print(f"PAI calling set mode {mode} : {self.num_cycles}")
1444        if not GPA.pc.get_silent():
1445            print(f"Module {self.name} calling set mode {mode} : {self.num_cycles}")
1446        # When switching back to neuron training mode convert candidates modules into accepted modules
1447        if mode == "n":
1448            if GPA.pc.get_verbose():
1449                print("So calling all the things to add to modules")
1450            # Copy weights/bias from correct candidates
1451            if self.num_dendrites == 1:
1452                self.dendrites_to_dendrites = nn.ParameterList()
1453                self.dendrites_to_dendrites.append(torch.tensor([]))
1454            if self.num_dendrites >= 1:
1455                self.dendrites_to_dendrites.append(
1456                    torch.nn.Parameter(
1457                        torch.zeros(
1458                            [self.num_dendrites, self.out_channels],
1459                            device=GPA.pc.get_device(),
1460                            dtype=GPA.pc.get_d_type(),
1461                        ),
1462                        # Grad is true if not pb or if pb and dendrite_update_mode is true
1463                        requires_grad=(not GPA.pc.get_perforated_backpropagation())
1464                        or GPA.pc.get_dendrite_update_mode(),
1465                    )
1466                )
1467            with torch.no_grad():
1468                if GPA.pc.get_global_candidates() > 1:
1469                    print(
1470                        "This was a flag that will be needed if using multiple candidates. "
1471                        "It's not set up yet but nice work finding it."
1472                    )
1473                    print(
1474                        "Note: with multiple candidates, best-score ranking in new_best() uses "
1475                        "unnormalized covariance (prev_dendrite_candidate_correlation) rather than "
1476                        "the normalized correlation coefficient. Candidates with larger output "
1477                        "magnitude will be favored regardless of true correlation quality. "
1478                        "Fix by tracking running sigma_V and sigma_E and dividing in new_best()."
1479                    )
1480                    pdb.set_trace()
1481                plane_max_index = 0
1482                self.layers.append(
1483                    UPA.deep_copy_pai(self.best_candidate_module[plane_max_index])
1484                )
1485                self.layers[self.num_dendrites].to(GPA.pc.get_device())
1486                if self.num_dendrites > 0:
1487                    self.dendrites_to_dendrites[self.num_dendrites].copy_(
1488                        self.best_dendrites_to_candidates_saved[plane_max_index]
1489                    )
1490                if type(self.parent_module) in GPA.pc.get_modules_with_processing():
1491                    self.processors.append(self.candidate_processors[plane_max_index])
1492                if (
1493                    type(self.parent_module).__name__
1494                    in GPA.pc.get_module_names_with_processing()
1495                ):
1496                    self.processors.append(self.candidate_processors[plane_max_index])
1497            if GPA.pc.get_perforated_backpropagation():
1498                MPB.set_pb_mode(self, mode)
1499            del self.candidate_module, self.best_candidate_module
1500
1501            self.num_dendrites += 1
1502            if GPA.pc.get_perforated_backpropagation():
1503                MPB.set_dendrite_parameters(self.dendrites_to_dendrites)
1504                MPB.set_dendrite_parameters(self.layers)
1505
1506    def forward(self, *args, **kwargs):
1507        """Forward pass for dendrite module.
1508
1509        Parameters
1510        ----------
1511        *args : tuple
1512            Positional arguments for the forward pass.
1513        **kwargs : dict
1514            Keyword arguments for the forward pass.
1515
1516        Returns
1517        -------
1518        Any
1519            The output of the module after processing through the neuron and dendrite modules.
1520        Any
1521            Remaining outputs are only used for Perforated Backpropagation.
1522        Any
1523            Remaining outputs are only used for Perforated Backpropagation.
1524        Any
1525            Remaining outputs are only used for Perforated Backpropagation.
1526
1527        Notes
1528        -----
1529        If using Perforated Backpropagation, the additional outputs will be moved around in
1530        this code but left unused and only passed into separate PB functions.
1531        """
1532
1533        outs = {}
1534
1535        # For all modules apply processors, call the modules, then apply post processors
1536        args2, kwargs2 = args, kwargs
1537        for c in range(0, self.num_dendrites):
1538            if GPA.pc.get_perforated_backpropagation():
1539                args2, kwargs2 = MPB.preprocess_pb(*args, **kwargs)
1540            if self.processors != []:
1541                try:
1542                    args2, kwargs2 = self.processors[c].pre_d(*args2, **kwargs2)
1543                except Exception as e:
1544                    traceback.print_exc(limit=None, chain=True)
1545                    print(f"Your pre_d processor for {self.name} caused this error")
1546                    print(
1547                        f"You must check how this is defined and ensure that it is properly"
1548                    )
1549                    print(
1550                        f"accepting inputs to the PAIModule and returning what will then be"
1551                    )
1552                    print(f"the input to the dendrite module")
1553                    sys.exit()
1554            out_values = self.layers[c](*args2, **kwargs2)
1555            if self.processors != []:
1556                try:
1557                    outs[c] = self.processors[c].post_d(out_values)
1558                except Exception as e:
1559                    traceback.print_exc(limit=None, chain=True)
1560                    print(f"Your post_d processor for {self.name} caused this error")
1561                    print(
1562                        f"You must check how this is defined and ensure that it is properly"
1563                    )
1564                    print(
1565                        f"accepting outputs from the dendrite module and returning the"
1566                    )
1567                    print(
1568                        f"single tensor to be combined with the neurons output tensor"
1569                    )
1570                    sys.exit()
1571            else:
1572                outs[c] = out_values
1573
1574        # Create dendrite outputs
1575        # Each dendrite has input from previously created dendrites
1576        # So activation is added before the nonlinearity is called
1577        view_tuple = []
1578        for out_index in range(0, self.num_dendrites):
1579            current_out = outs[out_index]
1580            view_tuple = []
1581            for dim in range(len(current_out.shape)):
1582                if dim == self.this_node_index:
1583                    view_tuple.append(-1)
1584                    continue
1585                view_tuple.append(1)
1586
1587            for in_index in range(0, out_index):
1588                if view_tuple == [
1589                    1
1590                ]:  # This is only the case when passing a single datapoint rather than a batch
1591                    current_out = (
1592                        current_out
1593                        + self.dendrites_to_dendrites[out_index][in_index, :].to(
1594                            current_out.device
1595                        )
1596                        * outs[in_index]
1597                    )
1598                else:
1599                    current_out = (
1600                        current_out
1601                        + self.dendrites_to_dendrites[out_index][in_index, :]
1602                        .view(view_tuple)
1603                        .to(current_out.device)
1604                        * outs[in_index]
1605                    )
1606            outs[out_index] = GPA.pc.get_pai_forward_function()(current_out)
1607        # Return a dict which has all dendritic outputs after the activation functions were called
1608        if GPA.pc.get_perforated_backpropagation():
1609            candidate_outs, candidate_nonlinear_outs, candidate_non_zeroed = (
1610                MPB.forward_candidates(self, view_tuple, outs, *args2, **kwargs2)
1611            )
1612        else:
1613            candidate_outs, candidate_nonlinear_outs, candidate_non_zeroed = (
1614                {},
1615                {},
1616                {},
1617            )
1618        return outs, candidate_outs, candidate_nonlinear_outs, candidate_non_zeroed

Module containing all dendrites modules added to the neuron module.

PAIDendriteModule( initial_module, activation_function_value=0.3, name='no_name_given', output_dimensions=None)
1129    def __init__(
1130        self,
1131        initial_module,
1132        activation_function_value=0.3,
1133        name="no_name_given",
1134        output_dimensions=None,
1135    ):
1136        """Initialize PAINeuronModule.
1137
1138        This function sets up the dendrite module to create candidate and permanent
1139        dendrite modules based on the initial_module provided.
1140
1141        Parameters
1142        ----------
1143        initial_module : nn.Module
1144            The module to copy.
1145        activation_function_value : float, optional
1146            A value associated with the activation function, by default 0.3.
1147        name : str
1148            The name of the neuron module.
1149        output_dimensions : vector, optional
1150            The dimensions of the input vector
1151        """
1152        super(PAIDendriteModule, self).__init__()
1153
1154        if output_dimensions is None:
1155            output_dimensions = []
1156
1157        self.layers = nn.ModuleList([])
1158        self.processors = []
1159        self.candidate_processors = []
1160        self.num_dendrites = 0
1161        self._create_dendrite_fn = None
1162        # Number of dendrite cycles performed
1163        self.register_buffer(
1164            "num_cycles",
1165            torch.zeros(1, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()),
1166        )
1167        self.mode = "n"
1168        self.name = name
1169        # Create a copy of the parent module so you don't have a pointer to the real one which causes save errors
1170        self.parent_module = UPA.deep_copy_pai(initial_module)
1171        if GPA.pc.get_perforated_backpropagation():
1172            MPB.set_ignored_parameters(self.parent_module)
1173        # Setup the input dimensions and node index for combining dendrite outputs
1174        if GPA.pc.get_perforated_backpropagation():
1175            MPB.create_extra_tensors(self)
1176        if output_dimensions == []:
1177            self.register_buffer(
1178                "this_output_dimensions", torch.tensor(GPA.pc.get_output_dimensions())
1179            )
1180        else:
1181            self.register_buffer(
1182                "this_output_dimensions", output_dimensions.detach().clone()
1183            )
1184        if (self.this_output_dimensions == 0).sum() != 1:
1185            print(f"1 need exactly one 0 in the input dimensions: {self.name}")
1186            print(self.this_output_dimensions)
1187            sys.exit(-1)
1188        self.register_buffer(
1189            "this_node_index", torch.tensor(GPA.pc.get_output_dimensions().index(0))
1190        )
1191
1192        # Initialize dendrite to dendrite connections
1193        self.dendrites_to_candidates = nn.ParameterList()
1194        self.dendrites_to_dendrites = nn.ParameterList()
1195
1196        # Store an activation function value if required
1197        self.activation_function_value = activation_function_value
1198        self.dendrite_values = nn.ModuleList([])
1199        for j in range(0, GPA.pc.get_global_candidates()):
1200            if GPA.pc.get_verbose():
1201                print(f"creating dendrite Values for {self.name}")
1202            self.dendrite_values.append(
1203                DendriteValueTracker(
1204                    False,
1205                    self.activation_function_value,
1206                    self.name,
1207                    self.this_output_dimensions,
1208                )
1209            )
1210        if GPA.pc.get_perforated_backpropagation():
1211            self.apply_pb_grads = MPB.apply_pb_grads.__get__(self, type(self))
1212            self.apply_pb_zero = MPB.apply_pb_zero.__get__(self, type(self))

Initialize PAINeuronModule.

This function sets up the dendrite module to create candidate and permanent dendrite modules based on the initial_module provided.

Parameters
  • initial_module (nn.Module): The module to copy.
  • activation_function_value (float, optional): A value associated with the activation function, by default 0.3.
  • name (str): The name of the neuron module.
  • output_dimensions (vector, optional): The dimensions of the input vector
layers
processors
candidate_processors
num_dendrites
mode
name
parent_module
dendrites_to_candidates
dendrites_to_dendrites
activation_function_value
dendrite_values
def create_dendrite(self, parent_module):
1246    def create_dendrite(self, parent_module):
1247        """Create a dendrite module from the parent module.
1248
1249        Override this function via set_create_dendrite to control how the dendrite
1250        module is created (e.g. to avoid a deep copy).
1251
1252        Parameters
1253        ----------
1254        parent_module : nn.Module
1255            The module to create a dendrite from.
1256
1257        Returns
1258        -------
1259        nn.Module
1260            The new dendrite module.
1261        """
1262        if self._create_dendrite_fn is not None:
1263            return self._create_dendrite_fn(parent_module)
1264        return UPA.deep_copy_pai(parent_module)

Create a dendrite module from the parent module.

Override this function via set_create_dendrite to control how the dendrite module is created (e.g. to avoid a deep copy).

Parameters
  • parent_module (nn.Module): The module to create a dendrite from.
Returns
  • nn.Module: The new dendrite module.
def set_create_dendrite(self, fn):
1266    def set_create_dendrite(self, fn):
1267        """Set a custom function for creating dendrite modules.
1268
1269        Call this on a PAIDendriteModule instance to override how dendrites are
1270        created from the parent module. The function receives the parent module
1271        and must return a new nn.Module.
1272
1273        Parameters
1274        ----------
1275        fn : callable
1276            A function with signature ``fn(parent_module) -> nn.Module``.
1277
1278        Returns
1279        -------
1280        None
1281        """
1282        self._create_dendrite_fn = fn

Set a custom function for creating dendrite modules.

Call this on a PAIDendriteModule instance to override how dendrites are created from the parent module. The function receives the parent module and must return a new nn.Module.

Parameters
  • fn (callable): A function with signature fn(parent_module) -> nn.Module.
Returns
  • None
def set_this_output_dimensions(self, new_output_dimensions):
1285    def set_this_output_dimensions(self, new_output_dimensions):
1286        """Set input dimensions for dendrite module.
1287
1288        Signals to this DendriteModule that its input dimensions are different
1289        than the global default.
1290
1291        Parameters
1292        ----------
1293        new_output_dimensions : list
1294            A list or tensor specifying the new input dimensions.
1295        Returns
1296        -------
1297        None
1298
1299        """
1300
1301        if type(new_output_dimensions) is list:
1302            new_output_dimensions = torch.tensor(new_output_dimensions)
1303        delattr(self, "this_output_dimensions")
1304        self.register_buffer(
1305            "this_output_dimensions", new_output_dimensions.detach().clone()
1306        )
1307        if (new_output_dimensions == 0).sum() != 1:
1308            print(f"2 Need exactly one 0 in the input dimensions: {self.name}")
1309            print(new_output_dimensions)
1310            sys.exit(-1)
1311        self.this_node_index.copy_(
1312            (new_output_dimensions == 0).nonzero(as_tuple=True)[0][0]
1313        )
1314        for j in range(0, GPA.pc.get_global_candidates()):
1315            self.dendrite_values[j].set_this_output_dimensions(new_output_dimensions)

Set input dimensions for dendrite module.

Signals to this DendriteModule that its input dimensions are different than the global default.

Parameters
  • new_output_dimensions (list): A list or tensor specifying the new input dimensions.
Returns
  • None
def create_new_dendrite_module(self, neuron_main_module):
1317    def create_new_dendrite_module(self, neuron_main_module):
1318        """Add a new set of dendrites.
1319
1320        Parameters
1321        ----------
1322        neuron_main_module : Any
1323            PyTorch module to be used for dendritic learning.
1324                Typically a copy of the original neuron module.
1325
1326        Returns
1327        -------
1328        None
1329            This function does not return a value.
1330        """
1331        # Candidate module
1332        self.candidate_module = nn.ModuleList([])
1333        # Copy that is unused for open source version
1334        self.best_candidate_module = nn.ModuleList([])
1335        if GPA.pc.get_verbose():
1336            print(self.name)
1337            print("Setting candidate processors")
1338        self.candidate_processors = []
1339        with torch.no_grad():
1340            for i in range(0, GPA.pc.get_global_candidates()):
1341
1342                new_module = self.create_dendrite(self.parent_module)
1343                init_params(new_module, neuron_main_module)
1344                self.candidate_module.append(new_module)
1345                self.best_candidate_module.append(self.create_dendrite(new_module))
1346                if type(self.parent_module) in GPA.pc.get_modules_with_processing():
1347                    module_index = GPA.pc.get_modules_with_processing().index(
1348                        type(self.parent_module)
1349                    )
1350                    self.candidate_processors.append(
1351                        GPA.pc.get_modules_processing_classes()[module_index]()
1352                    )
1353                elif (
1354                    type(self.parent_module).__name__
1355                    in GPA.pc.get_module_names_with_processing()
1356                ):
1357                    module_index = GPA.pc.get_module_names_with_processing().index(
1358                        type(self.parent_module).__name__
1359                    )
1360                    self.candidate_processors.append(
1361                        GPA.pc.get_module_by_name_processing_classes()[module_index]()
1362                    )
1363                if GPA.pc.get_perforated_backpropagation():
1364                    MPB.set_candidate_parameters(self.candidate_module[i])
1365                    MPB.set_ignored_parameters(self.best_candidate_module[i])
1366
1367        for i in range(0, GPA.pc.get_global_candidates()):
1368            self.candidate_module[i].to(GPA.pc.get_device())
1369            self.best_candidate_module[i].to(GPA.pc.get_device())
1370
1371        # Reset the dendrite_values objects
1372        for j in range(0, GPA.pc.get_global_candidates()):
1373            self.dendrite_values[j].reinitialize_for_pai()
1374
1375        # If there are already dendrites initialize the dendrite to dendrite connections
1376        if self.num_dendrites > 0:
1377            self.dendrites_to_candidates = nn.ParameterList()
1378            for j in range(0, GPA.pc.get_global_candidates()):
1379                self.dendrites_to_candidates.append(
1380                    nn.Parameter(
1381                        torch.zeros(
1382                            (self.num_dendrites, self.out_channels),
1383                            device=GPA.pc.get_device(),
1384                            dtype=GPA.pc.get_d_type(),
1385                        ),
1386                        requires_grad=True,
1387                    )
1388                )
1389                if GPA.pc.get_perforated_backpropagation():
1390                    MPB.init_candidates(self, j)
1391            if GPA.pc.get_perforated_backpropagation():
1392                MPB.set_candidate_parameters(self.dendrites_to_candidates)
1393            # Initialize best_dendrites_to_candidates_saved to snapshot peak-correlation weights at epoch boundaries
1394            self.best_dendrites_to_candidates_saved = []
1395            for j in range(0, GPA.pc.get_global_candidates()):
1396                self.best_dendrites_to_candidates_saved.append(
1397                    torch.zeros(
1398                        (self.num_dendrites, self.out_channels),
1399                        device=GPA.pc.get_device(),
1400                        dtype=GPA.pc.get_d_type(),
1401                    )
1402                )

Add a new set of dendrites.

Parameters
  • neuron_main_module (Any): PyTorch module to be used for dendritic learning. Typically a copy of the original neuron module.
Returns
  • None: This function does not return a value.
def clear_processors(self):
1404    def clear_processors(self):
1405        """Clear processors.
1406
1407        Parameters
1408        ----------
1409        None
1410
1411        Returns
1412        -------
1413        None
1414            This function does not return a value.
1415        """
1416        for processor in self.processors:
1417            if not processor:
1418                continue
1419            else:
1420                processor.clear_processor()
1421        for processor in self.candidate_processors:
1422            if not processor:
1423                continue
1424            else:
1425                processor.clear_processor()

Clear processors.

Parameters
  • None
Returns
  • None: This function does not return a value.
def set_mode(self, mode):
1427    def set_mode(self, mode):
1428        """Perform actions when switching between neuron and dendrite training.
1429
1430        Parameters
1431        ----------
1432        mode : str
1433            The mode to set. Either "n" for neuron training or "p" for pai-dendrite training.
1434
1435        Returns
1436        -------
1437        None
1438        """
1439
1440        self.mode = mode
1441        self.num_cycles += 1
1442        if GPA.pc.get_verbose():
1443            print(f"PAI calling set mode {mode} : {self.num_cycles}")
1444        if not GPA.pc.get_silent():
1445            print(f"Module {self.name} calling set mode {mode} : {self.num_cycles}")
1446        # When switching back to neuron training mode convert candidates modules into accepted modules
1447        if mode == "n":
1448            if GPA.pc.get_verbose():
1449                print("So calling all the things to add to modules")
1450            # Copy weights/bias from correct candidates
1451            if self.num_dendrites == 1:
1452                self.dendrites_to_dendrites = nn.ParameterList()
1453                self.dendrites_to_dendrites.append(torch.tensor([]))
1454            if self.num_dendrites >= 1:
1455                self.dendrites_to_dendrites.append(
1456                    torch.nn.Parameter(
1457                        torch.zeros(
1458                            [self.num_dendrites, self.out_channels],
1459                            device=GPA.pc.get_device(),
1460                            dtype=GPA.pc.get_d_type(),
1461                        ),
1462                        # Grad is true if not pb or if pb and dendrite_update_mode is true
1463                        requires_grad=(not GPA.pc.get_perforated_backpropagation())
1464                        or GPA.pc.get_dendrite_update_mode(),
1465                    )
1466                )
1467            with torch.no_grad():
1468                if GPA.pc.get_global_candidates() > 1:
1469                    print(
1470                        "This was a flag that will be needed if using multiple candidates. "
1471                        "It's not set up yet but nice work finding it."
1472                    )
1473                    print(
1474                        "Note: with multiple candidates, best-score ranking in new_best() uses "
1475                        "unnormalized covariance (prev_dendrite_candidate_correlation) rather than "
1476                        "the normalized correlation coefficient. Candidates with larger output "
1477                        "magnitude will be favored regardless of true correlation quality. "
1478                        "Fix by tracking running sigma_V and sigma_E and dividing in new_best()."
1479                    )
1480                    pdb.set_trace()
1481                plane_max_index = 0
1482                self.layers.append(
1483                    UPA.deep_copy_pai(self.best_candidate_module[plane_max_index])
1484                )
1485                self.layers[self.num_dendrites].to(GPA.pc.get_device())
1486                if self.num_dendrites > 0:
1487                    self.dendrites_to_dendrites[self.num_dendrites].copy_(
1488                        self.best_dendrites_to_candidates_saved[plane_max_index]
1489                    )
1490                if type(self.parent_module) in GPA.pc.get_modules_with_processing():
1491                    self.processors.append(self.candidate_processors[plane_max_index])
1492                if (
1493                    type(self.parent_module).__name__
1494                    in GPA.pc.get_module_names_with_processing()
1495                ):
1496                    self.processors.append(self.candidate_processors[plane_max_index])
1497            if GPA.pc.get_perforated_backpropagation():
1498                MPB.set_pb_mode(self, mode)
1499            del self.candidate_module, self.best_candidate_module
1500
1501            self.num_dendrites += 1
1502            if GPA.pc.get_perforated_backpropagation():
1503                MPB.set_dendrite_parameters(self.dendrites_to_dendrites)
1504                MPB.set_dendrite_parameters(self.layers)

Perform actions when switching between neuron and dendrite training.

Parameters
  • mode (str): The mode to set. Either "n" for neuron training or "p" for pai-dendrite training.
Returns
  • None
def forward(self, *args, **kwargs):
1506    def forward(self, *args, **kwargs):
1507        """Forward pass for dendrite module.
1508
1509        Parameters
1510        ----------
1511        *args : tuple
1512            Positional arguments for the forward pass.
1513        **kwargs : dict
1514            Keyword arguments for the forward pass.
1515
1516        Returns
1517        -------
1518        Any
1519            The output of the module after processing through the neuron and dendrite modules.
1520        Any
1521            Remaining outputs are only used for Perforated Backpropagation.
1522        Any
1523            Remaining outputs are only used for Perforated Backpropagation.
1524        Any
1525            Remaining outputs are only used for Perforated Backpropagation.
1526
1527        Notes
1528        -----
1529        If using Perforated Backpropagation, the additional outputs will be moved around in
1530        this code but left unused and only passed into separate PB functions.
1531        """
1532
1533        outs = {}
1534
1535        # For all modules apply processors, call the modules, then apply post processors
1536        args2, kwargs2 = args, kwargs
1537        for c in range(0, self.num_dendrites):
1538            if GPA.pc.get_perforated_backpropagation():
1539                args2, kwargs2 = MPB.preprocess_pb(*args, **kwargs)
1540            if self.processors != []:
1541                try:
1542                    args2, kwargs2 = self.processors[c].pre_d(*args2, **kwargs2)
1543                except Exception as e:
1544                    traceback.print_exc(limit=None, chain=True)
1545                    print(f"Your pre_d processor for {self.name} caused this error")
1546                    print(
1547                        f"You must check how this is defined and ensure that it is properly"
1548                    )
1549                    print(
1550                        f"accepting inputs to the PAIModule and returning what will then be"
1551                    )
1552                    print(f"the input to the dendrite module")
1553                    sys.exit()
1554            out_values = self.layers[c](*args2, **kwargs2)
1555            if self.processors != []:
1556                try:
1557                    outs[c] = self.processors[c].post_d(out_values)
1558                except Exception as e:
1559                    traceback.print_exc(limit=None, chain=True)
1560                    print(f"Your post_d processor for {self.name} caused this error")
1561                    print(
1562                        f"You must check how this is defined and ensure that it is properly"
1563                    )
1564                    print(
1565                        f"accepting outputs from the dendrite module and returning the"
1566                    )
1567                    print(
1568                        f"single tensor to be combined with the neurons output tensor"
1569                    )
1570                    sys.exit()
1571            else:
1572                outs[c] = out_values
1573
1574        # Create dendrite outputs
1575        # Each dendrite has input from previously created dendrites
1576        # So activation is added before the nonlinearity is called
1577        view_tuple = []
1578        for out_index in range(0, self.num_dendrites):
1579            current_out = outs[out_index]
1580            view_tuple = []
1581            for dim in range(len(current_out.shape)):
1582                if dim == self.this_node_index:
1583                    view_tuple.append(-1)
1584                    continue
1585                view_tuple.append(1)
1586
1587            for in_index in range(0, out_index):
1588                if view_tuple == [
1589                    1
1590                ]:  # This is only the case when passing a single datapoint rather than a batch
1591                    current_out = (
1592                        current_out
1593                        + self.dendrites_to_dendrites[out_index][in_index, :].to(
1594                            current_out.device
1595                        )
1596                        * outs[in_index]
1597                    )
1598                else:
1599                    current_out = (
1600                        current_out
1601                        + self.dendrites_to_dendrites[out_index][in_index, :]
1602                        .view(view_tuple)
1603                        .to(current_out.device)
1604                        * outs[in_index]
1605                    )
1606            outs[out_index] = GPA.pc.get_pai_forward_function()(current_out)
1607        # Return a dict which has all dendritic outputs after the activation functions were called
1608        if GPA.pc.get_perforated_backpropagation():
1609            candidate_outs, candidate_nonlinear_outs, candidate_non_zeroed = (
1610                MPB.forward_candidates(self, view_tuple, outs, *args2, **kwargs2)
1611            )
1612        else:
1613            candidate_outs, candidate_nonlinear_outs, candidate_non_zeroed = (
1614                {},
1615                {},
1616                {},
1617            )
1618        return outs, candidate_outs, candidate_nonlinear_outs, candidate_non_zeroed

Forward pass for dendrite module.

Parameters
  • *args (tuple): Positional arguments for the forward pass.
  • **kwargs (dict): Keyword arguments for the forward pass.
Returns
  • Any: The output of the module after processing through the neuron and dendrite modules.
  • Any: Remaining outputs are only used for Perforated Backpropagation.
  • Any: Remaining outputs are only used for Perforated Backpropagation.
  • Any: Remaining outputs are only used for Perforated Backpropagation.
Notes

If using Perforated Backpropagation, the additional outputs will be moved around in this code but left unused and only passed into separate PB functions.

class DendriteValueTracker(torch.nn.modules.module.Module):
1621class DendriteValueTracker(nn.Module):
1622    """Tracker object that maintains certain values for each set of dendrites."""
1623
1624    def __init__(
1625        self,
1626        initialized,
1627        activation_function_value,
1628        name,
1629        output_dimensions,
1630        out_channels=-1,
1631    ):
1632        """Initialize DendriteValueTracker.
1633
1634        This function sets up the value tracker to maintain statistics and values
1635        for each set of dendrites.
1636
1637        Parameters
1638        ----------
1639        initialized : int
1640            Whether the dendrite has been initialized (1) or not (0).
1641        activation_function_value : float
1642            A value associated with the activation function.
1643        name : str
1644            The name of the associated neuron module.
1645        output_dimensions : vector
1646            The dimensions of the input vector.
1647        out_channels : int
1648            The number of output channels
1649        """
1650        super(DendriteValueTracker, self).__init__()
1651
1652        self.layer_name = name
1653        for val_name in DENDRITE_INIT_VALUES:
1654            self.register_buffer(
1655                val_name,
1656                torch.zeros(1, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()),
1657            )
1658        self.initialized[0] = initialized
1659        self.activation_function_value = activation_function_value
1660        self.register_buffer(
1661            "this_output_dimensions", output_dimensions.clone().detach()
1662        )
1663        if (self.this_output_dimensions == 0).sum() != 1:
1664            print(f"3 need exactly one 0 in the input dimensions: {self.layer_name}")
1665            print(self.this_output_dimensions)
1666            sys.exit(-1)
1667        self.register_buffer(
1668            "this_node_index", (output_dimensions == 0).nonzero(as_tuple=True)[0]
1669        )
1670        if out_channels != -1:
1671            ndim = len(output_dimensions)
1672            init_shape = [1] * ndim
1673            init_shape[(output_dimensions == 0).nonzero(as_tuple=True)[0].item()] = out_channels
1674            self.setup_arrays(init_shape)
1675        else:
1676            self.out_channels = -1
1677
1678    def print(self):
1679        """Print value tracker information.
1680
1681        Parameters
1682        ----------
1683        None
1684
1685        Returns
1686        -------
1687        None
1688            This function does not return a value.
1689        """
1690        total_string = "Value Tracker:"
1691        for val_name in DENDRITE_INIT_VALUES:
1692            total_string += f"\t{val_name}:\n\t\t"
1693            total_string += getattr(self, val_name).__repr__()
1694            total_string += "\n"
1695        for val_name in get_DENDRITE_TENSOR_VALUES():
1696            if getattr(self, val_name, None) is not None:
1697                total_string += f"\t{val_name}:\n\t\t"
1698                total_string += getattr(self, val_name).__repr__()
1699                total_string += "\n"
1700        print(total_string)
1701
1702    def set_this_output_dimensions(self, new_output_dimensions):
1703        """Set input dimensions for value tracker
1704
1705        Signals to this DendriteValueTracker that its input dimensions are different
1706        than the global default.
1707
1708        Parameters
1709        ----------
1710        new_output_dimensions : list
1711            A list or tensor specifying the new input dimensions.
1712        Returns
1713        -------
1714        None
1715
1716        """
1717        if type(new_output_dimensions) is list:
1718            new_output_dimensions = torch.tensor(new_output_dimensions)
1719        delattr(self, "this_output_dimensions")
1720        self.register_buffer(
1721            "this_output_dimensions", new_output_dimensions.detach().clone()
1722        )
1723        if (new_output_dimensions == 0).sum() != 1:
1724            print(f"4 need exactly one 0 in the input dimensions: {self.layer_name}")
1725            print(new_output_dimensions)
1726            sys.exit(-1)
1727        self.this_node_index.copy_(
1728            (new_output_dimensions == 0).nonzero(as_tuple=True)[0][0]
1729        )
1730
1731    def set_out_channels(self, shape_values):
1732        """Set output channels based on shape values and saved node index
1733
1734        Parameters
1735        ----------
1736        shape_values : list or torch.Size
1737            A list or tensor specifying the shape values.
1738
1739        Returns
1740        -------
1741        None
1742        """
1743        if type(shape_values) == torch.Size:
1744            self.out_channels = int(shape_values[self.this_node_index])
1745        else:
1746            self.out_channels = int(shape_values[self.this_node_index].item())
1747
1748    def setup_arrays(self, storage_shape):
1749        """Setup arrays for value tracker.
1750
1751        Parameters
1752        ----------
1753        storage_shape : list
1754            Shape for the tracking tensors: 1 at every dim except the channel
1755            dim (this_node_index), which holds out_channels.  E.g. [1, 5] for
1756            a linear layer with 5 outputs.
1757        Returns
1758        -------
1759        None
1760
1761        """
1762        # storage_shape is a list with 1 at every dim except the channel dim.
1763        # Passed in directly from filter_backward so it is always derived from
1764        # the live gradient — not from out_channels, which is not saved/loaded.
1765        self.out_channels = storage_shape[self.this_node_index.item()]
1766        self.register_buffer(
1767            "dendrite_storage_shape",
1768            torch.tensor(storage_shape, dtype=torch.long, device=GPA.pc.get_device()),
1769        )
1770        for val_name in get_DENDRITE_TENSOR_VALUES():
1771            self.register_buffer(
1772                val_name,
1773                torch.zeros(
1774                    storage_shape, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()
1775                ),
1776            )
1777
1778        for name in get_VALUE_TRACKER_ARRAYS():
1779            setattr(self, name, {})
1780            count = 1
1781            if torch.cuda.device_count() > count:
1782                count = torch.cuda.device_count()
1783            for i in range(count):
1784                getattr(self, name)[i] = []
1785        for val_name in get_DENDRITE_SINGLE_VALUES():
1786            self.register_buffer(
1787                val_name,
1788                torch.zeros(1, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()),
1789            )
1790
1791        # If fixed_input_sizes is enabled, register cache buffers now that the
1792        # final ndim is known (output_dimensions may have been corrected after
1793        # __init__ for Linear layers). Mirrors the pattern of this_output_dimensions
1794        # — registered as buffers so they are saved and loaded automatically.
1795        if GPA.pc.get_perforated_backpropagation() and GPA.pc.get_fixed_input_sizes():
1796            ndim = len(storage_shape)
1797            if not hasattr(self, 'math_tuple_cache'):
1798                self.register_buffer(
1799                    "math_tuple_cache",
1800                    torch.zeros(ndim, dtype=torch.long, device=GPA.pc.get_device()),
1801                )
1802                self.register_buffer(
1803                    "view_tuple_cache",
1804                    torch.zeros(ndim, dtype=torch.long, device=GPA.pc.get_device()),
1805                )
1806                self.register_buffer(
1807                    "full_mult_cache",
1808                    torch.zeros(1, dtype=torch.long, device=GPA.pc.get_device()),
1809            )
1810
1811    def reinitialize_for_pai(self):
1812        """Reinitialize value tracker to add the next set of dendrites
1813
1814        Parameters
1815        ----------
1816        None
1817
1818        Returns
1819        -------
1820        None
1821            This function does not return a value.
1822        """
1823
1824        if self.out_channels == -1:
1825            print("You have a perforated module that was never initialized")
1826            print("This likely means it is not being added to the autograd graph")
1827            print("Check your forward function that it is actually being used")
1828            print("If its not you should really delete it, but you can also add")
1829            print(self.layer_name)
1830            print("with:")
1831            print("GPA.pc.append_module_ids_to_track(['" + self.layer_name + "'])")
1832            print("This can also happen while testing_dendrite_capacity if you")
1833            print(
1834                "run a validation cycle and try to add Dendrites before doing any training.\n"
1835            )
1836            pdb.set_trace()
1837
1838        self.initialized[0] = 0
1839        if GPA.pc.get_perforated_backpropagation():
1840            MPB.reinitialize_for_pb(self)
1841        else:
1842            for val_name in get_DENDRITE_REINIT_VALUES():
1843                setattr(self, val_name, getattr(self, val_name) * 0)

Tracker object that maintains certain values for each set of dendrites.

DendriteValueTracker( initialized, activation_function_value, name, output_dimensions, out_channels=-1)
1624    def __init__(
1625        self,
1626        initialized,
1627        activation_function_value,
1628        name,
1629        output_dimensions,
1630        out_channels=-1,
1631    ):
1632        """Initialize DendriteValueTracker.
1633
1634        This function sets up the value tracker to maintain statistics and values
1635        for each set of dendrites.
1636
1637        Parameters
1638        ----------
1639        initialized : int
1640            Whether the dendrite has been initialized (1) or not (0).
1641        activation_function_value : float
1642            A value associated with the activation function.
1643        name : str
1644            The name of the associated neuron module.
1645        output_dimensions : vector
1646            The dimensions of the input vector.
1647        out_channels : int
1648            The number of output channels
1649        """
1650        super(DendriteValueTracker, self).__init__()
1651
1652        self.layer_name = name
1653        for val_name in DENDRITE_INIT_VALUES:
1654            self.register_buffer(
1655                val_name,
1656                torch.zeros(1, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()),
1657            )
1658        self.initialized[0] = initialized
1659        self.activation_function_value = activation_function_value
1660        self.register_buffer(
1661            "this_output_dimensions", output_dimensions.clone().detach()
1662        )
1663        if (self.this_output_dimensions == 0).sum() != 1:
1664            print(f"3 need exactly one 0 in the input dimensions: {self.layer_name}")
1665            print(self.this_output_dimensions)
1666            sys.exit(-1)
1667        self.register_buffer(
1668            "this_node_index", (output_dimensions == 0).nonzero(as_tuple=True)[0]
1669        )
1670        if out_channels != -1:
1671            ndim = len(output_dimensions)
1672            init_shape = [1] * ndim
1673            init_shape[(output_dimensions == 0).nonzero(as_tuple=True)[0].item()] = out_channels
1674            self.setup_arrays(init_shape)
1675        else:
1676            self.out_channels = -1

Initialize DendriteValueTracker.

This function sets up the value tracker to maintain statistics and values for each set of dendrites.

Parameters
  • initialized (int): Whether the dendrite has been initialized (1) or not (0).
  • activation_function_value (float): A value associated with the activation function.
  • name (str): The name of the associated neuron module.
  • output_dimensions (vector): The dimensions of the input vector.
  • out_channels (int): The number of output channels
layer_name
activation_function_value
def print(self):
1678    def print(self):
1679        """Print value tracker information.
1680
1681        Parameters
1682        ----------
1683        None
1684
1685        Returns
1686        -------
1687        None
1688            This function does not return a value.
1689        """
1690        total_string = "Value Tracker:"
1691        for val_name in DENDRITE_INIT_VALUES:
1692            total_string += f"\t{val_name}:\n\t\t"
1693            total_string += getattr(self, val_name).__repr__()
1694            total_string += "\n"
1695        for val_name in get_DENDRITE_TENSOR_VALUES():
1696            if getattr(self, val_name, None) is not None:
1697                total_string += f"\t{val_name}:\n\t\t"
1698                total_string += getattr(self, val_name).__repr__()
1699                total_string += "\n"
1700        print(total_string)

Print value tracker information.

Parameters
  • None
Returns
  • None: This function does not return a value.
def set_this_output_dimensions(self, new_output_dimensions):
1702    def set_this_output_dimensions(self, new_output_dimensions):
1703        """Set input dimensions for value tracker
1704
1705        Signals to this DendriteValueTracker that its input dimensions are different
1706        than the global default.
1707
1708        Parameters
1709        ----------
1710        new_output_dimensions : list
1711            A list or tensor specifying the new input dimensions.
1712        Returns
1713        -------
1714        None
1715
1716        """
1717        if type(new_output_dimensions) is list:
1718            new_output_dimensions = torch.tensor(new_output_dimensions)
1719        delattr(self, "this_output_dimensions")
1720        self.register_buffer(
1721            "this_output_dimensions", new_output_dimensions.detach().clone()
1722        )
1723        if (new_output_dimensions == 0).sum() != 1:
1724            print(f"4 need exactly one 0 in the input dimensions: {self.layer_name}")
1725            print(new_output_dimensions)
1726            sys.exit(-1)
1727        self.this_node_index.copy_(
1728            (new_output_dimensions == 0).nonzero(as_tuple=True)[0][0]
1729        )

Set input dimensions for value tracker

Signals to this DendriteValueTracker that its input dimensions are different than the global default.

Parameters
  • new_output_dimensions (list): A list or tensor specifying the new input dimensions.
Returns
  • None
def set_out_channels(self, shape_values):
1731    def set_out_channels(self, shape_values):
1732        """Set output channels based on shape values and saved node index
1733
1734        Parameters
1735        ----------
1736        shape_values : list or torch.Size
1737            A list or tensor specifying the shape values.
1738
1739        Returns
1740        -------
1741        None
1742        """
1743        if type(shape_values) == torch.Size:
1744            self.out_channels = int(shape_values[self.this_node_index])
1745        else:
1746            self.out_channels = int(shape_values[self.this_node_index].item())

Set output channels based on shape values and saved node index

Parameters
  • shape_values (list or torch.Size): A list or tensor specifying the shape values.
Returns
  • None
def setup_arrays(self, storage_shape):
1748    def setup_arrays(self, storage_shape):
1749        """Setup arrays for value tracker.
1750
1751        Parameters
1752        ----------
1753        storage_shape : list
1754            Shape for the tracking tensors: 1 at every dim except the channel
1755            dim (this_node_index), which holds out_channels.  E.g. [1, 5] for
1756            a linear layer with 5 outputs.
1757        Returns
1758        -------
1759        None
1760
1761        """
1762        # storage_shape is a list with 1 at every dim except the channel dim.
1763        # Passed in directly from filter_backward so it is always derived from
1764        # the live gradient — not from out_channels, which is not saved/loaded.
1765        self.out_channels = storage_shape[self.this_node_index.item()]
1766        self.register_buffer(
1767            "dendrite_storage_shape",
1768            torch.tensor(storage_shape, dtype=torch.long, device=GPA.pc.get_device()),
1769        )
1770        for val_name in get_DENDRITE_TENSOR_VALUES():
1771            self.register_buffer(
1772                val_name,
1773                torch.zeros(
1774                    storage_shape, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()
1775                ),
1776            )
1777
1778        for name in get_VALUE_TRACKER_ARRAYS():
1779            setattr(self, name, {})
1780            count = 1
1781            if torch.cuda.device_count() > count:
1782                count = torch.cuda.device_count()
1783            for i in range(count):
1784                getattr(self, name)[i] = []
1785        for val_name in get_DENDRITE_SINGLE_VALUES():
1786            self.register_buffer(
1787                val_name,
1788                torch.zeros(1, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()),
1789            )
1790
1791        # If fixed_input_sizes is enabled, register cache buffers now that the
1792        # final ndim is known (output_dimensions may have been corrected after
1793        # __init__ for Linear layers). Mirrors the pattern of this_output_dimensions
1794        # — registered as buffers so they are saved and loaded automatically.
1795        if GPA.pc.get_perforated_backpropagation() and GPA.pc.get_fixed_input_sizes():
1796            ndim = len(storage_shape)
1797            if not hasattr(self, 'math_tuple_cache'):
1798                self.register_buffer(
1799                    "math_tuple_cache",
1800                    torch.zeros(ndim, dtype=torch.long, device=GPA.pc.get_device()),
1801                )
1802                self.register_buffer(
1803                    "view_tuple_cache",
1804                    torch.zeros(ndim, dtype=torch.long, device=GPA.pc.get_device()),
1805                )
1806                self.register_buffer(
1807                    "full_mult_cache",
1808                    torch.zeros(1, dtype=torch.long, device=GPA.pc.get_device()),
1809            )

Setup arrays for value tracker.

Parameters
  • storage_shape (list): Shape for the tracking tensors: 1 at every dim except the channel dim (this_node_index), which holds out_channels. E.g. [1, 5] for a linear layer with 5 outputs.
Returns
  • None
def reinitialize_for_pai(self):
1811    def reinitialize_for_pai(self):
1812        """Reinitialize value tracker to add the next set of dendrites
1813
1814        Parameters
1815        ----------
1816        None
1817
1818        Returns
1819        -------
1820        None
1821            This function does not return a value.
1822        """
1823
1824        if self.out_channels == -1:
1825            print("You have a perforated module that was never initialized")
1826            print("This likely means it is not being added to the autograd graph")
1827            print("Check your forward function that it is actually being used")
1828            print("If its not you should really delete it, but you can also add")
1829            print(self.layer_name)
1830            print("with:")
1831            print("GPA.pc.append_module_ids_to_track(['" + self.layer_name + "'])")
1832            print("This can also happen while testing_dendrite_capacity if you")
1833            print(
1834                "run a validation cycle and try to add Dendrites before doing any training.\n"
1835            )
1836            pdb.set_trace()
1837
1838        self.initialized[0] = 0
1839        if GPA.pc.get_perforated_backpropagation():
1840            MPB.reinitialize_for_pb(self)
1841        else:
1842            for val_name in get_DENDRITE_REINIT_VALUES():
1843                setattr(self, val_name, getattr(self, val_name) * 0)

Reinitialize value tracker to add the next set of dendrites

Parameters
  • None
Returns
  • None: This function does not return a value.