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

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

PAINeuronModule(start_module, name)
309    def __init__(self, start_module, name):
310        """Initialize PAINeuronModule.
311
312        This function sets up the neuron module to wrap the start_module
313        and manage its dendritic connections.
314
315        Parameters
316        ----------
317        start_module : nn.Module
318            The module to wrap.
319        name : str
320            The name of the neuron module.
321        """
322        super(PAINeuronModule, self).__init__()
323
324        if isinstance(start_module, nn.Module):
325            self.main_module = start_module
326        else:
327            print("start_module must be nn.Module: %s" % name)
328            print(type(start_module))
329            print(start_module)
330            sys.exit(-1)
331        self.name = name
332        # Per-module config: loads custom settings from {save_name}_config.json if present.
333        # Passes both the instance name (id) and the module type so load_config can
334        # fall back to type-level settings when no name-specific entry exists.
335        _module_type_name = type(start_module).__name__
336        self.module_config = GPA.PAIConfig(
337            module_name=self.name, module_type=_module_type_name
338        )
339
340        set_wrapped_params(self.main_module)
341        if self.module_config.get_verbose():
342            print(
343                f"initing a module {self.name} with main type {type(self.main_module)}"
344            )
345            print(start_module)
346
347        # If this main_module is one that requires processing set the processor
348        if type(self.main_module) in self.module_config.get_modules_with_processing():
349            module_index = self.module_config.get_modules_with_processing().index(
350                type(self.main_module)
351            )
352            self.processor = self.module_config.get_modules_processing_classes()[
353                module_index
354            ]()
355            if self.module_config.get_verbose():
356                print("with processor")
357                print(self.processor)
358        elif (
359            type(self.main_module).__name__
360            in self.module_config.get_module_names_with_processing()
361        ):
362            module_index = self.module_config.get_module_names_with_processing().index(
363                type(self.main_module).__name__
364            )
365            self.processor = self.module_config.get_module_by_name_processing_classes()[
366                module_index
367            ]()
368            if self.module_config.get_verbose():
369                print("with processor")
370                print(self.processor)
371        else:
372            self.processor = None
373
374        # Field that can be filled in if your activation function requires a parameter
375        self.activation_function_value = -1
376        self.type = "neuron_module"
377
378        self.register_buffer(
379            "this_output_dimensions",
380            (torch.tensor(self.module_config.get_output_dimensions())),
381        )
382        if (self.this_output_dimensions == 0).sum() != 1:
383            print(f"5 Need exactly one 0 in the input dimensions: {self.name}")
384            print(self.this_output_dimensions)
385            sys.exit(-1)
386        self.register_buffer(
387            "this_node_index",
388            torch.tensor(self.module_config.get_output_dimensions().index(0)),
389        )
390        self.dendrite_modules_added = 0
391
392        # Values for dendrite to neuron weights
393        self.dendrites_to_top = nn.ParameterList()
394        self.register_parameter("newest_dendrite_to_top", None)
395        self.candidate_to_top = nn.ParameterList()
396        self.register_parameter("current_candidate_to_top", None)
397        # Create the dendrite module
398        self.dendrite_module = PAIDendriteModule(
399            self.main_module,
400            activation_function_value=self.activation_function_value,
401            name=self.name,
402            output_dimensions=self.this_output_dimensions,
403        )
404        # If it is linear and default has convolutional dimensions, automatically set to just be batch size and neuron indexes
405        if (
406            issubclass(type(start_module), nn.Linear)
407            or (
408                issubclass(type(start_module), GPA.PAISequential)
409                and issubclass(type(start_module.model[0]), nn.Linear)
410            )
411        ) and (
412            np.array(self.this_output_dimensions)[2:] == -1
413        ).all():  # Everything past 2 is a negative 1
414            self.set_this_output_dimensions(self.this_output_dimensions[0:2])
415        if (
416            issubclass(type(start_module), nn.Conv1d)
417            or (
418                issubclass(type(start_module), GPA.PAISequential)
419                and issubclass(type(start_module.model[0]), nn.Conv1d)
420            )
421        ) and (
422            np.array(self.this_output_dimensions)[3:] == -1
423        ).all():  # Everything past 2 is a negative 1
424            self.set_this_output_dimensions(self.this_output_dimensions[0:3])
425        # Apply per-module output_dimensions override from config if present
426        _custom_dims = self.module_config.__dict__.get("_output_dimensions")
427        if _custom_dims is not None:
428            self.set_this_output_dimensions(torch.tensor(_custom_dims))
429        GPA.pai_tracker.add_pai_neuron_module(self)
430        if self.module_config.get_perforated_backpropagation():
431            MPB.set_neuron_parameters(self.main_module)

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:
1167    def type(self, dst_type: dtype | str) -> Self:
1168        r"""Casts all parameters and buffers to :attr:`dst_type`.
1169
1170        .. note::
1171            This method modifies the module in-place.
1172
1173        Args:
1174            dst_type (type or string): the desired type
1175
1176        Returns:
1177            Module: self
1178        """
1179        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):
469    def apply_pb_grads(self):
470        """Apply perforated backpropagation gradients if enabled.
471
472        Parameters
473        ----------
474        None
475
476        Returns
477        -------
478        None
479            This function does not return a value.
480        """
481        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):
483    def apply_pb_zero(self):
484        """Clear leftover saved tensors if there are any.
485
486        Parameters
487        ----------
488        None
489
490        Returns
491        -------
492        None
493            This function does not return a value.
494        """
495        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):
497    def clear_processors(self):
498        """Clear processors if they save values for DeepCopy and save.
499
500        Parameters
501        ----------
502        None
503
504        Returns
505        -------
506        None
507        """
508
509        if not self.processor:
510            return
511        else:
512            self.processor.clear_processor()
513            self.dendrite_module.clear_processors()

Clear processors if they save values for DeepCopy and save.

Parameters
  • None
Returns
  • None
def clear_dendrites(self):
515    def clear_dendrites(self):
516        """Clear and reset dendrites before loading from a state dict.
517
518        Parameters
519        ----------
520        None
521
522        Returns
523        -------
524        None
525
526        """
527        # Loading a saved state reconstructs PAIDendriteModule before simulating
528        # its saved cycles. Preserve a registered variant factory so candidate
529        # creation does not silently fall back to deep-copying the parent module.
530        create_dendrite_fn = self.dendrite_module._create_dendrite_fn
531        self.dendrite_modules_added = 0
532        self.dendrites_to_top = nn.ParameterList()
533        self.candidate_to_top = nn.ParameterList()
534        self.dendrite_module = PAIDendriteModule(
535            self.main_module,
536            activation_function_value=self.activation_function_value,
537            name=self.name,
538            output_dimensions=self.this_output_dimensions,
539        )
540        if create_dendrite_fn is not None:
541            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):
573    def set_this_output_dimensions(self, new_output_dimensions):
574        """Set the input dimensions for the neuron and dendrite blocks.
575
576        Signals to this NeuronModule that its input dimensions are different
577        than the global default.
578
579        Parameters
580        ----------
581        new_output_dimensions : list
582            A list or tensor specifying the new input dimensions.
583        Returns
584        -------
585        None
586
587        """
588        if type(new_output_dimensions) is list:
589            new_output_dimensions = torch.tensor(new_output_dimensions)
590        delattr(self, "this_output_dimensions")
591        self.register_buffer(
592            "this_output_dimensions", new_output_dimensions.detach().clone()
593        )
594        if (new_output_dimensions == 0).sum() != 1:
595            print(f"6 need exactly one 0 in the input dimensions: {self.name}")
596            print(new_output_dimensions)
597        self.this_node_index.copy_(
598            (new_output_dimensions == 0).nonzero(as_tuple=True)[0][0]
599        )
600        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):
602    def set_create_dendrite(self, fn):
603        """Set a custom function for creating dendrite modules.
604
605        Override how dendrite copies are created from the parent module.  By default
606        the parent module is deep-copied.  Pass any callable with the signature
607        ``fn(parent_module) -> nn.Module`` to replace that behaviour.
608
609        Parameters
610        ----------
611        fn : callable
612            A function with signature ``fn(parent_module) -> nn.Module``.
613
614        Returns
615        -------
616        None
617        """
618        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):
621    def set_mode(self, mode):
622        """Switch between neuron training and dendrite training.
623
624        Parameters
625        ----------
626        mode : str
627            The mode to set. Either "n" for neuron training or "p" for pai-dendrite training.
628
629        Returns
630        -------
631        bool
632            True if mode was set successfully, False otherwise.
633
634        Notes
635        -----
636        If False is returned, the mode was not changed due to an error.
637        This is a problem that should not be ignored, but it can be ignored
638        by calling PGA.pc.set_checked_skipped_modules(True)
639        """
640
641        if self.module_config.get_verbose():
642            print(f"{self.name} calling set mode {mode}")
643        # If returning to neuron training
644        if mode == "n":
645            self.dendrite_module.set_mode(mode)
646            # Initialize the dendrite to neuron connections
647            if self.dendrite_modules_added > 0:
648                if self.module_config.get_learn_dendrites_live():
649                    values = torch.cat(
650                        (
651                            self.dendrites_to_top[self.dendrite_modules_added - 1],
652                            nn.Parameter(
653                                self.candidate_to_top.detach()
654                                .clone()
655                                .to(dtype=self.module_config.get_d_type())
656                            ),
657                        ),
658                        0,
659                    )
660                else:
661                    values = torch.cat(
662                        (
663                            self.dendrites_to_top[self.dendrite_modules_added - 1],
664                            nn.Parameter(
665                                torch.zeros(
666                                    (1, self.out_channels),
667                                    device=self.dendrites_to_top[
668                                        self.dendrite_modules_added - 1
669                                    ].device,
670                                    dtype=self.module_config.get_d_type(),
671                                )
672                            ),
673                        ),
674                        0,
675                    )
676                self.dendrites_to_top.append(
677                    nn.Parameter(
678                        values.detach()
679                        .clone()
680                        .to(
681                            device=self.module_config.get_device(),
682                            dtype=self.module_config.get_d_type(),
683                        ),
684                        requires_grad=True,
685                    )
686                )
687            else:
688                if self.module_config.get_learn_dendrites_live():
689                    self.dendrites_to_top.append(
690                        nn.Parameter(
691                            self.candidate_to_top.detach()
692                            .clone()
693                            .to(dtype=self.module_config.get_d_type()),
694                            requires_grad=True,
695                        )
696                    )
697                else:
698                    self.dendrites_to_top.append(
699                        nn.Parameter(
700                            torch.zeros(
701                                (1, self.out_channels),
702                                device=self.module_config.get_device(),
703                                dtype=self.module_config.get_d_type(),
704                            )
705                            .detach()
706                            .clone(),
707                            requires_grad=True,
708                        )
709                    )
710            self.dendrite_modules_added += 1
711            if self.module_config.get_perforated_backpropagation():
712                MPB.set_module_n_pb(self)
713                MPB.set_neuron_parameters(self.dendrites_to_top)
714
715        # If starting dendrite training
716        else:
717            try:
718                # Save the values that were calculated in filter_backward
719                self.out_channels = self.dendrite_module.dendrite_values[0].out_channels
720                self.dendrite_module.out_channels = (
721                    self.dendrite_module.dendrite_values[0].out_channels
722                )
723            except Exception as e:
724                print(e)
725                print(
726                    f"this occurred in module: {self.dendrite_module.dendrite_values[0].layer_name}"
727                )
728                print(
729                    "Module should be added to module_names_to_track so it doesn't have dendrites added"
730                )
731                print("If you are getting here but out_channels has not been set")
732                print(
733                    "A common reason is that this module never had gradients flow through it."
734                )
735                print("I have seen this happen because:")
736                print("-The weights were frozen (requires_grad = False)")
737                print(
738                    "-A model is added but not used so it was converted to a perforated module initialized"
739                )
740                print(
741                    "-A module was converted that doesn't have weights that get modified so backward doesn't flow through it"
742                )
743                print(
744                    "If this is normal behavior set GPA.pc.set_checked_skipped_modules(True) in the main to ignore"
745                )
746                print(
747                    "You can also set right now in this pdb terminal to have this not happen more after checking all modules this cycle."
748                )
749                if not self.module_config.get_checked_skipped_modules():
750                    pdb.set_trace()
751                return False
752            # Only change mode if it makes it past the above exception
753            self.dendrite_module.set_mode(mode)
754            if self.module_config.get_perforated_backpropagation():
755                MPB.set_module_p_pb(self)
756        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):
758    def create_new_dendrite_module(self):
759        """Add an additional dendrite module.
760
761        Parameters
762        ----------
763        None
764
765        Returns
766        -------
767        None
768        """
769        self.dendrite_module.create_new_dendrite_module(self.main_module)

Add an additional dendrite module.

Parameters
  • None
Returns
  • None
def forward(self, *args, **kwargs):
771    def forward(self, *args, **kwargs):
772        """Forward pass through the neuron module.
773
774        Parameters
775        ----------
776        *args : tuple
777            Positional arguments for the forward pass.
778        **kwargs : dict
779            Keyword arguments for the forward pass.
780
781        Returns
782        -------
783        Any
784            The output of the module after processing through the neuron and dendrite modules.
785
786        Notes
787        -----
788            The output of this forward function will have the same format as the output
789            of the original module
790        """
791
792        # If debugging all input dimensions, quit program on first forward call
793        if self.module_config.get_debugging_output_dimensions() == 2:
794            print("all input dim problems now printed")
795            sys.exit(0)
796        if self.module_config.get_extra_verbose():
797            print(f"{self.name} calling forward")
798        # Call the main modules forward
799        out = self.main_module(*args, **kwargs)
800        # Filter with the processor if required
801        if self.processor is not None:
802            try:
803                out = self.processor.post_n1(out)
804            except Exception as e:
805                traceback.print_exc(limit=None, chain=True)
806                print(f"Your post_n1 processor for {self.name} caused this error")
807                print(
808                    f"You must check how this is defined and ensure that it is properly"
809                )
810                print(f"accepting outputs from the neuron module and returning the")
811                print(f"single tensor to be combined with the dendrites output tensor")
812                sys.exit()
813        # Call the forwards for all of the Dendrites
814        (
815            dendrite_outs,
816            candidate_outs,
817            candidate_nonlinear_outs,
818            candidate_outs_non_zeroed,
819        ) = self.dendrite_module(*args, **kwargs)
820        # If there are dendrites add all of their outputs to the neurons output
821        if self.dendrite_modules_added > 0:
822            for i in range(0, self.dendrite_modules_added):
823                to_top = self.dendrites_to_top[self.dendrite_modules_added - 1][i, :]
824                for dim in range(len(dendrite_outs[i].shape)):
825                    if dim == self.this_node_index:
826                        continue
827                    to_top = to_top.unsqueeze(dim)
828                if self.module_config.get_confirm_correct_sizes():
829                    to_top = to_top.expand(
830                        list(dendrite_outs[i].size())[0 : self.this_node_index]
831                        + [self.out_channels]
832                        + list(dendrite_outs[i].size())[self.this_node_index + 1 :]
833                    )
834                out = out + (dendrite_outs[i].to(out.device) * to_top.to(out.device))
835
836        # If learning live, add the candidate's output to the neuron's output via the live weight
837        if self.module_config.get_perforated_backpropagation():
838            out = MPB.apply_live_candidate_to_output(
839                self, out, candidate_nonlinear_outs
840            )
841
842        # Catch if processors are required
843        if type(out) is tuple:
844            print(self)
845            print(
846                f"The output of the above module {self.name} is a tuple when it must be a single tensor"
847            )
848            print(
849                "This must be fixed to enable the dendrite and neuron output to be combined"
850            )
851            print(
852                "Look in the API customization.md at section 2.2 regarding processors to fix this."
853            )
854            pdb.set_trace()
855
856        # Call filter backward to ensure the neuron index is setup correctly
857        if out.requires_grad:
858            out.register_hook(
859                lambda grad: filter_backward(grad, self.dendrite_module.dendrite_values)
860            )
861
862        # If there is a processor apply the second neuron stage
863        if self.processor is not None:
864            try:
865                out = self.processor.post_n2(out)
866            except Exception as e:
867                traceback.print_exc(limit=None, chain=True)
868                print(f"Your post_n2 processor for {self.name} caused this error")
869                print(
870                    f"You must check how this is defined and ensure that it is properly"
871                )
872                print(
873                    f"accepting the output tensor after combining the neuron's output "
874                )
875                print(f"with the dendrite's output and returning something that is the")
876                print(f"same format as your original module's return")
877                sys.exit()
878        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):
 881class TrackedNeuronModule(nn.Module):
 882    """Wrapper for modules you don't want to add dendrites to. Ensures all modules are accounted for."""
 883
 884    def __init__(self, start_module, name):
 885        """Initialize TrackedNeuronModule.
 886
 887        This function sets up the tracked neuron module to wrap the start_module
 888        without adding dendrites.
 889
 890        Parameters
 891        ----------
 892        start_module : nn.Module
 893            The module to wrap.
 894        name : str
 895            The name of the neuron module.
 896        """
 897        super(TrackedNeuronModule, self).__init__()
 898
 899        if isinstance(start_module, nn.Module):
 900            self.main_module = start_module
 901        else:
 902            print("start_module must be nn.Module: %s" % name)
 903            print(type(start_module))
 904            print(start_module)
 905            sys.exit(-1)
 906        self.name = name
 907
 908        self.type = "tracked_module"
 909        set_tracked_params(self.main_module)
 910        if GPA.pc.get_verbose():
 911            print(
 912                f"tracking a module {self.name} with main type {type(self.main_module)}"
 913            )
 914            print(start_module)
 915        GPA.pai_tracker.add_tracked_neuron_module(self)
 916        if GPA.pc.get_perforated_backpropagation():
 917            MPB.set_neuron_parameters(self.main_module)
 918
 919    def __getattr__(self, name):
 920        """Get member variables from the main module.
 921
 922        Parameters
 923        ----------
 924        name : str
 925            The name of the variable to retrieve.
 926        Returns
 927        -------
 928        The requested variable.
 929
 930        Notes
 931        -----
 932        This method first attempts to retrieve the attribute from the PAINeuronModule instance.
 933        If it fails, it tries to get the attribute from the wrapped main_module.
 934        This allows seamless access to the main module's attributes without modifying original code.
 935        """
 936        try:
 937            return super().__getattr__(name)
 938        except AttributeError:
 939            return getattr(self.main_module, name)
 940
 941    def __getitem__(self, index):
 942        """Support indexing operations on the main module.
 943
 944        Parameters
 945        ----------
 946        index : int or slice
 947            The index or slice to retrieve.
 948
 949        Returns
 950        -------
 951        The indexed item from the main module.
 952        """
 953        return self.main_module[index]
 954
 955    def set_mode(self, mode):
 956        """Set mode for tracked module.
 957
 958        Parameters
 959        ----------
 960        mode : str
 961            The mode to set. Either "n" for neuron training or "p" for pai-dendrite training.
 962
 963        Returns
 964        -------
 965        bool
 966            True.
 967
 968        Notes
 969        -----
 970        This function does not change any behavior since this is a tracked module.
 971        """
 972
 973        if GPA.pc.get_verbose():
 974            print(f"{self.name} calling set mode {mode}")
 975        return True
 976
 977    def forward(self, *args, **kwargs):
 978        """Forward pass for tracked module.
 979
 980        Parameters
 981        ----------
 982        *args : tuple
 983            Positional arguments for the forward pass.
 984        **kwargs : dict
 985            Keyword arguments for the forward pass.
 986
 987        Returns
 988        -------
 989        Any
 990            The output of the module
 991
 992        Notes
 993        -----
 994            The output of this forward function will have the same format as the output
 995            of the original module
 996        """
 997        return self.main_module(*args, **kwargs)
 998
 999    def __str__(self):
1000        """String representation of the module.
1001
1002        Parameters
1003        ----------
1004        None
1005
1006        Returns
1007        -------
1008        str
1009            String representation of the module.
1010
1011        Notes
1012        -----
1013        Setting for verbose changes level of details in the string output.
1014        """
1015
1016        if GPA.pc.get_verbose():
1017            total_string = self.main_module.__str__()
1018            total_string = "PAITrackedModule(" + total_string + ")"
1019            return total_string
1020        else:
1021            total_string = self.main_module.__str__()
1022            total_string = "PAITrackedModule(" + total_string + ")"
1023            return total_string
1024
1025    def __repr__(self):
1026        """Representation of the module."""
1027        return self.__str__()

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

TrackedNeuronModule(start_module, name)
884    def __init__(self, start_module, name):
885        """Initialize TrackedNeuronModule.
886
887        This function sets up the tracked neuron module to wrap the start_module
888        without adding dendrites.
889
890        Parameters
891        ----------
892        start_module : nn.Module
893            The module to wrap.
894        name : str
895            The name of the neuron module.
896        """
897        super(TrackedNeuronModule, self).__init__()
898
899        if isinstance(start_module, nn.Module):
900            self.main_module = start_module
901        else:
902            print("start_module must be nn.Module: %s" % name)
903            print(type(start_module))
904            print(start_module)
905            sys.exit(-1)
906        self.name = name
907
908        self.type = "tracked_module"
909        set_tracked_params(self.main_module)
910        if GPA.pc.get_verbose():
911            print(
912                f"tracking a module {self.name} with main type {type(self.main_module)}"
913            )
914            print(start_module)
915        GPA.pai_tracker.add_tracked_neuron_module(self)
916        if GPA.pc.get_perforated_backpropagation():
917            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:
1167    def type(self, dst_type: dtype | str) -> Self:
1168        r"""Casts all parameters and buffers to :attr:`dst_type`.
1169
1170        .. note::
1171            This method modifies the module in-place.
1172
1173        Args:
1174            dst_type (type or string): the desired type
1175
1176        Returns:
1177            Module: self
1178        """
1179        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):
955    def set_mode(self, mode):
956        """Set mode for tracked module.
957
958        Parameters
959        ----------
960        mode : str
961            The mode to set. Either "n" for neuron training or "p" for pai-dendrite training.
962
963        Returns
964        -------
965        bool
966            True.
967
968        Notes
969        -----
970        This function does not change any behavior since this is a tracked module.
971        """
972
973        if GPA.pc.get_verbose():
974            print(f"{self.name} calling set mode {mode}")
975        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):
977    def forward(self, *args, **kwargs):
978        """Forward pass for tracked module.
979
980        Parameters
981        ----------
982        *args : tuple
983            Positional arguments for the forward pass.
984        **kwargs : dict
985            Keyword arguments for the forward pass.
986
987        Returns
988        -------
989        Any
990            The output of the module
991
992        Notes
993        -----
994            The output of this forward function will have the same format as the output
995            of the original module
996        """
997        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):
1030def init_params(module, neuron_main_module):
1031    """Randomize weights after duplicating the main module for the next set of dendrites.
1032
1033    Parameters
1034    ----------
1035    module : nn.Module
1036        The new dendrite module to initialize.
1037    neuron_main_module : nn.Module
1038        The main module of the neuron for potential weight scaling.
1039
1040
1041    Returns
1042    -------
1043    None
1044        This function does not return a value.
1045    """
1046    for param in module.parameters():
1047        if param.dtype == torch.uint8:
1048            param.data = torch.randint(0, 256, param.size(), dtype=torch.uint8)
1049        else:
1050            # If factoring in the main modules weights multiply the randn()
1051            #  by the average abs value of the main modules weights
1052            if GPA.pc.get_candidate_weight_init_by_main():
1053                main_module_abs = 0
1054                total_main_params = 0
1055                for main_param in neuron_main_module.parameters():
1056                    main_module_abs += main_param.abs().sum().item()
1057                    total_main_params += main_param.numel()
1058                if total_main_params > 0:
1059                    main_module_abs /= total_main_params
1060                else:
1061                    main_module_abs = 1.0
1062                multiplier = main_module_abs
1063            else:
1064                multiplier = 1.0
1065            param.data = (
1066                torch.randn(param.size(), dtype=param.dtype)
1067                * GPA.pc.get_candidate_weight_initialization_multiplier()
1068                * multiplier
1069            )

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

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):
1350    def clear_processors(self):
1351        """Clear processors.
1352
1353        Parameters
1354        ----------
1355        None
1356
1357        Returns
1358        -------
1359        None
1360            This function does not return a value.
1361        """
1362        for processor in self.processors:
1363            if not processor:
1364                continue
1365            else:
1366                processor.clear_processor()
1367        for processor in self.candidate_processors:
1368            if not processor:
1369                continue
1370            else:
1371                processor.clear_processor()

Clear processors.

Parameters
  • None
Returns
  • None: This function does not return a value.
def set_mode(self, mode):
1373    def set_mode(self, mode):
1374        """Perform actions when switching between neuron and dendrite training.
1375
1376        Parameters
1377        ----------
1378        mode : str
1379            The mode to set. Either "n" for neuron training or "p" for pai-dendrite training.
1380
1381        Returns
1382        -------
1383        None
1384        """
1385
1386        self.mode = mode
1387        self.num_cycles += 1
1388        if GPA.pc.get_verbose():
1389            print(f"PAI calling set mode {mode} : {self.num_cycles}")
1390        if not GPA.pc.get_silent():
1391            print(f"Module {self.name} calling set mode {mode} : {self.num_cycles}")
1392        # When switching back to neuron training mode convert candidates modules into accepted modules
1393        if mode == "n":
1394            if GPA.pc.get_verbose():
1395                print("So calling all the things to add to modules")
1396            # Copy weights/bias from correct candidates
1397            if self.num_dendrites == 1:
1398                self.dendrites_to_dendrites = nn.ParameterList()
1399                self.dendrites_to_dendrites.append(torch.tensor([]))
1400            if self.num_dendrites >= 1:
1401                self.dendrites_to_dendrites.append(
1402                    torch.nn.Parameter(
1403                        torch.zeros(
1404                            [self.num_dendrites, self.out_channels],
1405                            device=GPA.pc.get_device(),
1406                            dtype=GPA.pc.get_d_type(),
1407                        ),
1408                        # Grad is true if not pb or if pb and dendrite_update_mode is true
1409                        requires_grad=(not GPA.pc.get_perforated_backpropagation())
1410                        or GPA.pc.get_dendrite_update_mode(),
1411                    )
1412                )
1413            with torch.no_grad():
1414                if GPA.pc.get_global_candidates() > 1:
1415                    print(
1416                        "This was a flag that will be needed if using multiple candidates. "
1417                        "It's not set up yet but nice work finding it."
1418                    )
1419                    print(
1420                        "Note: with multiple candidates, best-score ranking in new_best() uses "
1421                        "unnormalized covariance (prev_dendrite_candidate_correlation) rather than "
1422                        "the normalized correlation coefficient. Candidates with larger output "
1423                        "magnitude will be favored regardless of true correlation quality. "
1424                        "Fix by tracking running sigma_V and sigma_E and dividing in new_best()."
1425                    )
1426                    pdb.set_trace()
1427                plane_max_index = 0
1428                self.layers.append(
1429                    UPA.deep_copy_pai(self.best_candidate_module[plane_max_index])
1430                )
1431                self.layers[self.num_dendrites].to(GPA.pc.get_device())
1432                if self.num_dendrites > 0:
1433                    self.dendrites_to_dendrites[self.num_dendrites].copy_(
1434                        self.best_dendrites_to_candidates_saved[plane_max_index]
1435                    )
1436                if type(self.parent_module) in GPA.pc.get_modules_with_processing():
1437                    self.processors.append(self.candidate_processors[plane_max_index])
1438                if (
1439                    type(self.parent_module).__name__
1440                    in GPA.pc.get_module_names_with_processing()
1441                ):
1442                    self.processors.append(self.candidate_processors[plane_max_index])
1443            if GPA.pc.get_perforated_backpropagation():
1444                MPB.set_pb_mode(self, mode)
1445            del self.candidate_module, self.best_candidate_module
1446
1447            self.num_dendrites += 1
1448            if GPA.pc.get_perforated_backpropagation():
1449                MPB.set_dendrite_parameters(self.dendrites_to_dendrites)
1450                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):
1452    def forward(self, *args, **kwargs):
1453        """Forward pass for dendrite module.
1454
1455        Parameters
1456        ----------
1457        *args : tuple
1458            Positional arguments for the forward pass.
1459        **kwargs : dict
1460            Keyword arguments for the forward pass.
1461
1462        Returns
1463        -------
1464        Any
1465            The output of the module after processing through the neuron and dendrite modules.
1466        Any
1467            Remaining outputs are only used for Perforated Backpropagation.
1468        Any
1469            Remaining outputs are only used for Perforated Backpropagation.
1470        Any
1471            Remaining outputs are only used for Perforated Backpropagation.
1472
1473        Notes
1474        -----
1475        If using Perforated Backpropagation, the additional outputs will be moved around in
1476        this code but left unused and only passed into separate PB functions.
1477        """
1478
1479        outs = {}
1480
1481        # For all modules apply processors, call the modules, then apply post processors
1482        args2, kwargs2 = args, kwargs
1483        for c in range(0, self.num_dendrites):
1484            if GPA.pc.get_perforated_backpropagation():
1485                args2, kwargs2 = MPB.preprocess_pb(*args, **kwargs)
1486            if self.processors != []:
1487                try:
1488                    args2, kwargs2 = self.processors[c].pre_d(*args2, **kwargs2)
1489                except Exception as e:
1490                    traceback.print_exc(limit=None, chain=True)
1491                    print(f"Your pre_d processor for {self.name} caused this error")
1492                    print(
1493                        f"You must check how this is defined and ensure that it is properly"
1494                    )
1495                    print(
1496                        f"accepting inputs to the PAIModule and returning what will then be"
1497                    )
1498                    print(f"the input to the dendrite module")
1499                    sys.exit()
1500            out_values = self.layers[c](*args2, **kwargs2)
1501            if self.processors != []:
1502                try:
1503                    outs[c] = self.processors[c].post_d(out_values)
1504                except Exception as e:
1505                    traceback.print_exc(limit=None, chain=True)
1506                    print(f"Your post_d processor for {self.name} caused this error")
1507                    print(
1508                        f"You must check how this is defined and ensure that it is properly"
1509                    )
1510                    print(
1511                        f"accepting outputs from the dendrite module and returning the"
1512                    )
1513                    print(
1514                        f"single tensor to be combined with the neurons output tensor"
1515                    )
1516                    sys.exit()
1517            else:
1518                outs[c] = out_values
1519
1520        # Create dendrite outputs
1521        # Each dendrite has input from previously created dendrites
1522        # So activation is added before the nonlinearity is called
1523        view_tuple = []
1524        for out_index in range(0, self.num_dendrites):
1525            current_out = outs[out_index]
1526            view_tuple = []
1527            for dim in range(len(current_out.shape)):
1528                if dim == self.this_node_index:
1529                    view_tuple.append(-1)
1530                    continue
1531                view_tuple.append(1)
1532
1533            for in_index in range(0, out_index):
1534                if view_tuple == [
1535                    1
1536                ]:  # This is only the case when passing a single datapoint rather than a batch
1537                    current_out = (
1538                        current_out
1539                        + self.dendrites_to_dendrites[out_index][in_index, :].to(
1540                            current_out.device
1541                        )
1542                        * outs[in_index]
1543                    )
1544                else:
1545                    current_out = (
1546                        current_out
1547                        + self.dendrites_to_dendrites[out_index][in_index, :]
1548                        .view(view_tuple)
1549                        .to(current_out.device)
1550                        * outs[in_index]
1551                    )
1552            outs[out_index] = GPA.pc.get_pai_forward_function()(current_out)
1553        # Return a dict which has all dendritic outputs after the activation functions were called
1554        if GPA.pc.get_perforated_backpropagation():
1555            candidate_outs, candidate_nonlinear_outs, candidate_non_zeroed = (
1556                MPB.forward_candidates(self, view_tuple, outs, *args2, **kwargs2)
1557            )
1558        else:
1559            candidate_outs, candidate_nonlinear_outs, candidate_non_zeroed = (
1560                {},
1561                {},
1562                {},
1563            )
1564        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):
1567class DendriteValueTracker(nn.Module):
1568    """Tracker object that maintains certain values for each set of dendrites."""
1569
1570    def __init__(
1571        self,
1572        initialized,
1573        activation_function_value,
1574        name,
1575        output_dimensions,
1576        out_channels=-1,
1577    ):
1578        """Initialize DendriteValueTracker.
1579
1580        This function sets up the value tracker to maintain statistics and values
1581        for each set of dendrites.
1582
1583        Parameters
1584        ----------
1585        initialized : int
1586            Whether the dendrite has been initialized (1) or not (0).
1587        activation_function_value : float
1588            A value associated with the activation function.
1589        name : str
1590            The name of the associated neuron module.
1591        output_dimensions : vector
1592            The dimensions of the input vector.
1593        out_channels : int
1594            The number of output channels
1595        """
1596        super(DendriteValueTracker, self).__init__()
1597
1598        self.layer_name = name
1599        for val_name in DENDRITE_INIT_VALUES:
1600            self.register_buffer(
1601                val_name,
1602                torch.zeros(1, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()),
1603            )
1604        self.initialized[0] = initialized
1605        self.activation_function_value = activation_function_value
1606        self.register_buffer(
1607            "this_output_dimensions", output_dimensions.clone().detach()
1608        )
1609        if (self.this_output_dimensions == 0).sum() != 1:
1610            print(f"3 need exactly one 0 in the input dimensions: {self.layer_name}")
1611            print(self.this_output_dimensions)
1612            sys.exit(-1)
1613        self.register_buffer(
1614            "this_node_index", (output_dimensions == 0).nonzero(as_tuple=True)[0]
1615        )
1616        if out_channels != -1:
1617            ndim = len(output_dimensions)
1618            init_shape = [1] * ndim
1619            init_shape[(output_dimensions == 0).nonzero(as_tuple=True)[0].item()] = out_channels
1620            self.setup_arrays(init_shape)
1621        else:
1622            self.out_channels = -1
1623
1624    def print(self):
1625        """Print value tracker information.
1626
1627        Parameters
1628        ----------
1629        None
1630
1631        Returns
1632        -------
1633        None
1634            This function does not return a value.
1635        """
1636        total_string = "Value Tracker:"
1637        for val_name in DENDRITE_INIT_VALUES:
1638            total_string += f"\t{val_name}:\n\t\t"
1639            total_string += getattr(self, val_name).__repr__()
1640            total_string += "\n"
1641        for val_name in get_DENDRITE_TENSOR_VALUES():
1642            if getattr(self, val_name, None) is not None:
1643                total_string += f"\t{val_name}:\n\t\t"
1644                total_string += getattr(self, val_name).__repr__()
1645                total_string += "\n"
1646        print(total_string)
1647
1648    def set_this_output_dimensions(self, new_output_dimensions):
1649        """Set input dimensions for value tracker
1650
1651        Signals to this DendriteValueTracker that its input dimensions are different
1652        than the global default.
1653
1654        Parameters
1655        ----------
1656        new_output_dimensions : list
1657            A list or tensor specifying the new input dimensions.
1658        Returns
1659        -------
1660        None
1661
1662        """
1663        if type(new_output_dimensions) is list:
1664            new_output_dimensions = torch.tensor(new_output_dimensions)
1665        delattr(self, "this_output_dimensions")
1666        self.register_buffer(
1667            "this_output_dimensions", new_output_dimensions.detach().clone()
1668        )
1669        if (new_output_dimensions == 0).sum() != 1:
1670            print(f"4 need exactly one 0 in the input dimensions: {self.layer_name}")
1671            print(new_output_dimensions)
1672            sys.exit(-1)
1673        self.this_node_index.copy_(
1674            (new_output_dimensions == 0).nonzero(as_tuple=True)[0][0]
1675        )
1676
1677    def set_out_channels(self, shape_values):
1678        """Set output channels based on shape values and saved node index
1679
1680        Parameters
1681        ----------
1682        shape_values : list or torch.Size
1683            A list or tensor specifying the shape values.
1684
1685        Returns
1686        -------
1687        None
1688        """
1689        if type(shape_values) == torch.Size:
1690            self.out_channels = int(shape_values[self.this_node_index])
1691        else:
1692            self.out_channels = int(shape_values[self.this_node_index].item())
1693
1694    def setup_arrays(self, storage_shape):
1695        """Setup arrays for value tracker.
1696
1697        Parameters
1698        ----------
1699        storage_shape : list
1700            Shape for the tracking tensors: 1 at every dim except the channel
1701            dim (this_node_index), which holds out_channels.  E.g. [1, 5] for
1702            a linear layer with 5 outputs.
1703        Returns
1704        -------
1705        None
1706
1707        """
1708        # storage_shape is a list with 1 at every dim except the channel dim.
1709        # Passed in directly from filter_backward so it is always derived from
1710        # the live gradient — not from out_channels, which is not saved/loaded.
1711        self.out_channels = storage_shape[self.this_node_index.item()]
1712        self.register_buffer(
1713            "dendrite_storage_shape",
1714            torch.tensor(storage_shape, dtype=torch.long, device=GPA.pc.get_device()),
1715        )
1716        for val_name in get_DENDRITE_TENSOR_VALUES():
1717            self.register_buffer(
1718                val_name,
1719                torch.zeros(
1720                    storage_shape, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()
1721                ),
1722            )
1723
1724        for name in get_VALUE_TRACKER_ARRAYS():
1725            setattr(self, name, {})
1726            count = 1
1727            if torch.cuda.device_count() > count:
1728                count = torch.cuda.device_count()
1729            for i in range(count):
1730                getattr(self, name)[i] = []
1731        for val_name in get_DENDRITE_SINGLE_VALUES():
1732            self.register_buffer(
1733                val_name,
1734                torch.zeros(1, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()),
1735            )
1736
1737    def reinitialize_for_pai(self):
1738        """Reinitialize value tracker to add the next set of dendrites
1739
1740        Parameters
1741        ----------
1742        None
1743
1744        Returns
1745        -------
1746        None
1747            This function does not return a value.
1748        """
1749
1750        if self.out_channels == -1:
1751            print("You have a perforated module that was never initialized")
1752            print("This likely means it is not being added to the autograd graph")
1753            print("Check your forward function that it is actually being used")
1754            print("If its not you should really delete it, but you can also add")
1755            print(self.layer_name)
1756            print("with:")
1757            print("GPA.pc.append_module_ids_to_track(['" + self.layer_name + "'])")
1758            print("This can also happen while testing_dendrite_capacity if you")
1759            print(
1760                "run a validation cycle and try to add Dendrites before doing any training.\n"
1761            )
1762            pdb.set_trace()
1763
1764        self.initialized[0] = 0
1765        if GPA.pc.get_perforated_backpropagation():
1766            MPB.reinitialize_for_pb(self)
1767        else:
1768            for val_name in get_DENDRITE_REINIT_VALUES():
1769                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)
1570    def __init__(
1571        self,
1572        initialized,
1573        activation_function_value,
1574        name,
1575        output_dimensions,
1576        out_channels=-1,
1577    ):
1578        """Initialize DendriteValueTracker.
1579
1580        This function sets up the value tracker to maintain statistics and values
1581        for each set of dendrites.
1582
1583        Parameters
1584        ----------
1585        initialized : int
1586            Whether the dendrite has been initialized (1) or not (0).
1587        activation_function_value : float
1588            A value associated with the activation function.
1589        name : str
1590            The name of the associated neuron module.
1591        output_dimensions : vector
1592            The dimensions of the input vector.
1593        out_channels : int
1594            The number of output channels
1595        """
1596        super(DendriteValueTracker, self).__init__()
1597
1598        self.layer_name = name
1599        for val_name in DENDRITE_INIT_VALUES:
1600            self.register_buffer(
1601                val_name,
1602                torch.zeros(1, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()),
1603            )
1604        self.initialized[0] = initialized
1605        self.activation_function_value = activation_function_value
1606        self.register_buffer(
1607            "this_output_dimensions", output_dimensions.clone().detach()
1608        )
1609        if (self.this_output_dimensions == 0).sum() != 1:
1610            print(f"3 need exactly one 0 in the input dimensions: {self.layer_name}")
1611            print(self.this_output_dimensions)
1612            sys.exit(-1)
1613        self.register_buffer(
1614            "this_node_index", (output_dimensions == 0).nonzero(as_tuple=True)[0]
1615        )
1616        if out_channels != -1:
1617            ndim = len(output_dimensions)
1618            init_shape = [1] * ndim
1619            init_shape[(output_dimensions == 0).nonzero(as_tuple=True)[0].item()] = out_channels
1620            self.setup_arrays(init_shape)
1621        else:
1622            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):
1624    def print(self):
1625        """Print value tracker information.
1626
1627        Parameters
1628        ----------
1629        None
1630
1631        Returns
1632        -------
1633        None
1634            This function does not return a value.
1635        """
1636        total_string = "Value Tracker:"
1637        for val_name in DENDRITE_INIT_VALUES:
1638            total_string += f"\t{val_name}:\n\t\t"
1639            total_string += getattr(self, val_name).__repr__()
1640            total_string += "\n"
1641        for val_name in get_DENDRITE_TENSOR_VALUES():
1642            if getattr(self, val_name, None) is not None:
1643                total_string += f"\t{val_name}:\n\t\t"
1644                total_string += getattr(self, val_name).__repr__()
1645                total_string += "\n"
1646        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):
1648    def set_this_output_dimensions(self, new_output_dimensions):
1649        """Set input dimensions for value tracker
1650
1651        Signals to this DendriteValueTracker that its input dimensions are different
1652        than the global default.
1653
1654        Parameters
1655        ----------
1656        new_output_dimensions : list
1657            A list or tensor specifying the new input dimensions.
1658        Returns
1659        -------
1660        None
1661
1662        """
1663        if type(new_output_dimensions) is list:
1664            new_output_dimensions = torch.tensor(new_output_dimensions)
1665        delattr(self, "this_output_dimensions")
1666        self.register_buffer(
1667            "this_output_dimensions", new_output_dimensions.detach().clone()
1668        )
1669        if (new_output_dimensions == 0).sum() != 1:
1670            print(f"4 need exactly one 0 in the input dimensions: {self.layer_name}")
1671            print(new_output_dimensions)
1672            sys.exit(-1)
1673        self.this_node_index.copy_(
1674            (new_output_dimensions == 0).nonzero(as_tuple=True)[0][0]
1675        )

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):
1677    def set_out_channels(self, shape_values):
1678        """Set output channels based on shape values and saved node index
1679
1680        Parameters
1681        ----------
1682        shape_values : list or torch.Size
1683            A list or tensor specifying the shape values.
1684
1685        Returns
1686        -------
1687        None
1688        """
1689        if type(shape_values) == torch.Size:
1690            self.out_channels = int(shape_values[self.this_node_index])
1691        else:
1692            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):
1694    def setup_arrays(self, storage_shape):
1695        """Setup arrays for value tracker.
1696
1697        Parameters
1698        ----------
1699        storage_shape : list
1700            Shape for the tracking tensors: 1 at every dim except the channel
1701            dim (this_node_index), which holds out_channels.  E.g. [1, 5] for
1702            a linear layer with 5 outputs.
1703        Returns
1704        -------
1705        None
1706
1707        """
1708        # storage_shape is a list with 1 at every dim except the channel dim.
1709        # Passed in directly from filter_backward so it is always derived from
1710        # the live gradient — not from out_channels, which is not saved/loaded.
1711        self.out_channels = storage_shape[self.this_node_index.item()]
1712        self.register_buffer(
1713            "dendrite_storage_shape",
1714            torch.tensor(storage_shape, dtype=torch.long, device=GPA.pc.get_device()),
1715        )
1716        for val_name in get_DENDRITE_TENSOR_VALUES():
1717            self.register_buffer(
1718                val_name,
1719                torch.zeros(
1720                    storage_shape, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()
1721                ),
1722            )
1723
1724        for name in get_VALUE_TRACKER_ARRAYS():
1725            setattr(self, name, {})
1726            count = 1
1727            if torch.cuda.device_count() > count:
1728                count = torch.cuda.device_count()
1729            for i in range(count):
1730                getattr(self, name)[i] = []
1731        for val_name in get_DENDRITE_SINGLE_VALUES():
1732            self.register_buffer(
1733                val_name,
1734                torch.zeros(1, device=GPA.pc.get_device(), dtype=GPA.pc.get_d_type()),
1735            )

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):
1737    def reinitialize_for_pai(self):
1738        """Reinitialize value tracker to add the next set of dendrites
1739
1740        Parameters
1741        ----------
1742        None
1743
1744        Returns
1745        -------
1746        None
1747            This function does not return a value.
1748        """
1749
1750        if self.out_channels == -1:
1751            print("You have a perforated module that was never initialized")
1752            print("This likely means it is not being added to the autograd graph")
1753            print("Check your forward function that it is actually being used")
1754            print("If its not you should really delete it, but you can also add")
1755            print(self.layer_name)
1756            print("with:")
1757            print("GPA.pc.append_module_ids_to_track(['" + self.layer_name + "'])")
1758            print("This can also happen while testing_dendrite_capacity if you")
1759            print(
1760                "run a validation cycle and try to add Dendrites before doing any training.\n"
1761            )
1762            pdb.set_trace()
1763
1764        self.initialized[0] = 0
1765        if GPA.pc.get_perforated_backpropagation():
1766            MPB.reinitialize_for_pb(self)
1767        else:
1768            for val_name in get_DENDRITE_REINIT_VALUES():
1769                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.