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 __getstate__(self):
 507        """Tell pickle what to save when this object is serialized (e.g. torch.save).
 508
 509        The get_*/set_*/append_* methods attached to each PAIConfig instance are
 510        locally-defined closures and cannot be pickled.  Only the underlying data
 511        values (stored as _<name> in __dict__) need to be saved; __setstate__
 512        will recreate the methods by calling __init__ on load.
 513        """
 514        import types
 515
 516        # Walk every attribute on this instance and keep only the plain data values.
 517        # Bound methods (the get_*/set_*/append_* closures) are skipped because
 518        # they cannot be serialized by pickle.
 519        pickle_safe_state = {}
 520        for attr_name, attr_value in self.__dict__.items():
 521            if not isinstance(attr_value, types.MethodType):
 522                pickle_safe_state[attr_name] = attr_value
 523
 524        return pickle_safe_state
 525
 526    def __setstate__(self, saved_state):
 527        """Restore this object from a pickled state (e.g. torch.load).
 528
 529        Calls __init__ to rebuild all the get_*/set_*/append_* methods, then
 530        overlays the saved data values on top so all settings are preserved.
 531        """
 532        # Grab the identity fields needed to reconstruct the right flavor of config
 533        # (global config vs. per-module config).
 534        saved_module_name = saved_state.get("_module_name")
 535        saved_module_type = saved_state.get("_module_type")
 536
 537        # Re-run __init__ so all the get_*/set_*/append_* methods exist again.
 538        # This also sets default values for every variable, which we overwrite below.
 539        self.__init__(module_name=saved_module_name, module_type=saved_module_type)
 540
 541        # Overwrite the defaults with the actual saved values.
 542        # Only restore private data attributes (prefixed with _); skip the identity
 543        # fields we already handled above, and skip _config_file for now so that
 544        # the setter calls above do not trigger auto-save during the restore.
 545        skip_on_first_pass = {"_module_name", "_module_type", "_config_file"}
 546        for attr_name, attr_value in saved_state.items():
 547            if attr_name.startswith("_") and attr_name not in skip_on_first_pass:
 548                self.__dict__[attr_name] = attr_value
 549
 550        # Restore the config file path last.  Auto-save is driven by _config_file
 551        # being set, so we only put it back after all values are already in place.
 552        if saved_state.get("_config_file"):
 553            self.__dict__["_config_file"] = saved_state["_config_file"]
 554
 555    def __getattr__(self, name):
 556        """Handle missing attributes gracefully, especially for PB variables.
 557
 558        Parameters
 559        ----------
 560        name : str
 561            The name of the attribute being accessed.
 562
 563        Returns
 564        -------
 565        None or raises AttributeError
 566            Returns None for missing set_ methods, raises AttributeError otherwise.
 567        """
 568        if name.startswith("set_"):
 569            print(f"Variable '{name[4:]}' does not exist.  Ignoring set attempt.")
 570            return lambda x: None
 571        if name.startswith("append_"):
 572            print(
 573                f"List Variable '{name[7:]}' does not exist.  Ignoring append attempt."
 574            )
 575            return lambda x: None
 576        if name.startswith("get_") and self.__dict__.get("_module_name") is not None:
 577            # Module-specific config: check for a per-module override stored directly in
 578            # __dict__ (written by load_config when custom JSON data was found for this
 579            # module).  This covers CUSTOMIZABLE vars that are not initialised for
 580            # per-module configs (e.g. output_dimensions, which lives inside the
 581            # ``if not module_name:`` block).
 582            private_key = f"_{name[4:]}"
 583            if private_key in self.__dict__:
 584                stored = self.__dict__[private_key]
 585                return lambda: stored
 586            # Fall back to the global pc instance for vars not set on this instance.
 587            global_getter = getattr(pc, name, None)
 588            if global_getter is not None:
 589                return global_getter
 590        raise AttributeError(
 591            f"'{self.__class__.__name__}' object has no attribute '{name}'"
 592        )
 593
 594    def __init__(self, module_name=None, module_type=None):
 595        """Initialize PAIConfig with default settings.
 596
 597        module_name=None means this is the main global config.
 598        If module_name is set this is a per-module config that loads
 599        custom settings from module_settings[module_name] (by id) or
 600        module_settings[module_type] (by type) in the JSON file.
 601        """
 602        # Must be first: prevents __getattr__ from firing for _config_file
 603        # during construction (before add_pai_config_var_functions sets it).
 604        # Also disables auto-save in setters until the end of __init__.
 605        self.__dict__["_config_file"] = None
 606        # None = global config; any string = per-module config
 607        self.__dict__["_module_name"] = module_name
 608        # Short class name of the wrapped module (e.g. 'Conv2d'), used as
 609        # a fallback lookup key when the specific name has no saved settings.
 610        self.__dict__["_module_type"] = module_type
 611
 612        if not module_name:
 613            ### Global Constants
 614            # Device configuration
 615            self.use_cuda = torch.cuda.is_available()
 616            add_pai_config_var_functions(self, "use_cuda", self.use_cuda)
 617            self.device = torch.device("cuda" if self.use_cuda else "cpu")
 618            add_pai_config_var_functions(self, "device", self.device)
 619
 620            self.save_name = ""
 621            add_pai_config_var_functions(self, "save_name", self.save_name)
 622
 623            # Debug settings
 624            self.debugging_output_dimensions = 0
 625            add_pai_config_var_functions(
 626                self, "debugging_output_dimensions", self.debugging_output_dimensions
 627            )
 628            # Debugging input tensor sizes.
 629            # This will slow things down very slightly and is not necessary but can help
 630            # catch when dimensions were not filled in correctly.
 631            self.confirm_correct_sizes = False
 632            add_pai_config_var_functions(
 633                self, "confirm_correct_sizes", self.confirm_correct_sizes
 634            )
 635
 636            # Confirmation flags for non-recommended options
 637            self.unwrapped_modules_confirmed = False
 638            add_pai_config_var_functions(
 639                self, "unwrapped_modules_confirmed", self.unwrapped_modules_confirmed
 640            )
 641            self.weight_decay_accepted = False
 642            add_pai_config_var_functions(
 643                self, "weight_decay_accepted", self.weight_decay_accepted
 644            )
 645            self.checked_skipped_modules = False
 646            add_pai_config_var_functions(
 647                self, "checked_skipped_modules", self.checked_skipped_modules
 648            )
 649            # Analysis settings
 650            self.save_old_graph_scores = True
 651            add_pai_config_var_functions(
 652                self, "save_old_graph_scores", self.save_old_graph_scores
 653            )
 654            # Testing settings
 655            self.testing_dendrite_capacity = True
 656            add_pai_config_var_functions(
 657                self, "testing_dendrite_capacity", self.testing_dendrite_capacity
 658            )
 659
 660            # File format settings
 661            self.using_safe_tensors = True
 662            add_pai_config_var_functions(
 663                self, "using_safe_tensors", self.using_safe_tensors
 664            )
 665
 666            # Checkpoint loading settings
 667            # Whether to use strict=True when loading state_dict
 668            # Set to False if loading old checkpoints that are missing new fields
 669            self.strict_loading = True
 670            add_pai_config_var_functions(
 671                self, "strict_loading", self.strict_loading
 672            )
 673
 674            # Graph and visualization settings
 675            # A graph setting which can be set to false if you want to do your own
 676            # training visualizations
 677            self.drawing_pai = True
 678            add_pai_config_var_functions(self, "drawing_pai", self.drawing_pai)
 679
 680            # Drawing extra graphs beyond the standard ones.
 681            self.drawing_extra_graphs = True
 682            add_pai_config_var_functions(
 683                self, "drawing_extra_graphs", self.drawing_extra_graphs
 684            )
 685
 686            # Saving test intermediary models, good for experimentation, bad for memory
 687            self.test_saves = True
 688            add_pai_config_var_functions(self, "test_saves", self.test_saves)
 689            # To be filled in later. pai_saves will remove some extra scaffolding for
 690            # slight memory and speed improvements
 691            self.pai_saves = False
 692            add_pai_config_var_functions(self, "pai_saves", self.pai_saves)
 693            # Improvement thresholds
 694            # Percentage improvement increase needed to call a new best validation score
 695            self.improvement_threshold = [0.001, 0.0001, 0.0]
 696            add_pai_config_var_functions(
 697                self, "improvement_threshold", self.improvement_threshold
 698            )
 699
 700            # Raw increase needed
 701            self.improvement_threshold_raw = 1e-5
 702            add_pai_config_var_functions(
 703                self, "improvement_threshold_raw", self.improvement_threshold_raw
 704            )
 705            # SWITCH MODE SETTINGS
 706
 707            # Add dendrites every time to debug implementation
 708            self.DOING_SWITCH_EVERY_TIME = 0
 709
 710            # Switch when validation hasn't improved over x epochs
 711            self.DOING_HISTORY = 1
 712            # Epochs to try before deciding to load previous best and add dendrites
 713            # Be sure this is higher than scheduler patience
 714            self.n_epochs_to_switch = 10
 715            add_pai_config_var_functions(
 716                self, "n_epochs_to_switch", self.n_epochs_to_switch
 717            )
 718            # Number to average validation scores over
 719            self.history_lookback = 1
 720            add_pai_config_var_functions(
 721                self, "history_lookback", self.history_lookback
 722            )
 723            # Amount of epochs to run after adding a new set of dendrites before checking
 724            # to add more
 725            self.initial_history_after_switches = 0
 726            add_pai_config_var_functions(
 727                self,
 728                "initial_history_after_switches",
 729                self.initial_history_after_switches,
 730            )
 731
 732            # Switch after a fixed number of epochs
 733            self.DOING_FIXED_SWITCH = 2
 734            # Number of epochs to complete before switching
 735            self.fixed_switch_num = 250
 736            add_pai_config_var_functions(
 737                self, "fixed_switch_num", self.fixed_switch_num
 738            )
 739            # An additional flag if you want your first switch to occur later than all the
 740            # rest for initial pretraining.  This is a new minimum, if its lower than
 741            # the above it will be ignored.
 742            self.first_fixed_switch_num = 1
 743            add_pai_config_var_functions(
 744                self, "first_fixed_switch_num", self.first_fixed_switch_num
 745            )
 746
 747            # A setting to not add dendrites and just do regular training
 748            # Warning, this will also never trigger training_complete
 749            self.DOING_NO_SWITCH = 3
 750
 751            # Default switch mode
 752            self.switch_mode = self.DOING_HISTORY
 753            add_pai_config_var_functions(self, "switch_mode", self.switch_mode)
 754
 755            # Reset settings
 756            # Resets score on switch
 757            # This can be useful if you need many epochs to catch up to the best score
 758            # from the previous version after adding dendrites
 759            self.reset_best_score_on_switch = True
 760            add_pai_config_var_functions(
 761                self, "reset_best_score_on_switch", self.reset_best_score_on_switch
 762            )
 763
 764            # Advanced settings
 765            # Not used in open source implementation, leave as default
 766            self.learn_dendrites_live = False
 767            add_pai_config_var_functions(
 768                self, "learn_dendrites_live", self.learn_dendrites_live
 769            )
 770            self.no_extra_n_modes = True
 771            add_pai_config_var_functions(
 772                self, "no_extra_n_modes", self.no_extra_n_modes
 773            )
 774
 775            # Data type for new modules and dendrite to dendrite / dendrite to neuron
 776            # weights
 777            self.d_type = torch.float
 778            add_pai_config_var_functions(self, "d_type", self.d_type)
 779
 780            # Learning rate management
 781            # A setting to automatically sweep over previously used learning rates when
 782            # adding new dendrites
 783            # Sometimes it's best to go back to initial LR, but often its best to start
 784            # at a lower LR
 785            self.find_best_lr = True
 786            add_pai_config_var_functions(self, "find_best_lr", self.find_best_lr)
 787            # Enforces the above even if the previous epoch didn't lower the learning rate
 788            self.dont_give_up_unless_learning_rate_lowered = True
 789            add_pai_config_var_functions(
 790                self,
 791                "dont_give_up_unless_learning_rate_lowered",
 792                self.dont_give_up_unless_learning_rate_lowered,
 793            )
 794
 795            # Dendrite attempt settings
 796            # Set to 1 if you want to quit as soon as one dendrite fails
 797            # Higher values will try new random dendrite weights this many times before
 798            # accepting that more dendrites don't improve
 799            self.max_dendrite_tries = 2
 800            add_pai_config_var_functions(
 801                self, "max_dendrite_tries", self.max_dendrite_tries
 802            )
 803
 804            # Scheduler parameter settings
 805            # Have learning rate params be by total epoch
 806            self.PARAM_VALS_BY_TOTAL_EPOCH = 0
 807            # Reset the params at every switch
 808            self.PARAM_VALS_BY_UPDATE_EPOCH = 1
 809            # Reset params for dendrite starts but not for normal restarts
 810            # Not used for open source version
 811            self.PARAM_VALS_BY_NEURON_EPOCH_START = 2
 812            # Default setting
 813            self.param_vals_setting = self.PARAM_VALS_BY_UPDATE_EPOCH
 814            add_pai_config_var_functions(
 815                self, "param_vals_setting", self.param_vals_setting
 816            )
 817            # Lists for module types and names to add dendrites to
 818            # For these lists no specifier means type, name is module name
 819            # and ids is the individual modules id, eg. model.conv2
 820            self.modules_to_perforate = []
 821            add_pai_config_var_functions(
 822                self, "modules_to_perforate", self.modules_to_perforate, list_type=True
 823            )
 824            self.module_names_to_perforate = [
 825                "PAISequential",
 826                "Conv1d",
 827                "Conv2d",
 828                "Conv3d",
 829                "Linear",
 830            ]
 831            add_pai_config_var_functions(
 832                self,
 833                "module_names_to_perforate",
 834                self.module_names_to_perforate,
 835                list_type=True,
 836            )
 837            self.module_ids_to_perforate = []
 838            add_pai_config_var_functions(
 839                self,
 840                "module_ids_to_perforate",
 841                self.module_ids_to_perforate,
 842                list_type=True,
 843            )
 844
 845            # All modules should either be perforated or tracked to ensure all modules
 846            # are accounted for
 847            self.modules_to_track = []
 848            add_pai_config_var_functions(
 849                self, "modules_to_track", self.modules_to_track, list_type=True
 850            )
 851            self.module_names_to_track = []
 852            add_pai_config_var_functions(
 853                self,
 854                "module_names_to_track",
 855                self.module_names_to_track,
 856                list_type=True,
 857            )
 858            # IDs are for if you want to pass only a single module by its assigned ID rather than the module type by name
 859            self.module_ids_to_track = []
 860            add_pai_config_var_functions(
 861                self, "module_ids_to_track", self.module_ids_to_track, list_type=True
 862            )
 863
 864            # Parameter IDs to track as neuron parameters without recursive behavior
 865            # (e.g., [".my_custom_parameter"]).
 866            self.parameter_ids_to_track = []
 867            add_pai_config_var_functions(
 868                self,
 869                "parameter_ids_to_track",
 870                self.parameter_ids_to_track,
 871                list_type=True,
 872            )
 873
 874            # Replacement modules happen before the conversion,
 875            # so replaced modules will then also be run through the conversion steps
 876            # These are for modules that need to be replaced before addition of dendrites
 877            # See the resnet example in models_perforatedai
 878            self.modules_to_replace = []
 879            add_pai_config_var_functions(
 880                self, "modules_to_replace", self.modules_to_replace, list_type=True
 881            )
 882            # Modules to replace the above modules with
 883            self.replacement_modules = []
 884            add_pai_config_var_functions(
 885                self, "replacement_modules", self.replacement_modules, list_type=True
 886            )
 887
 888            # Dendrites default to modules which are one tensor input and one tensor
 889            # output in forward()
 890            # Other modules require to be labeled as modules with processing and assigned
 891            # processing classes
 892            # This can be done by module type or module name see customization.md in API
 893            # for example
 894            self.modules_with_processing = []
 895            add_pai_config_var_functions(
 896                self,
 897                "modules_with_processing",
 898                self.modules_with_processing,
 899                list_type=True,
 900            )
 901            self.modules_processing_classes = []
 902            add_pai_config_var_functions(
 903                self,
 904                "modules_processing_classes",
 905                self.modules_processing_classes,
 906                list_type=True,
 907            )
 908            self.module_names_with_processing = []
 909            add_pai_config_var_functions(
 910                self,
 911                "module_names_with_processing",
 912                self.module_names_with_processing,
 913                list_type=True,
 914            )
 915            self.module_by_name_processing_classes = []
 916            add_pai_config_var_functions(
 917                self,
 918                "module_by_name_processing_classes",
 919                self.module_by_name_processing_classes,
 920                list_type=True,
 921            )
 922
 923            # Similarly here as above. Some huggingface models have multiple pointers to
 924            # the same modules which cause problems
 925            # If you want to only save one of the multiple pointers you can set which ones
 926            # not to save here
 927            self.module_names_to_not_save = [".base_model"]
 928            add_pai_config_var_functions(
 929                self,
 930                "module_names_to_not_save",
 931                self.module_names_to_not_save,
 932                list_type=True,
 933            )
 934
 935            # Perforated Backpropagation settings
 936            self.perforated_backpropagation = False
 937            add_pai_config_var_functions(
 938                self, "perforated_backpropagation", self.perforated_backpropagation
 939            )
 940            
 941            # This is specifically a workaround for weight tying
 942            # Not to be used for a duplicate pointer that isn't actually run twice
 943            self.weight_tying_experimental = False
 944            add_pai_config_var_functions(
 945                self, "weight_tying_experimental", self.weight_tying_experimental
 946            )
 947
 948            # Dashboard event streaming settings
 949            self.dashboard_events_enabled = False
 950            add_pai_config_var_functions(
 951                self, "dashboard_events_enabled", self.dashboard_events_enabled
 952            )
 953            self.dashboard_url = "http://localhost:3002"
 954            add_pai_config_var_functions(
 955                self, "dashboard_url", self.dashboard_url
 956            )
 957            self.dashboard_debug = False
 958            add_pai_config_var_functions(
 959                self, "dashboard_debug", self.dashboard_debug
 960            )
 961
 962            # These are settings where libraries must be doing the scoring adding to
 963            # message from your main script what metric to use
 964            self.library_validation_score = ""
 965            add_pai_config_var_functions(
 966                self, "library_validation_score", self.library_validation_score
 967            )
 968            self.library_extra_scores = []
 969            add_pai_config_var_functions(
 970                self,
 971                "library_extra_scores",
 972                self.library_extra_scores,
 973                list_type=True,
 974            )
 975            self.library_extra_scores_without_graphing = []
 976            add_pai_config_var_functions(
 977                self,
 978                "library_extra_scores_without_graphing",
 979                self.library_extra_scores_without_graphing,
 980                list_type=True,
 981            )
 982
 983            # Input dimensions needs to be set every time. It is set to what format of
 984            # planes you are expecting.
 985            # Neuron index should be set to 0, variable indexes should be set to -1.
 986            # For example, if your format is [batchsize, nodes, x, y]
 987            # output_dimensions is [-1, 0, -1, -1].
 988            # if your format is, [batchsize, time index, nodes] output_dimensions is
 989            # [-1, -1, 0]
 990            self.output_dimensions = [-1, 0, -1, -1]
 991            add_pai_config_var_functions(
 992                self, "output_dimensions", self.output_dimensions, list_type=True
 993            )
 994        # Verbosity settings
 995        self.verbose = False
 996        add_pai_config_var_functions(self, "verbose", self.verbose)
 997        self.extra_verbose = False
 998        add_pai_config_var_functions(self, "extra_verbose", self.extra_verbose)
 999        # Suppress all PAI prints
1000        self.silent = False
1001        add_pai_config_var_functions(self, "silent", self.silent)
1002
1003        # In place for future implementation options of adding multiple candidate
1004        # dendrites together
1005        self.global_candidates = 1
1006        add_pai_config_var_functions(self, "global_candidates", self.global_candidates)
1007
1008        # Weight initialization settings
1009        # Multiplier when randomizing dendrite weights
1010        self.candidate_weight_initialization_multiplier = 0.01
1011        add_pai_config_var_functions(
1012            self,
1013            "candidate_weight_initialization_multiplier",
1014            self.candidate_weight_initialization_multiplier,
1015        )
1016        # Multiplier when randomizing dendrite weights
1017        self.candidate_weight_init_by_main = False
1018        add_pai_config_var_functions(
1019            self,
1020            "candidate_weight_init_by_main",
1021            self.candidate_weight_init_by_main,
1022        )
1023
1024        # Dendrite retention settings
1025        # A setting to keep dendrites even if they do not improve scores
1026        self.retain_all_dendrites = False
1027        add_pai_config_var_functions(
1028            self, "retain_all_dendrites", self.retain_all_dendrites
1029        )
1030
1031        # Max dendrites to add even if they do continue improving scores
1032        self.max_dendrites = 100
1033        add_pai_config_var_functions(self, "max_dendrites", self.max_dendrites)
1034
1035        # Activation function settings
1036        # The activation function to use for dendrites
1037        self.pai_forward_function = torch.sigmoid
1038        add_pai_config_var_functions(
1039            self, "pai_forward_function", self.pai_forward_function
1040        )
1041
1042        # ------------------------------------------------------------------
1043        # Config file will be set when save_name is assigned (in perforate_model)
1044        # ------------------------------------------------------------------
1045        # _config_file stays None until save_name is set to a non-empty value
1046
1047    # ------------------------------------------------------------------
1048
1049    def save_config(self, filename):
1050        """Save the current PAIConfig state to a JSON file.
1051
1052        Parameters
1053        ----------
1054        filename : str
1055            Destination file path (created or overwritten).
1056
1057        Notes
1058        -----
1059        Values that are not natively JSON-serialisable (torch.device,
1060        torch.dtype, nn.Module subclasses, callables) are stored as their
1061        dotted-string representations so they can be round-tripped by
1062        :py:meth:`load_config`.
1063
1064        Returns
1065        -------
1066        None
1067            This function does not return a value.
1068        """
1069        import json
1070
1071        config_dict = {}
1072
1073        # Private-storage vars added by add_pai_config_var_functions
1074        # e.g. self._module_ids_to_perforate → key 'module_ids_to_perforate'
1075        for key, val in sorted(self.__dict__.items()):
1076            if not (key.startswith("_") and not key.startswith("__")):
1077                continue
1078            # Skip internal bookkeeping keys that must not round-trip through JSON
1079            if key in ("_config_file", "_module_name", "_module_type"):
1080                continue
1081            if callable(val):  # skip bound method refs
1082                continue
1083            clean_key = key[1:]
1084            try:
1085                config_dict[clean_key] = _serialize_pai_value(val)
1086            except Exception:
1087                config_dict[clean_key] = str(val)
1088
1089        # Plain constants (DOING_*, PARAM_VALS_BY_*, etc.)
1090        for key, val in self.__dict__.items():
1091            if key.startswith("_") or callable(val):
1092                continue
1093            if key not in config_dict:
1094                try:
1095                    config_dict[key] = _serialize_pai_value(val)
1096                except Exception:
1097                    config_dict[key] = str(val)
1098
1099        # Merge short class names from modules_to_perforate into module_names_to_perforate
1100        # so the UI (and JS) only needs to check one array.
1101        type_short_names = [
1102            cls.__name__
1103            for cls in self.__dict__.get("_modules_to_perforate", [])
1104            if isinstance(cls, type)
1105        ]
1106        existing = config_dict.get("module_names_to_perforate", [])
1107        config_dict["module_names_to_perforate"] = existing + [
1108            n for n in type_short_names if n not in existing
1109        ]
1110
1111        # Preserve any per-module settings written by the Studio frontend.
1112        _existing_ms: dict = {}
1113        try:
1114            with open(filename, "r") as _f:
1115                _existing_ms = json.load(_f).get("module_settings", {})
1116        except Exception:
1117            pass
1118        config_dict["module_settings"] = _existing_ms
1119
1120        # Publish customizable field type names so the Studio frontend can
1121        # render appropriate editors without needing to import the library.
1122        config_dict["_customizable_fields"] = {
1123            k: (v.__name__ if hasattr(v, "__name__") else str(v))
1124            for k, v in PAIConfig._CUSTOMIZABLE.items()
1125        }
1126
1127        # Ensure the directory exists before saving
1128        import os
1129
1130        os.makedirs(os.path.dirname(filename), exist_ok=True)
1131
1132        with open(filename, "w") as f:
1133            json.dump(config_dict, f, indent=2)
1134        print(f"[PAI Config] Saved {len(config_dict)} variables \u2192 {filename}")
1135
1136    def load_config(self, filename, module_name=None, module_type=None):
1137        """Load PAIConfig state from a JSON file produced by :py:meth:`save_config`.
1138
1139        If *module_name* is ``None`` (default) every serialisable variable in
1140        the file is restored on this instance.
1141
1142        If *module_name* is given the lookup priority is:
1143          1. ``module_settings[module_name]`` (exact name / id match)
1144          2. ``module_settings[module_type]``  (type-level fallback)
1145          3. No-op — the defaults already set by ``__init__`` are kept.
1146
1147        Parameters
1148        ----------
1149        filename : str
1150            Path to the JSON file to read.
1151        module_name : str, optional
1152            Display name (id) of the module whose custom settings should be loaded.
1153        module_type : str, optional
1154            Short class name of the module type, used as a fallback key.
1155
1156        Returns
1157        -------
1158        None
1159            This function does not return a value.
1160        """
1161        import json
1162
1163        with open(filename, "r") as f:
1164            config_dict = json.load(f)
1165
1166        if module_name is not None:
1167            # ── Per-module load ──────────────────────────────────────────────
1168            module_settings = config_dict.get("module_settings", {})
1169            # Priority: exact name → type fallback → no-op
1170            if module_name in module_settings:
1171                custom = module_settings[module_name]
1172                resolved_key = module_name
1173            elif module_type and module_type in module_settings:
1174                custom = module_settings[module_type]
1175                resolved_key = module_type
1176            else:
1177                # No custom settings for this module or type — keep defaults.
1178                return
1179            loaded = 0
1180            skipped = 0
1181            for key, json_val in custom.items():
1182                if key not in PAIConfig._CUSTOMIZABLE:
1183                    continue
1184                type_hint = PAIConfig._TYPES.get(key)
1185                private_key = f"_{key}"
1186                # Write directly to __dict__ so we bypass any setter guards and also
1187                # correctly handle vars that are not pre-initialised on per-module
1188                # configs (e.g. output_dimensions, which only lives inside the
1189                # ``if not module_name:`` block of __init__).
1190                try:
1191                    self.__dict__[private_key] = (
1192                        _deserialize_pai_value(json_val, type_hint)
1193                        if type_hint is not None
1194                        else json_val
1195                    )
1196                    loaded += 1
1197                except Exception as exc:
1198                    print(
1199                        f"[PAI Config] Warning: could not load '{key}' for '{resolved_key}': {exc}"
1200                    )
1201                    skipped += 1
1202            print(
1203                f"[PAI Config] Loaded {loaded} custom vars for '{resolved_key}' from {filename}"
1204                + (f" ({skipped} skipped)" if skipped else "")
1205            )
1206            return
1207
1208        # ── Global load: every variable in the JSON ──────────────────────────
1209        loaded = 0
1210        skipped = 0
1211        for key, json_val in config_dict.items():
1212            # Skip internal bookkeeping and Studio-only metadata keys.
1213            # 'module_name' and 'module_type' must never overwrite the
1214            # instance's _module_name/_module_type (they are internal only).
1215            if key in (
1216                "config_file",
1217                "module_settings",
1218                "module_name",
1219                "module_type",
1220            ) or key.startswith("_"):
1221                continue
1222            type_hint = PAIConfig._TYPES.get(key)
1223            private_key = f"_{key}"
1224            if hasattr(self, private_key):
1225                try:
1226                    setattr(
1227                        self,
1228                        private_key,
1229                        (
1230                            _deserialize_pai_value(json_val, type_hint)
1231                            if type_hint is not None
1232                            else json_val
1233                        ),
1234                    )
1235                    loaded += 1
1236                except Exception as exc:
1237                    print(f"[PAI Config] Warning: could not load '{key}': {exc}")
1238                    skipped += 1
1239            elif hasattr(self, key) and not callable(getattr(self, key, None)):
1240                try:
1241                    setattr(self, key, json_val)
1242                    loaded += 1
1243                except Exception:
1244                    skipped += 1
1245
1246        print(
1247            f"[PAI Config] Loaded {loaded} variables from {filename}"
1248            + (f" ({skipped} skipped)" if skipped else "")
1249        )
1250
1251
1252class PAISequential(nn.Sequential):
1253    """Sequential module wrapper for PAI.
1254
1255    This wrapper takes an array of layers and creates a sequential container
1256    that is compatible with PAI's dendrite addition system. It should be used
1257    for normalization layers and can be used for final output layers.
1258
1259    Parameters
1260    ----------
1261    layer_array : list
1262        List of PyTorch nn.Module objects to be executed sequentially.
1263
1264    Examples
1265    --------
1266    >>> layers = [nn.Linear(2 * hidden_dim, seq_width),
1267    ...           nn.LayerNorm(seq_width)]
1268    >>> sequential_block = PAISequential(layers)
1269
1270    Notes
1271    -----
1272    This should be used for:
1273        - All normalization layers (LayerNorm, BatchNorm, etc.)
1274    This can be used for:
1275        - Final output layer and softmax combinations
1276    """
1277
1278    def __init__(self, layer_array):
1279        """Initialize PAISequential with a list of layers.
1280
1281        Parameters
1282        ----------
1283        layer_array : list
1284            List of PyTorch modules to execute in sequence.
1285        """
1286        super(PAISequential, self).__init__()
1287        self.model = nn.Sequential(*layer_array)
1288
1289    def forward(self, *args, **kwargs):
1290        """Forward pass through the sequential layers.
1291
1292        Parameters
1293        ----------
1294        *args
1295            Positional arguments passed to the first layer.
1296        **kwargs
1297            Keyword arguments passed to the layers.
1298
1299        Returns
1300        -------
1301        torch.Tensor
1302            Output from the final layer in the sequence.
1303        """
1304        return self.model(*args, **kwargs)
1305
1306
1307### Global objects and variables
1308
1309### Global Modules
1310pc = PAIConfig()
1311"""Global PAIConfig instance.
1312
1313This is the primary configuration object used throughout the PAI system.
1314Modify settings through this instance to control PAI behavior.
1315"""
1316
1317"""Pointer to the PAI Tracker.
1318
1319This will be populated with the PAI Tracker instance which handles
1320the addition of dendrites during training. Initially an empty list.
1321"""
1322pai_tracker = []
1323
1324pai_scaler = None
1325
1326# This will be set to true if perforated backpropagation is available
1327# Do not just set this to True without the library and a license, it will cause errors
1328try:
1329    import perforatedbp.globals_pbp as perforatedbp_globals
1330
1331    print("Building dendrites with Perforated Backpropagation")
1332
1333    pc.set_perforated_backpropagation(True)
1334    # This is default to True for open source version
1335    # But defaults to False for perforated backpropagation
1336    pc.set_no_extra_n_modes(False)
1337
1338    # Loop through the vars module's attributes and add them dynamically
1339    for var_name in dir(perforatedbp_globals):
1340        if not var_name.startswith("_"):
1341            add_pai_config_var_functions(
1342                pc, var_name, getattr(perforatedbp_globals, var_name)
1343            )
1344
1345    # Merge PBP type hints into PAIConfig._TYPES so load_config can correctly
1346    # round-trip all perforatedbp variables from JSON.
1347    if hasattr(perforatedbp_globals, "_TYPES"):
1348        PAIConfig._TYPES.update(perforatedbp_globals._TYPES)
1349
1350except ImportError:
1351    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 __getstate__(self):
 508        """Tell pickle what to save when this object is serialized (e.g. torch.save).
 509
 510        The get_*/set_*/append_* methods attached to each PAIConfig instance are
 511        locally-defined closures and cannot be pickled.  Only the underlying data
 512        values (stored as _<name> in __dict__) need to be saved; __setstate__
 513        will recreate the methods by calling __init__ on load.
 514        """
 515        import types
 516
 517        # Walk every attribute on this instance and keep only the plain data values.
 518        # Bound methods (the get_*/set_*/append_* closures) are skipped because
 519        # they cannot be serialized by pickle.
 520        pickle_safe_state = {}
 521        for attr_name, attr_value in self.__dict__.items():
 522            if not isinstance(attr_value, types.MethodType):
 523                pickle_safe_state[attr_name] = attr_value
 524
 525        return pickle_safe_state
 526
 527    def __setstate__(self, saved_state):
 528        """Restore this object from a pickled state (e.g. torch.load).
 529
 530        Calls __init__ to rebuild all the get_*/set_*/append_* methods, then
 531        overlays the saved data values on top so all settings are preserved.
 532        """
 533        # Grab the identity fields needed to reconstruct the right flavor of config
 534        # (global config vs. per-module config).
 535        saved_module_name = saved_state.get("_module_name")
 536        saved_module_type = saved_state.get("_module_type")
 537
 538        # Re-run __init__ so all the get_*/set_*/append_* methods exist again.
 539        # This also sets default values for every variable, which we overwrite below.
 540        self.__init__(module_name=saved_module_name, module_type=saved_module_type)
 541
 542        # Overwrite the defaults with the actual saved values.
 543        # Only restore private data attributes (prefixed with _); skip the identity
 544        # fields we already handled above, and skip _config_file for now so that
 545        # the setter calls above do not trigger auto-save during the restore.
 546        skip_on_first_pass = {"_module_name", "_module_type", "_config_file"}
 547        for attr_name, attr_value in saved_state.items():
 548            if attr_name.startswith("_") and attr_name not in skip_on_first_pass:
 549                self.__dict__[attr_name] = attr_value
 550
 551        # Restore the config file path last.  Auto-save is driven by _config_file
 552        # being set, so we only put it back after all values are already in place.
 553        if saved_state.get("_config_file"):
 554            self.__dict__["_config_file"] = saved_state["_config_file"]
 555
 556    def __getattr__(self, name):
 557        """Handle missing attributes gracefully, especially for PB variables.
 558
 559        Parameters
 560        ----------
 561        name : str
 562            The name of the attribute being accessed.
 563
 564        Returns
 565        -------
 566        None or raises AttributeError
 567            Returns None for missing set_ methods, raises AttributeError otherwise.
 568        """
 569        if name.startswith("set_"):
 570            print(f"Variable '{name[4:]}' does not exist.  Ignoring set attempt.")
 571            return lambda x: None
 572        if name.startswith("append_"):
 573            print(
 574                f"List Variable '{name[7:]}' does not exist.  Ignoring append attempt."
 575            )
 576            return lambda x: None
 577        if name.startswith("get_") and self.__dict__.get("_module_name") is not None:
 578            # Module-specific config: check for a per-module override stored directly in
 579            # __dict__ (written by load_config when custom JSON data was found for this
 580            # module).  This covers CUSTOMIZABLE vars that are not initialised for
 581            # per-module configs (e.g. output_dimensions, which lives inside the
 582            # ``if not module_name:`` block).
 583            private_key = f"_{name[4:]}"
 584            if private_key in self.__dict__:
 585                stored = self.__dict__[private_key]
 586                return lambda: stored
 587            # Fall back to the global pc instance for vars not set on this instance.
 588            global_getter = getattr(pc, name, None)
 589            if global_getter is not None:
 590                return global_getter
 591        raise AttributeError(
 592            f"'{self.__class__.__name__}' object has no attribute '{name}'"
 593        )
 594
 595    def __init__(self, module_name=None, module_type=None):
 596        """Initialize PAIConfig with default settings.
 597
 598        module_name=None means this is the main global config.
 599        If module_name is set this is a per-module config that loads
 600        custom settings from module_settings[module_name] (by id) or
 601        module_settings[module_type] (by type) in the JSON file.
 602        """
 603        # Must be first: prevents __getattr__ from firing for _config_file
 604        # during construction (before add_pai_config_var_functions sets it).
 605        # Also disables auto-save in setters until the end of __init__.
 606        self.__dict__["_config_file"] = None
 607        # None = global config; any string = per-module config
 608        self.__dict__["_module_name"] = module_name
 609        # Short class name of the wrapped module (e.g. 'Conv2d'), used as
 610        # a fallback lookup key when the specific name has no saved settings.
 611        self.__dict__["_module_type"] = module_type
 612
 613        if not module_name:
 614            ### Global Constants
 615            # Device configuration
 616            self.use_cuda = torch.cuda.is_available()
 617            add_pai_config_var_functions(self, "use_cuda", self.use_cuda)
 618            self.device = torch.device("cuda" if self.use_cuda else "cpu")
 619            add_pai_config_var_functions(self, "device", self.device)
 620
 621            self.save_name = ""
 622            add_pai_config_var_functions(self, "save_name", self.save_name)
 623
 624            # Debug settings
 625            self.debugging_output_dimensions = 0
 626            add_pai_config_var_functions(
 627                self, "debugging_output_dimensions", self.debugging_output_dimensions
 628            )
 629            # Debugging input tensor sizes.
 630            # This will slow things down very slightly and is not necessary but can help
 631            # catch when dimensions were not filled in correctly.
 632            self.confirm_correct_sizes = False
 633            add_pai_config_var_functions(
 634                self, "confirm_correct_sizes", self.confirm_correct_sizes
 635            )
 636
 637            # Confirmation flags for non-recommended options
 638            self.unwrapped_modules_confirmed = False
 639            add_pai_config_var_functions(
 640                self, "unwrapped_modules_confirmed", self.unwrapped_modules_confirmed
 641            )
 642            self.weight_decay_accepted = False
 643            add_pai_config_var_functions(
 644                self, "weight_decay_accepted", self.weight_decay_accepted
 645            )
 646            self.checked_skipped_modules = False
 647            add_pai_config_var_functions(
 648                self, "checked_skipped_modules", self.checked_skipped_modules
 649            )
 650            # Analysis settings
 651            self.save_old_graph_scores = True
 652            add_pai_config_var_functions(
 653                self, "save_old_graph_scores", self.save_old_graph_scores
 654            )
 655            # Testing settings
 656            self.testing_dendrite_capacity = True
 657            add_pai_config_var_functions(
 658                self, "testing_dendrite_capacity", self.testing_dendrite_capacity
 659            )
 660
 661            # File format settings
 662            self.using_safe_tensors = True
 663            add_pai_config_var_functions(
 664                self, "using_safe_tensors", self.using_safe_tensors
 665            )
 666
 667            # Checkpoint loading settings
 668            # Whether to use strict=True when loading state_dict
 669            # Set to False if loading old checkpoints that are missing new fields
 670            self.strict_loading = True
 671            add_pai_config_var_functions(
 672                self, "strict_loading", self.strict_loading
 673            )
 674
 675            # Graph and visualization settings
 676            # A graph setting which can be set to false if you want to do your own
 677            # training visualizations
 678            self.drawing_pai = True
 679            add_pai_config_var_functions(self, "drawing_pai", self.drawing_pai)
 680
 681            # Drawing extra graphs beyond the standard ones.
 682            self.drawing_extra_graphs = True
 683            add_pai_config_var_functions(
 684                self, "drawing_extra_graphs", self.drawing_extra_graphs
 685            )
 686
 687            # Saving test intermediary models, good for experimentation, bad for memory
 688            self.test_saves = True
 689            add_pai_config_var_functions(self, "test_saves", self.test_saves)
 690            # To be filled in later. pai_saves will remove some extra scaffolding for
 691            # slight memory and speed improvements
 692            self.pai_saves = False
 693            add_pai_config_var_functions(self, "pai_saves", self.pai_saves)
 694            # Improvement thresholds
 695            # Percentage improvement increase needed to call a new best validation score
 696            self.improvement_threshold = [0.001, 0.0001, 0.0]
 697            add_pai_config_var_functions(
 698                self, "improvement_threshold", self.improvement_threshold
 699            )
 700
 701            # Raw increase needed
 702            self.improvement_threshold_raw = 1e-5
 703            add_pai_config_var_functions(
 704                self, "improvement_threshold_raw", self.improvement_threshold_raw
 705            )
 706            # SWITCH MODE SETTINGS
 707
 708            # Add dendrites every time to debug implementation
 709            self.DOING_SWITCH_EVERY_TIME = 0
 710
 711            # Switch when validation hasn't improved over x epochs
 712            self.DOING_HISTORY = 1
 713            # Epochs to try before deciding to load previous best and add dendrites
 714            # Be sure this is higher than scheduler patience
 715            self.n_epochs_to_switch = 10
 716            add_pai_config_var_functions(
 717                self, "n_epochs_to_switch", self.n_epochs_to_switch
 718            )
 719            # Number to average validation scores over
 720            self.history_lookback = 1
 721            add_pai_config_var_functions(
 722                self, "history_lookback", self.history_lookback
 723            )
 724            # Amount of epochs to run after adding a new set of dendrites before checking
 725            # to add more
 726            self.initial_history_after_switches = 0
 727            add_pai_config_var_functions(
 728                self,
 729                "initial_history_after_switches",
 730                self.initial_history_after_switches,
 731            )
 732
 733            # Switch after a fixed number of epochs
 734            self.DOING_FIXED_SWITCH = 2
 735            # Number of epochs to complete before switching
 736            self.fixed_switch_num = 250
 737            add_pai_config_var_functions(
 738                self, "fixed_switch_num", self.fixed_switch_num
 739            )
 740            # An additional flag if you want your first switch to occur later than all the
 741            # rest for initial pretraining.  This is a new minimum, if its lower than
 742            # the above it will be ignored.
 743            self.first_fixed_switch_num = 1
 744            add_pai_config_var_functions(
 745                self, "first_fixed_switch_num", self.first_fixed_switch_num
 746            )
 747
 748            # A setting to not add dendrites and just do regular training
 749            # Warning, this will also never trigger training_complete
 750            self.DOING_NO_SWITCH = 3
 751
 752            # Default switch mode
 753            self.switch_mode = self.DOING_HISTORY
 754            add_pai_config_var_functions(self, "switch_mode", self.switch_mode)
 755
 756            # Reset settings
 757            # Resets score on switch
 758            # This can be useful if you need many epochs to catch up to the best score
 759            # from the previous version after adding dendrites
 760            self.reset_best_score_on_switch = True
 761            add_pai_config_var_functions(
 762                self, "reset_best_score_on_switch", self.reset_best_score_on_switch
 763            )
 764
 765            # Advanced settings
 766            # Not used in open source implementation, leave as default
 767            self.learn_dendrites_live = False
 768            add_pai_config_var_functions(
 769                self, "learn_dendrites_live", self.learn_dendrites_live
 770            )
 771            self.no_extra_n_modes = True
 772            add_pai_config_var_functions(
 773                self, "no_extra_n_modes", self.no_extra_n_modes
 774            )
 775
 776            # Data type for new modules and dendrite to dendrite / dendrite to neuron
 777            # weights
 778            self.d_type = torch.float
 779            add_pai_config_var_functions(self, "d_type", self.d_type)
 780
 781            # Learning rate management
 782            # A setting to automatically sweep over previously used learning rates when
 783            # adding new dendrites
 784            # Sometimes it's best to go back to initial LR, but often its best to start
 785            # at a lower LR
 786            self.find_best_lr = True
 787            add_pai_config_var_functions(self, "find_best_lr", self.find_best_lr)
 788            # Enforces the above even if the previous epoch didn't lower the learning rate
 789            self.dont_give_up_unless_learning_rate_lowered = True
 790            add_pai_config_var_functions(
 791                self,
 792                "dont_give_up_unless_learning_rate_lowered",
 793                self.dont_give_up_unless_learning_rate_lowered,
 794            )
 795
 796            # Dendrite attempt settings
 797            # Set to 1 if you want to quit as soon as one dendrite fails
 798            # Higher values will try new random dendrite weights this many times before
 799            # accepting that more dendrites don't improve
 800            self.max_dendrite_tries = 2
 801            add_pai_config_var_functions(
 802                self, "max_dendrite_tries", self.max_dendrite_tries
 803            )
 804
 805            # Scheduler parameter settings
 806            # Have learning rate params be by total epoch
 807            self.PARAM_VALS_BY_TOTAL_EPOCH = 0
 808            # Reset the params at every switch
 809            self.PARAM_VALS_BY_UPDATE_EPOCH = 1
 810            # Reset params for dendrite starts but not for normal restarts
 811            # Not used for open source version
 812            self.PARAM_VALS_BY_NEURON_EPOCH_START = 2
 813            # Default setting
 814            self.param_vals_setting = self.PARAM_VALS_BY_UPDATE_EPOCH
 815            add_pai_config_var_functions(
 816                self, "param_vals_setting", self.param_vals_setting
 817            )
 818            # Lists for module types and names to add dendrites to
 819            # For these lists no specifier means type, name is module name
 820            # and ids is the individual modules id, eg. model.conv2
 821            self.modules_to_perforate = []
 822            add_pai_config_var_functions(
 823                self, "modules_to_perforate", self.modules_to_perforate, list_type=True
 824            )
 825            self.module_names_to_perforate = [
 826                "PAISequential",
 827                "Conv1d",
 828                "Conv2d",
 829                "Conv3d",
 830                "Linear",
 831            ]
 832            add_pai_config_var_functions(
 833                self,
 834                "module_names_to_perforate",
 835                self.module_names_to_perforate,
 836                list_type=True,
 837            )
 838            self.module_ids_to_perforate = []
 839            add_pai_config_var_functions(
 840                self,
 841                "module_ids_to_perforate",
 842                self.module_ids_to_perforate,
 843                list_type=True,
 844            )
 845
 846            # All modules should either be perforated or tracked to ensure all modules
 847            # are accounted for
 848            self.modules_to_track = []
 849            add_pai_config_var_functions(
 850                self, "modules_to_track", self.modules_to_track, list_type=True
 851            )
 852            self.module_names_to_track = []
 853            add_pai_config_var_functions(
 854                self,
 855                "module_names_to_track",
 856                self.module_names_to_track,
 857                list_type=True,
 858            )
 859            # IDs are for if you want to pass only a single module by its assigned ID rather than the module type by name
 860            self.module_ids_to_track = []
 861            add_pai_config_var_functions(
 862                self, "module_ids_to_track", self.module_ids_to_track, list_type=True
 863            )
 864
 865            # Parameter IDs to track as neuron parameters without recursive behavior
 866            # (e.g., [".my_custom_parameter"]).
 867            self.parameter_ids_to_track = []
 868            add_pai_config_var_functions(
 869                self,
 870                "parameter_ids_to_track",
 871                self.parameter_ids_to_track,
 872                list_type=True,
 873            )
 874
 875            # Replacement modules happen before the conversion,
 876            # so replaced modules will then also be run through the conversion steps
 877            # These are for modules that need to be replaced before addition of dendrites
 878            # See the resnet example in models_perforatedai
 879            self.modules_to_replace = []
 880            add_pai_config_var_functions(
 881                self, "modules_to_replace", self.modules_to_replace, list_type=True
 882            )
 883            # Modules to replace the above modules with
 884            self.replacement_modules = []
 885            add_pai_config_var_functions(
 886                self, "replacement_modules", self.replacement_modules, list_type=True
 887            )
 888
 889            # Dendrites default to modules which are one tensor input and one tensor
 890            # output in forward()
 891            # Other modules require to be labeled as modules with processing and assigned
 892            # processing classes
 893            # This can be done by module type or module name see customization.md in API
 894            # for example
 895            self.modules_with_processing = []
 896            add_pai_config_var_functions(
 897                self,
 898                "modules_with_processing",
 899                self.modules_with_processing,
 900                list_type=True,
 901            )
 902            self.modules_processing_classes = []
 903            add_pai_config_var_functions(
 904                self,
 905                "modules_processing_classes",
 906                self.modules_processing_classes,
 907                list_type=True,
 908            )
 909            self.module_names_with_processing = []
 910            add_pai_config_var_functions(
 911                self,
 912                "module_names_with_processing",
 913                self.module_names_with_processing,
 914                list_type=True,
 915            )
 916            self.module_by_name_processing_classes = []
 917            add_pai_config_var_functions(
 918                self,
 919                "module_by_name_processing_classes",
 920                self.module_by_name_processing_classes,
 921                list_type=True,
 922            )
 923
 924            # Similarly here as above. Some huggingface models have multiple pointers to
 925            # the same modules which cause problems
 926            # If you want to only save one of the multiple pointers you can set which ones
 927            # not to save here
 928            self.module_names_to_not_save = [".base_model"]
 929            add_pai_config_var_functions(
 930                self,
 931                "module_names_to_not_save",
 932                self.module_names_to_not_save,
 933                list_type=True,
 934            )
 935
 936            # Perforated Backpropagation settings
 937            self.perforated_backpropagation = False
 938            add_pai_config_var_functions(
 939                self, "perforated_backpropagation", self.perforated_backpropagation
 940            )
 941            
 942            # This is specifically a workaround for weight tying
 943            # Not to be used for a duplicate pointer that isn't actually run twice
 944            self.weight_tying_experimental = False
 945            add_pai_config_var_functions(
 946                self, "weight_tying_experimental", self.weight_tying_experimental
 947            )
 948
 949            # Dashboard event streaming settings
 950            self.dashboard_events_enabled = False
 951            add_pai_config_var_functions(
 952                self, "dashboard_events_enabled", self.dashboard_events_enabled
 953            )
 954            self.dashboard_url = "http://localhost:3002"
 955            add_pai_config_var_functions(
 956                self, "dashboard_url", self.dashboard_url
 957            )
 958            self.dashboard_debug = False
 959            add_pai_config_var_functions(
 960                self, "dashboard_debug", self.dashboard_debug
 961            )
 962
 963            # These are settings where libraries must be doing the scoring adding to
 964            # message from your main script what metric to use
 965            self.library_validation_score = ""
 966            add_pai_config_var_functions(
 967                self, "library_validation_score", self.library_validation_score
 968            )
 969            self.library_extra_scores = []
 970            add_pai_config_var_functions(
 971                self,
 972                "library_extra_scores",
 973                self.library_extra_scores,
 974                list_type=True,
 975            )
 976            self.library_extra_scores_without_graphing = []
 977            add_pai_config_var_functions(
 978                self,
 979                "library_extra_scores_without_graphing",
 980                self.library_extra_scores_without_graphing,
 981                list_type=True,
 982            )
 983
 984            # Input dimensions needs to be set every time. It is set to what format of
 985            # planes you are expecting.
 986            # Neuron index should be set to 0, variable indexes should be set to -1.
 987            # For example, if your format is [batchsize, nodes, x, y]
 988            # output_dimensions is [-1, 0, -1, -1].
 989            # if your format is, [batchsize, time index, nodes] output_dimensions is
 990            # [-1, -1, 0]
 991            self.output_dimensions = [-1, 0, -1, -1]
 992            add_pai_config_var_functions(
 993                self, "output_dimensions", self.output_dimensions, list_type=True
 994            )
 995        # Verbosity settings
 996        self.verbose = False
 997        add_pai_config_var_functions(self, "verbose", self.verbose)
 998        self.extra_verbose = False
 999        add_pai_config_var_functions(self, "extra_verbose", self.extra_verbose)
1000        # Suppress all PAI prints
1001        self.silent = False
1002        add_pai_config_var_functions(self, "silent", self.silent)
1003
1004        # In place for future implementation options of adding multiple candidate
1005        # dendrites together
1006        self.global_candidates = 1
1007        add_pai_config_var_functions(self, "global_candidates", self.global_candidates)
1008
1009        # Weight initialization settings
1010        # Multiplier when randomizing dendrite weights
1011        self.candidate_weight_initialization_multiplier = 0.01
1012        add_pai_config_var_functions(
1013            self,
1014            "candidate_weight_initialization_multiplier",
1015            self.candidate_weight_initialization_multiplier,
1016        )
1017        # Multiplier when randomizing dendrite weights
1018        self.candidate_weight_init_by_main = False
1019        add_pai_config_var_functions(
1020            self,
1021            "candidate_weight_init_by_main",
1022            self.candidate_weight_init_by_main,
1023        )
1024
1025        # Dendrite retention settings
1026        # A setting to keep dendrites even if they do not improve scores
1027        self.retain_all_dendrites = False
1028        add_pai_config_var_functions(
1029            self, "retain_all_dendrites", self.retain_all_dendrites
1030        )
1031
1032        # Max dendrites to add even if they do continue improving scores
1033        self.max_dendrites = 100
1034        add_pai_config_var_functions(self, "max_dendrites", self.max_dendrites)
1035
1036        # Activation function settings
1037        # The activation function to use for dendrites
1038        self.pai_forward_function = torch.sigmoid
1039        add_pai_config_var_functions(
1040            self, "pai_forward_function", self.pai_forward_function
1041        )
1042
1043        # ------------------------------------------------------------------
1044        # Config file will be set when save_name is assigned (in perforate_model)
1045        # ------------------------------------------------------------------
1046        # _config_file stays None until save_name is set to a non-empty value
1047
1048    # ------------------------------------------------------------------
1049
1050    def save_config(self, filename):
1051        """Save the current PAIConfig state to a JSON file.
1052
1053        Parameters
1054        ----------
1055        filename : str
1056            Destination file path (created or overwritten).
1057
1058        Notes
1059        -----
1060        Values that are not natively JSON-serialisable (torch.device,
1061        torch.dtype, nn.Module subclasses, callables) are stored as their
1062        dotted-string representations so they can be round-tripped by
1063        :py:meth:`load_config`.
1064
1065        Returns
1066        -------
1067        None
1068            This function does not return a value.
1069        """
1070        import json
1071
1072        config_dict = {}
1073
1074        # Private-storage vars added by add_pai_config_var_functions
1075        # e.g. self._module_ids_to_perforate → key 'module_ids_to_perforate'
1076        for key, val in sorted(self.__dict__.items()):
1077            if not (key.startswith("_") and not key.startswith("__")):
1078                continue
1079            # Skip internal bookkeeping keys that must not round-trip through JSON
1080            if key in ("_config_file", "_module_name", "_module_type"):
1081                continue
1082            if callable(val):  # skip bound method refs
1083                continue
1084            clean_key = key[1:]
1085            try:
1086                config_dict[clean_key] = _serialize_pai_value(val)
1087            except Exception:
1088                config_dict[clean_key] = str(val)
1089
1090        # Plain constants (DOING_*, PARAM_VALS_BY_*, etc.)
1091        for key, val in self.__dict__.items():
1092            if key.startswith("_") or callable(val):
1093                continue
1094            if key not in config_dict:
1095                try:
1096                    config_dict[key] = _serialize_pai_value(val)
1097                except Exception:
1098                    config_dict[key] = str(val)
1099
1100        # Merge short class names from modules_to_perforate into module_names_to_perforate
1101        # so the UI (and JS) only needs to check one array.
1102        type_short_names = [
1103            cls.__name__
1104            for cls in self.__dict__.get("_modules_to_perforate", [])
1105            if isinstance(cls, type)
1106        ]
1107        existing = config_dict.get("module_names_to_perforate", [])
1108        config_dict["module_names_to_perforate"] = existing + [
1109            n for n in type_short_names if n not in existing
1110        ]
1111
1112        # Preserve any per-module settings written by the Studio frontend.
1113        _existing_ms: dict = {}
1114        try:
1115            with open(filename, "r") as _f:
1116                _existing_ms = json.load(_f).get("module_settings", {})
1117        except Exception:
1118            pass
1119        config_dict["module_settings"] = _existing_ms
1120
1121        # Publish customizable field type names so the Studio frontend can
1122        # render appropriate editors without needing to import the library.
1123        config_dict["_customizable_fields"] = {
1124            k: (v.__name__ if hasattr(v, "__name__") else str(v))
1125            for k, v in PAIConfig._CUSTOMIZABLE.items()
1126        }
1127
1128        # Ensure the directory exists before saving
1129        import os
1130
1131        os.makedirs(os.path.dirname(filename), exist_ok=True)
1132
1133        with open(filename, "w") as f:
1134            json.dump(config_dict, f, indent=2)
1135        print(f"[PAI Config] Saved {len(config_dict)} variables \u2192 {filename}")
1136
1137    def load_config(self, filename, module_name=None, module_type=None):
1138        """Load PAIConfig state from a JSON file produced by :py:meth:`save_config`.
1139
1140        If *module_name* is ``None`` (default) every serialisable variable in
1141        the file is restored on this instance.
1142
1143        If *module_name* is given the lookup priority is:
1144          1. ``module_settings[module_name]`` (exact name / id match)
1145          2. ``module_settings[module_type]``  (type-level fallback)
1146          3. No-op — the defaults already set by ``__init__`` are kept.
1147
1148        Parameters
1149        ----------
1150        filename : str
1151            Path to the JSON file to read.
1152        module_name : str, optional
1153            Display name (id) of the module whose custom settings should be loaded.
1154        module_type : str, optional
1155            Short class name of the module type, used as a fallback key.
1156
1157        Returns
1158        -------
1159        None
1160            This function does not return a value.
1161        """
1162        import json
1163
1164        with open(filename, "r") as f:
1165            config_dict = json.load(f)
1166
1167        if module_name is not None:
1168            # ── Per-module load ──────────────────────────────────────────────
1169            module_settings = config_dict.get("module_settings", {})
1170            # Priority: exact name → type fallback → no-op
1171            if module_name in module_settings:
1172                custom = module_settings[module_name]
1173                resolved_key = module_name
1174            elif module_type and module_type in module_settings:
1175                custom = module_settings[module_type]
1176                resolved_key = module_type
1177            else:
1178                # No custom settings for this module or type — keep defaults.
1179                return
1180            loaded = 0
1181            skipped = 0
1182            for key, json_val in custom.items():
1183                if key not in PAIConfig._CUSTOMIZABLE:
1184                    continue
1185                type_hint = PAIConfig._TYPES.get(key)
1186                private_key = f"_{key}"
1187                # Write directly to __dict__ so we bypass any setter guards and also
1188                # correctly handle vars that are not pre-initialised on per-module
1189                # configs (e.g. output_dimensions, which only lives inside the
1190                # ``if not module_name:`` block of __init__).
1191                try:
1192                    self.__dict__[private_key] = (
1193                        _deserialize_pai_value(json_val, type_hint)
1194                        if type_hint is not None
1195                        else json_val
1196                    )
1197                    loaded += 1
1198                except Exception as exc:
1199                    print(
1200                        f"[PAI Config] Warning: could not load '{key}' for '{resolved_key}': {exc}"
1201                    )
1202                    skipped += 1
1203            print(
1204                f"[PAI Config] Loaded {loaded} custom vars for '{resolved_key}' from {filename}"
1205                + (f" ({skipped} skipped)" if skipped else "")
1206            )
1207            return
1208
1209        # ── Global load: every variable in the JSON ──────────────────────────
1210        loaded = 0
1211        skipped = 0
1212        for key, json_val in config_dict.items():
1213            # Skip internal bookkeeping and Studio-only metadata keys.
1214            # 'module_name' and 'module_type' must never overwrite the
1215            # instance's _module_name/_module_type (they are internal only).
1216            if key in (
1217                "config_file",
1218                "module_settings",
1219                "module_name",
1220                "module_type",
1221            ) or key.startswith("_"):
1222                continue
1223            type_hint = PAIConfig._TYPES.get(key)
1224            private_key = f"_{key}"
1225            if hasattr(self, private_key):
1226                try:
1227                    setattr(
1228                        self,
1229                        private_key,
1230                        (
1231                            _deserialize_pai_value(json_val, type_hint)
1232                            if type_hint is not None
1233                            else json_val
1234                        ),
1235                    )
1236                    loaded += 1
1237                except Exception as exc:
1238                    print(f"[PAI Config] Warning: could not load '{key}': {exc}")
1239                    skipped += 1
1240            elif hasattr(self, key) and not callable(getattr(self, key, None)):
1241                try:
1242                    setattr(self, key, json_val)
1243                    loaded += 1
1244                except Exception:
1245                    skipped += 1
1246
1247        print(
1248            f"[PAI Config] Loaded {loaded} variables from {filename}"
1249            + (f" ({skipped} skipped)" if skipped else "")
1250        )

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)
 595    def __init__(self, module_name=None, module_type=None):
 596        """Initialize PAIConfig with default settings.
 597
 598        module_name=None means this is the main global config.
 599        If module_name is set this is a per-module config that loads
 600        custom settings from module_settings[module_name] (by id) or
 601        module_settings[module_type] (by type) in the JSON file.
 602        """
 603        # Must be first: prevents __getattr__ from firing for _config_file
 604        # during construction (before add_pai_config_var_functions sets it).
 605        # Also disables auto-save in setters until the end of __init__.
 606        self.__dict__["_config_file"] = None
 607        # None = global config; any string = per-module config
 608        self.__dict__["_module_name"] = module_name
 609        # Short class name of the wrapped module (e.g. 'Conv2d'), used as
 610        # a fallback lookup key when the specific name has no saved settings.
 611        self.__dict__["_module_type"] = module_type
 612
 613        if not module_name:
 614            ### Global Constants
 615            # Device configuration
 616            self.use_cuda = torch.cuda.is_available()
 617            add_pai_config_var_functions(self, "use_cuda", self.use_cuda)
 618            self.device = torch.device("cuda" if self.use_cuda else "cpu")
 619            add_pai_config_var_functions(self, "device", self.device)
 620
 621            self.save_name = ""
 622            add_pai_config_var_functions(self, "save_name", self.save_name)
 623
 624            # Debug settings
 625            self.debugging_output_dimensions = 0
 626            add_pai_config_var_functions(
 627                self, "debugging_output_dimensions", self.debugging_output_dimensions
 628            )
 629            # Debugging input tensor sizes.
 630            # This will slow things down very slightly and is not necessary but can help
 631            # catch when dimensions were not filled in correctly.
 632            self.confirm_correct_sizes = False
 633            add_pai_config_var_functions(
 634                self, "confirm_correct_sizes", self.confirm_correct_sizes
 635            )
 636
 637            # Confirmation flags for non-recommended options
 638            self.unwrapped_modules_confirmed = False
 639            add_pai_config_var_functions(
 640                self, "unwrapped_modules_confirmed", self.unwrapped_modules_confirmed
 641            )
 642            self.weight_decay_accepted = False
 643            add_pai_config_var_functions(
 644                self, "weight_decay_accepted", self.weight_decay_accepted
 645            )
 646            self.checked_skipped_modules = False
 647            add_pai_config_var_functions(
 648                self, "checked_skipped_modules", self.checked_skipped_modules
 649            )
 650            # Analysis settings
 651            self.save_old_graph_scores = True
 652            add_pai_config_var_functions(
 653                self, "save_old_graph_scores", self.save_old_graph_scores
 654            )
 655            # Testing settings
 656            self.testing_dendrite_capacity = True
 657            add_pai_config_var_functions(
 658                self, "testing_dendrite_capacity", self.testing_dendrite_capacity
 659            )
 660
 661            # File format settings
 662            self.using_safe_tensors = True
 663            add_pai_config_var_functions(
 664                self, "using_safe_tensors", self.using_safe_tensors
 665            )
 666
 667            # Checkpoint loading settings
 668            # Whether to use strict=True when loading state_dict
 669            # Set to False if loading old checkpoints that are missing new fields
 670            self.strict_loading = True
 671            add_pai_config_var_functions(
 672                self, "strict_loading", self.strict_loading
 673            )
 674
 675            # Graph and visualization settings
 676            # A graph setting which can be set to false if you want to do your own
 677            # training visualizations
 678            self.drawing_pai = True
 679            add_pai_config_var_functions(self, "drawing_pai", self.drawing_pai)
 680
 681            # Drawing extra graphs beyond the standard ones.
 682            self.drawing_extra_graphs = True
 683            add_pai_config_var_functions(
 684                self, "drawing_extra_graphs", self.drawing_extra_graphs
 685            )
 686
 687            # Saving test intermediary models, good for experimentation, bad for memory
 688            self.test_saves = True
 689            add_pai_config_var_functions(self, "test_saves", self.test_saves)
 690            # To be filled in later. pai_saves will remove some extra scaffolding for
 691            # slight memory and speed improvements
 692            self.pai_saves = False
 693            add_pai_config_var_functions(self, "pai_saves", self.pai_saves)
 694            # Improvement thresholds
 695            # Percentage improvement increase needed to call a new best validation score
 696            self.improvement_threshold = [0.001, 0.0001, 0.0]
 697            add_pai_config_var_functions(
 698                self, "improvement_threshold", self.improvement_threshold
 699            )
 700
 701            # Raw increase needed
 702            self.improvement_threshold_raw = 1e-5
 703            add_pai_config_var_functions(
 704                self, "improvement_threshold_raw", self.improvement_threshold_raw
 705            )
 706            # SWITCH MODE SETTINGS
 707
 708            # Add dendrites every time to debug implementation
 709            self.DOING_SWITCH_EVERY_TIME = 0
 710
 711            # Switch when validation hasn't improved over x epochs
 712            self.DOING_HISTORY = 1
 713            # Epochs to try before deciding to load previous best and add dendrites
 714            # Be sure this is higher than scheduler patience
 715            self.n_epochs_to_switch = 10
 716            add_pai_config_var_functions(
 717                self, "n_epochs_to_switch", self.n_epochs_to_switch
 718            )
 719            # Number to average validation scores over
 720            self.history_lookback = 1
 721            add_pai_config_var_functions(
 722                self, "history_lookback", self.history_lookback
 723            )
 724            # Amount of epochs to run after adding a new set of dendrites before checking
 725            # to add more
 726            self.initial_history_after_switches = 0
 727            add_pai_config_var_functions(
 728                self,
 729                "initial_history_after_switches",
 730                self.initial_history_after_switches,
 731            )
 732
 733            # Switch after a fixed number of epochs
 734            self.DOING_FIXED_SWITCH = 2
 735            # Number of epochs to complete before switching
 736            self.fixed_switch_num = 250
 737            add_pai_config_var_functions(
 738                self, "fixed_switch_num", self.fixed_switch_num
 739            )
 740            # An additional flag if you want your first switch to occur later than all the
 741            # rest for initial pretraining.  This is a new minimum, if its lower than
 742            # the above it will be ignored.
 743            self.first_fixed_switch_num = 1
 744            add_pai_config_var_functions(
 745                self, "first_fixed_switch_num", self.first_fixed_switch_num
 746            )
 747
 748            # A setting to not add dendrites and just do regular training
 749            # Warning, this will also never trigger training_complete
 750            self.DOING_NO_SWITCH = 3
 751
 752            # Default switch mode
 753            self.switch_mode = self.DOING_HISTORY
 754            add_pai_config_var_functions(self, "switch_mode", self.switch_mode)
 755
 756            # Reset settings
 757            # Resets score on switch
 758            # This can be useful if you need many epochs to catch up to the best score
 759            # from the previous version after adding dendrites
 760            self.reset_best_score_on_switch = True
 761            add_pai_config_var_functions(
 762                self, "reset_best_score_on_switch", self.reset_best_score_on_switch
 763            )
 764
 765            # Advanced settings
 766            # Not used in open source implementation, leave as default
 767            self.learn_dendrites_live = False
 768            add_pai_config_var_functions(
 769                self, "learn_dendrites_live", self.learn_dendrites_live
 770            )
 771            self.no_extra_n_modes = True
 772            add_pai_config_var_functions(
 773                self, "no_extra_n_modes", self.no_extra_n_modes
 774            )
 775
 776            # Data type for new modules and dendrite to dendrite / dendrite to neuron
 777            # weights
 778            self.d_type = torch.float
 779            add_pai_config_var_functions(self, "d_type", self.d_type)
 780
 781            # Learning rate management
 782            # A setting to automatically sweep over previously used learning rates when
 783            # adding new dendrites
 784            # Sometimes it's best to go back to initial LR, but often its best to start
 785            # at a lower LR
 786            self.find_best_lr = True
 787            add_pai_config_var_functions(self, "find_best_lr", self.find_best_lr)
 788            # Enforces the above even if the previous epoch didn't lower the learning rate
 789            self.dont_give_up_unless_learning_rate_lowered = True
 790            add_pai_config_var_functions(
 791                self,
 792                "dont_give_up_unless_learning_rate_lowered",
 793                self.dont_give_up_unless_learning_rate_lowered,
 794            )
 795
 796            # Dendrite attempt settings
 797            # Set to 1 if you want to quit as soon as one dendrite fails
 798            # Higher values will try new random dendrite weights this many times before
 799            # accepting that more dendrites don't improve
 800            self.max_dendrite_tries = 2
 801            add_pai_config_var_functions(
 802                self, "max_dendrite_tries", self.max_dendrite_tries
 803            )
 804
 805            # Scheduler parameter settings
 806            # Have learning rate params be by total epoch
 807            self.PARAM_VALS_BY_TOTAL_EPOCH = 0
 808            # Reset the params at every switch
 809            self.PARAM_VALS_BY_UPDATE_EPOCH = 1
 810            # Reset params for dendrite starts but not for normal restarts
 811            # Not used for open source version
 812            self.PARAM_VALS_BY_NEURON_EPOCH_START = 2
 813            # Default setting
 814            self.param_vals_setting = self.PARAM_VALS_BY_UPDATE_EPOCH
 815            add_pai_config_var_functions(
 816                self, "param_vals_setting", self.param_vals_setting
 817            )
 818            # Lists for module types and names to add dendrites to
 819            # For these lists no specifier means type, name is module name
 820            # and ids is the individual modules id, eg. model.conv2
 821            self.modules_to_perforate = []
 822            add_pai_config_var_functions(
 823                self, "modules_to_perforate", self.modules_to_perforate, list_type=True
 824            )
 825            self.module_names_to_perforate = [
 826                "PAISequential",
 827                "Conv1d",
 828                "Conv2d",
 829                "Conv3d",
 830                "Linear",
 831            ]
 832            add_pai_config_var_functions(
 833                self,
 834                "module_names_to_perforate",
 835                self.module_names_to_perforate,
 836                list_type=True,
 837            )
 838            self.module_ids_to_perforate = []
 839            add_pai_config_var_functions(
 840                self,
 841                "module_ids_to_perforate",
 842                self.module_ids_to_perforate,
 843                list_type=True,
 844            )
 845
 846            # All modules should either be perforated or tracked to ensure all modules
 847            # are accounted for
 848            self.modules_to_track = []
 849            add_pai_config_var_functions(
 850                self, "modules_to_track", self.modules_to_track, list_type=True
 851            )
 852            self.module_names_to_track = []
 853            add_pai_config_var_functions(
 854                self,
 855                "module_names_to_track",
 856                self.module_names_to_track,
 857                list_type=True,
 858            )
 859            # IDs are for if you want to pass only a single module by its assigned ID rather than the module type by name
 860            self.module_ids_to_track = []
 861            add_pai_config_var_functions(
 862                self, "module_ids_to_track", self.module_ids_to_track, list_type=True
 863            )
 864
 865            # Parameter IDs to track as neuron parameters without recursive behavior
 866            # (e.g., [".my_custom_parameter"]).
 867            self.parameter_ids_to_track = []
 868            add_pai_config_var_functions(
 869                self,
 870                "parameter_ids_to_track",
 871                self.parameter_ids_to_track,
 872                list_type=True,
 873            )
 874
 875            # Replacement modules happen before the conversion,
 876            # so replaced modules will then also be run through the conversion steps
 877            # These are for modules that need to be replaced before addition of dendrites
 878            # See the resnet example in models_perforatedai
 879            self.modules_to_replace = []
 880            add_pai_config_var_functions(
 881                self, "modules_to_replace", self.modules_to_replace, list_type=True
 882            )
 883            # Modules to replace the above modules with
 884            self.replacement_modules = []
 885            add_pai_config_var_functions(
 886                self, "replacement_modules", self.replacement_modules, list_type=True
 887            )
 888
 889            # Dendrites default to modules which are one tensor input and one tensor
 890            # output in forward()
 891            # Other modules require to be labeled as modules with processing and assigned
 892            # processing classes
 893            # This can be done by module type or module name see customization.md in API
 894            # for example
 895            self.modules_with_processing = []
 896            add_pai_config_var_functions(
 897                self,
 898                "modules_with_processing",
 899                self.modules_with_processing,
 900                list_type=True,
 901            )
 902            self.modules_processing_classes = []
 903            add_pai_config_var_functions(
 904                self,
 905                "modules_processing_classes",
 906                self.modules_processing_classes,
 907                list_type=True,
 908            )
 909            self.module_names_with_processing = []
 910            add_pai_config_var_functions(
 911                self,
 912                "module_names_with_processing",
 913                self.module_names_with_processing,
 914                list_type=True,
 915            )
 916            self.module_by_name_processing_classes = []
 917            add_pai_config_var_functions(
 918                self,
 919                "module_by_name_processing_classes",
 920                self.module_by_name_processing_classes,
 921                list_type=True,
 922            )
 923
 924            # Similarly here as above. Some huggingface models have multiple pointers to
 925            # the same modules which cause problems
 926            # If you want to only save one of the multiple pointers you can set which ones
 927            # not to save here
 928            self.module_names_to_not_save = [".base_model"]
 929            add_pai_config_var_functions(
 930                self,
 931                "module_names_to_not_save",
 932                self.module_names_to_not_save,
 933                list_type=True,
 934            )
 935
 936            # Perforated Backpropagation settings
 937            self.perforated_backpropagation = False
 938            add_pai_config_var_functions(
 939                self, "perforated_backpropagation", self.perforated_backpropagation
 940            )
 941            
 942            # This is specifically a workaround for weight tying
 943            # Not to be used for a duplicate pointer that isn't actually run twice
 944            self.weight_tying_experimental = False
 945            add_pai_config_var_functions(
 946                self, "weight_tying_experimental", self.weight_tying_experimental
 947            )
 948
 949            # Dashboard event streaming settings
 950            self.dashboard_events_enabled = False
 951            add_pai_config_var_functions(
 952                self, "dashboard_events_enabled", self.dashboard_events_enabled
 953            )
 954            self.dashboard_url = "http://localhost:3002"
 955            add_pai_config_var_functions(
 956                self, "dashboard_url", self.dashboard_url
 957            )
 958            self.dashboard_debug = False
 959            add_pai_config_var_functions(
 960                self, "dashboard_debug", self.dashboard_debug
 961            )
 962
 963            # These are settings where libraries must be doing the scoring adding to
 964            # message from your main script what metric to use
 965            self.library_validation_score = ""
 966            add_pai_config_var_functions(
 967                self, "library_validation_score", self.library_validation_score
 968            )
 969            self.library_extra_scores = []
 970            add_pai_config_var_functions(
 971                self,
 972                "library_extra_scores",
 973                self.library_extra_scores,
 974                list_type=True,
 975            )
 976            self.library_extra_scores_without_graphing = []
 977            add_pai_config_var_functions(
 978                self,
 979                "library_extra_scores_without_graphing",
 980                self.library_extra_scores_without_graphing,
 981                list_type=True,
 982            )
 983
 984            # Input dimensions needs to be set every time. It is set to what format of
 985            # planes you are expecting.
 986            # Neuron index should be set to 0, variable indexes should be set to -1.
 987            # For example, if your format is [batchsize, nodes, x, y]
 988            # output_dimensions is [-1, 0, -1, -1].
 989            # if your format is, [batchsize, time index, nodes] output_dimensions is
 990            # [-1, -1, 0]
 991            self.output_dimensions = [-1, 0, -1, -1]
 992            add_pai_config_var_functions(
 993                self, "output_dimensions", self.output_dimensions, list_type=True
 994            )
 995        # Verbosity settings
 996        self.verbose = False
 997        add_pai_config_var_functions(self, "verbose", self.verbose)
 998        self.extra_verbose = False
 999        add_pai_config_var_functions(self, "extra_verbose", self.extra_verbose)
1000        # Suppress all PAI prints
1001        self.silent = False
1002        add_pai_config_var_functions(self, "silent", self.silent)
1003
1004        # In place for future implementation options of adding multiple candidate
1005        # dendrites together
1006        self.global_candidates = 1
1007        add_pai_config_var_functions(self, "global_candidates", self.global_candidates)
1008
1009        # Weight initialization settings
1010        # Multiplier when randomizing dendrite weights
1011        self.candidate_weight_initialization_multiplier = 0.01
1012        add_pai_config_var_functions(
1013            self,
1014            "candidate_weight_initialization_multiplier",
1015            self.candidate_weight_initialization_multiplier,
1016        )
1017        # Multiplier when randomizing dendrite weights
1018        self.candidate_weight_init_by_main = False
1019        add_pai_config_var_functions(
1020            self,
1021            "candidate_weight_init_by_main",
1022            self.candidate_weight_init_by_main,
1023        )
1024
1025        # Dendrite retention settings
1026        # A setting to keep dendrites even if they do not improve scores
1027        self.retain_all_dendrites = False
1028        add_pai_config_var_functions(
1029            self, "retain_all_dendrites", self.retain_all_dendrites
1030        )
1031
1032        # Max dendrites to add even if they do continue improving scores
1033        self.max_dendrites = 100
1034        add_pai_config_var_functions(self, "max_dendrites", self.max_dendrites)
1035
1036        # Activation function settings
1037        # The activation function to use for dendrites
1038        self.pai_forward_function = torch.sigmoid
1039        add_pai_config_var_functions(
1040            self, "pai_forward_function", self.pai_forward_function
1041        )
1042
1043        # ------------------------------------------------------------------
1044        # Config file will be set when save_name is assigned (in perforate_model)
1045        # ------------------------------------------------------------------
1046        # _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):
1050    def save_config(self, filename):
1051        """Save the current PAIConfig state to a JSON file.
1052
1053        Parameters
1054        ----------
1055        filename : str
1056            Destination file path (created or overwritten).
1057
1058        Notes
1059        -----
1060        Values that are not natively JSON-serialisable (torch.device,
1061        torch.dtype, nn.Module subclasses, callables) are stored as their
1062        dotted-string representations so they can be round-tripped by
1063        :py:meth:`load_config`.
1064
1065        Returns
1066        -------
1067        None
1068            This function does not return a value.
1069        """
1070        import json
1071
1072        config_dict = {}
1073
1074        # Private-storage vars added by add_pai_config_var_functions
1075        # e.g. self._module_ids_to_perforate → key 'module_ids_to_perforate'
1076        for key, val in sorted(self.__dict__.items()):
1077            if not (key.startswith("_") and not key.startswith("__")):
1078                continue
1079            # Skip internal bookkeeping keys that must not round-trip through JSON
1080            if key in ("_config_file", "_module_name", "_module_type"):
1081                continue
1082            if callable(val):  # skip bound method refs
1083                continue
1084            clean_key = key[1:]
1085            try:
1086                config_dict[clean_key] = _serialize_pai_value(val)
1087            except Exception:
1088                config_dict[clean_key] = str(val)
1089
1090        # Plain constants (DOING_*, PARAM_VALS_BY_*, etc.)
1091        for key, val in self.__dict__.items():
1092            if key.startswith("_") or callable(val):
1093                continue
1094            if key not in config_dict:
1095                try:
1096                    config_dict[key] = _serialize_pai_value(val)
1097                except Exception:
1098                    config_dict[key] = str(val)
1099
1100        # Merge short class names from modules_to_perforate into module_names_to_perforate
1101        # so the UI (and JS) only needs to check one array.
1102        type_short_names = [
1103            cls.__name__
1104            for cls in self.__dict__.get("_modules_to_perforate", [])
1105            if isinstance(cls, type)
1106        ]
1107        existing = config_dict.get("module_names_to_perforate", [])
1108        config_dict["module_names_to_perforate"] = existing + [
1109            n for n in type_short_names if n not in existing
1110        ]
1111
1112        # Preserve any per-module settings written by the Studio frontend.
1113        _existing_ms: dict = {}
1114        try:
1115            with open(filename, "r") as _f:
1116                _existing_ms = json.load(_f).get("module_settings", {})
1117        except Exception:
1118            pass
1119        config_dict["module_settings"] = _existing_ms
1120
1121        # Publish customizable field type names so the Studio frontend can
1122        # render appropriate editors without needing to import the library.
1123        config_dict["_customizable_fields"] = {
1124            k: (v.__name__ if hasattr(v, "__name__") else str(v))
1125            for k, v in PAIConfig._CUSTOMIZABLE.items()
1126        }
1127
1128        # Ensure the directory exists before saving
1129        import os
1130
1131        os.makedirs(os.path.dirname(filename), exist_ok=True)
1132
1133        with open(filename, "w") as f:
1134            json.dump(config_dict, f, indent=2)
1135        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):
1137    def load_config(self, filename, module_name=None, module_type=None):
1138        """Load PAIConfig state from a JSON file produced by :py:meth:`save_config`.
1139
1140        If *module_name* is ``None`` (default) every serialisable variable in
1141        the file is restored on this instance.
1142
1143        If *module_name* is given the lookup priority is:
1144          1. ``module_settings[module_name]`` (exact name / id match)
1145          2. ``module_settings[module_type]``  (type-level fallback)
1146          3. No-op — the defaults already set by ``__init__`` are kept.
1147
1148        Parameters
1149        ----------
1150        filename : str
1151            Path to the JSON file to read.
1152        module_name : str, optional
1153            Display name (id) of the module whose custom settings should be loaded.
1154        module_type : str, optional
1155            Short class name of the module type, used as a fallback key.
1156
1157        Returns
1158        -------
1159        None
1160            This function does not return a value.
1161        """
1162        import json
1163
1164        with open(filename, "r") as f:
1165            config_dict = json.load(f)
1166
1167        if module_name is not None:
1168            # ── Per-module load ──────────────────────────────────────────────
1169            module_settings = config_dict.get("module_settings", {})
1170            # Priority: exact name → type fallback → no-op
1171            if module_name in module_settings:
1172                custom = module_settings[module_name]
1173                resolved_key = module_name
1174            elif module_type and module_type in module_settings:
1175                custom = module_settings[module_type]
1176                resolved_key = module_type
1177            else:
1178                # No custom settings for this module or type — keep defaults.
1179                return
1180            loaded = 0
1181            skipped = 0
1182            for key, json_val in custom.items():
1183                if key not in PAIConfig._CUSTOMIZABLE:
1184                    continue
1185                type_hint = PAIConfig._TYPES.get(key)
1186                private_key = f"_{key}"
1187                # Write directly to __dict__ so we bypass any setter guards and also
1188                # correctly handle vars that are not pre-initialised on per-module
1189                # configs (e.g. output_dimensions, which only lives inside the
1190                # ``if not module_name:`` block of __init__).
1191                try:
1192                    self.__dict__[private_key] = (
1193                        _deserialize_pai_value(json_val, type_hint)
1194                        if type_hint is not None
1195                        else json_val
1196                    )
1197                    loaded += 1
1198                except Exception as exc:
1199                    print(
1200                        f"[PAI Config] Warning: could not load '{key}' for '{resolved_key}': {exc}"
1201                    )
1202                    skipped += 1
1203            print(
1204                f"[PAI Config] Loaded {loaded} custom vars for '{resolved_key}' from {filename}"
1205                + (f" ({skipped} skipped)" if skipped else "")
1206            )
1207            return
1208
1209        # ── Global load: every variable in the JSON ──────────────────────────
1210        loaded = 0
1211        skipped = 0
1212        for key, json_val in config_dict.items():
1213            # Skip internal bookkeeping and Studio-only metadata keys.
1214            # 'module_name' and 'module_type' must never overwrite the
1215            # instance's _module_name/_module_type (they are internal only).
1216            if key in (
1217                "config_file",
1218                "module_settings",
1219                "module_name",
1220                "module_type",
1221            ) or key.startswith("_"):
1222                continue
1223            type_hint = PAIConfig._TYPES.get(key)
1224            private_key = f"_{key}"
1225            if hasattr(self, private_key):
1226                try:
1227                    setattr(
1228                        self,
1229                        private_key,
1230                        (
1231                            _deserialize_pai_value(json_val, type_hint)
1232                            if type_hint is not None
1233                            else json_val
1234                        ),
1235                    )
1236                    loaded += 1
1237                except Exception as exc:
1238                    print(f"[PAI Config] Warning: could not load '{key}': {exc}")
1239                    skipped += 1
1240            elif hasattr(self, key) and not callable(getattr(self, key, None)):
1241                try:
1242                    setattr(self, key, json_val)
1243                    loaded += 1
1244                except Exception:
1245                    skipped += 1
1246
1247        print(
1248            f"[PAI Config] Loaded {loaded} variables from {filename}"
1249            + (f" ({skipped} skipped)" if skipped else "")
1250        )

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):
1253class PAISequential(nn.Sequential):
1254    """Sequential module wrapper for PAI.
1255
1256    This wrapper takes an array of layers and creates a sequential container
1257    that is compatible with PAI's dendrite addition system. It should be used
1258    for normalization layers and can be used for final output layers.
1259
1260    Parameters
1261    ----------
1262    layer_array : list
1263        List of PyTorch nn.Module objects to be executed sequentially.
1264
1265    Examples
1266    --------
1267    >>> layers = [nn.Linear(2 * hidden_dim, seq_width),
1268    ...           nn.LayerNorm(seq_width)]
1269    >>> sequential_block = PAISequential(layers)
1270
1271    Notes
1272    -----
1273    This should be used for:
1274        - All normalization layers (LayerNorm, BatchNorm, etc.)
1275    This can be used for:
1276        - Final output layer and softmax combinations
1277    """
1278
1279    def __init__(self, layer_array):
1280        """Initialize PAISequential with a list of layers.
1281
1282        Parameters
1283        ----------
1284        layer_array : list
1285            List of PyTorch modules to execute in sequence.
1286        """
1287        super(PAISequential, self).__init__()
1288        self.model = nn.Sequential(*layer_array)
1289
1290    def forward(self, *args, **kwargs):
1291        """Forward pass through the sequential layers.
1292
1293        Parameters
1294        ----------
1295        *args
1296            Positional arguments passed to the first layer.
1297        **kwargs
1298            Keyword arguments passed to the layers.
1299
1300        Returns
1301        -------
1302        torch.Tensor
1303            Output from the final layer in the sequence.
1304        """
1305        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)
1279    def __init__(self, layer_array):
1280        """Initialize PAISequential with a list of layers.
1281
1282        Parameters
1283        ----------
1284        layer_array : list
1285            List of PyTorch modules to execute in sequence.
1286        """
1287        super(PAISequential, self).__init__()
1288        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):
1290    def forward(self, *args, **kwargs):
1291        """Forward pass through the sequential layers.
1292
1293        Parameters
1294        ----------
1295        *args
1296            Positional arguments passed to the first layer.
1297        **kwargs
1298            Keyword arguments passed to the layers.
1299
1300        Returns
1301        -------
1302        torch.Tensor
1303            Output from the final layer in the sequence.
1304        """
1305        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