perforatedai.globals_perforatedai

PAI configuration file.

This module provides configuration classes and utilities for Perforated AI (PAI), including device settings, dendrite management, module conversion options, and training parameters.

   1# Copyright (c) 2025 Perforated AI
   2"""PAI configuration file.
   3
   4This module provides configuration classes and utilities for Perforated AI (PAI),
   5including device settings, dendrite management, module conversion options,
   6and training parameters.
   7"""
   8
   9import math
  10import sys
  11
  12import torch
  13import torch.nn as nn
  14
  15
  16def _validate_module_id(module_id):
  17    """Validate that a module ID string uses dot notation.
  18
  19    Module IDs must start with '.' and must not contain '[' or ']'.
  20    For example, 'model.layers[1].module' should be written as '.layers.1.module'.
  21    """
  22    if not isinstance(module_id, str) or not module_id.startswith("."):
  23        raise ValueError(
  24            f"Module ID '{module_id}' must start with '.' - model.module should be '.module'"
  25        )
  26    if "[" in module_id or "]" in module_id:
  27        raise ValueError(
  28            f"Module ID '{module_id}' must not contain '[' or ']'. "
  29            "Use dot notation instead, e.g. '.layers.1.module'"
  30        )
  31
  32
  33def add_pai_config_var_functions(obj, var_name, initial_value, list_type=False):
  34    """Dynamically add a property with getter and setter to an object.
  35
  36    This function adds a private variable along with getter and setter methods
  37    to a given object instance. Used for integrating initial and Perforated
  38    Backpropagation variables into the PAIConfig class.
  39
  40    Parameters
  41    ----------
  42    obj : object
  43        The object to which the property will be added.
  44    var_name : str
  45        Name of the variable/property to create.
  46    initial_value : any
  47        Initial value for the property.
  48
  49    Returns
  50    -------
  51    None
  52
  53    Notes
  54    -----
  55    Creates three attributes on obj:
  56        - _{var_name}: private storage
  57        - get_{var_name}: getter method
  58        - set_{var_name}: setter method
  59    """
  60    private_name = f"_{var_name}"
  61
  62    # Add the private variable to the instance
  63    setattr(obj, private_name, initial_value)
  64
  65    # Define getter and setter and appender
  66
  67    def getter_val(self):
  68        """Get the current value of the property.
  69
  70        If the property a individual value but is set to be a list,
  71        return the element corresponding to the
  72        current number of dendrites added. Otherwise, return the value directly.
  73
  74        Returns
  75        -------
  76        Any
  77            Current value of the property.
  78
  79        Notes:
  80        -----
  81        Many variables have optimal settings that must change as dendrites are added
  82        this enables those values to be dynamically set very easily.
  83
  84        Parameters
  85        ----------
  86        None
  87
  88        """
  89        global pai_tracker
  90        if type(getattr(self, private_name)) is list:
  91            return getattr(self, private_name)[
  92                min(
  93                    len(getattr(self, private_name)) - 1,
  94                    pai_tracker.member_vars["num_dendrites_added"],
  95                )
  96            ]
  97        return getattr(self, private_name)
  98
  99    def getter_list(self):
 100        """Get the underlying list value for this dynamic config property.
 101
 102        Returns
 103        -------
 104        list
 105            Raw list stored for this property.
 106
 107        Parameters
 108        ----------
 109        None
 110
 111        """
 112        return getattr(self, private_name)
 113
 114    def setter(self, value):
 115        """Set the value of the property.
 116
 117        Parameters
 118        ----------
 119        value : Any
 120            New value to assign to the dynamic configuration field.
 121
 122        Returns
 123        -------
 124        None
 125            Updates the property and may trigger configuration auto-save.
 126        """
 127        if (
 128            self.__dict__.get("_module_name") is not None
 129            or self.__dict__.get("_module_type") is not None
 130        ):
 131            raise RuntimeError(
 132                "Setting custom module config values should only be done "
 133                "from JSON config files or the GUI"
 134            )
 135        if var_name in (
 136            "module_ids_to_track",
 137            "module_ids_to_perforate",
 138            "parameter_ids_to_track",
 139        ):
 140            for module_id in value:
 141                _validate_module_id(module_id)
 142        setattr(self, private_name, value)
 143        # Auto-save: if a config file has been configured (set when save_name is set),
 144        # persist the new value immediately so the JSON stays in sync.
 145        config_file = self.__dict__.get("_config_file")
 146        # Special case: if save_name changed to non-empty, update config file path
 147        if var_name == "save_name" and value:
 148            import os as _os
 149
 150            _save_folder = _os.path.join(_os.getcwd(), value)
 151            config_file = _os.path.join(_save_folder, f"{value}_config.json")
 152            self.__dict__["_config_file"] = config_file
 153        elif config_file and not self.__dict__.get("_testing_dendrite_capacity", False):
 154            self.save_config(config_file)
 155
 156    def appender(self, value):
 157        """Append a value to the property if it is a list.
 158
 159        Parameters
 160        ----------
 161        value : Any
 162            Value or values to append to the underlying list property.
 163
 164        Returns
 165        -------
 166        None
 167            Appends items in-place and prints the resulting list value.
 168        """
 169        if isinstance(getattr(self, private_name), list):
 170            if var_name in (
 171                "module_ids_to_track",
 172                "module_ids_to_perforate",
 173                "parameter_ids_to_track",
 174            ):
 175                for module_id in value:
 176                    _validate_module_id(module_id)
 177            setattr(self, private_name, getattr(self, private_name) + value)
 178            print(
 179                'New list value of "{}": {}'.format(
 180                    private_name, getattr(self, private_name)
 181                )
 182            )
 183        else:
 184            raise TypeError(f"Cannot append to non-list attribute '{var_name}'")
 185
 186    # Attach methods to the instance
 187    if list_type:
 188        setattr(obj, f"get_{var_name}", getter_list.__get__(obj))
 189    else:
 190        setattr(obj, f"get_{var_name}", getter_val.__get__(obj))
 191    setattr(obj, f"set_{var_name}", setter.__get__(obj))
 192    setattr(obj, f"append_{var_name}", appender.__get__(obj))
 193
 194
 195# ---------------------------------------------------------------------------
 196# JSON serialization helpers  (used by PAIConfig.save_config / load_config)
 197# ---------------------------------------------------------------------------
 198
 199
 200def _resolve_dotted_name(dotted_name):
 201    """Import and return an object identified by a dotted module path.
 202
 203    E.g. 'torch.nn.modules.conv.Conv2d'  → nn.Conv2d class
 204         'torch.sigmoid'                 → torch.sigmoid function
 205
 206    Returns None if the name cannot be resolved.
 207    """
 208    import importlib
 209
 210    parts = dotted_name.rsplit(".", 1)
 211    if len(parts) == 2:
 212        try:
 213            mod = importlib.import_module(parts[0])
 214            return getattr(mod, parts[1], None)
 215        except Exception:
 216            pass
 217    # Fall back: try the whole string as a single attribute of builtins
 218    import builtins
 219
 220    return getattr(builtins, dotted_name, None)
 221
 222
 223def _serialize_pai_value(val):
 224    """Recursively convert a PAIConfig value to a JSON-serialisable form."""
 225    if isinstance(val, bool):
 226        return val
 227    if isinstance(val, (int, float, str)):
 228        return val
 229    if isinstance(val, torch.device):
 230        return str(val)
 231    if isinstance(val, torch.dtype):
 232        return str(val)
 233    if isinstance(val, list):
 234        return [_serialize_pai_value(v) for v in val]
 235    if isinstance(val, type):
 236        mod = getattr(val, "__module__", "") or ""
 237        return f"{mod}.{val.__name__}" if mod else val.__name__
 238    if callable(val):
 239        name = getattr(val, "__name__", None)
 240        mod = getattr(val, "__module__", None)
 241        if name and mod:
 242            return f"{mod}.{name}"
 243        return str(val)
 244    return str(val)
 245
 246
 247def _deserialize_pai_value(json_val, type_hint):
 248    """Convert a JSON value to its Python type using an explicit type hint."""
 249    if json_val is None:
 250        return None
 251    if type_hint is torch.device:
 252        return torch.device(str(json_val))
 253    if type_hint is torch.dtype:
 254        v = getattr(torch, str(json_val).split(".", 1)[-1], None)
 255        return v if isinstance(v, torch.dtype) else json_val
 256    if type_hint is callable:
 257        v = _resolve_dotted_name(json_val) if isinstance(json_val, str) else None
 258        return v if (v and callable(v)) else json_val
 259    if type_hint == [type]:  # list whose elements are class objects
 260        return [
 261            (_resolve_dotted_name(v) if isinstance(v, str) else v)
 262            for v in (json_val or [])
 263        ]
 264    return json_val  # bool, int, float, str, list — JSON value is already correct
 265
 266
 267class PAIConfig:
 268    """Configuration class for PAI settings.
 269
 270    This class manages all configuration parameters for the Perforated AI system,
 271    including device settings, dendrite behavior, module conversion rules,
 272    training parameters, and debugging options.
 273
 274    Attributes
 275    ----------
 276    use_cuda : bool
 277        Whether CUDA is available and should be used.
 278    device : torch.device
 279        The device to use for computation (CPU, CUDA, etc.).
 280    save_name : str
 281        Name used for saving models (should not be set manually).
 282    debugging_output_dimensions : int
 283        Debug level for input dimension checking.
 284    confirm_correct_sizes : bool
 285        Whether to verify tensor sizes during execution.
 286    unwrapped_modules_confirmed : bool
 287        Confirmation flag for using unwrapped modules.
 288    weight_decay_accepted : bool
 289        Confirmation flag for accepting weight decay.
 290    checked_skipped_modules : bool
 291        Whether skipped modules have been verified.
 292    verbose : bool
 293        Enable verbose logging output.
 294    extra_verbose : bool
 295        Enable extra verbose logging output.
 296    silent : bool
 297        Suppress all PAI print statements.
 298    save_old_graph_scores : bool
 299        Whether to save historical graph scores.
 300    testing_dendrite_capacity : bool
 301        Enable dendrite capacity testing mode.
 302    using_safe_tensors : bool
 303        Use safe tensors file format for saving.
 304    global_candidates : int
 305        Number of global candidate dendrites.
 306    drawing_pai : bool
 307        Enable PAI visualization graphs.
 308    test_saves : bool
 309        Save intermediary test models.
 310    pai_saves : bool
 311        Save PAI-specific format models.
 312    output_dimensions : list
 313        Format specification for input tensor dimensions.
 314    improvement_threshold : float
 315        Relative improvement threshold for validation scores.
 316    improvement_threshold_raw : float
 317        Absolute improvement threshold for validation scores.
 318    candidate_weight_initialization_multiplier : float
 319        Multiplier for random dendrite weight initialization.
 320    DOING_SWITCH_EVERY_TIME : int
 321        Constant for switch mode: add dendrites every epoch.
 322    DOING_HISTORY : int
 323        Constant for switch mode: add dendrites based on validation history.
 324    n_epochs_to_switch : int
 325        Number of epochs without improvement before switching.
 326    history_lookback : int
 327        Number of epochs to average for validation history.
 328    initial_history_after_switches : int
 329        Epochs to wait after adding dendrites before beggining checks.
 330    DOING_FIXED_SWITCH : int
 331        Constant for switch mode: add dendrites at fixed intervals.
 332    fixed_switch_num : int
 333        Number of epochs between fixed switches.
 334    first_fixed_switch_num : int
 335        Number of epochs before first switch (for pretraining).
 336    DOING_NO_SWITCH : int
 337        Constant for switch mode: never add dendrites.
 338    switch_mode : int
 339        Current switch mode setting.
 340    reset_best_score_on_switch : bool
 341        Whether to reset best score when adding dendrites.
 342    learn_dendrites_live : bool
 343        Enable live dendrite learning (advanced feature).
 344    no_extra_n_modes : bool
 345        Disable extra neuron modes (advanced feature).
 346    d_type : torch.dtype
 347        Data type for dendrite weights.
 348    retain_all_dendrites : bool
 349        Keep dendrites even if they don't improve performance.
 350    find_best_lr : bool
 351        Automatically sweep learning rates when adding dendrites.
 352    dont_give_up_unless_learning_rate_lowered : bool
 353        Ensure search lowers learning rate at least once.
 354    max_dendrite_tries : int
 355        Maximum attempts to add dendrites with random initializations.
 356    max_dendrites : int
 357        Maximum total number of dendrites to add.
 358    PARAM_VALS_BY_TOTAL_EPOCH : int
 359        Constant: scheduler params tracked by total epochs.
 360    PARAM_VALS_BY_UPDATE_EPOCH : int
 361        Constant: scheduler params reset at each switch.
 362    PARAM_VALS_BY_NEURON_EPOCH_START : int
 363        Constant: scheduler params reset for neuron starts only.
 364    param_vals_setting : int
 365        Current parameter tracking mode.
 366    pai_forward_function : callable
 367        Activation function used for dendrites.
 368    modules_to_perforate : list
 369        Module types to convert to PAI modules for perforation.
 370    module_names_to_perforate : list
 371        Module names to convert to PAI modules for perforation.
 372    module_ids_to_perforate : list
 373        Specific module IDs to convert to PAI modules for perforation.
 374    modules_to_track : list
 375        Module types to track but not convert.
 376    module_names_to_track : list
 377        Module names to track but not convert.
 378    module_ids_to_track : list
 379        Specific module IDs to track but not convert.
 380    modules_to_replace : list
 381        Module types to replace before conversion.
 382    replacement_modules : list
 383        Replacement modules for modules_to_replace.
 384    modules_with_processing : list
 385        Module types requiring custom processing.
 386    modules_processing_classes : list
 387        Processing classes for modules_with_processing.
 388    module_names_with_processing : list
 389        Module names requiring custom processing.
 390    module_by_name_processing_classes : list
 391        Processing classes for module_names_with_processing.
 392    module_names_to_not_save : list
 393        Module names to exclude from saving.
 394    perforated_backpropagation : bool
 395        Whether Perforated Backpropagation is enabled.
 396    """
 397
 398    # Explicit type map for every config variable — used by load_config to coerce JSON values.
 399    # [type] means "list whose elements are class/type objects" (need dotted-name resolution).
 400    _TYPES: dict = {
 401        **{
 402            k: bool
 403            for k in (
 404                "use_cuda",
 405                "confirm_correct_sizes",
 406                "unwrapped_modules_confirmed",
 407                "weight_decay_accepted",
 408                "checked_skipped_modules",
 409                "verbose",
 410                "extra_verbose",
 411                "silent",
 412                "save_old_graph_scores",
 413                "testing_dendrite_capacity",
 414                "using_safe_tensors",
 415                "drawing_pai",
 416                "drawing_extra_graphs",
 417                "test_saves",
 418                "pai_saves",
 419                "reset_best_score_on_switch",
 420                "learn_dendrites_live",
 421                "no_extra_n_modes",
 422                "retain_all_dendrites",
 423                "find_best_lr",
 424                "dont_give_up_unless_learning_rate_lowered",
 425                "candidate_weight_init_by_main",
 426                "perforated_backpropagation",
 427                "weight_tying_experimental",
 428                "dashboard_events_enabled",
 429                "dashboard_debug",
 430            )
 431        },
 432        **{
 433            k: int
 434            for k in (
 435                "debugging_output_dimensions",
 436                "global_candidates",
 437                "n_epochs_to_switch",
 438                "history_lookback",
 439                "initial_history_after_switches",
 440                "fixed_switch_num",
 441                "first_fixed_switch_num",
 442                "switch_mode",
 443                "max_dendrite_tries",
 444                "max_dendrites",
 445                "param_vals_setting",
 446            )
 447        },
 448        **{
 449            k: float
 450            for k in (
 451                "improvement_threshold_raw",
 452                "candidate_weight_initialization_multiplier",
 453            )
 454        },
 455        **{k: str for k in ("save_name", "library_validation_score", "dashboard_url")},
 456        "device": torch.device,
 457        "d_type": torch.dtype,
 458        "pai_forward_function": callable,
 459        **{
 460            k: list
 461            for k in (
 462                "output_dimensions",
 463                "improvement_threshold",
 464                "module_names_to_perforate",
 465                "module_ids_to_perforate",
 466                "module_names_to_track",
 467                "module_ids_to_track",
 468                "parameter_ids_to_track",
 469                "module_names_with_processing",
 470                "module_names_to_not_save",
 471                "library_extra_scores",
 472                "library_extra_scores_without_graphing",
 473            )
 474        },
 475        **{
 476            k: [type]
 477            for k in (
 478                "modules_to_perforate",
 479                "modules_to_track",
 480                "modules_to_replace",
 481                "replacement_modules",
 482                "modules_with_processing",
 483                "modules_processing_classes",
 484                "module_by_name_processing_classes",
 485            )
 486        },
 487    }
 488
 489    # Subset of _TYPES: variables that can be overridden on a per-module basis
 490    # via the Studio.  These are set outside the ``if not module_name:`` block
 491    # in :py:meth:`__init__`, so they are meaningful when constructing a
 492    # module-specific PAIConfig instance.
 493    _CUSTOMIZABLE: dict = {
 494        "verbose": bool,
 495        "extra_verbose": bool,
 496        "silent": bool,
 497        "global_candidates": int,
 498        "output_dimensions": list,
 499        "candidate_weight_initialization_multiplier": float,
 500        "candidate_weight_init_by_main": bool,
 501        "retain_all_dendrites": bool,
 502        "max_dendrites": int,
 503        "pai_forward_function": callable,
 504    }
 505
 506    def __getattr__(self, name):
 507        """Handle missing attributes gracefully, especially for PB variables.
 508
 509        Parameters
 510        ----------
 511        name : str
 512            The name of the attribute being accessed.
 513
 514        Returns
 515        -------
 516        None or raises AttributeError
 517            Returns None for missing set_ methods, raises AttributeError otherwise.
 518        """
 519        if name.startswith("set_"):
 520            print(f"Variable '{name[4:]}' does not exist.  Ignoring set attempt.")
 521            return lambda x: None
 522        if name.startswith("append_"):
 523            print(
 524                f"List Variable '{name[7:]}' does not exist.  Ignoring append attempt."
 525            )
 526            return lambda x: None
 527        if name.startswith("get_") and self.__dict__.get("_module_name") is not None:
 528            # Module-specific config: check for a per-module override stored directly in
 529            # __dict__ (written by load_config when custom JSON data was found for this
 530            # module).  This covers CUSTOMIZABLE vars that are not initialised for
 531            # per-module configs (e.g. output_dimensions, which lives inside the
 532            # ``if not module_name:`` block).
 533            private_key = f"_{name[4:]}"
 534            if private_key in self.__dict__:
 535                stored = self.__dict__[private_key]
 536                return lambda: stored
 537            # Fall back to the global pc instance for vars not set on this instance.
 538            global_getter = getattr(pc, name, None)
 539            if global_getter is not None:
 540                return global_getter
 541        raise AttributeError(
 542            f"'{self.__class__.__name__}' object has no attribute '{name}'"
 543        )
 544
 545    def __init__(self, module_name=None, module_type=None):
 546        """Initialize PAIConfig with default settings.
 547
 548        module_name=None means this is the main global config.
 549        If module_name is set this is a per-module config that loads
 550        custom settings from module_settings[module_name] (by id) or
 551        module_settings[module_type] (by type) in the JSON file.
 552        """
 553        # Must be first: prevents __getattr__ from firing for _config_file
 554        # during construction (before add_pai_config_var_functions sets it).
 555        # Also disables auto-save in setters until the end of __init__.
 556        self.__dict__["_config_file"] = None
 557        # None = global config; any string = per-module config
 558        self.__dict__["_module_name"] = module_name
 559        # Short class name of the wrapped module (e.g. 'Conv2d'), used as
 560        # a fallback lookup key when the specific name has no saved settings.
 561        self.__dict__["_module_type"] = module_type
 562
 563        if not module_name:
 564            ### Global Constants
 565            # Device configuration
 566            self.use_cuda = torch.cuda.is_available()
 567            add_pai_config_var_functions(self, "use_cuda", self.use_cuda)
 568            self.device = torch.device("cuda" if self.use_cuda else "cpu")
 569            add_pai_config_var_functions(self, "device", self.device)
 570
 571            self.save_name = ""
 572            add_pai_config_var_functions(self, "save_name", self.save_name)
 573
 574            # Debug settings
 575            self.debugging_output_dimensions = 0
 576            add_pai_config_var_functions(
 577                self, "debugging_output_dimensions", self.debugging_output_dimensions
 578            )
 579            # Debugging input tensor sizes.
 580            # This will slow things down very slightly and is not necessary but can help
 581            # catch when dimensions were not filled in correctly.
 582            self.confirm_correct_sizes = False
 583            add_pai_config_var_functions(
 584                self, "confirm_correct_sizes", self.confirm_correct_sizes
 585            )
 586
 587            # Confirmation flags for non-recommended options
 588            self.unwrapped_modules_confirmed = False
 589            add_pai_config_var_functions(
 590                self, "unwrapped_modules_confirmed", self.unwrapped_modules_confirmed
 591            )
 592            self.weight_decay_accepted = False
 593            add_pai_config_var_functions(
 594                self, "weight_decay_accepted", self.weight_decay_accepted
 595            )
 596            self.checked_skipped_modules = False
 597            add_pai_config_var_functions(
 598                self, "checked_skipped_modules", self.checked_skipped_modules
 599            )
 600            # Analysis settings
 601            self.save_old_graph_scores = True
 602            add_pai_config_var_functions(
 603                self, "save_old_graph_scores", self.save_old_graph_scores
 604            )
 605            # Testing settings
 606            self.testing_dendrite_capacity = True
 607            add_pai_config_var_functions(
 608                self, "testing_dendrite_capacity", self.testing_dendrite_capacity
 609            )
 610
 611            # File format settings
 612            self.using_safe_tensors = True
 613            add_pai_config_var_functions(
 614                self, "using_safe_tensors", self.using_safe_tensors
 615            )
 616
 617            # Checkpoint loading settings
 618            # Whether to use strict=True when loading state_dict
 619            # Set to False if loading old checkpoints that are missing new fields
 620            self.strict_loading = True
 621            add_pai_config_var_functions(
 622                self, "strict_loading", self.strict_loading
 623            )
 624
 625            # Graph and visualization settings
 626            # A graph setting which can be set to false if you want to do your own
 627            # training visualizations
 628            self.drawing_pai = True
 629            add_pai_config_var_functions(self, "drawing_pai", self.drawing_pai)
 630
 631            # Drawing extra graphs beyond the standard ones.
 632            self.drawing_extra_graphs = True
 633            add_pai_config_var_functions(
 634                self, "drawing_extra_graphs", self.drawing_extra_graphs
 635            )
 636
 637            # Saving test intermediary models, good for experimentation, bad for memory
 638            self.test_saves = True
 639            add_pai_config_var_functions(self, "test_saves", self.test_saves)
 640            # To be filled in later. pai_saves will remove some extra scaffolding for
 641            # slight memory and speed improvements
 642            self.pai_saves = False
 643            add_pai_config_var_functions(self, "pai_saves", self.pai_saves)
 644            # Improvement thresholds
 645            # Percentage improvement increase needed to call a new best validation score
 646            self.improvement_threshold = [0.001, 0.0001, 0.0]
 647            add_pai_config_var_functions(
 648                self, "improvement_threshold", self.improvement_threshold
 649            )
 650
 651            # Raw increase needed
 652            self.improvement_threshold_raw = 1e-5
 653            add_pai_config_var_functions(
 654                self, "improvement_threshold_raw", self.improvement_threshold_raw
 655            )
 656            # SWITCH MODE SETTINGS
 657
 658            # Add dendrites every time to debug implementation
 659            self.DOING_SWITCH_EVERY_TIME = 0
 660
 661            # Switch when validation hasn't improved over x epochs
 662            self.DOING_HISTORY = 1
 663            # Epochs to try before deciding to load previous best and add dendrites
 664            # Be sure this is higher than scheduler patience
 665            self.n_epochs_to_switch = 10
 666            add_pai_config_var_functions(
 667                self, "n_epochs_to_switch", self.n_epochs_to_switch
 668            )
 669            # Number to average validation scores over
 670            self.history_lookback = 1
 671            add_pai_config_var_functions(
 672                self, "history_lookback", self.history_lookback
 673            )
 674            # Amount of epochs to run after adding a new set of dendrites before checking
 675            # to add more
 676            self.initial_history_after_switches = 0
 677            add_pai_config_var_functions(
 678                self,
 679                "initial_history_after_switches",
 680                self.initial_history_after_switches,
 681            )
 682
 683            # Switch after a fixed number of epochs
 684            self.DOING_FIXED_SWITCH = 2
 685            # Number of epochs to complete before switching
 686            self.fixed_switch_num = 250
 687            add_pai_config_var_functions(
 688                self, "fixed_switch_num", self.fixed_switch_num
 689            )
 690            # An additional flag if you want your first switch to occur later than all the
 691            # rest for initial pretraining.  This is a new minimum, if its lower than
 692            # the above it will be ignored.
 693            self.first_fixed_switch_num = 1
 694            add_pai_config_var_functions(
 695                self, "first_fixed_switch_num", self.first_fixed_switch_num
 696            )
 697
 698            # A setting to not add dendrites and just do regular training
 699            # Warning, this will also never trigger training_complete
 700            self.DOING_NO_SWITCH = 3
 701
 702            # Default switch mode
 703            self.switch_mode = self.DOING_HISTORY
 704            add_pai_config_var_functions(self, "switch_mode", self.switch_mode)
 705
 706            # Reset settings
 707            # Resets score on switch
 708            # This can be useful if you need many epochs to catch up to the best score
 709            # from the previous version after adding dendrites
 710            self.reset_best_score_on_switch = True
 711            add_pai_config_var_functions(
 712                self, "reset_best_score_on_switch", self.reset_best_score_on_switch
 713            )
 714
 715            # Advanced settings
 716            # Not used in open source implementation, leave as default
 717            self.learn_dendrites_live = False
 718            add_pai_config_var_functions(
 719                self, "learn_dendrites_live", self.learn_dendrites_live
 720            )
 721            self.no_extra_n_modes = True
 722            add_pai_config_var_functions(
 723                self, "no_extra_n_modes", self.no_extra_n_modes
 724            )
 725
 726            # Data type for new modules and dendrite to dendrite / dendrite to neuron
 727            # weights
 728            self.d_type = torch.float
 729            add_pai_config_var_functions(self, "d_type", self.d_type)
 730
 731            # Learning rate management
 732            # A setting to automatically sweep over previously used learning rates when
 733            # adding new dendrites
 734            # Sometimes it's best to go back to initial LR, but often its best to start
 735            # at a lower LR
 736            self.find_best_lr = True
 737            add_pai_config_var_functions(self, "find_best_lr", self.find_best_lr)
 738            # Enforces the above even if the previous epoch didn't lower the learning rate
 739            self.dont_give_up_unless_learning_rate_lowered = True
 740            add_pai_config_var_functions(
 741                self,
 742                "dont_give_up_unless_learning_rate_lowered",
 743                self.dont_give_up_unless_learning_rate_lowered,
 744            )
 745
 746            # Dendrite attempt settings
 747            # Set to 1 if you want to quit as soon as one dendrite fails
 748            # Higher values will try new random dendrite weights this many times before
 749            # accepting that more dendrites don't improve
 750            self.max_dendrite_tries = 2
 751            add_pai_config_var_functions(
 752                self, "max_dendrite_tries", self.max_dendrite_tries
 753            )
 754
 755            # Scheduler parameter settings
 756            # Have learning rate params be by total epoch
 757            self.PARAM_VALS_BY_TOTAL_EPOCH = 0
 758            # Reset the params at every switch
 759            self.PARAM_VALS_BY_UPDATE_EPOCH = 1
 760            # Reset params for dendrite starts but not for normal restarts
 761            # Not used for open source version
 762            self.PARAM_VALS_BY_NEURON_EPOCH_START = 2
 763            # Default setting
 764            self.param_vals_setting = self.PARAM_VALS_BY_UPDATE_EPOCH
 765            add_pai_config_var_functions(
 766                self, "param_vals_setting", self.param_vals_setting
 767            )
 768            # Lists for module types and names to add dendrites to
 769            # For these lists no specifier means type, name is module name
 770            # and ids is the individual modules id, eg. model.conv2
 771            self.modules_to_perforate = []
 772            add_pai_config_var_functions(
 773                self, "modules_to_perforate", self.modules_to_perforate, list_type=True
 774            )
 775            self.module_names_to_perforate = [
 776                "PAISequential",
 777                "Conv1d",
 778                "Conv2d",
 779                "Conv3d",
 780                "Linear",
 781            ]
 782            add_pai_config_var_functions(
 783                self,
 784                "module_names_to_perforate",
 785                self.module_names_to_perforate,
 786                list_type=True,
 787            )
 788            self.module_ids_to_perforate = []
 789            add_pai_config_var_functions(
 790                self,
 791                "module_ids_to_perforate",
 792                self.module_ids_to_perforate,
 793                list_type=True,
 794            )
 795
 796            # All modules should either be perforated or tracked to ensure all modules
 797            # are accounted for
 798            self.modules_to_track = []
 799            add_pai_config_var_functions(
 800                self, "modules_to_track", self.modules_to_track, list_type=True
 801            )
 802            self.module_names_to_track = []
 803            add_pai_config_var_functions(
 804                self,
 805                "module_names_to_track",
 806                self.module_names_to_track,
 807                list_type=True,
 808            )
 809            # IDs are for if you want to pass only a single module by its assigned ID rather than the module type by name
 810            self.module_ids_to_track = []
 811            add_pai_config_var_functions(
 812                self, "module_ids_to_track", self.module_ids_to_track, list_type=True
 813            )
 814
 815            # Parameter IDs to track as neuron parameters without recursive behavior
 816            # (e.g., [".my_custom_parameter"]).
 817            self.parameter_ids_to_track = []
 818            add_pai_config_var_functions(
 819                self,
 820                "parameter_ids_to_track",
 821                self.parameter_ids_to_track,
 822                list_type=True,
 823            )
 824
 825            # Replacement modules happen before the conversion,
 826            # so replaced modules will then also be run through the conversion steps
 827            # These are for modules that need to be replaced before addition of dendrites
 828            # See the resnet example in models_perforatedai
 829            self.modules_to_replace = []
 830            add_pai_config_var_functions(
 831                self, "modules_to_replace", self.modules_to_replace, list_type=True
 832            )
 833            # Modules to replace the above modules with
 834            self.replacement_modules = []
 835            add_pai_config_var_functions(
 836                self, "replacement_modules", self.replacement_modules, list_type=True
 837            )
 838
 839            # Dendrites default to modules which are one tensor input and one tensor
 840            # output in forward()
 841            # Other modules require to be labeled as modules with processing and assigned
 842            # processing classes
 843            # This can be done by module type or module name see customization.md in API
 844            # for example
 845            self.modules_with_processing = []
 846            add_pai_config_var_functions(
 847                self,
 848                "modules_with_processing",
 849                self.modules_with_processing,
 850                list_type=True,
 851            )
 852            self.modules_processing_classes = []
 853            add_pai_config_var_functions(
 854                self,
 855                "modules_processing_classes",
 856                self.modules_processing_classes,
 857                list_type=True,
 858            )
 859            self.module_names_with_processing = []
 860            add_pai_config_var_functions(
 861                self,
 862                "module_names_with_processing",
 863                self.module_names_with_processing,
 864                list_type=True,
 865            )
 866            self.module_by_name_processing_classes = []
 867            add_pai_config_var_functions(
 868                self,
 869                "module_by_name_processing_classes",
 870                self.module_by_name_processing_classes,
 871                list_type=True,
 872            )
 873
 874            # Similarly here as above. Some huggingface models have multiple pointers to
 875            # the same modules which cause problems
 876            # If you want to only save one of the multiple pointers you can set which ones
 877            # not to save here
 878            self.module_names_to_not_save = [".base_model"]
 879            add_pai_config_var_functions(
 880                self,
 881                "module_names_to_not_save",
 882                self.module_names_to_not_save,
 883                list_type=True,
 884            )
 885
 886            # Perforated Backpropagation settings
 887            self.perforated_backpropagation = False
 888            add_pai_config_var_functions(
 889                self, "perforated_backpropagation", self.perforated_backpropagation
 890            )
 891            
 892            # This is specifically a workaround for weight tying
 893            # Not to be used for a duplicate pointer that isn't actually run twice
 894            self.weight_tying_experimental = False
 895            add_pai_config_var_functions(
 896                self, "weight_tying_experimental", self.weight_tying_experimental
 897            )
 898
 899            # Dashboard event streaming settings
 900            self.dashboard_events_enabled = False
 901            add_pai_config_var_functions(
 902                self, "dashboard_events_enabled", self.dashboard_events_enabled
 903            )
 904            self.dashboard_url = "http://localhost:3002"
 905            add_pai_config_var_functions(
 906                self, "dashboard_url", self.dashboard_url
 907            )
 908            self.dashboard_debug = False
 909            add_pai_config_var_functions(
 910                self, "dashboard_debug", self.dashboard_debug
 911            )
 912
 913            # These are settings where libraries must be doing the scoring adding to
 914            # message from your main script what metric to use
 915            self.library_validation_score = ""
 916            add_pai_config_var_functions(
 917                self, "library_validation_score", self.library_validation_score
 918            )
 919            self.library_extra_scores = []
 920            add_pai_config_var_functions(
 921                self,
 922                "library_extra_scores",
 923                self.library_extra_scores,
 924                list_type=True,
 925            )
 926            self.library_extra_scores_without_graphing = []
 927            add_pai_config_var_functions(
 928                self,
 929                "library_extra_scores_without_graphing",
 930                self.library_extra_scores_without_graphing,
 931                list_type=True,
 932            )
 933
 934            # Input dimensions needs to be set every time. It is set to what format of
 935            # planes you are expecting.
 936            # Neuron index should be set to 0, variable indexes should be set to -1.
 937            # For example, if your format is [batchsize, nodes, x, y]
 938            # output_dimensions is [-1, 0, -1, -1].
 939            # if your format is, [batchsize, time index, nodes] output_dimensions is
 940            # [-1, -1, 0]
 941            self.output_dimensions = [-1, 0, -1, -1]
 942            add_pai_config_var_functions(
 943                self, "output_dimensions", self.output_dimensions, list_type=True
 944            )
 945        # Verbosity settings
 946        self.verbose = False
 947        add_pai_config_var_functions(self, "verbose", self.verbose)
 948        self.extra_verbose = False
 949        add_pai_config_var_functions(self, "extra_verbose", self.extra_verbose)
 950        # Suppress all PAI prints
 951        self.silent = False
 952        add_pai_config_var_functions(self, "silent", self.silent)
 953
 954        # In place for future implementation options of adding multiple candidate
 955        # dendrites together
 956        self.global_candidates = 1
 957        add_pai_config_var_functions(self, "global_candidates", self.global_candidates)
 958
 959        # Weight initialization settings
 960        # Multiplier when randomizing dendrite weights
 961        self.candidate_weight_initialization_multiplier = 0.01
 962        add_pai_config_var_functions(
 963            self,
 964            "candidate_weight_initialization_multiplier",
 965            self.candidate_weight_initialization_multiplier,
 966        )
 967        # Multiplier when randomizing dendrite weights
 968        self.candidate_weight_init_by_main = False
 969        add_pai_config_var_functions(
 970            self,
 971            "candidate_weight_init_by_main",
 972            self.candidate_weight_init_by_main,
 973        )
 974
 975        # Dendrite retention settings
 976        # A setting to keep dendrites even if they do not improve scores
 977        self.retain_all_dendrites = False
 978        add_pai_config_var_functions(
 979            self, "retain_all_dendrites", self.retain_all_dendrites
 980        )
 981
 982        # Max dendrites to add even if they do continue improving scores
 983        self.max_dendrites = 100
 984        add_pai_config_var_functions(self, "max_dendrites", self.max_dendrites)
 985
 986        # Activation function settings
 987        # The activation function to use for dendrites
 988        self.pai_forward_function = torch.sigmoid
 989        add_pai_config_var_functions(
 990            self, "pai_forward_function", self.pai_forward_function
 991        )
 992
 993        # ------------------------------------------------------------------
 994        # Config file will be set when save_name is assigned (in perforate_model)
 995        # ------------------------------------------------------------------
 996        # _config_file stays None until save_name is set to a non-empty value
 997
 998    # ------------------------------------------------------------------
 999
1000    def save_config(self, filename):
1001        """Save the current PAIConfig state to a JSON file.
1002
1003        Parameters
1004        ----------
1005        filename : str
1006            Destination file path (created or overwritten).
1007
1008        Notes
1009        -----
1010        Values that are not natively JSON-serialisable (torch.device,
1011        torch.dtype, nn.Module subclasses, callables) are stored as their
1012        dotted-string representations so they can be round-tripped by
1013        :py:meth:`load_config`.
1014
1015        Returns
1016        -------
1017        None
1018            This function does not return a value.
1019        """
1020        import json
1021
1022        config_dict = {}
1023
1024        # Private-storage vars added by add_pai_config_var_functions
1025        # e.g. self._module_ids_to_perforate → key 'module_ids_to_perforate'
1026        for key, val in sorted(self.__dict__.items()):
1027            if not (key.startswith("_") and not key.startswith("__")):
1028                continue
1029            # Skip internal bookkeeping keys that must not round-trip through JSON
1030            if key in ("_config_file", "_module_name", "_module_type"):
1031                continue
1032            if callable(val):  # skip bound method refs
1033                continue
1034            clean_key = key[1:]
1035            try:
1036                config_dict[clean_key] = _serialize_pai_value(val)
1037            except Exception:
1038                config_dict[clean_key] = str(val)
1039
1040        # Plain constants (DOING_*, PARAM_VALS_BY_*, etc.)
1041        for key, val in self.__dict__.items():
1042            if key.startswith("_") or callable(val):
1043                continue
1044            if key not in config_dict:
1045                try:
1046                    config_dict[key] = _serialize_pai_value(val)
1047                except Exception:
1048                    config_dict[key] = str(val)
1049
1050        # Merge short class names from modules_to_perforate into module_names_to_perforate
1051        # so the UI (and JS) only needs to check one array.
1052        type_short_names = [
1053            cls.__name__
1054            for cls in self.__dict__.get("_modules_to_perforate", [])
1055            if isinstance(cls, type)
1056        ]
1057        existing = config_dict.get("module_names_to_perforate", [])
1058        config_dict["module_names_to_perforate"] = existing + [
1059            n for n in type_short_names if n not in existing
1060        ]
1061
1062        # Preserve any per-module settings written by the Studio frontend.
1063        _existing_ms: dict = {}
1064        try:
1065            with open(filename, "r") as _f:
1066                _existing_ms = json.load(_f).get("module_settings", {})
1067        except Exception:
1068            pass
1069        config_dict["module_settings"] = _existing_ms
1070
1071        # Publish customizable field type names so the Studio frontend can
1072        # render appropriate editors without needing to import the library.
1073        config_dict["_customizable_fields"] = {
1074            k: (v.__name__ if hasattr(v, "__name__") else str(v))
1075            for k, v in PAIConfig._CUSTOMIZABLE.items()
1076        }
1077
1078        # Ensure the directory exists before saving
1079        import os
1080
1081        os.makedirs(os.path.dirname(filename), exist_ok=True)
1082
1083        with open(filename, "w") as f:
1084            json.dump(config_dict, f, indent=2)
1085        print(f"[PAI Config] Saved {len(config_dict)} variables \u2192 {filename}")
1086
1087    def load_config(self, filename, module_name=None, module_type=None):
1088        """Load PAIConfig state from a JSON file produced by :py:meth:`save_config`.
1089
1090        If *module_name* is ``None`` (default) every serialisable variable in
1091        the file is restored on this instance.
1092
1093        If *module_name* is given the lookup priority is:
1094          1. ``module_settings[module_name]`` (exact name / id match)
1095          2. ``module_settings[module_type]``  (type-level fallback)
1096          3. No-op — the defaults already set by ``__init__`` are kept.
1097
1098        Parameters
1099        ----------
1100        filename : str
1101            Path to the JSON file to read.
1102        module_name : str, optional
1103            Display name (id) of the module whose custom settings should be loaded.
1104        module_type : str, optional
1105            Short class name of the module type, used as a fallback key.
1106
1107        Returns
1108        -------
1109        None
1110            This function does not return a value.
1111        """
1112        import json
1113
1114        with open(filename, "r") as f:
1115            config_dict = json.load(f)
1116
1117        if module_name is not None:
1118            # ── Per-module load ──────────────────────────────────────────────
1119            module_settings = config_dict.get("module_settings", {})
1120            # Priority: exact name → type fallback → no-op
1121            if module_name in module_settings:
1122                custom = module_settings[module_name]
1123                resolved_key = module_name
1124            elif module_type and module_type in module_settings:
1125                custom = module_settings[module_type]
1126                resolved_key = module_type
1127            else:
1128                # No custom settings for this module or type — keep defaults.
1129                return
1130            loaded = 0
1131            skipped = 0
1132            for key, json_val in custom.items():
1133                if key not in PAIConfig._CUSTOMIZABLE:
1134                    continue
1135                type_hint = PAIConfig._TYPES.get(key)
1136                private_key = f"_{key}"
1137                # Write directly to __dict__ so we bypass any setter guards and also
1138                # correctly handle vars that are not pre-initialised on per-module
1139                # configs (e.g. output_dimensions, which only lives inside the
1140                # ``if not module_name:`` block of __init__).
1141                try:
1142                    self.__dict__[private_key] = (
1143                        _deserialize_pai_value(json_val, type_hint)
1144                        if type_hint is not None
1145                        else json_val
1146                    )
1147                    loaded += 1
1148                except Exception as exc:
1149                    print(
1150                        f"[PAI Config] Warning: could not load '{key}' for '{resolved_key}': {exc}"
1151                    )
1152                    skipped += 1
1153            print(
1154                f"[PAI Config] Loaded {loaded} custom vars for '{resolved_key}' from {filename}"
1155                + (f" ({skipped} skipped)" if skipped else "")
1156            )
1157            return
1158
1159        # ── Global load: every variable in the JSON ──────────────────────────
1160        loaded = 0
1161        skipped = 0
1162        for key, json_val in config_dict.items():
1163            # Skip internal bookkeeping and Studio-only metadata keys.
1164            # 'module_name' and 'module_type' must never overwrite the
1165            # instance's _module_name/_module_type (they are internal only).
1166            if key in (
1167                "config_file",
1168                "module_settings",
1169                "module_name",
1170                "module_type",
1171            ) or key.startswith("_"):
1172                continue
1173            type_hint = PAIConfig._TYPES.get(key)
1174            private_key = f"_{key}"
1175            if hasattr(self, private_key):
1176                try:
1177                    setattr(
1178                        self,
1179                        private_key,
1180                        (
1181                            _deserialize_pai_value(json_val, type_hint)
1182                            if type_hint is not None
1183                            else json_val
1184                        ),
1185                    )
1186                    loaded += 1
1187                except Exception as exc:
1188                    print(f"[PAI Config] Warning: could not load '{key}': {exc}")
1189                    skipped += 1
1190            elif hasattr(self, key) and not callable(getattr(self, key, None)):
1191                try:
1192                    setattr(self, key, json_val)
1193                    loaded += 1
1194                except Exception:
1195                    skipped += 1
1196
1197        print(
1198            f"[PAI Config] Loaded {loaded} variables from {filename}"
1199            + (f" ({skipped} skipped)" if skipped else "")
1200        )
1201
1202
1203class PAISequential(nn.Sequential):
1204    """Sequential module wrapper for PAI.
1205
1206    This wrapper takes an array of layers and creates a sequential container
1207    that is compatible with PAI's dendrite addition system. It should be used
1208    for normalization layers and can be used for final output layers.
1209
1210    Parameters
1211    ----------
1212    layer_array : list
1213        List of PyTorch nn.Module objects to be executed sequentially.
1214
1215    Examples
1216    --------
1217    >>> layers = [nn.Linear(2 * hidden_dim, seq_width),
1218    ...           nn.LayerNorm(seq_width)]
1219    >>> sequential_block = PAISequential(layers)
1220
1221    Notes
1222    -----
1223    This should be used for:
1224        - All normalization layers (LayerNorm, BatchNorm, etc.)
1225    This can be used for:
1226        - Final output layer and softmax combinations
1227    """
1228
1229    def __init__(self, layer_array):
1230        """Initialize PAISequential with a list of layers.
1231
1232        Parameters
1233        ----------
1234        layer_array : list
1235            List of PyTorch modules to execute in sequence.
1236        """
1237        super(PAISequential, self).__init__()
1238        self.model = nn.Sequential(*layer_array)
1239
1240    def forward(self, *args, **kwargs):
1241        """Forward pass through the sequential layers.
1242
1243        Parameters
1244        ----------
1245        *args
1246            Positional arguments passed to the first layer.
1247        **kwargs
1248            Keyword arguments passed to the layers.
1249
1250        Returns
1251        -------
1252        torch.Tensor
1253            Output from the final layer in the sequence.
1254        """
1255        return self.model(*args, **kwargs)
1256
1257
1258### Global objects and variables
1259
1260### Global Modules
1261pc = PAIConfig()
1262"""Global PAIConfig instance.
1263
1264This is the primary configuration object used throughout the PAI system.
1265Modify settings through this instance to control PAI behavior.
1266"""
1267
1268"""Pointer to the PAI Tracker.
1269
1270This will be populated with the PAI Tracker instance which handles
1271the addition of dendrites during training. Initially an empty list.
1272"""
1273pai_tracker = []
1274
1275pai_scaler = None
1276
1277# This will be set to true if perforated backpropagation is available
1278# Do not just set this to True without the library and a license, it will cause errors
1279try:
1280    import perforatedbp.globals_pbp as perforatedbp_globals
1281
1282    print("Building dendrites with Perforated Backpropagation")
1283
1284    pc.set_perforated_backpropagation(True)
1285    # This is default to True for open source version
1286    # But defaults to False for perforated backpropagation
1287    pc.set_no_extra_n_modes(False)
1288
1289    # Loop through the vars module's attributes and add them dynamically
1290    for var_name in dir(perforatedbp_globals):
1291        if not var_name.startswith("_"):
1292            add_pai_config_var_functions(
1293                pc, var_name, getattr(perforatedbp_globals, var_name)
1294            )
1295
1296    # Merge PBP type hints into PAIConfig._TYPES so load_config can correctly
1297    # round-trip all perforatedbp variables from JSON.
1298    if hasattr(perforatedbp_globals, "_TYPES"):
1299        PAIConfig._TYPES.update(perforatedbp_globals._TYPES)
1300
1301except ImportError:
1302    print("Building dendrites without Perforated Backpropagation")
def add_pai_config_var_functions(obj, var_name, initial_value, list_type=False):
 34def add_pai_config_var_functions(obj, var_name, initial_value, list_type=False):
 35    """Dynamically add a property with getter and setter to an object.
 36
 37    This function adds a private variable along with getter and setter methods
 38    to a given object instance. Used for integrating initial and Perforated
 39    Backpropagation variables into the PAIConfig class.
 40
 41    Parameters
 42    ----------
 43    obj : object
 44        The object to which the property will be added.
 45    var_name : str
 46        Name of the variable/property to create.
 47    initial_value : any
 48        Initial value for the property.
 49
 50    Returns
 51    -------
 52    None
 53
 54    Notes
 55    -----
 56    Creates three attributes on obj:
 57        - _{var_name}: private storage
 58        - get_{var_name}: getter method
 59        - set_{var_name}: setter method
 60    """
 61    private_name = f"_{var_name}"
 62
 63    # Add the private variable to the instance
 64    setattr(obj, private_name, initial_value)
 65
 66    # Define getter and setter and appender
 67
 68    def getter_val(self):
 69        """Get the current value of the property.
 70
 71        If the property a individual value but is set to be a list,
 72        return the element corresponding to the
 73        current number of dendrites added. Otherwise, return the value directly.
 74
 75        Returns
 76        -------
 77        Any
 78            Current value of the property.
 79
 80        Notes:
 81        -----
 82        Many variables have optimal settings that must change as dendrites are added
 83        this enables those values to be dynamically set very easily.
 84
 85        Parameters
 86        ----------
 87        None
 88
 89        """
 90        global pai_tracker
 91        if type(getattr(self, private_name)) is list:
 92            return getattr(self, private_name)[
 93                min(
 94                    len(getattr(self, private_name)) - 1,
 95                    pai_tracker.member_vars["num_dendrites_added"],
 96                )
 97            ]
 98        return getattr(self, private_name)
 99
100    def getter_list(self):
101        """Get the underlying list value for this dynamic config property.
102
103        Returns
104        -------
105        list
106            Raw list stored for this property.
107
108        Parameters
109        ----------
110        None
111
112        """
113        return getattr(self, private_name)
114
115    def setter(self, value):
116        """Set the value of the property.
117
118        Parameters
119        ----------
120        value : Any
121            New value to assign to the dynamic configuration field.
122
123        Returns
124        -------
125        None
126            Updates the property and may trigger configuration auto-save.
127        """
128        if (
129            self.__dict__.get("_module_name") is not None
130            or self.__dict__.get("_module_type") is not None
131        ):
132            raise RuntimeError(
133                "Setting custom module config values should only be done "
134                "from JSON config files or the GUI"
135            )
136        if var_name in (
137            "module_ids_to_track",
138            "module_ids_to_perforate",
139            "parameter_ids_to_track",
140        ):
141            for module_id in value:
142                _validate_module_id(module_id)
143        setattr(self, private_name, value)
144        # Auto-save: if a config file has been configured (set when save_name is set),
145        # persist the new value immediately so the JSON stays in sync.
146        config_file = self.__dict__.get("_config_file")
147        # Special case: if save_name changed to non-empty, update config file path
148        if var_name == "save_name" and value:
149            import os as _os
150
151            _save_folder = _os.path.join(_os.getcwd(), value)
152            config_file = _os.path.join(_save_folder, f"{value}_config.json")
153            self.__dict__["_config_file"] = config_file
154        elif config_file and not self.__dict__.get("_testing_dendrite_capacity", False):
155            self.save_config(config_file)
156
157    def appender(self, value):
158        """Append a value to the property if it is a list.
159
160        Parameters
161        ----------
162        value : Any
163            Value or values to append to the underlying list property.
164
165        Returns
166        -------
167        None
168            Appends items in-place and prints the resulting list value.
169        """
170        if isinstance(getattr(self, private_name), list):
171            if var_name in (
172                "module_ids_to_track",
173                "module_ids_to_perforate",
174                "parameter_ids_to_track",
175            ):
176                for module_id in value:
177                    _validate_module_id(module_id)
178            setattr(self, private_name, getattr(self, private_name) + value)
179            print(
180                'New list value of "{}": {}'.format(
181                    private_name, getattr(self, private_name)
182                )
183            )
184        else:
185            raise TypeError(f"Cannot append to non-list attribute '{var_name}'")
186
187    # Attach methods to the instance
188    if list_type:
189        setattr(obj, f"get_{var_name}", getter_list.__get__(obj))
190    else:
191        setattr(obj, f"get_{var_name}", getter_val.__get__(obj))
192    setattr(obj, f"set_{var_name}", setter.__get__(obj))
193    setattr(obj, f"append_{var_name}", appender.__get__(obj))

Dynamically add a property with getter and setter to an object.

This function adds a private variable along with getter and setter methods to a given object instance. Used for integrating initial and Perforated Backpropagation variables into the PAIConfig class.

Parameters
  • obj (object): The object to which the property will be added.
  • var_name (str): Name of the variable/property to create.
  • initial_value (any): Initial value for the property.
Returns
  • None
Notes

Creates three attributes on obj: - _{var_name}: private storage - get_{var_name}: getter method - set_{var_name}: setter method

class PAIConfig:
 268class PAIConfig:
 269    """Configuration class for PAI settings.
 270
 271    This class manages all configuration parameters for the Perforated AI system,
 272    including device settings, dendrite behavior, module conversion rules,
 273    training parameters, and debugging options.
 274
 275    Attributes
 276    ----------
 277    use_cuda : bool
 278        Whether CUDA is available and should be used.
 279    device : torch.device
 280        The device to use for computation (CPU, CUDA, etc.).
 281    save_name : str
 282        Name used for saving models (should not be set manually).
 283    debugging_output_dimensions : int
 284        Debug level for input dimension checking.
 285    confirm_correct_sizes : bool
 286        Whether to verify tensor sizes during execution.
 287    unwrapped_modules_confirmed : bool
 288        Confirmation flag for using unwrapped modules.
 289    weight_decay_accepted : bool
 290        Confirmation flag for accepting weight decay.
 291    checked_skipped_modules : bool
 292        Whether skipped modules have been verified.
 293    verbose : bool
 294        Enable verbose logging output.
 295    extra_verbose : bool
 296        Enable extra verbose logging output.
 297    silent : bool
 298        Suppress all PAI print statements.
 299    save_old_graph_scores : bool
 300        Whether to save historical graph scores.
 301    testing_dendrite_capacity : bool
 302        Enable dendrite capacity testing mode.
 303    using_safe_tensors : bool
 304        Use safe tensors file format for saving.
 305    global_candidates : int
 306        Number of global candidate dendrites.
 307    drawing_pai : bool
 308        Enable PAI visualization graphs.
 309    test_saves : bool
 310        Save intermediary test models.
 311    pai_saves : bool
 312        Save PAI-specific format models.
 313    output_dimensions : list
 314        Format specification for input tensor dimensions.
 315    improvement_threshold : float
 316        Relative improvement threshold for validation scores.
 317    improvement_threshold_raw : float
 318        Absolute improvement threshold for validation scores.
 319    candidate_weight_initialization_multiplier : float
 320        Multiplier for random dendrite weight initialization.
 321    DOING_SWITCH_EVERY_TIME : int
 322        Constant for switch mode: add dendrites every epoch.
 323    DOING_HISTORY : int
 324        Constant for switch mode: add dendrites based on validation history.
 325    n_epochs_to_switch : int
 326        Number of epochs without improvement before switching.
 327    history_lookback : int
 328        Number of epochs to average for validation history.
 329    initial_history_after_switches : int
 330        Epochs to wait after adding dendrites before beggining checks.
 331    DOING_FIXED_SWITCH : int
 332        Constant for switch mode: add dendrites at fixed intervals.
 333    fixed_switch_num : int
 334        Number of epochs between fixed switches.
 335    first_fixed_switch_num : int
 336        Number of epochs before first switch (for pretraining).
 337    DOING_NO_SWITCH : int
 338        Constant for switch mode: never add dendrites.
 339    switch_mode : int
 340        Current switch mode setting.
 341    reset_best_score_on_switch : bool
 342        Whether to reset best score when adding dendrites.
 343    learn_dendrites_live : bool
 344        Enable live dendrite learning (advanced feature).
 345    no_extra_n_modes : bool
 346        Disable extra neuron modes (advanced feature).
 347    d_type : torch.dtype
 348        Data type for dendrite weights.
 349    retain_all_dendrites : bool
 350        Keep dendrites even if they don't improve performance.
 351    find_best_lr : bool
 352        Automatically sweep learning rates when adding dendrites.
 353    dont_give_up_unless_learning_rate_lowered : bool
 354        Ensure search lowers learning rate at least once.
 355    max_dendrite_tries : int
 356        Maximum attempts to add dendrites with random initializations.
 357    max_dendrites : int
 358        Maximum total number of dendrites to add.
 359    PARAM_VALS_BY_TOTAL_EPOCH : int
 360        Constant: scheduler params tracked by total epochs.
 361    PARAM_VALS_BY_UPDATE_EPOCH : int
 362        Constant: scheduler params reset at each switch.
 363    PARAM_VALS_BY_NEURON_EPOCH_START : int
 364        Constant: scheduler params reset for neuron starts only.
 365    param_vals_setting : int
 366        Current parameter tracking mode.
 367    pai_forward_function : callable
 368        Activation function used for dendrites.
 369    modules_to_perforate : list
 370        Module types to convert to PAI modules for perforation.
 371    module_names_to_perforate : list
 372        Module names to convert to PAI modules for perforation.
 373    module_ids_to_perforate : list
 374        Specific module IDs to convert to PAI modules for perforation.
 375    modules_to_track : list
 376        Module types to track but not convert.
 377    module_names_to_track : list
 378        Module names to track but not convert.
 379    module_ids_to_track : list
 380        Specific module IDs to track but not convert.
 381    modules_to_replace : list
 382        Module types to replace before conversion.
 383    replacement_modules : list
 384        Replacement modules for modules_to_replace.
 385    modules_with_processing : list
 386        Module types requiring custom processing.
 387    modules_processing_classes : list
 388        Processing classes for modules_with_processing.
 389    module_names_with_processing : list
 390        Module names requiring custom processing.
 391    module_by_name_processing_classes : list
 392        Processing classes for module_names_with_processing.
 393    module_names_to_not_save : list
 394        Module names to exclude from saving.
 395    perforated_backpropagation : bool
 396        Whether Perforated Backpropagation is enabled.
 397    """
 398
 399    # Explicit type map for every config variable — used by load_config to coerce JSON values.
 400    # [type] means "list whose elements are class/type objects" (need dotted-name resolution).
 401    _TYPES: dict = {
 402        **{
 403            k: bool
 404            for k in (
 405                "use_cuda",
 406                "confirm_correct_sizes",
 407                "unwrapped_modules_confirmed",
 408                "weight_decay_accepted",
 409                "checked_skipped_modules",
 410                "verbose",
 411                "extra_verbose",
 412                "silent",
 413                "save_old_graph_scores",
 414                "testing_dendrite_capacity",
 415                "using_safe_tensors",
 416                "drawing_pai",
 417                "drawing_extra_graphs",
 418                "test_saves",
 419                "pai_saves",
 420                "reset_best_score_on_switch",
 421                "learn_dendrites_live",
 422                "no_extra_n_modes",
 423                "retain_all_dendrites",
 424                "find_best_lr",
 425                "dont_give_up_unless_learning_rate_lowered",
 426                "candidate_weight_init_by_main",
 427                "perforated_backpropagation",
 428                "weight_tying_experimental",
 429                "dashboard_events_enabled",
 430                "dashboard_debug",
 431            )
 432        },
 433        **{
 434            k: int
 435            for k in (
 436                "debugging_output_dimensions",
 437                "global_candidates",
 438                "n_epochs_to_switch",
 439                "history_lookback",
 440                "initial_history_after_switches",
 441                "fixed_switch_num",
 442                "first_fixed_switch_num",
 443                "switch_mode",
 444                "max_dendrite_tries",
 445                "max_dendrites",
 446                "param_vals_setting",
 447            )
 448        },
 449        **{
 450            k: float
 451            for k in (
 452                "improvement_threshold_raw",
 453                "candidate_weight_initialization_multiplier",
 454            )
 455        },
 456        **{k: str for k in ("save_name", "library_validation_score", "dashboard_url")},
 457        "device": torch.device,
 458        "d_type": torch.dtype,
 459        "pai_forward_function": callable,
 460        **{
 461            k: list
 462            for k in (
 463                "output_dimensions",
 464                "improvement_threshold",
 465                "module_names_to_perforate",
 466                "module_ids_to_perforate",
 467                "module_names_to_track",
 468                "module_ids_to_track",
 469                "parameter_ids_to_track",
 470                "module_names_with_processing",
 471                "module_names_to_not_save",
 472                "library_extra_scores",
 473                "library_extra_scores_without_graphing",
 474            )
 475        },
 476        **{
 477            k: [type]
 478            for k in (
 479                "modules_to_perforate",
 480                "modules_to_track",
 481                "modules_to_replace",
 482                "replacement_modules",
 483                "modules_with_processing",
 484                "modules_processing_classes",
 485                "module_by_name_processing_classes",
 486            )
 487        },
 488    }
 489
 490    # Subset of _TYPES: variables that can be overridden on a per-module basis
 491    # via the Studio.  These are set outside the ``if not module_name:`` block
 492    # in :py:meth:`__init__`, so they are meaningful when constructing a
 493    # module-specific PAIConfig instance.
 494    _CUSTOMIZABLE: dict = {
 495        "verbose": bool,
 496        "extra_verbose": bool,
 497        "silent": bool,
 498        "global_candidates": int,
 499        "output_dimensions": list,
 500        "candidate_weight_initialization_multiplier": float,
 501        "candidate_weight_init_by_main": bool,
 502        "retain_all_dendrites": bool,
 503        "max_dendrites": int,
 504        "pai_forward_function": callable,
 505    }
 506
 507    def __getattr__(self, name):
 508        """Handle missing attributes gracefully, especially for PB variables.
 509
 510        Parameters
 511        ----------
 512        name : str
 513            The name of the attribute being accessed.
 514
 515        Returns
 516        -------
 517        None or raises AttributeError
 518            Returns None for missing set_ methods, raises AttributeError otherwise.
 519        """
 520        if name.startswith("set_"):
 521            print(f"Variable '{name[4:]}' does not exist.  Ignoring set attempt.")
 522            return lambda x: None
 523        if name.startswith("append_"):
 524            print(
 525                f"List Variable '{name[7:]}' does not exist.  Ignoring append attempt."
 526            )
 527            return lambda x: None
 528        if name.startswith("get_") and self.__dict__.get("_module_name") is not None:
 529            # Module-specific config: check for a per-module override stored directly in
 530            # __dict__ (written by load_config when custom JSON data was found for this
 531            # module).  This covers CUSTOMIZABLE vars that are not initialised for
 532            # per-module configs (e.g. output_dimensions, which lives inside the
 533            # ``if not module_name:`` block).
 534            private_key = f"_{name[4:]}"
 535            if private_key in self.__dict__:
 536                stored = self.__dict__[private_key]
 537                return lambda: stored
 538            # Fall back to the global pc instance for vars not set on this instance.
 539            global_getter = getattr(pc, name, None)
 540            if global_getter is not None:
 541                return global_getter
 542        raise AttributeError(
 543            f"'{self.__class__.__name__}' object has no attribute '{name}'"
 544        )
 545
 546    def __init__(self, module_name=None, module_type=None):
 547        """Initialize PAIConfig with default settings.
 548
 549        module_name=None means this is the main global config.
 550        If module_name is set this is a per-module config that loads
 551        custom settings from module_settings[module_name] (by id) or
 552        module_settings[module_type] (by type) in the JSON file.
 553        """
 554        # Must be first: prevents __getattr__ from firing for _config_file
 555        # during construction (before add_pai_config_var_functions sets it).
 556        # Also disables auto-save in setters until the end of __init__.
 557        self.__dict__["_config_file"] = None
 558        # None = global config; any string = per-module config
 559        self.__dict__["_module_name"] = module_name
 560        # Short class name of the wrapped module (e.g. 'Conv2d'), used as
 561        # a fallback lookup key when the specific name has no saved settings.
 562        self.__dict__["_module_type"] = module_type
 563
 564        if not module_name:
 565            ### Global Constants
 566            # Device configuration
 567            self.use_cuda = torch.cuda.is_available()
 568            add_pai_config_var_functions(self, "use_cuda", self.use_cuda)
 569            self.device = torch.device("cuda" if self.use_cuda else "cpu")
 570            add_pai_config_var_functions(self, "device", self.device)
 571
 572            self.save_name = ""
 573            add_pai_config_var_functions(self, "save_name", self.save_name)
 574
 575            # Debug settings
 576            self.debugging_output_dimensions = 0
 577            add_pai_config_var_functions(
 578                self, "debugging_output_dimensions", self.debugging_output_dimensions
 579            )
 580            # Debugging input tensor sizes.
 581            # This will slow things down very slightly and is not necessary but can help
 582            # catch when dimensions were not filled in correctly.
 583            self.confirm_correct_sizes = False
 584            add_pai_config_var_functions(
 585                self, "confirm_correct_sizes", self.confirm_correct_sizes
 586            )
 587
 588            # Confirmation flags for non-recommended options
 589            self.unwrapped_modules_confirmed = False
 590            add_pai_config_var_functions(
 591                self, "unwrapped_modules_confirmed", self.unwrapped_modules_confirmed
 592            )
 593            self.weight_decay_accepted = False
 594            add_pai_config_var_functions(
 595                self, "weight_decay_accepted", self.weight_decay_accepted
 596            )
 597            self.checked_skipped_modules = False
 598            add_pai_config_var_functions(
 599                self, "checked_skipped_modules", self.checked_skipped_modules
 600            )
 601            # Analysis settings
 602            self.save_old_graph_scores = True
 603            add_pai_config_var_functions(
 604                self, "save_old_graph_scores", self.save_old_graph_scores
 605            )
 606            # Testing settings
 607            self.testing_dendrite_capacity = True
 608            add_pai_config_var_functions(
 609                self, "testing_dendrite_capacity", self.testing_dendrite_capacity
 610            )
 611
 612            # File format settings
 613            self.using_safe_tensors = True
 614            add_pai_config_var_functions(
 615                self, "using_safe_tensors", self.using_safe_tensors
 616            )
 617
 618            # Checkpoint loading settings
 619            # Whether to use strict=True when loading state_dict
 620            # Set to False if loading old checkpoints that are missing new fields
 621            self.strict_loading = True
 622            add_pai_config_var_functions(
 623                self, "strict_loading", self.strict_loading
 624            )
 625
 626            # Graph and visualization settings
 627            # A graph setting which can be set to false if you want to do your own
 628            # training visualizations
 629            self.drawing_pai = True
 630            add_pai_config_var_functions(self, "drawing_pai", self.drawing_pai)
 631
 632            # Drawing extra graphs beyond the standard ones.
 633            self.drawing_extra_graphs = True
 634            add_pai_config_var_functions(
 635                self, "drawing_extra_graphs", self.drawing_extra_graphs
 636            )
 637
 638            # Saving test intermediary models, good for experimentation, bad for memory
 639            self.test_saves = True
 640            add_pai_config_var_functions(self, "test_saves", self.test_saves)
 641            # To be filled in later. pai_saves will remove some extra scaffolding for
 642            # slight memory and speed improvements
 643            self.pai_saves = False
 644            add_pai_config_var_functions(self, "pai_saves", self.pai_saves)
 645            # Improvement thresholds
 646            # Percentage improvement increase needed to call a new best validation score
 647            self.improvement_threshold = [0.001, 0.0001, 0.0]
 648            add_pai_config_var_functions(
 649                self, "improvement_threshold", self.improvement_threshold
 650            )
 651
 652            # Raw increase needed
 653            self.improvement_threshold_raw = 1e-5
 654            add_pai_config_var_functions(
 655                self, "improvement_threshold_raw", self.improvement_threshold_raw
 656            )
 657            # SWITCH MODE SETTINGS
 658
 659            # Add dendrites every time to debug implementation
 660            self.DOING_SWITCH_EVERY_TIME = 0
 661
 662            # Switch when validation hasn't improved over x epochs
 663            self.DOING_HISTORY = 1
 664            # Epochs to try before deciding to load previous best and add dendrites
 665            # Be sure this is higher than scheduler patience
 666            self.n_epochs_to_switch = 10
 667            add_pai_config_var_functions(
 668                self, "n_epochs_to_switch", self.n_epochs_to_switch
 669            )
 670            # Number to average validation scores over
 671            self.history_lookback = 1
 672            add_pai_config_var_functions(
 673                self, "history_lookback", self.history_lookback
 674            )
 675            # Amount of epochs to run after adding a new set of dendrites before checking
 676            # to add more
 677            self.initial_history_after_switches = 0
 678            add_pai_config_var_functions(
 679                self,
 680                "initial_history_after_switches",
 681                self.initial_history_after_switches,
 682            )
 683
 684            # Switch after a fixed number of epochs
 685            self.DOING_FIXED_SWITCH = 2
 686            # Number of epochs to complete before switching
 687            self.fixed_switch_num = 250
 688            add_pai_config_var_functions(
 689                self, "fixed_switch_num", self.fixed_switch_num
 690            )
 691            # An additional flag if you want your first switch to occur later than all the
 692            # rest for initial pretraining.  This is a new minimum, if its lower than
 693            # the above it will be ignored.
 694            self.first_fixed_switch_num = 1
 695            add_pai_config_var_functions(
 696                self, "first_fixed_switch_num", self.first_fixed_switch_num
 697            )
 698
 699            # A setting to not add dendrites and just do regular training
 700            # Warning, this will also never trigger training_complete
 701            self.DOING_NO_SWITCH = 3
 702
 703            # Default switch mode
 704            self.switch_mode = self.DOING_HISTORY
 705            add_pai_config_var_functions(self, "switch_mode", self.switch_mode)
 706
 707            # Reset settings
 708            # Resets score on switch
 709            # This can be useful if you need many epochs to catch up to the best score
 710            # from the previous version after adding dendrites
 711            self.reset_best_score_on_switch = True
 712            add_pai_config_var_functions(
 713                self, "reset_best_score_on_switch", self.reset_best_score_on_switch
 714            )
 715
 716            # Advanced settings
 717            # Not used in open source implementation, leave as default
 718            self.learn_dendrites_live = False
 719            add_pai_config_var_functions(
 720                self, "learn_dendrites_live", self.learn_dendrites_live
 721            )
 722            self.no_extra_n_modes = True
 723            add_pai_config_var_functions(
 724                self, "no_extra_n_modes", self.no_extra_n_modes
 725            )
 726
 727            # Data type for new modules and dendrite to dendrite / dendrite to neuron
 728            # weights
 729            self.d_type = torch.float
 730            add_pai_config_var_functions(self, "d_type", self.d_type)
 731
 732            # Learning rate management
 733            # A setting to automatically sweep over previously used learning rates when
 734            # adding new dendrites
 735            # Sometimes it's best to go back to initial LR, but often its best to start
 736            # at a lower LR
 737            self.find_best_lr = True
 738            add_pai_config_var_functions(self, "find_best_lr", self.find_best_lr)
 739            # Enforces the above even if the previous epoch didn't lower the learning rate
 740            self.dont_give_up_unless_learning_rate_lowered = True
 741            add_pai_config_var_functions(
 742                self,
 743                "dont_give_up_unless_learning_rate_lowered",
 744                self.dont_give_up_unless_learning_rate_lowered,
 745            )
 746
 747            # Dendrite attempt settings
 748            # Set to 1 if you want to quit as soon as one dendrite fails
 749            # Higher values will try new random dendrite weights this many times before
 750            # accepting that more dendrites don't improve
 751            self.max_dendrite_tries = 2
 752            add_pai_config_var_functions(
 753                self, "max_dendrite_tries", self.max_dendrite_tries
 754            )
 755
 756            # Scheduler parameter settings
 757            # Have learning rate params be by total epoch
 758            self.PARAM_VALS_BY_TOTAL_EPOCH = 0
 759            # Reset the params at every switch
 760            self.PARAM_VALS_BY_UPDATE_EPOCH = 1
 761            # Reset params for dendrite starts but not for normal restarts
 762            # Not used for open source version
 763            self.PARAM_VALS_BY_NEURON_EPOCH_START = 2
 764            # Default setting
 765            self.param_vals_setting = self.PARAM_VALS_BY_UPDATE_EPOCH
 766            add_pai_config_var_functions(
 767                self, "param_vals_setting", self.param_vals_setting
 768            )
 769            # Lists for module types and names to add dendrites to
 770            # For these lists no specifier means type, name is module name
 771            # and ids is the individual modules id, eg. model.conv2
 772            self.modules_to_perforate = []
 773            add_pai_config_var_functions(
 774                self, "modules_to_perforate", self.modules_to_perforate, list_type=True
 775            )
 776            self.module_names_to_perforate = [
 777                "PAISequential",
 778                "Conv1d",
 779                "Conv2d",
 780                "Conv3d",
 781                "Linear",
 782            ]
 783            add_pai_config_var_functions(
 784                self,
 785                "module_names_to_perforate",
 786                self.module_names_to_perforate,
 787                list_type=True,
 788            )
 789            self.module_ids_to_perforate = []
 790            add_pai_config_var_functions(
 791                self,
 792                "module_ids_to_perforate",
 793                self.module_ids_to_perforate,
 794                list_type=True,
 795            )
 796
 797            # All modules should either be perforated or tracked to ensure all modules
 798            # are accounted for
 799            self.modules_to_track = []
 800            add_pai_config_var_functions(
 801                self, "modules_to_track", self.modules_to_track, list_type=True
 802            )
 803            self.module_names_to_track = []
 804            add_pai_config_var_functions(
 805                self,
 806                "module_names_to_track",
 807                self.module_names_to_track,
 808                list_type=True,
 809            )
 810            # IDs are for if you want to pass only a single module by its assigned ID rather than the module type by name
 811            self.module_ids_to_track = []
 812            add_pai_config_var_functions(
 813                self, "module_ids_to_track", self.module_ids_to_track, list_type=True
 814            )
 815
 816            # Parameter IDs to track as neuron parameters without recursive behavior
 817            # (e.g., [".my_custom_parameter"]).
 818            self.parameter_ids_to_track = []
 819            add_pai_config_var_functions(
 820                self,
 821                "parameter_ids_to_track",
 822                self.parameter_ids_to_track,
 823                list_type=True,
 824            )
 825
 826            # Replacement modules happen before the conversion,
 827            # so replaced modules will then also be run through the conversion steps
 828            # These are for modules that need to be replaced before addition of dendrites
 829            # See the resnet example in models_perforatedai
 830            self.modules_to_replace = []
 831            add_pai_config_var_functions(
 832                self, "modules_to_replace", self.modules_to_replace, list_type=True
 833            )
 834            # Modules to replace the above modules with
 835            self.replacement_modules = []
 836            add_pai_config_var_functions(
 837                self, "replacement_modules", self.replacement_modules, list_type=True
 838            )
 839
 840            # Dendrites default to modules which are one tensor input and one tensor
 841            # output in forward()
 842            # Other modules require to be labeled as modules with processing and assigned
 843            # processing classes
 844            # This can be done by module type or module name see customization.md in API
 845            # for example
 846            self.modules_with_processing = []
 847            add_pai_config_var_functions(
 848                self,
 849                "modules_with_processing",
 850                self.modules_with_processing,
 851                list_type=True,
 852            )
 853            self.modules_processing_classes = []
 854            add_pai_config_var_functions(
 855                self,
 856                "modules_processing_classes",
 857                self.modules_processing_classes,
 858                list_type=True,
 859            )
 860            self.module_names_with_processing = []
 861            add_pai_config_var_functions(
 862                self,
 863                "module_names_with_processing",
 864                self.module_names_with_processing,
 865                list_type=True,
 866            )
 867            self.module_by_name_processing_classes = []
 868            add_pai_config_var_functions(
 869                self,
 870                "module_by_name_processing_classes",
 871                self.module_by_name_processing_classes,
 872                list_type=True,
 873            )
 874
 875            # Similarly here as above. Some huggingface models have multiple pointers to
 876            # the same modules which cause problems
 877            # If you want to only save one of the multiple pointers you can set which ones
 878            # not to save here
 879            self.module_names_to_not_save = [".base_model"]
 880            add_pai_config_var_functions(
 881                self,
 882                "module_names_to_not_save",
 883                self.module_names_to_not_save,
 884                list_type=True,
 885            )
 886
 887            # Perforated Backpropagation settings
 888            self.perforated_backpropagation = False
 889            add_pai_config_var_functions(
 890                self, "perforated_backpropagation", self.perforated_backpropagation
 891            )
 892            
 893            # This is specifically a workaround for weight tying
 894            # Not to be used for a duplicate pointer that isn't actually run twice
 895            self.weight_tying_experimental = False
 896            add_pai_config_var_functions(
 897                self, "weight_tying_experimental", self.weight_tying_experimental
 898            )
 899
 900            # Dashboard event streaming settings
 901            self.dashboard_events_enabled = False
 902            add_pai_config_var_functions(
 903                self, "dashboard_events_enabled", self.dashboard_events_enabled
 904            )
 905            self.dashboard_url = "http://localhost:3002"
 906            add_pai_config_var_functions(
 907                self, "dashboard_url", self.dashboard_url
 908            )
 909            self.dashboard_debug = False
 910            add_pai_config_var_functions(
 911                self, "dashboard_debug", self.dashboard_debug
 912            )
 913
 914            # These are settings where libraries must be doing the scoring adding to
 915            # message from your main script what metric to use
 916            self.library_validation_score = ""
 917            add_pai_config_var_functions(
 918                self, "library_validation_score", self.library_validation_score
 919            )
 920            self.library_extra_scores = []
 921            add_pai_config_var_functions(
 922                self,
 923                "library_extra_scores",
 924                self.library_extra_scores,
 925                list_type=True,
 926            )
 927            self.library_extra_scores_without_graphing = []
 928            add_pai_config_var_functions(
 929                self,
 930                "library_extra_scores_without_graphing",
 931                self.library_extra_scores_without_graphing,
 932                list_type=True,
 933            )
 934
 935            # Input dimensions needs to be set every time. It is set to what format of
 936            # planes you are expecting.
 937            # Neuron index should be set to 0, variable indexes should be set to -1.
 938            # For example, if your format is [batchsize, nodes, x, y]
 939            # output_dimensions is [-1, 0, -1, -1].
 940            # if your format is, [batchsize, time index, nodes] output_dimensions is
 941            # [-1, -1, 0]
 942            self.output_dimensions = [-1, 0, -1, -1]
 943            add_pai_config_var_functions(
 944                self, "output_dimensions", self.output_dimensions, list_type=True
 945            )
 946        # Verbosity settings
 947        self.verbose = False
 948        add_pai_config_var_functions(self, "verbose", self.verbose)
 949        self.extra_verbose = False
 950        add_pai_config_var_functions(self, "extra_verbose", self.extra_verbose)
 951        # Suppress all PAI prints
 952        self.silent = False
 953        add_pai_config_var_functions(self, "silent", self.silent)
 954
 955        # In place for future implementation options of adding multiple candidate
 956        # dendrites together
 957        self.global_candidates = 1
 958        add_pai_config_var_functions(self, "global_candidates", self.global_candidates)
 959
 960        # Weight initialization settings
 961        # Multiplier when randomizing dendrite weights
 962        self.candidate_weight_initialization_multiplier = 0.01
 963        add_pai_config_var_functions(
 964            self,
 965            "candidate_weight_initialization_multiplier",
 966            self.candidate_weight_initialization_multiplier,
 967        )
 968        # Multiplier when randomizing dendrite weights
 969        self.candidate_weight_init_by_main = False
 970        add_pai_config_var_functions(
 971            self,
 972            "candidate_weight_init_by_main",
 973            self.candidate_weight_init_by_main,
 974        )
 975
 976        # Dendrite retention settings
 977        # A setting to keep dendrites even if they do not improve scores
 978        self.retain_all_dendrites = False
 979        add_pai_config_var_functions(
 980            self, "retain_all_dendrites", self.retain_all_dendrites
 981        )
 982
 983        # Max dendrites to add even if they do continue improving scores
 984        self.max_dendrites = 100
 985        add_pai_config_var_functions(self, "max_dendrites", self.max_dendrites)
 986
 987        # Activation function settings
 988        # The activation function to use for dendrites
 989        self.pai_forward_function = torch.sigmoid
 990        add_pai_config_var_functions(
 991            self, "pai_forward_function", self.pai_forward_function
 992        )
 993
 994        # ------------------------------------------------------------------
 995        # Config file will be set when save_name is assigned (in perforate_model)
 996        # ------------------------------------------------------------------
 997        # _config_file stays None until save_name is set to a non-empty value
 998
 999    # ------------------------------------------------------------------
1000
1001    def save_config(self, filename):
1002        """Save the current PAIConfig state to a JSON file.
1003
1004        Parameters
1005        ----------
1006        filename : str
1007            Destination file path (created or overwritten).
1008
1009        Notes
1010        -----
1011        Values that are not natively JSON-serialisable (torch.device,
1012        torch.dtype, nn.Module subclasses, callables) are stored as their
1013        dotted-string representations so they can be round-tripped by
1014        :py:meth:`load_config`.
1015
1016        Returns
1017        -------
1018        None
1019            This function does not return a value.
1020        """
1021        import json
1022
1023        config_dict = {}
1024
1025        # Private-storage vars added by add_pai_config_var_functions
1026        # e.g. self._module_ids_to_perforate → key 'module_ids_to_perforate'
1027        for key, val in sorted(self.__dict__.items()):
1028            if not (key.startswith("_") and not key.startswith("__")):
1029                continue
1030            # Skip internal bookkeeping keys that must not round-trip through JSON
1031            if key in ("_config_file", "_module_name", "_module_type"):
1032                continue
1033            if callable(val):  # skip bound method refs
1034                continue
1035            clean_key = key[1:]
1036            try:
1037                config_dict[clean_key] = _serialize_pai_value(val)
1038            except Exception:
1039                config_dict[clean_key] = str(val)
1040
1041        # Plain constants (DOING_*, PARAM_VALS_BY_*, etc.)
1042        for key, val in self.__dict__.items():
1043            if key.startswith("_") or callable(val):
1044                continue
1045            if key not in config_dict:
1046                try:
1047                    config_dict[key] = _serialize_pai_value(val)
1048                except Exception:
1049                    config_dict[key] = str(val)
1050
1051        # Merge short class names from modules_to_perforate into module_names_to_perforate
1052        # so the UI (and JS) only needs to check one array.
1053        type_short_names = [
1054            cls.__name__
1055            for cls in self.__dict__.get("_modules_to_perforate", [])
1056            if isinstance(cls, type)
1057        ]
1058        existing = config_dict.get("module_names_to_perforate", [])
1059        config_dict["module_names_to_perforate"] = existing + [
1060            n for n in type_short_names if n not in existing
1061        ]
1062
1063        # Preserve any per-module settings written by the Studio frontend.
1064        _existing_ms: dict = {}
1065        try:
1066            with open(filename, "r") as _f:
1067                _existing_ms = json.load(_f).get("module_settings", {})
1068        except Exception:
1069            pass
1070        config_dict["module_settings"] = _existing_ms
1071
1072        # Publish customizable field type names so the Studio frontend can
1073        # render appropriate editors without needing to import the library.
1074        config_dict["_customizable_fields"] = {
1075            k: (v.__name__ if hasattr(v, "__name__") else str(v))
1076            for k, v in PAIConfig._CUSTOMIZABLE.items()
1077        }
1078
1079        # Ensure the directory exists before saving
1080        import os
1081
1082        os.makedirs(os.path.dirname(filename), exist_ok=True)
1083
1084        with open(filename, "w") as f:
1085            json.dump(config_dict, f, indent=2)
1086        print(f"[PAI Config] Saved {len(config_dict)} variables \u2192 {filename}")
1087
1088    def load_config(self, filename, module_name=None, module_type=None):
1089        """Load PAIConfig state from a JSON file produced by :py:meth:`save_config`.
1090
1091        If *module_name* is ``None`` (default) every serialisable variable in
1092        the file is restored on this instance.
1093
1094        If *module_name* is given the lookup priority is:
1095          1. ``module_settings[module_name]`` (exact name / id match)
1096          2. ``module_settings[module_type]``  (type-level fallback)
1097          3. No-op — the defaults already set by ``__init__`` are kept.
1098
1099        Parameters
1100        ----------
1101        filename : str
1102            Path to the JSON file to read.
1103        module_name : str, optional
1104            Display name (id) of the module whose custom settings should be loaded.
1105        module_type : str, optional
1106            Short class name of the module type, used as a fallback key.
1107
1108        Returns
1109        -------
1110        None
1111            This function does not return a value.
1112        """
1113        import json
1114
1115        with open(filename, "r") as f:
1116            config_dict = json.load(f)
1117
1118        if module_name is not None:
1119            # ── Per-module load ──────────────────────────────────────────────
1120            module_settings = config_dict.get("module_settings", {})
1121            # Priority: exact name → type fallback → no-op
1122            if module_name in module_settings:
1123                custom = module_settings[module_name]
1124                resolved_key = module_name
1125            elif module_type and module_type in module_settings:
1126                custom = module_settings[module_type]
1127                resolved_key = module_type
1128            else:
1129                # No custom settings for this module or type — keep defaults.
1130                return
1131            loaded = 0
1132            skipped = 0
1133            for key, json_val in custom.items():
1134                if key not in PAIConfig._CUSTOMIZABLE:
1135                    continue
1136                type_hint = PAIConfig._TYPES.get(key)
1137                private_key = f"_{key}"
1138                # Write directly to __dict__ so we bypass any setter guards and also
1139                # correctly handle vars that are not pre-initialised on per-module
1140                # configs (e.g. output_dimensions, which only lives inside the
1141                # ``if not module_name:`` block of __init__).
1142                try:
1143                    self.__dict__[private_key] = (
1144                        _deserialize_pai_value(json_val, type_hint)
1145                        if type_hint is not None
1146                        else json_val
1147                    )
1148                    loaded += 1
1149                except Exception as exc:
1150                    print(
1151                        f"[PAI Config] Warning: could not load '{key}' for '{resolved_key}': {exc}"
1152                    )
1153                    skipped += 1
1154            print(
1155                f"[PAI Config] Loaded {loaded} custom vars for '{resolved_key}' from {filename}"
1156                + (f" ({skipped} skipped)" if skipped else "")
1157            )
1158            return
1159
1160        # ── Global load: every variable in the JSON ──────────────────────────
1161        loaded = 0
1162        skipped = 0
1163        for key, json_val in config_dict.items():
1164            # Skip internal bookkeeping and Studio-only metadata keys.
1165            # 'module_name' and 'module_type' must never overwrite the
1166            # instance's _module_name/_module_type (they are internal only).
1167            if key in (
1168                "config_file",
1169                "module_settings",
1170                "module_name",
1171                "module_type",
1172            ) or key.startswith("_"):
1173                continue
1174            type_hint = PAIConfig._TYPES.get(key)
1175            private_key = f"_{key}"
1176            if hasattr(self, private_key):
1177                try:
1178                    setattr(
1179                        self,
1180                        private_key,
1181                        (
1182                            _deserialize_pai_value(json_val, type_hint)
1183                            if type_hint is not None
1184                            else json_val
1185                        ),
1186                    )
1187                    loaded += 1
1188                except Exception as exc:
1189                    print(f"[PAI Config] Warning: could not load '{key}': {exc}")
1190                    skipped += 1
1191            elif hasattr(self, key) and not callable(getattr(self, key, None)):
1192                try:
1193                    setattr(self, key, json_val)
1194                    loaded += 1
1195                except Exception:
1196                    skipped += 1
1197
1198        print(
1199            f"[PAI Config] Loaded {loaded} variables from {filename}"
1200            + (f" ({skipped} skipped)" if skipped else "")
1201        )

Configuration class for PAI settings.

This class manages all configuration parameters for the Perforated AI system, including device settings, dendrite behavior, module conversion rules, training parameters, and debugging options.

Attributes
  • use_cuda (bool): Whether CUDA is available and should be used.
  • device (torch.device): The device to use for computation (CPU, CUDA, etc.).
  • save_name (str): Name used for saving models (should not be set manually).
  • debugging_output_dimensions (int): Debug level for input dimension checking.
  • confirm_correct_sizes (bool): Whether to verify tensor sizes during execution.
  • unwrapped_modules_confirmed (bool): Confirmation flag for using unwrapped modules.
  • weight_decay_accepted (bool): Confirmation flag for accepting weight decay.
  • checked_skipped_modules (bool): Whether skipped modules have been verified.
  • verbose (bool): Enable verbose logging output.
  • extra_verbose (bool): Enable extra verbose logging output.
  • silent (bool): Suppress all PAI print statements.
  • save_old_graph_scores (bool): Whether to save historical graph scores.
  • testing_dendrite_capacity (bool): Enable dendrite capacity testing mode.
  • using_safe_tensors (bool): Use safe tensors file format for saving.
  • global_candidates (int): Number of global candidate dendrites.
  • drawing_pai (bool): Enable PAI visualization graphs.
  • test_saves (bool): Save intermediary test models.
  • pai_saves (bool): Save PAI-specific format models.
  • output_dimensions (list): Format specification for input tensor dimensions.
  • improvement_threshold (float): Relative improvement threshold for validation scores.
  • improvement_threshold_raw (float): Absolute improvement threshold for validation scores.
  • candidate_weight_initialization_multiplier (float): Multiplier for random dendrite weight initialization.
  • DOING_SWITCH_EVERY_TIME (int): Constant for switch mode: add dendrites every epoch.
  • DOING_HISTORY (int): Constant for switch mode: add dendrites based on validation history.
  • n_epochs_to_switch (int): Number of epochs without improvement before switching.
  • history_lookback (int): Number of epochs to average for validation history.
  • initial_history_after_switches (int): Epochs to wait after adding dendrites before beggining checks.
  • DOING_FIXED_SWITCH (int): Constant for switch mode: add dendrites at fixed intervals.
  • fixed_switch_num (int): Number of epochs between fixed switches.
  • first_fixed_switch_num (int): Number of epochs before first switch (for pretraining).
  • DOING_NO_SWITCH (int): Constant for switch mode: never add dendrites.
  • switch_mode (int): Current switch mode setting.
  • reset_best_score_on_switch (bool): Whether to reset best score when adding dendrites.
  • learn_dendrites_live (bool): Enable live dendrite learning (advanced feature).
  • no_extra_n_modes (bool): Disable extra neuron modes (advanced feature).
  • d_type (torch.dtype): Data type for dendrite weights.
  • retain_all_dendrites (bool): Keep dendrites even if they don't improve performance.
  • find_best_lr (bool): Automatically sweep learning rates when adding dendrites.
  • dont_give_up_unless_learning_rate_lowered (bool): Ensure search lowers learning rate at least once.
  • max_dendrite_tries (int): Maximum attempts to add dendrites with random initializations.
  • max_dendrites (int): Maximum total number of dendrites to add.
  • PARAM_VALS_BY_TOTAL_EPOCH (int): Constant: scheduler params tracked by total epochs.
  • PARAM_VALS_BY_UPDATE_EPOCH (int): Constant: scheduler params reset at each switch.
  • PARAM_VALS_BY_NEURON_EPOCH_START (int): Constant: scheduler params reset for neuron starts only.
  • param_vals_setting (int): Current parameter tracking mode.
  • pai_forward_function (callable): Activation function used for dendrites.
  • modules_to_perforate (list): Module types to convert to PAI modules for perforation.
  • module_names_to_perforate (list): Module names to convert to PAI modules for perforation.
  • module_ids_to_perforate (list): Specific module IDs to convert to PAI modules for perforation.
  • modules_to_track (list): Module types to track but not convert.
  • module_names_to_track (list): Module names to track but not convert.
  • module_ids_to_track (list): Specific module IDs to track but not convert.
  • modules_to_replace (list): Module types to replace before conversion.
  • replacement_modules (list): Replacement modules for modules_to_replace.
  • modules_with_processing (list): Module types requiring custom processing.
  • modules_processing_classes (list): Processing classes for modules_with_processing.
  • module_names_with_processing (list): Module names requiring custom processing.
  • module_by_name_processing_classes (list): Processing classes for module_names_with_processing.
  • module_names_to_not_save (list): Module names to exclude from saving.
  • perforated_backpropagation (bool): Whether Perforated Backpropagation is enabled.
PAIConfig(module_name=None, module_type=None)
546    def __init__(self, module_name=None, module_type=None):
547        """Initialize PAIConfig with default settings.
548
549        module_name=None means this is the main global config.
550        If module_name is set this is a per-module config that loads
551        custom settings from module_settings[module_name] (by id) or
552        module_settings[module_type] (by type) in the JSON file.
553        """
554        # Must be first: prevents __getattr__ from firing for _config_file
555        # during construction (before add_pai_config_var_functions sets it).
556        # Also disables auto-save in setters until the end of __init__.
557        self.__dict__["_config_file"] = None
558        # None = global config; any string = per-module config
559        self.__dict__["_module_name"] = module_name
560        # Short class name of the wrapped module (e.g. 'Conv2d'), used as
561        # a fallback lookup key when the specific name has no saved settings.
562        self.__dict__["_module_type"] = module_type
563
564        if not module_name:
565            ### Global Constants
566            # Device configuration
567            self.use_cuda = torch.cuda.is_available()
568            add_pai_config_var_functions(self, "use_cuda", self.use_cuda)
569            self.device = torch.device("cuda" if self.use_cuda else "cpu")
570            add_pai_config_var_functions(self, "device", self.device)
571
572            self.save_name = ""
573            add_pai_config_var_functions(self, "save_name", self.save_name)
574
575            # Debug settings
576            self.debugging_output_dimensions = 0
577            add_pai_config_var_functions(
578                self, "debugging_output_dimensions", self.debugging_output_dimensions
579            )
580            # Debugging input tensor sizes.
581            # This will slow things down very slightly and is not necessary but can help
582            # catch when dimensions were not filled in correctly.
583            self.confirm_correct_sizes = False
584            add_pai_config_var_functions(
585                self, "confirm_correct_sizes", self.confirm_correct_sizes
586            )
587
588            # Confirmation flags for non-recommended options
589            self.unwrapped_modules_confirmed = False
590            add_pai_config_var_functions(
591                self, "unwrapped_modules_confirmed", self.unwrapped_modules_confirmed
592            )
593            self.weight_decay_accepted = False
594            add_pai_config_var_functions(
595                self, "weight_decay_accepted", self.weight_decay_accepted
596            )
597            self.checked_skipped_modules = False
598            add_pai_config_var_functions(
599                self, "checked_skipped_modules", self.checked_skipped_modules
600            )
601            # Analysis settings
602            self.save_old_graph_scores = True
603            add_pai_config_var_functions(
604                self, "save_old_graph_scores", self.save_old_graph_scores
605            )
606            # Testing settings
607            self.testing_dendrite_capacity = True
608            add_pai_config_var_functions(
609                self, "testing_dendrite_capacity", self.testing_dendrite_capacity
610            )
611
612            # File format settings
613            self.using_safe_tensors = True
614            add_pai_config_var_functions(
615                self, "using_safe_tensors", self.using_safe_tensors
616            )
617
618            # Checkpoint loading settings
619            # Whether to use strict=True when loading state_dict
620            # Set to False if loading old checkpoints that are missing new fields
621            self.strict_loading = True
622            add_pai_config_var_functions(
623                self, "strict_loading", self.strict_loading
624            )
625
626            # Graph and visualization settings
627            # A graph setting which can be set to false if you want to do your own
628            # training visualizations
629            self.drawing_pai = True
630            add_pai_config_var_functions(self, "drawing_pai", self.drawing_pai)
631
632            # Drawing extra graphs beyond the standard ones.
633            self.drawing_extra_graphs = True
634            add_pai_config_var_functions(
635                self, "drawing_extra_graphs", self.drawing_extra_graphs
636            )
637
638            # Saving test intermediary models, good for experimentation, bad for memory
639            self.test_saves = True
640            add_pai_config_var_functions(self, "test_saves", self.test_saves)
641            # To be filled in later. pai_saves will remove some extra scaffolding for
642            # slight memory and speed improvements
643            self.pai_saves = False
644            add_pai_config_var_functions(self, "pai_saves", self.pai_saves)
645            # Improvement thresholds
646            # Percentage improvement increase needed to call a new best validation score
647            self.improvement_threshold = [0.001, 0.0001, 0.0]
648            add_pai_config_var_functions(
649                self, "improvement_threshold", self.improvement_threshold
650            )
651
652            # Raw increase needed
653            self.improvement_threshold_raw = 1e-5
654            add_pai_config_var_functions(
655                self, "improvement_threshold_raw", self.improvement_threshold_raw
656            )
657            # SWITCH MODE SETTINGS
658
659            # Add dendrites every time to debug implementation
660            self.DOING_SWITCH_EVERY_TIME = 0
661
662            # Switch when validation hasn't improved over x epochs
663            self.DOING_HISTORY = 1
664            # Epochs to try before deciding to load previous best and add dendrites
665            # Be sure this is higher than scheduler patience
666            self.n_epochs_to_switch = 10
667            add_pai_config_var_functions(
668                self, "n_epochs_to_switch", self.n_epochs_to_switch
669            )
670            # Number to average validation scores over
671            self.history_lookback = 1
672            add_pai_config_var_functions(
673                self, "history_lookback", self.history_lookback
674            )
675            # Amount of epochs to run after adding a new set of dendrites before checking
676            # to add more
677            self.initial_history_after_switches = 0
678            add_pai_config_var_functions(
679                self,
680                "initial_history_after_switches",
681                self.initial_history_after_switches,
682            )
683
684            # Switch after a fixed number of epochs
685            self.DOING_FIXED_SWITCH = 2
686            # Number of epochs to complete before switching
687            self.fixed_switch_num = 250
688            add_pai_config_var_functions(
689                self, "fixed_switch_num", self.fixed_switch_num
690            )
691            # An additional flag if you want your first switch to occur later than all the
692            # rest for initial pretraining.  This is a new minimum, if its lower than
693            # the above it will be ignored.
694            self.first_fixed_switch_num = 1
695            add_pai_config_var_functions(
696                self, "first_fixed_switch_num", self.first_fixed_switch_num
697            )
698
699            # A setting to not add dendrites and just do regular training
700            # Warning, this will also never trigger training_complete
701            self.DOING_NO_SWITCH = 3
702
703            # Default switch mode
704            self.switch_mode = self.DOING_HISTORY
705            add_pai_config_var_functions(self, "switch_mode", self.switch_mode)
706
707            # Reset settings
708            # Resets score on switch
709            # This can be useful if you need many epochs to catch up to the best score
710            # from the previous version after adding dendrites
711            self.reset_best_score_on_switch = True
712            add_pai_config_var_functions(
713                self, "reset_best_score_on_switch", self.reset_best_score_on_switch
714            )
715
716            # Advanced settings
717            # Not used in open source implementation, leave as default
718            self.learn_dendrites_live = False
719            add_pai_config_var_functions(
720                self, "learn_dendrites_live", self.learn_dendrites_live
721            )
722            self.no_extra_n_modes = True
723            add_pai_config_var_functions(
724                self, "no_extra_n_modes", self.no_extra_n_modes
725            )
726
727            # Data type for new modules and dendrite to dendrite / dendrite to neuron
728            # weights
729            self.d_type = torch.float
730            add_pai_config_var_functions(self, "d_type", self.d_type)
731
732            # Learning rate management
733            # A setting to automatically sweep over previously used learning rates when
734            # adding new dendrites
735            # Sometimes it's best to go back to initial LR, but often its best to start
736            # at a lower LR
737            self.find_best_lr = True
738            add_pai_config_var_functions(self, "find_best_lr", self.find_best_lr)
739            # Enforces the above even if the previous epoch didn't lower the learning rate
740            self.dont_give_up_unless_learning_rate_lowered = True
741            add_pai_config_var_functions(
742                self,
743                "dont_give_up_unless_learning_rate_lowered",
744                self.dont_give_up_unless_learning_rate_lowered,
745            )
746
747            # Dendrite attempt settings
748            # Set to 1 if you want to quit as soon as one dendrite fails
749            # Higher values will try new random dendrite weights this many times before
750            # accepting that more dendrites don't improve
751            self.max_dendrite_tries = 2
752            add_pai_config_var_functions(
753                self, "max_dendrite_tries", self.max_dendrite_tries
754            )
755
756            # Scheduler parameter settings
757            # Have learning rate params be by total epoch
758            self.PARAM_VALS_BY_TOTAL_EPOCH = 0
759            # Reset the params at every switch
760            self.PARAM_VALS_BY_UPDATE_EPOCH = 1
761            # Reset params for dendrite starts but not for normal restarts
762            # Not used for open source version
763            self.PARAM_VALS_BY_NEURON_EPOCH_START = 2
764            # Default setting
765            self.param_vals_setting = self.PARAM_VALS_BY_UPDATE_EPOCH
766            add_pai_config_var_functions(
767                self, "param_vals_setting", self.param_vals_setting
768            )
769            # Lists for module types and names to add dendrites to
770            # For these lists no specifier means type, name is module name
771            # and ids is the individual modules id, eg. model.conv2
772            self.modules_to_perforate = []
773            add_pai_config_var_functions(
774                self, "modules_to_perforate", self.modules_to_perforate, list_type=True
775            )
776            self.module_names_to_perforate = [
777                "PAISequential",
778                "Conv1d",
779                "Conv2d",
780                "Conv3d",
781                "Linear",
782            ]
783            add_pai_config_var_functions(
784                self,
785                "module_names_to_perforate",
786                self.module_names_to_perforate,
787                list_type=True,
788            )
789            self.module_ids_to_perforate = []
790            add_pai_config_var_functions(
791                self,
792                "module_ids_to_perforate",
793                self.module_ids_to_perforate,
794                list_type=True,
795            )
796
797            # All modules should either be perforated or tracked to ensure all modules
798            # are accounted for
799            self.modules_to_track = []
800            add_pai_config_var_functions(
801                self, "modules_to_track", self.modules_to_track, list_type=True
802            )
803            self.module_names_to_track = []
804            add_pai_config_var_functions(
805                self,
806                "module_names_to_track",
807                self.module_names_to_track,
808                list_type=True,
809            )
810            # IDs are for if you want to pass only a single module by its assigned ID rather than the module type by name
811            self.module_ids_to_track = []
812            add_pai_config_var_functions(
813                self, "module_ids_to_track", self.module_ids_to_track, list_type=True
814            )
815
816            # Parameter IDs to track as neuron parameters without recursive behavior
817            # (e.g., [".my_custom_parameter"]).
818            self.parameter_ids_to_track = []
819            add_pai_config_var_functions(
820                self,
821                "parameter_ids_to_track",
822                self.parameter_ids_to_track,
823                list_type=True,
824            )
825
826            # Replacement modules happen before the conversion,
827            # so replaced modules will then also be run through the conversion steps
828            # These are for modules that need to be replaced before addition of dendrites
829            # See the resnet example in models_perforatedai
830            self.modules_to_replace = []
831            add_pai_config_var_functions(
832                self, "modules_to_replace", self.modules_to_replace, list_type=True
833            )
834            # Modules to replace the above modules with
835            self.replacement_modules = []
836            add_pai_config_var_functions(
837                self, "replacement_modules", self.replacement_modules, list_type=True
838            )
839
840            # Dendrites default to modules which are one tensor input and one tensor
841            # output in forward()
842            # Other modules require to be labeled as modules with processing and assigned
843            # processing classes
844            # This can be done by module type or module name see customization.md in API
845            # for example
846            self.modules_with_processing = []
847            add_pai_config_var_functions(
848                self,
849                "modules_with_processing",
850                self.modules_with_processing,
851                list_type=True,
852            )
853            self.modules_processing_classes = []
854            add_pai_config_var_functions(
855                self,
856                "modules_processing_classes",
857                self.modules_processing_classes,
858                list_type=True,
859            )
860            self.module_names_with_processing = []
861            add_pai_config_var_functions(
862                self,
863                "module_names_with_processing",
864                self.module_names_with_processing,
865                list_type=True,
866            )
867            self.module_by_name_processing_classes = []
868            add_pai_config_var_functions(
869                self,
870                "module_by_name_processing_classes",
871                self.module_by_name_processing_classes,
872                list_type=True,
873            )
874
875            # Similarly here as above. Some huggingface models have multiple pointers to
876            # the same modules which cause problems
877            # If you want to only save one of the multiple pointers you can set which ones
878            # not to save here
879            self.module_names_to_not_save = [".base_model"]
880            add_pai_config_var_functions(
881                self,
882                "module_names_to_not_save",
883                self.module_names_to_not_save,
884                list_type=True,
885            )
886
887            # Perforated Backpropagation settings
888            self.perforated_backpropagation = False
889            add_pai_config_var_functions(
890                self, "perforated_backpropagation", self.perforated_backpropagation
891            )
892            
893            # This is specifically a workaround for weight tying
894            # Not to be used for a duplicate pointer that isn't actually run twice
895            self.weight_tying_experimental = False
896            add_pai_config_var_functions(
897                self, "weight_tying_experimental", self.weight_tying_experimental
898            )
899
900            # Dashboard event streaming settings
901            self.dashboard_events_enabled = False
902            add_pai_config_var_functions(
903                self, "dashboard_events_enabled", self.dashboard_events_enabled
904            )
905            self.dashboard_url = "http://localhost:3002"
906            add_pai_config_var_functions(
907                self, "dashboard_url", self.dashboard_url
908            )
909            self.dashboard_debug = False
910            add_pai_config_var_functions(
911                self, "dashboard_debug", self.dashboard_debug
912            )
913
914            # These are settings where libraries must be doing the scoring adding to
915            # message from your main script what metric to use
916            self.library_validation_score = ""
917            add_pai_config_var_functions(
918                self, "library_validation_score", self.library_validation_score
919            )
920            self.library_extra_scores = []
921            add_pai_config_var_functions(
922                self,
923                "library_extra_scores",
924                self.library_extra_scores,
925                list_type=True,
926            )
927            self.library_extra_scores_without_graphing = []
928            add_pai_config_var_functions(
929                self,
930                "library_extra_scores_without_graphing",
931                self.library_extra_scores_without_graphing,
932                list_type=True,
933            )
934
935            # Input dimensions needs to be set every time. It is set to what format of
936            # planes you are expecting.
937            # Neuron index should be set to 0, variable indexes should be set to -1.
938            # For example, if your format is [batchsize, nodes, x, y]
939            # output_dimensions is [-1, 0, -1, -1].
940            # if your format is, [batchsize, time index, nodes] output_dimensions is
941            # [-1, -1, 0]
942            self.output_dimensions = [-1, 0, -1, -1]
943            add_pai_config_var_functions(
944                self, "output_dimensions", self.output_dimensions, list_type=True
945            )
946        # Verbosity settings
947        self.verbose = False
948        add_pai_config_var_functions(self, "verbose", self.verbose)
949        self.extra_verbose = False
950        add_pai_config_var_functions(self, "extra_verbose", self.extra_verbose)
951        # Suppress all PAI prints
952        self.silent = False
953        add_pai_config_var_functions(self, "silent", self.silent)
954
955        # In place for future implementation options of adding multiple candidate
956        # dendrites together
957        self.global_candidates = 1
958        add_pai_config_var_functions(self, "global_candidates", self.global_candidates)
959
960        # Weight initialization settings
961        # Multiplier when randomizing dendrite weights
962        self.candidate_weight_initialization_multiplier = 0.01
963        add_pai_config_var_functions(
964            self,
965            "candidate_weight_initialization_multiplier",
966            self.candidate_weight_initialization_multiplier,
967        )
968        # Multiplier when randomizing dendrite weights
969        self.candidate_weight_init_by_main = False
970        add_pai_config_var_functions(
971            self,
972            "candidate_weight_init_by_main",
973            self.candidate_weight_init_by_main,
974        )
975
976        # Dendrite retention settings
977        # A setting to keep dendrites even if they do not improve scores
978        self.retain_all_dendrites = False
979        add_pai_config_var_functions(
980            self, "retain_all_dendrites", self.retain_all_dendrites
981        )
982
983        # Max dendrites to add even if they do continue improving scores
984        self.max_dendrites = 100
985        add_pai_config_var_functions(self, "max_dendrites", self.max_dendrites)
986
987        # Activation function settings
988        # The activation function to use for dendrites
989        self.pai_forward_function = torch.sigmoid
990        add_pai_config_var_functions(
991            self, "pai_forward_function", self.pai_forward_function
992        )
993
994        # ------------------------------------------------------------------
995        # Config file will be set when save_name is assigned (in perforate_model)
996        # ------------------------------------------------------------------
997        # _config_file stays None until save_name is set to a non-empty value

Initialize PAIConfig with default settings.

module_name=None means this is the main global config. If module_name is set this is a per-module config that loads custom settings from module_settings[module_name] (by id) or module_settings[module_type] (by type) in the JSON file.

verbose
extra_verbose
silent
global_candidates
candidate_weight_initialization_multiplier
candidate_weight_init_by_main
retain_all_dendrites
max_dendrites
pai_forward_function
def save_config(self, filename):
1001    def save_config(self, filename):
1002        """Save the current PAIConfig state to a JSON file.
1003
1004        Parameters
1005        ----------
1006        filename : str
1007            Destination file path (created or overwritten).
1008
1009        Notes
1010        -----
1011        Values that are not natively JSON-serialisable (torch.device,
1012        torch.dtype, nn.Module subclasses, callables) are stored as their
1013        dotted-string representations so they can be round-tripped by
1014        :py:meth:`load_config`.
1015
1016        Returns
1017        -------
1018        None
1019            This function does not return a value.
1020        """
1021        import json
1022
1023        config_dict = {}
1024
1025        # Private-storage vars added by add_pai_config_var_functions
1026        # e.g. self._module_ids_to_perforate → key 'module_ids_to_perforate'
1027        for key, val in sorted(self.__dict__.items()):
1028            if not (key.startswith("_") and not key.startswith("__")):
1029                continue
1030            # Skip internal bookkeeping keys that must not round-trip through JSON
1031            if key in ("_config_file", "_module_name", "_module_type"):
1032                continue
1033            if callable(val):  # skip bound method refs
1034                continue
1035            clean_key = key[1:]
1036            try:
1037                config_dict[clean_key] = _serialize_pai_value(val)
1038            except Exception:
1039                config_dict[clean_key] = str(val)
1040
1041        # Plain constants (DOING_*, PARAM_VALS_BY_*, etc.)
1042        for key, val in self.__dict__.items():
1043            if key.startswith("_") or callable(val):
1044                continue
1045            if key not in config_dict:
1046                try:
1047                    config_dict[key] = _serialize_pai_value(val)
1048                except Exception:
1049                    config_dict[key] = str(val)
1050
1051        # Merge short class names from modules_to_perforate into module_names_to_perforate
1052        # so the UI (and JS) only needs to check one array.
1053        type_short_names = [
1054            cls.__name__
1055            for cls in self.__dict__.get("_modules_to_perforate", [])
1056            if isinstance(cls, type)
1057        ]
1058        existing = config_dict.get("module_names_to_perforate", [])
1059        config_dict["module_names_to_perforate"] = existing + [
1060            n for n in type_short_names if n not in existing
1061        ]
1062
1063        # Preserve any per-module settings written by the Studio frontend.
1064        _existing_ms: dict = {}
1065        try:
1066            with open(filename, "r") as _f:
1067                _existing_ms = json.load(_f).get("module_settings", {})
1068        except Exception:
1069            pass
1070        config_dict["module_settings"] = _existing_ms
1071
1072        # Publish customizable field type names so the Studio frontend can
1073        # render appropriate editors without needing to import the library.
1074        config_dict["_customizable_fields"] = {
1075            k: (v.__name__ if hasattr(v, "__name__") else str(v))
1076            for k, v in PAIConfig._CUSTOMIZABLE.items()
1077        }
1078
1079        # Ensure the directory exists before saving
1080        import os
1081
1082        os.makedirs(os.path.dirname(filename), exist_ok=True)
1083
1084        with open(filename, "w") as f:
1085            json.dump(config_dict, f, indent=2)
1086        print(f"[PAI Config] Saved {len(config_dict)} variables \u2192 {filename}")

Save the current PAIConfig state to a JSON file.

Parameters
  • filename (str): Destination file path (created or overwritten).
Notes

Values that are not natively JSON-serialisable (torch.device, torch.dtype, nn.Module subclasses, callables) are stored as their dotted-string representations so they can be round-tripped by load_config().

Returns
  • None: This function does not return a value.
def load_config(self, filename, module_name=None, module_type=None):
1088    def load_config(self, filename, module_name=None, module_type=None):
1089        """Load PAIConfig state from a JSON file produced by :py:meth:`save_config`.
1090
1091        If *module_name* is ``None`` (default) every serialisable variable in
1092        the file is restored on this instance.
1093
1094        If *module_name* is given the lookup priority is:
1095          1. ``module_settings[module_name]`` (exact name / id match)
1096          2. ``module_settings[module_type]``  (type-level fallback)
1097          3. No-op — the defaults already set by ``__init__`` are kept.
1098
1099        Parameters
1100        ----------
1101        filename : str
1102            Path to the JSON file to read.
1103        module_name : str, optional
1104            Display name (id) of the module whose custom settings should be loaded.
1105        module_type : str, optional
1106            Short class name of the module type, used as a fallback key.
1107
1108        Returns
1109        -------
1110        None
1111            This function does not return a value.
1112        """
1113        import json
1114
1115        with open(filename, "r") as f:
1116            config_dict = json.load(f)
1117
1118        if module_name is not None:
1119            # ── Per-module load ──────────────────────────────────────────────
1120            module_settings = config_dict.get("module_settings", {})
1121            # Priority: exact name → type fallback → no-op
1122            if module_name in module_settings:
1123                custom = module_settings[module_name]
1124                resolved_key = module_name
1125            elif module_type and module_type in module_settings:
1126                custom = module_settings[module_type]
1127                resolved_key = module_type
1128            else:
1129                # No custom settings for this module or type — keep defaults.
1130                return
1131            loaded = 0
1132            skipped = 0
1133            for key, json_val in custom.items():
1134                if key not in PAIConfig._CUSTOMIZABLE:
1135                    continue
1136                type_hint = PAIConfig._TYPES.get(key)
1137                private_key = f"_{key}"
1138                # Write directly to __dict__ so we bypass any setter guards and also
1139                # correctly handle vars that are not pre-initialised on per-module
1140                # configs (e.g. output_dimensions, which only lives inside the
1141                # ``if not module_name:`` block of __init__).
1142                try:
1143                    self.__dict__[private_key] = (
1144                        _deserialize_pai_value(json_val, type_hint)
1145                        if type_hint is not None
1146                        else json_val
1147                    )
1148                    loaded += 1
1149                except Exception as exc:
1150                    print(
1151                        f"[PAI Config] Warning: could not load '{key}' for '{resolved_key}': {exc}"
1152                    )
1153                    skipped += 1
1154            print(
1155                f"[PAI Config] Loaded {loaded} custom vars for '{resolved_key}' from {filename}"
1156                + (f" ({skipped} skipped)" if skipped else "")
1157            )
1158            return
1159
1160        # ── Global load: every variable in the JSON ──────────────────────────
1161        loaded = 0
1162        skipped = 0
1163        for key, json_val in config_dict.items():
1164            # Skip internal bookkeeping and Studio-only metadata keys.
1165            # 'module_name' and 'module_type' must never overwrite the
1166            # instance's _module_name/_module_type (they are internal only).
1167            if key in (
1168                "config_file",
1169                "module_settings",
1170                "module_name",
1171                "module_type",
1172            ) or key.startswith("_"):
1173                continue
1174            type_hint = PAIConfig._TYPES.get(key)
1175            private_key = f"_{key}"
1176            if hasattr(self, private_key):
1177                try:
1178                    setattr(
1179                        self,
1180                        private_key,
1181                        (
1182                            _deserialize_pai_value(json_val, type_hint)
1183                            if type_hint is not None
1184                            else json_val
1185                        ),
1186                    )
1187                    loaded += 1
1188                except Exception as exc:
1189                    print(f"[PAI Config] Warning: could not load '{key}': {exc}")
1190                    skipped += 1
1191            elif hasattr(self, key) and not callable(getattr(self, key, None)):
1192                try:
1193                    setattr(self, key, json_val)
1194                    loaded += 1
1195                except Exception:
1196                    skipped += 1
1197
1198        print(
1199            f"[PAI Config] Loaded {loaded} variables from {filename}"
1200            + (f" ({skipped} skipped)" if skipped else "")
1201        )

Load PAIConfig state from a JSON file produced by save_config().

If module_name is None (default) every serialisable variable in the file is restored on this instance.

If module_name is given the lookup priority is:

  1. module_settings[module_name] (exact name / id match)
  2. module_settings[module_type] (type-level fallback)
  3. No-op — the defaults already set by __init__ are kept.
Parameters
  • filename (str): Path to the JSON file to read.
  • module_name (str, optional): Display name (id) of the module whose custom settings should be loaded.
  • module_type (str, optional): Short class name of the module type, used as a fallback key.
Returns
  • None: This function does not return a value.
class PAISequential(torch.nn.modules.container.Sequential):
1204class PAISequential(nn.Sequential):
1205    """Sequential module wrapper for PAI.
1206
1207    This wrapper takes an array of layers and creates a sequential container
1208    that is compatible with PAI's dendrite addition system. It should be used
1209    for normalization layers and can be used for final output layers.
1210
1211    Parameters
1212    ----------
1213    layer_array : list
1214        List of PyTorch nn.Module objects to be executed sequentially.
1215
1216    Examples
1217    --------
1218    >>> layers = [nn.Linear(2 * hidden_dim, seq_width),
1219    ...           nn.LayerNorm(seq_width)]
1220    >>> sequential_block = PAISequential(layers)
1221
1222    Notes
1223    -----
1224    This should be used for:
1225        - All normalization layers (LayerNorm, BatchNorm, etc.)
1226    This can be used for:
1227        - Final output layer and softmax combinations
1228    """
1229
1230    def __init__(self, layer_array):
1231        """Initialize PAISequential with a list of layers.
1232
1233        Parameters
1234        ----------
1235        layer_array : list
1236            List of PyTorch modules to execute in sequence.
1237        """
1238        super(PAISequential, self).__init__()
1239        self.model = nn.Sequential(*layer_array)
1240
1241    def forward(self, *args, **kwargs):
1242        """Forward pass through the sequential layers.
1243
1244        Parameters
1245        ----------
1246        *args
1247            Positional arguments passed to the first layer.
1248        **kwargs
1249            Keyword arguments passed to the layers.
1250
1251        Returns
1252        -------
1253        torch.Tensor
1254            Output from the final layer in the sequence.
1255        """
1256        return self.model(*args, **kwargs)

Sequential module wrapper for PAI.

This wrapper takes an array of layers and creates a sequential container that is compatible with PAI's dendrite addition system. It should be used for normalization layers and can be used for final output layers.

Parameters
  • layer_array (list): List of PyTorch nn.Module objects to be executed sequentially.
Examples
>>> layers = [nn.Linear(2 * hidden_dim, seq_width),
...           nn.LayerNorm(seq_width)]
>>> sequential_block = PAISequential(layers)
Notes

This should be used for: - All normalization layers (LayerNorm, BatchNorm, etc.) This can be used for: - Final output layer and softmax combinations

PAISequential(layer_array)
1230    def __init__(self, layer_array):
1231        """Initialize PAISequential with a list of layers.
1232
1233        Parameters
1234        ----------
1235        layer_array : list
1236            List of PyTorch modules to execute in sequence.
1237        """
1238        super(PAISequential, self).__init__()
1239        self.model = nn.Sequential(*layer_array)

Initialize PAISequential with a list of layers.

Parameters
  • layer_array (list): List of PyTorch modules to execute in sequence.
model
def forward(self, *args, **kwargs):
1241    def forward(self, *args, **kwargs):
1242        """Forward pass through the sequential layers.
1243
1244        Parameters
1245        ----------
1246        *args
1247            Positional arguments passed to the first layer.
1248        **kwargs
1249            Keyword arguments passed to the layers.
1250
1251        Returns
1252        -------
1253        torch.Tensor
1254            Output from the final layer in the sequence.
1255        """
1256        return self.model(*args, **kwargs)

Forward pass through the sequential layers.

Parameters
  • *args: Positional arguments passed to the first layer.
  • **kwargs: Keyword arguments passed to the layers.
Returns
  • torch.Tensor: Output from the final layer in the sequence.
pc = <PAIConfig object>

Global PAIConfig instance.

This is the primary configuration object used throughout the PAI system. Modify settings through this instance to control PAI behavior.

pai_tracker = []
pai_scaler = None