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 if not self.__dict__.get("_loading_config_values", False): 144 self.__dict__.setdefault("_manually_set_keys", set()).add(var_name) 145 if var_name == "config_file": 146 self.sync_config_sources() 147 148 def appender(self, value): 149 """Append a value to the property if it is a list. 150 151 Parameters 152 ---------- 153 value : Any 154 Value or values to append to the underlying list property. 155 156 Returns 157 ------- 158 None 159 Appends items in-place and prints the resulting list value. 160 """ 161 if isinstance(getattr(self, private_name), list): 162 if var_name in ( 163 "module_ids_to_track", 164 "module_ids_to_perforate", 165 "parameter_ids_to_track", 166 ): 167 for module_id in value: 168 _validate_module_id(module_id) 169 setattr(self, private_name, getattr(self, private_name) + value) 170 print( 171 'New list value of "{}": {}'.format( 172 private_name, getattr(self, private_name) 173 ) 174 ) 175 else: 176 raise TypeError(f"Cannot append to non-list attribute '{var_name}'") 177 178 # Attach methods to the instance 179 if list_type: 180 setattr(obj, f"get_{var_name}", getter_list.__get__(obj)) 181 else: 182 setattr(obj, f"get_{var_name}", getter_val.__get__(obj)) 183 setattr(obj, f"set_{var_name}", setter.__get__(obj)) 184 setattr(obj, f"append_{var_name}", appender.__get__(obj)) 185 186 187# --------------------------------------------------------------------------- 188# JSON serialization helpers (used by PAIConfig.save_config / load_config) 189# --------------------------------------------------------------------------- 190 191 192def _resolve_dotted_name(dotted_name): 193 """Import and return an object identified by a dotted module path. 194 195 E.g. 'torch.nn.modules.conv.Conv2d' → nn.Conv2d class 196 'torch.sigmoid' → torch.sigmoid function 197 198 Returns None if the name cannot be resolved. 199 """ 200 import importlib 201 202 # Common shorthand names accepted in user-facing config files. 203 if dotted_name == "sigmoid": 204 return torch.sigmoid 205 if dotted_name == "relu": 206 return torch.relu 207 if dotted_name == "tanh": 208 return torch.tanh 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 223_TORCH_ACTIVATION_SHORTHAND = ("sigmoid", "relu", "tanh") 224 225 226def callable_config_repr(val): 227 """Canonical string form of a callable config value (e.g. pai_forward_function). 228 229 Shared by config serialisation and the interactive configuration display so 230 the two never disagree on how a callable is named. 231 """ 232 name = getattr(val, "__name__", None) or getattr(val, "__qualname__", None) 233 mod = getattr(val, "__module__", None) 234 if name and mod: 235 if name in _TORCH_ACTIVATION_SHORTHAND: 236 return f"torch.{name}" 237 return f"{mod}.{name}" 238 if name: 239 return str(name) 240 return repr(val) 241 242 243def _serialize_pai_value(val): 244 """Recursively convert a PAIConfig value to a JSON-serialisable form.""" 245 if isinstance(val, bool): 246 return val 247 if isinstance(val, (int, float, str)): 248 return val 249 if isinstance(val, torch.device): 250 return str(val) 251 if isinstance(val, torch.dtype): 252 return str(val) 253 if isinstance(val, list): 254 return [_serialize_pai_value(v) for v in val] 255 if isinstance(val, type): 256 mod = getattr(val, "__module__", "") or "" 257 return f"{mod}.{val.__name__}" if mod else val.__name__ 258 if callable(val): 259 return callable_config_repr(val) 260 return str(val) 261 262 263def _deserialize_pai_value(json_val, type_hint): 264 """Convert a JSON value to its Python type using an explicit type hint.""" 265 if json_val is None: 266 return None 267 if type_hint is torch.device: 268 return torch.device(str(json_val)) 269 if type_hint is torch.dtype: 270 v = getattr(torch, str(json_val).split(".", 1)[-1], None) 271 return v if isinstance(v, torch.dtype) else json_val 272 if type_hint is callable: 273 v = _resolve_dotted_name(json_val) if isinstance(json_val, str) else None 274 return v if (v and callable(v)) else json_val 275 if type_hint == [type]: # list whose elements are class objects 276 return [ 277 (_resolve_dotted_name(v) if isinstance(v, str) else v) 278 for v in (json_val or []) 279 ] 280 return json_val # bool, int, float, str, list — JSON value is already correct 281 282 283class PAIConfig: 284 """Configuration class for PAI settings. 285 286 This class manages all configuration parameters for the Perforated AI system, 287 including device settings, dendrite behavior, module conversion rules, 288 training parameters, and debugging options. 289 290 Attributes 291 ---------- 292 use_cuda : bool 293 Whether CUDA is available and should be used. 294 device : torch.device 295 The device to use for computation (CPU, CUDA, etc.). 296 save_name : str 297 Name used for saving models (should not be set manually). 298 debugging_output_dimensions : int 299 Debug level for input dimension checking. 300 confirm_correct_sizes : bool 301 Whether to verify tensor sizes during execution. 302 unwrapped_modules_confirmed : bool 303 Confirmation flag for using unwrapped modules. 304 weight_decay_accepted : bool 305 Confirmation flag for accepting weight decay. 306 checked_skipped_modules : bool 307 Whether skipped modules have been verified. 308 verbose : bool 309 Enable verbose logging output. 310 extra_verbose : bool 311 Enable extra verbose logging output. 312 silent : bool 313 Suppress all PAI print statements. 314 save_old_graph_scores : bool 315 Whether to save historical graph scores. 316 testing_dendrite_capacity : bool 317 Enable dendrite capacity testing mode. 318 using_safe_tensors : bool 319 Use safe tensors file format for saving. 320 global_candidates : int 321 Number of global candidate dendrites. 322 drawing_pai : bool 323 Enable PAI visualization graphs. 324 test_saves : bool 325 Save intermediary test models. 326 pai_saves : bool 327 Save PAI-specific format models. 328 output_dimensions : list 329 Format specification for input tensor dimensions. 330 improvement_threshold : float 331 Relative improvement threshold for validation scores. 332 improvement_threshold_raw : float 333 Absolute improvement threshold for validation scores. 334 candidate_weight_initialization_multiplier : float 335 Multiplier for random dendrite weight initialization. 336 DOING_SWITCH_EVERY_TIME : int 337 Constant for switch mode: add dendrites every epoch. 338 DOING_HISTORY : int 339 Constant for switch mode: add dendrites based on validation history. 340 n_epochs_to_switch : int 341 Number of epochs without improvement before switching. 342 history_lookback : int 343 Number of epochs to average for validation history. 344 initial_history_after_switches : int 345 Epochs to wait after adding dendrites before beggining checks. 346 DOING_FIXED_SWITCH : int 347 Constant for switch mode: add dendrites at fixed intervals. 348 fixed_switch_num : int 349 Number of epochs between fixed switches. 350 first_fixed_switch_num : int 351 Number of epochs before first switch (for pretraining). 352 DOING_NO_SWITCH : int 353 Constant for switch mode: never add dendrites. 354 switch_mode : int 355 Current switch mode setting. 356 reset_best_score_on_switch : bool 357 Whether to reset best score when adding dendrites. 358 learn_dendrites_live : bool 359 Enable live dendrite learning (advanced feature). 360 no_extra_n_modes : bool 361 Disable extra neuron modes (advanced feature). 362 d_type : torch.dtype 363 Data type for dendrite weights. 364 retain_all_dendrites : bool 365 Keep dendrites even if they don't improve performance. 366 find_best_lr : bool 367 Automatically sweep learning rates when adding dendrites. 368 dont_give_up_unless_learning_rate_lowered : bool 369 Ensure search lowers learning rate at least once. 370 max_dendrite_tries : int 371 Maximum attempts to add dendrites with random initializations. 372 max_dendrites : int 373 Maximum total number of dendrites to add. 374 PARAM_VALS_BY_TOTAL_EPOCH : int 375 Constant: scheduler params tracked by total epochs. 376 PARAM_VALS_BY_UPDATE_EPOCH : int 377 Constant: scheduler params reset at each switch. 378 PARAM_VALS_BY_NEURON_EPOCH_START : int 379 Constant: scheduler params reset for neuron starts only. 380 param_vals_setting : int 381 Current parameter tracking mode. 382 pai_forward_function : callable 383 Activation function used for dendrites. 384 modules_to_perforate : list 385 Module types to convert to PAI modules for perforation. 386 module_names_to_perforate : list 387 Module names to convert to PAI modules for perforation. 388 module_ids_to_perforate : list 389 Specific module IDs to convert to PAI modules for perforation. 390 modules_to_track : list 391 Module types to track but not convert. 392 module_names_to_track : list 393 Module names to track but not convert. 394 module_ids_to_track : list 395 Specific module IDs to track but not convert. 396 modules_to_replace : list 397 Module types to replace before conversion. 398 replacement_modules : list 399 Replacement modules for modules_to_replace. 400 modules_with_processing : list 401 Module types requiring custom processing. 402 modules_processing_classes : list 403 Processing classes for modules_with_processing. 404 module_names_with_processing : list 405 Module names requiring custom processing. 406 module_by_name_processing_classes : list 407 Processing classes for module_names_with_processing. 408 module_names_to_not_save : list 409 Module names to exclude from saving. 410 perforated_backpropagation : bool 411 Whether Perforated Backpropagation is enabled. 412 """ 413 414 # Explicit type map for every config variable — used by load_config to coerce JSON values. 415 # [type] means "list whose elements are class/type objects" (need dotted-name resolution). 416 _TYPES: dict = { 417 **{ 418 k: bool 419 for k in ( 420 "use_cuda", 421 "confirm_correct_sizes", 422 "unwrapped_modules_confirmed", 423 "weight_decay_accepted", 424 "checked_skipped_modules", 425 "configuration_confirmed", 426 "verbose", 427 "extra_verbose", 428 "silent", 429 "save_old_graph_scores", 430 "testing_dendrite_capacity", 431 "strict_loading", 432 "using_safe_tensors", 433 "drawing_pai", 434 "drawing_extra_graphs", 435 "test_saves", 436 "pai_saves", 437 "reset_best_score_on_switch", 438 "learn_dendrites_live", 439 "no_extra_n_modes", 440 "retain_all_dendrites", 441 "find_best_lr", 442 "dont_give_up_unless_learning_rate_lowered", 443 "maximizing_score", 444 "candidate_weight_init_by_main", 445 "perforated_backpropagation", 446 "weight_tying_experimental", 447 "dashboard_events_enabled", 448 "dashboard_debug", 449 ) 450 }, 451 **{ 452 k: int 453 for k in ( 454 "debugging_output_dimensions", 455 "global_candidates", 456 "n_epochs_to_switch", 457 "history_lookback", 458 "initial_history_after_switches", 459 "fixed_switch_num", 460 "first_fixed_switch_num", 461 "switch_mode", 462 "max_dendrite_tries", 463 "max_dendrites", 464 "param_vals_setting", 465 ) 466 }, 467 **{ 468 k: float 469 for k in ( 470 "improvement_threshold_raw", 471 "candidate_weight_initialization_multiplier", 472 ) 473 }, 474 **{ 475 k: str 476 for k in ( 477 "save_name", 478 "library_validation_score", 479 "dashboard_url", 480 ) 481 }, 482 "device": torch.device, 483 "d_type": torch.dtype, 484 "pai_forward_function": callable, 485 **{ 486 k: list 487 for k in ( 488 "output_dimensions", 489 "improvement_threshold", 490 "module_names_to_perforate", 491 "module_ids_to_perforate", 492 "module_names_to_track", 493 "module_ids_to_track", 494 "parameter_ids_to_track", 495 "module_names_with_processing", 496 "module_names_to_not_save", 497 "library_extra_scores", 498 "library_extra_scores_without_graphing", 499 ) 500 }, 501 **{ 502 k: [type] 503 for k in ( 504 "modules_to_perforate", 505 "modules_to_track", 506 "modules_to_replace", 507 "replacement_modules", 508 "modules_with_processing", 509 "modules_processing_classes", 510 "module_by_name_processing_classes", 511 ) 512 }, 513 } 514 515 # Subset of _TYPES: variables that can be overridden on a per-module basis 516 # via the Studio. These are set outside the ``if not module_name:`` block 517 # in :py:meth:`__init__`, so they are meaningful when constructing a 518 # module-specific PAIConfig instance. 519 _CUSTOMIZABLE: dict = { 520 #"verbose": bool, 521 #"extra_verbose": bool, 522 #"silent": bool, 523 #"global_candidates": int, 524 "output_dimensions": list, 525 #"candidate_weight_initialization_multiplier": float, 526 #"candidate_weight_init_by_main": bool, 527 #"retain_all_dendrites": bool, 528 #"max_dendrites": int, 529 #"pai_forward_function": callable, 530 } 531 532 def __getstate__(self): 533 """Tell pickle what to save when this object is serialized (e.g. torch.save). 534 535 The get_*/set_*/append_* methods attached to each PAIConfig instance are 536 locally-defined closures and cannot be pickled. Only the underlying data 537 values (stored as _<name> in __dict__) need to be saved; __setstate__ 538 will recreate the methods by calling __init__ on load. 539 """ 540 import types 541 542 # Walk every attribute on this instance and keep only the plain data values. 543 # Bound methods (the get_*/set_*/append_* closures) are skipped because 544 # they cannot be serialized by pickle. 545 pickle_safe_state = {} 546 for attr_name, attr_value in self.__dict__.items(): 547 if not isinstance(attr_value, types.MethodType): 548 pickle_safe_state[attr_name] = attr_value 549 550 return pickle_safe_state 551 552 def __setstate__(self, saved_state): 553 """Restore this object from a pickled state (e.g. torch.load). 554 555 Calls __init__ to rebuild all the get_*/set_*/append_* methods, then 556 overlays the saved data values on top so all settings are preserved. 557 """ 558 # Grab the identity fields needed to reconstruct the right flavor of config 559 # (global config vs. per-module config). 560 saved_module_name = saved_state.get("_module_name") 561 saved_module_type = saved_state.get("_module_type") 562 563 # Re-run __init__ so all the get_*/set_*/append_* methods exist again. 564 # This also sets default values for every variable, which we overwrite below. 565 self.__init__(module_name=saved_module_name, module_type=saved_module_type) 566 567 # Overwrite the defaults with the actual saved values. 568 # Only restore private data attributes (prefixed with _); skip the identity 569 # fields we already handled above, and skip _config_file for now so that 570 # the setter calls above do not trigger auto-save during the restore. 571 skip_on_first_pass = {"_module_name", "_module_type", "_config_file"} 572 for attr_name, attr_value in saved_state.items(): 573 if attr_name.startswith("_") and attr_name not in skip_on_first_pass: 574 self.__dict__[attr_name] = attr_value 575 576 # Restore the config file path last. Auto-save is driven by _config_file 577 # being set, so we only put it back after all values are already in place. 578 if saved_state.get("_config_file"): 579 self.__dict__["_config_file"] = saved_state["_config_file"] 580 581 def __getattr__(self, name): 582 """Handle missing attributes gracefully, especially for PB variables. 583 584 Parameters 585 ---------- 586 name : str 587 The name of the attribute being accessed. 588 589 Returns 590 ------- 591 None or raises AttributeError 592 Returns None for missing set_ methods, raises AttributeError otherwise. 593 """ 594 if name.startswith("set_"): 595 print(f"Variable '{name[4:]}' does not exist. Ignoring set attempt.") 596 return lambda x: None 597 if name.startswith("append_"): 598 print( 599 f"List Variable '{name[7:]}' does not exist. Ignoring append attempt." 600 ) 601 return lambda x: None 602 if name.startswith("get_") and self.__dict__.get("_module_name") is not None: 603 # Module-specific config: check for a per-module override stored directly in 604 # __dict__ (written by load_config when custom JSON data was found for this 605 # module). This covers CUSTOMIZABLE vars that are not initialised for 606 # per-module configs (e.g. output_dimensions, which lives inside the 607 # ``if not module_name:`` block). 608 private_key = f"_{name[4:]}" 609 if private_key in self.__dict__: 610 stored = self.__dict__[private_key] 611 return lambda: stored 612 # Fall back to the global pc instance for vars not set on this instance. 613 global_getter = getattr(pc, name, None) 614 if global_getter is not None: 615 return global_getter 616 raise AttributeError( 617 f"'{self.__class__.__name__}' object has no attribute '{name}'" 618 ) 619 620 def __init__(self, module_name=None, module_type=None): 621 """Initialize PAIConfig with default settings. 622 623 module_name=None means this is the main global config. 624 If module_name is set this is a per-module config that loads 625 custom settings from module_settings[module_name] (by id) or 626 module_settings[module_type] (by type) in the JSON file. 627 """ 628 # Must be first: prevents __getattr__ from firing for _config_file 629 # during construction (before add_pai_config_var_functions sets it). 630 # Also disables auto-save in setters until the end of __init__. 631 self.__dict__["_config_file"] = None 632 self.__dict__["_manually_set_keys"] = set() 633 self.__dict__["_loading_config_values"] = False 634 self.__dict__["_auto_persist_config"] = True 635 # None = global config; any string = per-module config 636 self.__dict__["_module_name"] = module_name 637 # Short class name of the wrapped module (e.g. 'Conv2d'), used as 638 # a fallback lookup key when the specific name has no saved settings. 639 self.__dict__["_module_type"] = module_type 640 641 if not module_name: 642 ### Global Constants 643 # Device configuration 644 self.use_cuda = torch.cuda.is_available() 645 add_pai_config_var_functions(self, "use_cuda", self.use_cuda) 646 self.device = torch.device("cuda" if self.use_cuda else "cpu") 647 add_pai_config_var_functions(self, "device", self.device) 648 649 self.save_name = "" 650 add_pai_config_var_functions(self, "save_name", self.save_name) 651 652 self.config_file = None 653 add_pai_config_var_functions(self, "config_file", self.config_file) 654 655 # Debug settings 656 self.debugging_output_dimensions = 0 657 add_pai_config_var_functions( 658 self, "debugging_output_dimensions", self.debugging_output_dimensions 659 ) 660 # Debugging input tensor sizes. 661 # This will slow things down very slightly and is not necessary but can help 662 # catch when dimensions were not filled in correctly. 663 self.confirm_correct_sizes = False 664 add_pai_config_var_functions( 665 self, "confirm_correct_sizes", self.confirm_correct_sizes 666 ) 667 668 # Confirmation flags for non-recommended options 669 self.unwrapped_modules_confirmed = False 670 add_pai_config_var_functions( 671 self, "unwrapped_modules_confirmed", self.unwrapped_modules_confirmed 672 ) 673 # We used to be worried this caused problems, but it is now accepted. 674 # Leaving code in case future changes require revisiting this decision. 675 self.weight_decay_accepted = True 676 add_pai_config_var_functions( 677 self, "weight_decay_accepted", self.weight_decay_accepted 678 ) 679 self.checked_skipped_modules = False 680 add_pai_config_var_functions( 681 self, "checked_skipped_modules", self.checked_skipped_modules 682 ) 683 self.configuration_confirmed = False 684 add_pai_config_var_functions( 685 self, "configuration_confirmed", self.configuration_confirmed 686 ) 687 # Analysis settings 688 self.save_old_graph_scores = True 689 add_pai_config_var_functions( 690 self, "save_old_graph_scores", self.save_old_graph_scores 691 ) 692 # Testing settings 693 self.testing_dendrite_capacity = True 694 add_pai_config_var_functions( 695 self, "testing_dendrite_capacity", self.testing_dendrite_capacity 696 ) 697 698 # File format settings 699 self.using_safe_tensors = True 700 add_pai_config_var_functions( 701 self, "using_safe_tensors", self.using_safe_tensors 702 ) 703 704 # Checkpoint loading settings 705 # Whether to use strict=True when loading state_dict 706 # Set to False if loading old checkpoints that are missing new fields 707 self.strict_loading = True 708 add_pai_config_var_functions( 709 self, "strict_loading", self.strict_loading 710 ) 711 712 # Graph and visualization settings 713 # A graph setting which can be set to false if you want to do your own 714 # training visualizations 715 self.drawing_pai = True 716 add_pai_config_var_functions(self, "drawing_pai", self.drawing_pai) 717 718 # Drawing extra graphs beyond the standard ones. 719 self.drawing_extra_graphs = True 720 add_pai_config_var_functions( 721 self, "drawing_extra_graphs", self.drawing_extra_graphs 722 ) 723 724 # Saving test intermediary models, good for experimentation, bad for memory 725 self.test_saves = True 726 add_pai_config_var_functions(self, "test_saves", self.test_saves) 727 # To be filled in later. pai_saves will remove some extra scaffolding for 728 # slight memory and speed improvements 729 self.pai_saves = False 730 add_pai_config_var_functions(self, "pai_saves", self.pai_saves) 731 # Improvement thresholds 732 # Percentage improvement increase needed to call a new best validation score 733 self.improvement_threshold = [0.001, 0.0001, 0.0] 734 add_pai_config_var_functions( 735 self, "improvement_threshold", self.improvement_threshold 736 ) 737 738 # Raw increase needed 739 self.improvement_threshold_raw = 1e-5 740 add_pai_config_var_functions( 741 self, "improvement_threshold_raw", self.improvement_threshold_raw 742 ) 743 # SWITCH MODE SETTINGS 744 745 # Add dendrites every time to debug implementation 746 self.DOING_SWITCH_EVERY_TIME = 0 747 748 # Switch when validation hasn't improved over x epochs 749 self.DOING_HISTORY = 1 750 # Epochs to try before deciding to load previous best and add dendrites 751 # Be sure this is higher than scheduler patience 752 self.n_epochs_to_switch = 10 753 add_pai_config_var_functions( 754 self, "n_epochs_to_switch", self.n_epochs_to_switch 755 ) 756 # Number to average validation scores over 757 self.history_lookback = 1 758 add_pai_config_var_functions( 759 self, "history_lookback", self.history_lookback 760 ) 761 # Amount of epochs to run after adding a new set of dendrites before checking 762 # to add more 763 self.initial_history_after_switches = 0 764 add_pai_config_var_functions( 765 self, 766 "initial_history_after_switches", 767 self.initial_history_after_switches, 768 ) 769 770 # Switch after a fixed number of epochs 771 self.DOING_FIXED_SWITCH = 2 772 # Number of epochs to complete before switching 773 self.fixed_switch_num = 250 774 add_pai_config_var_functions( 775 self, "fixed_switch_num", self.fixed_switch_num 776 ) 777 # An additional flag if you want your first switch to occur later than all the 778 # rest for initial pretraining. This is a new minimum, if its lower than 779 # the above it will be ignored. 780 self.first_fixed_switch_num = -1 781 add_pai_config_var_functions( 782 self, "first_fixed_switch_num", self.first_fixed_switch_num 783 ) 784 785 # A setting to not add dendrites and just do regular training 786 # Warning, this will also never trigger training_complete 787 self.DOING_NO_SWITCH = 3 788 789 # Default switch mode 790 self.switch_mode = self.DOING_HISTORY 791 add_pai_config_var_functions(self, "switch_mode", self.switch_mode) 792 793 # Reset settings 794 # Resets score on switch 795 # This can be useful if you need many epochs to catch up to the best score 796 # from the previous version after adding dendrites 797 self.reset_best_score_on_switch = True 798 add_pai_config_var_functions( 799 self, "reset_best_score_on_switch", self.reset_best_score_on_switch 800 ) 801 802 # Advanced settings 803 # Not used in open source implementation, leave as default 804 self.learn_dendrites_live = False 805 add_pai_config_var_functions( 806 self, "learn_dendrites_live", self.learn_dendrites_live 807 ) 808 self.no_extra_n_modes = True 809 add_pai_config_var_functions( 810 self, "no_extra_n_modes", self.no_extra_n_modes 811 ) 812 813 # Data type for new modules and dendrite to dendrite / dendrite to neuron 814 # weights 815 self.d_type = torch.float 816 add_pai_config_var_functions(self, "d_type", self.d_type) 817 818 # Learning rate management 819 # A setting to automatically sweep over previously used learning rates when 820 # adding new dendrites 821 # Sometimes it's best to go back to initial LR, but often its best to start 822 # at a lower LR 823 self.find_best_lr = True 824 add_pai_config_var_functions(self, "find_best_lr", self.find_best_lr) 825 # Enforces the above even if the previous epoch didn't lower the learning rate 826 self.dont_give_up_unless_learning_rate_lowered = True 827 add_pai_config_var_functions( 828 self, 829 "dont_give_up_unless_learning_rate_lowered", 830 self.dont_give_up_unless_learning_rate_lowered, 831 ) 832 # Whether a higher validation score is better (True) or lower is better (False). 833 self.maximizing_score = True 834 add_pai_config_var_functions( 835 self, "maximizing_score", self.maximizing_score 836 ) 837 838 # Dendrite attempt settings 839 # Set to 1 if you want to quit as soon as one dendrite fails 840 # Higher values will try new random dendrite weights this many times before 841 # accepting that more dendrites don't improve 842 self.max_dendrite_tries = 2 843 add_pai_config_var_functions( 844 self, "max_dendrite_tries", self.max_dendrite_tries 845 ) 846 847 # Scheduler parameter settings 848 # Have learning rate params be by total epoch 849 self.PARAM_VALS_BY_TOTAL_EPOCH = 0 850 # Reset the params at every switch 851 self.PARAM_VALS_BY_UPDATE_EPOCH = 1 852 # Reset params for dendrite starts but not for normal restarts 853 # Not used for open source version 854 self.PARAM_VALS_BY_NEURON_EPOCH_START = 2 855 # Default setting 856 self.param_vals_setting = self.PARAM_VALS_BY_UPDATE_EPOCH 857 add_pai_config_var_functions( 858 self, "param_vals_setting", self.param_vals_setting 859 ) 860 # Lists for module types and names to add dendrites to 861 # For these lists no specifier means type, name is module name 862 # and ids is the individual modules id, eg. model.conv2 863 self.modules_to_perforate = [] 864 add_pai_config_var_functions( 865 self, "modules_to_perforate", self.modules_to_perforate, list_type=True 866 ) 867 self.module_names_to_perforate = [ 868 "PAISequential", 869 "Conv1d", 870 "Conv2d", 871 "Conv3d", 872 "Linear", 873 ] 874 add_pai_config_var_functions( 875 self, 876 "module_names_to_perforate", 877 self.module_names_to_perforate, 878 list_type=True, 879 ) 880 self.module_ids_to_perforate = [] 881 add_pai_config_var_functions( 882 self, 883 "module_ids_to_perforate", 884 self.module_ids_to_perforate, 885 list_type=True, 886 ) 887 888 # All modules should either be perforated or tracked to ensure all modules 889 # are accounted for 890 self.modules_to_track = [] 891 add_pai_config_var_functions( 892 self, "modules_to_track", self.modules_to_track, list_type=True 893 ) 894 self.module_names_to_track = [] 895 add_pai_config_var_functions( 896 self, 897 "module_names_to_track", 898 self.module_names_to_track, 899 list_type=True, 900 ) 901 # IDs are for if you want to pass only a single module by its assigned ID rather than the module type by name 902 self.module_ids_to_track = [] 903 add_pai_config_var_functions( 904 self, "module_ids_to_track", self.module_ids_to_track, list_type=True 905 ) 906 907 # Parameter IDs to track as neuron parameters without recursive behavior 908 # (e.g., [".my_custom_parameter"]). 909 self.parameter_ids_to_track = [] 910 add_pai_config_var_functions( 911 self, 912 "parameter_ids_to_track", 913 self.parameter_ids_to_track, 914 list_type=True, 915 ) 916 917 # Replacement modules happen before the conversion, 918 # so replaced modules will then also be run through the conversion steps 919 # These are for modules that need to be replaced before addition of dendrites 920 # See the resnet example in models_perforatedai 921 self.modules_to_replace = [] 922 add_pai_config_var_functions( 923 self, "modules_to_replace", self.modules_to_replace, list_type=True 924 ) 925 # Modules to replace the above modules with 926 self.replacement_modules = [] 927 add_pai_config_var_functions( 928 self, "replacement_modules", self.replacement_modules, list_type=True 929 ) 930 931 # Dendrites default to modules which are one tensor input and one tensor 932 # output in forward() 933 # Other modules require to be labeled as modules with processing and assigned 934 # processing classes 935 # This can be done by module type or module name see customization.md in API 936 # for example 937 self.modules_with_processing = [] 938 add_pai_config_var_functions( 939 self, 940 "modules_with_processing", 941 self.modules_with_processing, 942 list_type=True, 943 ) 944 self.modules_processing_classes = [] 945 add_pai_config_var_functions( 946 self, 947 "modules_processing_classes", 948 self.modules_processing_classes, 949 list_type=True, 950 ) 951 self.module_names_with_processing = [] 952 add_pai_config_var_functions( 953 self, 954 "module_names_with_processing", 955 self.module_names_with_processing, 956 list_type=True, 957 ) 958 self.module_by_name_processing_classes = [] 959 add_pai_config_var_functions( 960 self, 961 "module_by_name_processing_classes", 962 self.module_by_name_processing_classes, 963 list_type=True, 964 ) 965 966 # Similarly here as above. Some huggingface models have multiple pointers to 967 # the same modules which cause problems 968 # If you want to only save one of the multiple pointers you can set which ones 969 # not to save here 970 self.module_names_to_not_save = [".base_model"] 971 add_pai_config_var_functions( 972 self, 973 "module_names_to_not_save", 974 self.module_names_to_not_save, 975 list_type=True, 976 ) 977 978 # Perforated Backpropagation settings 979 self.perforated_backpropagation = False 980 add_pai_config_var_functions( 981 self, "perforated_backpropagation", self.perforated_backpropagation 982 ) 983 984 # This is specifically a workaround for weight tying 985 # Not to be used for a duplicate pointer that isn't actually run twice 986 self.weight_tying_experimental = False 987 add_pai_config_var_functions( 988 self, "weight_tying_experimental", self.weight_tying_experimental 989 ) 990 991 # Dashboard event streaming settings 992 self.dashboard_events_enabled = False 993 add_pai_config_var_functions( 994 self, "dashboard_events_enabled", self.dashboard_events_enabled 995 ) 996 self.dashboard_url = "http://localhost:3002" 997 add_pai_config_var_functions( 998 self, "dashboard_url", self.dashboard_url 999 ) 1000 self.dashboard_debug = False 1001 add_pai_config_var_functions( 1002 self, "dashboard_debug", self.dashboard_debug 1003 ) 1004 1005 # These are settings where libraries must be doing the scoring adding to 1006 # message from your main script what metric to use 1007 self.library_validation_score = "" 1008 add_pai_config_var_functions( 1009 self, "library_validation_score", self.library_validation_score 1010 ) 1011 self.library_extra_scores = [] 1012 add_pai_config_var_functions( 1013 self, 1014 "library_extra_scores", 1015 self.library_extra_scores, 1016 list_type=True, 1017 ) 1018 self.library_extra_scores_without_graphing = [] 1019 add_pai_config_var_functions( 1020 self, 1021 "library_extra_scores_without_graphing", 1022 self.library_extra_scores_without_graphing, 1023 list_type=True, 1024 ) 1025 1026 # Input dimensions needs to be set every time. It is set to what format of 1027 # planes you are expecting. 1028 # Neuron index should be set to 0, variable indexes should be set to -1. 1029 # For example, if your format is [batchsize, nodes, x, y] 1030 # output_dimensions is [-1, 0, -1, -1]. 1031 # if your format is, [batchsize, time index, nodes] output_dimensions is 1032 # [-1, -1, 0] 1033 self.output_dimensions = [-1, 0, -1, -1] 1034 add_pai_config_var_functions( 1035 self, "output_dimensions", self.output_dimensions, list_type=True 1036 ) 1037 # Verbosity settings 1038 self.verbose = False 1039 add_pai_config_var_functions(self, "verbose", self.verbose) 1040 self.extra_verbose = False 1041 add_pai_config_var_functions(self, "extra_verbose", self.extra_verbose) 1042 # Suppress all PAI prints 1043 self.silent = False 1044 add_pai_config_var_functions(self, "silent", self.silent) 1045 1046 # In place for future implementation options of adding multiple candidate 1047 # dendrites together 1048 self.global_candidates = 1 1049 add_pai_config_var_functions(self, "global_candidates", self.global_candidates) 1050 1051 # Weight initialization settings 1052 # Multiplier when randomizing dendrite weights 1053 self.candidate_weight_initialization_multiplier = 0.01 1054 add_pai_config_var_functions( 1055 self, 1056 "candidate_weight_initialization_multiplier", 1057 self.candidate_weight_initialization_multiplier, 1058 ) 1059 # Multiplier when randomizing dendrite weights 1060 self.candidate_weight_init_by_main = False 1061 add_pai_config_var_functions( 1062 self, 1063 "candidate_weight_init_by_main", 1064 self.candidate_weight_init_by_main, 1065 ) 1066 1067 # Dendrite retention settings 1068 # A setting to keep dendrites even if they do not improve scores 1069 self.retain_all_dendrites = False 1070 add_pai_config_var_functions( 1071 self, "retain_all_dendrites", self.retain_all_dendrites 1072 ) 1073 1074 # Max dendrites to add even if they do continue improving scores 1075 self.max_dendrites = 100 1076 add_pai_config_var_functions(self, "max_dendrites", self.max_dendrites) 1077 1078 # Activation function settings 1079 # The activation function to use for dendrites 1080 self.pai_forward_function = torch.sigmoid 1081 add_pai_config_var_functions( 1082 self, "pai_forward_function", self.pai_forward_function 1083 ) 1084 1085 # Apply per-module overrides after defaults are defined so loaded values 1086 # win over initializer defaults. 1087 if module_name is not None: 1088 import os 1089 1090 global_pc = globals().get("pc") 1091 source_paths = [] 1092 if global_pc is not None: 1093 config_file = getattr(global_pc, "__dict__", {}).get("_config_file") 1094 run_config = global_pc.get_run_config_path() 1095 if config_file: 1096 source_paths.append(config_file) 1097 if run_config: 1098 source_paths.append(run_config) 1099 1100 for source_path in source_paths: 1101 if source_path and os.path.exists(source_path): 1102 self.load_config( 1103 source_path, 1104 module_name=module_name, 1105 module_type=module_type, 1106 ) 1107 break 1108 1109 # ------------------------------------------------------------------ 1110 # Config file will be set when save_name is assigned (in perforate_model) 1111 # ------------------------------------------------------------------ 1112 # _config_file stays None until save_name is set to a non-empty value 1113 1114 # ------------------------------------------------------------------ 1115 1116 def save_config(self, filename): 1117 """Save the current PAIConfig state to a JSON file. 1118 1119 Parameters 1120 ---------- 1121 filename : str 1122 Destination file path (created or overwritten). 1123 1124 Notes 1125 ----- 1126 Values that are not natively JSON-serialisable (torch.device, 1127 torch.dtype, nn.Module subclasses, callables) are stored as their 1128 dotted-string representations so they can be round-tripped by 1129 :py:meth:`load_config`. 1130 1131 Returns 1132 ------- 1133 None 1134 This function does not return a value. 1135 """ 1136 import json 1137 1138 config_dict = {} 1139 1140 # Private-storage vars added by add_pai_config_var_functions 1141 # e.g. self._module_ids_to_perforate → key 'module_ids_to_perforate' 1142 for key, val in sorted(self.__dict__.items()): 1143 if not (key.startswith("_") and not key.startswith("__")): 1144 continue 1145 # Skip internal bookkeeping keys that must not round-trip through JSON 1146 if key in ( 1147 "_config_file", 1148 "_module_name", 1149 "_module_type", 1150 "_manually_set_keys", 1151 "_loading_config_values", 1152 "_auto_persist_config", 1153 ): 1154 continue 1155 clean_key = key[1:] 1156 # Keep callable config fields (e.g. pai_forward_function) but skip 1157 # any other private callable values that are not typed as callable. 1158 if callable(val) and PAIConfig._TYPES.get(clean_key) is not callable: 1159 continue 1160 try: 1161 config_dict[clean_key] = _serialize_pai_value(val) 1162 except Exception: 1163 config_dict[clean_key] = str(val) 1164 1165 # Plain constants (DOING_*, PARAM_VALS_BY_*, etc.) 1166 for key, val in self.__dict__.items(): 1167 if key.startswith("_") or callable(val): 1168 continue 1169 if key not in config_dict: 1170 # Keep config_file runtime-only: do not persist pointer paths in 1171 # saved JSON snapshots. 1172 if key == "config_file": 1173 continue 1174 try: 1175 config_dict[key] = _serialize_pai_value(val) 1176 except Exception: 1177 config_dict[key] = str(val) 1178 1179 # Merge short class names from modules_to_perforate into module_names_to_perforate 1180 # so the UI (and JS) only needs to check one array. 1181 type_short_names = [ 1182 cls.__name__ 1183 for cls in self.__dict__.get("_modules_to_perforate", []) 1184 if isinstance(cls, type) 1185 ] 1186 existing = config_dict.get("module_names_to_perforate", []) 1187 config_dict["module_names_to_perforate"] = existing + [ 1188 n for n in type_short_names if n not in existing 1189 ] 1190 1191 # Preserve any per-module settings written by the Studio frontend. 1192 _existing_ms: dict = {} 1193 try: 1194 with open(filename, "r") as _f: 1195 _existing_ms = json.load(_f).get("module_settings", {}) 1196 except Exception: 1197 pass 1198 config_dict["module_settings"] = _existing_ms 1199 1200 # Publish customizable field type names so the Studio frontend can 1201 # render appropriate editors without needing to import the library. 1202 config_dict["_customizable_fields"] = { 1203 k: (v.__name__ if hasattr(v, "__name__") else str(v)) 1204 for k, v in PAIConfig._CUSTOMIZABLE.items() 1205 } 1206 1207 # Ensure the directory exists before saving 1208 import os 1209 1210 directory = os.path.dirname(filename) 1211 if directory: 1212 os.makedirs(directory, exist_ok=True) 1213 1214 with open(filename, "w") as f: 1215 json.dump(config_dict, f, indent=2) 1216 print(f"[PAI Config] Saved {len(config_dict)} variables \u2192 {filename}") 1217 1218 def load_config(self, filename, module_name=None, module_type=None, skip_keys=None): 1219 """Load PAIConfig state from a JSON file produced by :py:meth:`save_config`. 1220 1221 If *module_name* is ``None`` (default) every serialisable variable in 1222 the file is restored on this instance. 1223 1224 If *module_name* is given the lookup priority is: 1225 1. ``module_settings[module_name]`` (exact name / id match) 1226 2. ``module_settings[module_type]`` (type-level fallback) 1227 3. No-op — the defaults already set by ``__init__`` are kept. 1228 1229 Parameters 1230 ---------- 1231 filename : str 1232 Path to the JSON file to read. 1233 module_name : str, optional 1234 Display name (id) of the module whose custom settings should be loaded. 1235 module_type : str, optional 1236 Short class name of the module type, used as a fallback key. 1237 1238 skip_keys : set, optional 1239 Variable names to ignore while loading (manual overrides). 1240 1241 Returns 1242 ------- 1243 None 1244 This function does not return a value. 1245 """ 1246 import json 1247 1248 with open(filename, "r") as f: 1249 config_dict = json.load(f) 1250 1251 if module_name is not None: 1252 # ── Per-module load ────────────────────────────────────────────── 1253 module_settings = config_dict.get("module_settings", {}) 1254 # Priority: exact name → type fallback → no-op 1255 if module_name in module_settings: 1256 custom = module_settings[module_name] 1257 resolved_key = module_name 1258 elif module_type and module_type in module_settings: 1259 custom = module_settings[module_type] 1260 resolved_key = module_type 1261 else: 1262 # No custom settings for this module or type — keep defaults. 1263 return 1264 loaded = 0 1265 skipped = 0 1266 for key, json_val in custom.items(): 1267 if key not in PAIConfig._CUSTOMIZABLE: 1268 continue 1269 type_hint = PAIConfig._TYPES.get(key) 1270 private_key = f"_{key}" 1271 # Write directly to __dict__ so we bypass any setter guards and also 1272 # correctly handle vars that are not pre-initialised on per-module 1273 # configs (e.g. output_dimensions, which only lives inside the 1274 # ``if not module_name:`` block of __init__). 1275 try: 1276 self.__dict__[private_key] = ( 1277 _deserialize_pai_value(json_val, type_hint) 1278 if type_hint is not None 1279 else json_val 1280 ) 1281 loaded += 1 1282 except Exception as exc: 1283 print( 1284 f"[PAI Config] Warning: could not load '{key}' for '{resolved_key}': {exc}" 1285 ) 1286 skipped += 1 1287 print( 1288 f"[PAI Config] Loaded {loaded} custom vars for '{resolved_key}' from {filename}" 1289 + (f" ({skipped} skipped)" if skipped else "") 1290 ) 1291 return 1292 1293 # ── Global load: every variable in the JSON ────────────────────────── 1294 skip_keys = set(skip_keys or []) 1295 loaded = 0 1296 skipped = 0 1297 self.__dict__["_loading_config_values"] = True 1298 try: 1299 for key, json_val in config_dict.items(): 1300 # Skip internal bookkeeping and Studio-only metadata keys. 1301 # 'module_name' and 'module_type' must never overwrite the 1302 # instance's _module_name/_module_type (they are internal only). 1303 if key in ( 1304 "config_file", 1305 "module_settings", 1306 "module_name", 1307 "module_type", 1308 ) or key.startswith("_"): 1309 continue 1310 if key in skip_keys: 1311 continue 1312 type_hint = PAIConfig._TYPES.get(key) 1313 private_key = f"_{key}" 1314 if hasattr(self, private_key): 1315 try: 1316 setattr( 1317 self, 1318 private_key, 1319 ( 1320 _deserialize_pai_value(json_val, type_hint) 1321 if type_hint is not None 1322 else json_val 1323 ), 1324 ) 1325 loaded += 1 1326 except Exception as exc: 1327 print(f"[PAI Config] Warning: could not load '{key}': {exc}") 1328 skipped += 1 1329 elif hasattr(self, key) and not callable(getattr(self, key, None)): 1330 try: 1331 setattr(self, key, json_val) 1332 loaded += 1 1333 except Exception: 1334 skipped += 1 1335 finally: 1336 self.__dict__["_loading_config_values"] = False 1337 1338 print( 1339 f"[PAI Config] Loaded {loaded} variables from {filename}" 1340 + (f" ({skipped} skipped)" if skipped else "") 1341 ) 1342 1343 def get_run_config_path(self): 1344 """Get the run-specific config path derived from save_name. 1345 1346 Returns 1347 ------- 1348 str or None 1349 Path to the run config file, or None when save_name is unset. 1350 """ 1351 import os 1352 1353 save_name = self.__dict__.get("_save_name") 1354 if not save_name: 1355 return None 1356 return os.path.join(os.getcwd(), save_name, f"{save_name}_config.json") 1357 1358 def sync_config_sources(self): 1359 """Load config values from configured local JSON sources. 1360 1361 Load order: 1362 1. config_file pointer (if set) 1363 2. run config derived from save_name (if present) 1364 1365 Keys set manually in this process are never overwritten. 1366 """ 1367 import os 1368 1369 if self.__dict__.get("_module_name") is not None: 1370 return 1371 1372 skip_keys = self.__dict__.get("_manually_set_keys", set()) 1373 config_file = self.__dict__.get("_config_file") 1374 run_config = self.get_run_config_path() 1375 1376 if config_file and os.path.exists(config_file): 1377 self.load_config(config_file, skip_keys=skip_keys) 1378 if run_config and os.path.exists(run_config): 1379 self.load_config(run_config, skip_keys=skip_keys) 1380 1381 def persist_config_outputs(self, overwrite_config_file=False): 1382 """Persist current config state to local JSON outputs. 1383 1384 Parameters 1385 ---------- 1386 overwrite_config_file : bool 1387 When True, also writes the configured config_file pointer target. 1388 """ 1389 if self.__dict__.get("_module_name") is not None: 1390 return 1391 1392 run_config = self.get_run_config_path() 1393 if run_config: 1394 self.save_config(run_config) 1395 1396 if overwrite_config_file: 1397 config_file = self.__dict__.get("_config_file") 1398 if config_file: 1399 self.save_config(config_file) 1400 1401 1402class PAISequential(nn.Sequential): 1403 """Sequential module wrapper for PAI. 1404 1405 This wrapper takes an array of layers and creates a sequential container 1406 that is compatible with PAI's dendrite addition system. It should be used 1407 for normalization layers and can be used for final output layers. 1408 1409 Parameters 1410 ---------- 1411 layer_array : list 1412 List of PyTorch nn.Module objects to be executed sequentially. 1413 1414 Examples 1415 -------- 1416 >>> layers = [nn.Linear(2 * hidden_dim, seq_width), 1417 ... nn.LayerNorm(seq_width)] 1418 >>> sequential_block = PAISequential(layers) 1419 1420 Notes 1421 ----- 1422 This should be used for: 1423 - All normalization layers (LayerNorm, BatchNorm, etc.) 1424 This can be used for: 1425 - Final output layer and softmax combinations 1426 """ 1427 1428 def __init__(self, layer_array): 1429 """Initialize PAISequential with a list of layers. 1430 1431 Parameters 1432 ---------- 1433 layer_array : list 1434 List of PyTorch modules to execute in sequence. 1435 """ 1436 super(PAISequential, self).__init__() 1437 self.model = nn.Sequential(*layer_array) 1438 1439 def forward(self, *args, **kwargs): 1440 """Forward pass through the sequential layers. 1441 1442 Parameters 1443 ---------- 1444 *args 1445 Positional arguments passed to the first layer. 1446 **kwargs 1447 Keyword arguments passed to the layers. 1448 1449 Returns 1450 ------- 1451 torch.Tensor 1452 Output from the final layer in the sequence. 1453 """ 1454 return self.model(*args, **kwargs) 1455 1456 1457### Global objects and variables 1458 1459### Global Modules 1460pc = PAIConfig() 1461"""Global PAIConfig instance. 1462 1463This is the primary configuration object used throughout the PAI system. 1464Modify settings through this instance to control PAI behavior. 1465""" 1466 1467"""Pointer to the PAI Tracker. 1468 1469This will be populated with the PAI Tracker instance which handles 1470the addition of dendrites during training. Initially an empty list. 1471""" 1472pai_tracker = [] 1473 1474pai_scaler = None 1475 1476# This will be set to true if perforated backpropagation is available 1477# Do not just set this to True without the library and a license, it will cause errors 1478try: 1479 import perforatedbp.globals_pbp as perforatedbp_globals 1480 1481 print("Building dendrites with Perforated Backpropagation") 1482 1483 pc.set_perforated_backpropagation(True) 1484 # This is default to True for open source version 1485 # But defaults to False for perforated backpropagation 1486 pc.set_no_extra_n_modes(False) 1487 1488 # Loop through the vars module's attributes and add them dynamically 1489 for var_name in dir(perforatedbp_globals): 1490 if not var_name.startswith("_"): 1491 add_pai_config_var_functions( 1492 pc, var_name, getattr(perforatedbp_globals, var_name) 1493 ) 1494 1495 # Merge PBP type hints into PAIConfig._TYPES so load_config can correctly 1496 # round-trip all perforatedbp variables from JSON. 1497 if hasattr(perforatedbp_globals, "_TYPES"): 1498 PAIConfig._TYPES.update(perforatedbp_globals._TYPES) 1499 1500except ImportError: 1501 print("Building dendrites without Perforated Backpropagation")
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 if not self.__dict__.get("_loading_config_values", False): 145 self.__dict__.setdefault("_manually_set_keys", set()).add(var_name) 146 if var_name == "config_file": 147 self.sync_config_sources() 148 149 def appender(self, value): 150 """Append a value to the property if it is a list. 151 152 Parameters 153 ---------- 154 value : Any 155 Value or values to append to the underlying list property. 156 157 Returns 158 ------- 159 None 160 Appends items in-place and prints the resulting list value. 161 """ 162 if isinstance(getattr(self, private_name), list): 163 if var_name in ( 164 "module_ids_to_track", 165 "module_ids_to_perforate", 166 "parameter_ids_to_track", 167 ): 168 for module_id in value: 169 _validate_module_id(module_id) 170 setattr(self, private_name, getattr(self, private_name) + value) 171 print( 172 'New list value of "{}": {}'.format( 173 private_name, getattr(self, private_name) 174 ) 175 ) 176 else: 177 raise TypeError(f"Cannot append to non-list attribute '{var_name}'") 178 179 # Attach methods to the instance 180 if list_type: 181 setattr(obj, f"get_{var_name}", getter_list.__get__(obj)) 182 else: 183 setattr(obj, f"get_{var_name}", getter_val.__get__(obj)) 184 setattr(obj, f"set_{var_name}", setter.__get__(obj)) 185 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
227def callable_config_repr(val): 228 """Canonical string form of a callable config value (e.g. pai_forward_function). 229 230 Shared by config serialisation and the interactive configuration display so 231 the two never disagree on how a callable is named. 232 """ 233 name = getattr(val, "__name__", None) or getattr(val, "__qualname__", None) 234 mod = getattr(val, "__module__", None) 235 if name and mod: 236 if name in _TORCH_ACTIVATION_SHORTHAND: 237 return f"torch.{name}" 238 return f"{mod}.{name}" 239 if name: 240 return str(name) 241 return repr(val)
Canonical string form of a callable config value (e.g. pai_forward_function).
Shared by config serialisation and the interactive configuration display so the two never disagree on how a callable is named.
284class PAIConfig: 285 """Configuration class for PAI settings. 286 287 This class manages all configuration parameters for the Perforated AI system, 288 including device settings, dendrite behavior, module conversion rules, 289 training parameters, and debugging options. 290 291 Attributes 292 ---------- 293 use_cuda : bool 294 Whether CUDA is available and should be used. 295 device : torch.device 296 The device to use for computation (CPU, CUDA, etc.). 297 save_name : str 298 Name used for saving models (should not be set manually). 299 debugging_output_dimensions : int 300 Debug level for input dimension checking. 301 confirm_correct_sizes : bool 302 Whether to verify tensor sizes during execution. 303 unwrapped_modules_confirmed : bool 304 Confirmation flag for using unwrapped modules. 305 weight_decay_accepted : bool 306 Confirmation flag for accepting weight decay. 307 checked_skipped_modules : bool 308 Whether skipped modules have been verified. 309 verbose : bool 310 Enable verbose logging output. 311 extra_verbose : bool 312 Enable extra verbose logging output. 313 silent : bool 314 Suppress all PAI print statements. 315 save_old_graph_scores : bool 316 Whether to save historical graph scores. 317 testing_dendrite_capacity : bool 318 Enable dendrite capacity testing mode. 319 using_safe_tensors : bool 320 Use safe tensors file format for saving. 321 global_candidates : int 322 Number of global candidate dendrites. 323 drawing_pai : bool 324 Enable PAI visualization graphs. 325 test_saves : bool 326 Save intermediary test models. 327 pai_saves : bool 328 Save PAI-specific format models. 329 output_dimensions : list 330 Format specification for input tensor dimensions. 331 improvement_threshold : float 332 Relative improvement threshold for validation scores. 333 improvement_threshold_raw : float 334 Absolute improvement threshold for validation scores. 335 candidate_weight_initialization_multiplier : float 336 Multiplier for random dendrite weight initialization. 337 DOING_SWITCH_EVERY_TIME : int 338 Constant for switch mode: add dendrites every epoch. 339 DOING_HISTORY : int 340 Constant for switch mode: add dendrites based on validation history. 341 n_epochs_to_switch : int 342 Number of epochs without improvement before switching. 343 history_lookback : int 344 Number of epochs to average for validation history. 345 initial_history_after_switches : int 346 Epochs to wait after adding dendrites before beggining checks. 347 DOING_FIXED_SWITCH : int 348 Constant for switch mode: add dendrites at fixed intervals. 349 fixed_switch_num : int 350 Number of epochs between fixed switches. 351 first_fixed_switch_num : int 352 Number of epochs before first switch (for pretraining). 353 DOING_NO_SWITCH : int 354 Constant for switch mode: never add dendrites. 355 switch_mode : int 356 Current switch mode setting. 357 reset_best_score_on_switch : bool 358 Whether to reset best score when adding dendrites. 359 learn_dendrites_live : bool 360 Enable live dendrite learning (advanced feature). 361 no_extra_n_modes : bool 362 Disable extra neuron modes (advanced feature). 363 d_type : torch.dtype 364 Data type for dendrite weights. 365 retain_all_dendrites : bool 366 Keep dendrites even if they don't improve performance. 367 find_best_lr : bool 368 Automatically sweep learning rates when adding dendrites. 369 dont_give_up_unless_learning_rate_lowered : bool 370 Ensure search lowers learning rate at least once. 371 max_dendrite_tries : int 372 Maximum attempts to add dendrites with random initializations. 373 max_dendrites : int 374 Maximum total number of dendrites to add. 375 PARAM_VALS_BY_TOTAL_EPOCH : int 376 Constant: scheduler params tracked by total epochs. 377 PARAM_VALS_BY_UPDATE_EPOCH : int 378 Constant: scheduler params reset at each switch. 379 PARAM_VALS_BY_NEURON_EPOCH_START : int 380 Constant: scheduler params reset for neuron starts only. 381 param_vals_setting : int 382 Current parameter tracking mode. 383 pai_forward_function : callable 384 Activation function used for dendrites. 385 modules_to_perforate : list 386 Module types to convert to PAI modules for perforation. 387 module_names_to_perforate : list 388 Module names to convert to PAI modules for perforation. 389 module_ids_to_perforate : list 390 Specific module IDs to convert to PAI modules for perforation. 391 modules_to_track : list 392 Module types to track but not convert. 393 module_names_to_track : list 394 Module names to track but not convert. 395 module_ids_to_track : list 396 Specific module IDs to track but not convert. 397 modules_to_replace : list 398 Module types to replace before conversion. 399 replacement_modules : list 400 Replacement modules for modules_to_replace. 401 modules_with_processing : list 402 Module types requiring custom processing. 403 modules_processing_classes : list 404 Processing classes for modules_with_processing. 405 module_names_with_processing : list 406 Module names requiring custom processing. 407 module_by_name_processing_classes : list 408 Processing classes for module_names_with_processing. 409 module_names_to_not_save : list 410 Module names to exclude from saving. 411 perforated_backpropagation : bool 412 Whether Perforated Backpropagation is enabled. 413 """ 414 415 # Explicit type map for every config variable — used by load_config to coerce JSON values. 416 # [type] means "list whose elements are class/type objects" (need dotted-name resolution). 417 _TYPES: dict = { 418 **{ 419 k: bool 420 for k in ( 421 "use_cuda", 422 "confirm_correct_sizes", 423 "unwrapped_modules_confirmed", 424 "weight_decay_accepted", 425 "checked_skipped_modules", 426 "configuration_confirmed", 427 "verbose", 428 "extra_verbose", 429 "silent", 430 "save_old_graph_scores", 431 "testing_dendrite_capacity", 432 "strict_loading", 433 "using_safe_tensors", 434 "drawing_pai", 435 "drawing_extra_graphs", 436 "test_saves", 437 "pai_saves", 438 "reset_best_score_on_switch", 439 "learn_dendrites_live", 440 "no_extra_n_modes", 441 "retain_all_dendrites", 442 "find_best_lr", 443 "dont_give_up_unless_learning_rate_lowered", 444 "maximizing_score", 445 "candidate_weight_init_by_main", 446 "perforated_backpropagation", 447 "weight_tying_experimental", 448 "dashboard_events_enabled", 449 "dashboard_debug", 450 ) 451 }, 452 **{ 453 k: int 454 for k in ( 455 "debugging_output_dimensions", 456 "global_candidates", 457 "n_epochs_to_switch", 458 "history_lookback", 459 "initial_history_after_switches", 460 "fixed_switch_num", 461 "first_fixed_switch_num", 462 "switch_mode", 463 "max_dendrite_tries", 464 "max_dendrites", 465 "param_vals_setting", 466 ) 467 }, 468 **{ 469 k: float 470 for k in ( 471 "improvement_threshold_raw", 472 "candidate_weight_initialization_multiplier", 473 ) 474 }, 475 **{ 476 k: str 477 for k in ( 478 "save_name", 479 "library_validation_score", 480 "dashboard_url", 481 ) 482 }, 483 "device": torch.device, 484 "d_type": torch.dtype, 485 "pai_forward_function": callable, 486 **{ 487 k: list 488 for k in ( 489 "output_dimensions", 490 "improvement_threshold", 491 "module_names_to_perforate", 492 "module_ids_to_perforate", 493 "module_names_to_track", 494 "module_ids_to_track", 495 "parameter_ids_to_track", 496 "module_names_with_processing", 497 "module_names_to_not_save", 498 "library_extra_scores", 499 "library_extra_scores_without_graphing", 500 ) 501 }, 502 **{ 503 k: [type] 504 for k in ( 505 "modules_to_perforate", 506 "modules_to_track", 507 "modules_to_replace", 508 "replacement_modules", 509 "modules_with_processing", 510 "modules_processing_classes", 511 "module_by_name_processing_classes", 512 ) 513 }, 514 } 515 516 # Subset of _TYPES: variables that can be overridden on a per-module basis 517 # via the Studio. These are set outside the ``if not module_name:`` block 518 # in :py:meth:`__init__`, so they are meaningful when constructing a 519 # module-specific PAIConfig instance. 520 _CUSTOMIZABLE: dict = { 521 #"verbose": bool, 522 #"extra_verbose": bool, 523 #"silent": bool, 524 #"global_candidates": int, 525 "output_dimensions": list, 526 #"candidate_weight_initialization_multiplier": float, 527 #"candidate_weight_init_by_main": bool, 528 #"retain_all_dendrites": bool, 529 #"max_dendrites": int, 530 #"pai_forward_function": callable, 531 } 532 533 def __getstate__(self): 534 """Tell pickle what to save when this object is serialized (e.g. torch.save). 535 536 The get_*/set_*/append_* methods attached to each PAIConfig instance are 537 locally-defined closures and cannot be pickled. Only the underlying data 538 values (stored as _<name> in __dict__) need to be saved; __setstate__ 539 will recreate the methods by calling __init__ on load. 540 """ 541 import types 542 543 # Walk every attribute on this instance and keep only the plain data values. 544 # Bound methods (the get_*/set_*/append_* closures) are skipped because 545 # they cannot be serialized by pickle. 546 pickle_safe_state = {} 547 for attr_name, attr_value in self.__dict__.items(): 548 if not isinstance(attr_value, types.MethodType): 549 pickle_safe_state[attr_name] = attr_value 550 551 return pickle_safe_state 552 553 def __setstate__(self, saved_state): 554 """Restore this object from a pickled state (e.g. torch.load). 555 556 Calls __init__ to rebuild all the get_*/set_*/append_* methods, then 557 overlays the saved data values on top so all settings are preserved. 558 """ 559 # Grab the identity fields needed to reconstruct the right flavor of config 560 # (global config vs. per-module config). 561 saved_module_name = saved_state.get("_module_name") 562 saved_module_type = saved_state.get("_module_type") 563 564 # Re-run __init__ so all the get_*/set_*/append_* methods exist again. 565 # This also sets default values for every variable, which we overwrite below. 566 self.__init__(module_name=saved_module_name, module_type=saved_module_type) 567 568 # Overwrite the defaults with the actual saved values. 569 # Only restore private data attributes (prefixed with _); skip the identity 570 # fields we already handled above, and skip _config_file for now so that 571 # the setter calls above do not trigger auto-save during the restore. 572 skip_on_first_pass = {"_module_name", "_module_type", "_config_file"} 573 for attr_name, attr_value in saved_state.items(): 574 if attr_name.startswith("_") and attr_name not in skip_on_first_pass: 575 self.__dict__[attr_name] = attr_value 576 577 # Restore the config file path last. Auto-save is driven by _config_file 578 # being set, so we only put it back after all values are already in place. 579 if saved_state.get("_config_file"): 580 self.__dict__["_config_file"] = saved_state["_config_file"] 581 582 def __getattr__(self, name): 583 """Handle missing attributes gracefully, especially for PB variables. 584 585 Parameters 586 ---------- 587 name : str 588 The name of the attribute being accessed. 589 590 Returns 591 ------- 592 None or raises AttributeError 593 Returns None for missing set_ methods, raises AttributeError otherwise. 594 """ 595 if name.startswith("set_"): 596 print(f"Variable '{name[4:]}' does not exist. Ignoring set attempt.") 597 return lambda x: None 598 if name.startswith("append_"): 599 print( 600 f"List Variable '{name[7:]}' does not exist. Ignoring append attempt." 601 ) 602 return lambda x: None 603 if name.startswith("get_") and self.__dict__.get("_module_name") is not None: 604 # Module-specific config: check for a per-module override stored directly in 605 # __dict__ (written by load_config when custom JSON data was found for this 606 # module). This covers CUSTOMIZABLE vars that are not initialised for 607 # per-module configs (e.g. output_dimensions, which lives inside the 608 # ``if not module_name:`` block). 609 private_key = f"_{name[4:]}" 610 if private_key in self.__dict__: 611 stored = self.__dict__[private_key] 612 return lambda: stored 613 # Fall back to the global pc instance for vars not set on this instance. 614 global_getter = getattr(pc, name, None) 615 if global_getter is not None: 616 return global_getter 617 raise AttributeError( 618 f"'{self.__class__.__name__}' object has no attribute '{name}'" 619 ) 620 621 def __init__(self, module_name=None, module_type=None): 622 """Initialize PAIConfig with default settings. 623 624 module_name=None means this is the main global config. 625 If module_name is set this is a per-module config that loads 626 custom settings from module_settings[module_name] (by id) or 627 module_settings[module_type] (by type) in the JSON file. 628 """ 629 # Must be first: prevents __getattr__ from firing for _config_file 630 # during construction (before add_pai_config_var_functions sets it). 631 # Also disables auto-save in setters until the end of __init__. 632 self.__dict__["_config_file"] = None 633 self.__dict__["_manually_set_keys"] = set() 634 self.__dict__["_loading_config_values"] = False 635 self.__dict__["_auto_persist_config"] = True 636 # None = global config; any string = per-module config 637 self.__dict__["_module_name"] = module_name 638 # Short class name of the wrapped module (e.g. 'Conv2d'), used as 639 # a fallback lookup key when the specific name has no saved settings. 640 self.__dict__["_module_type"] = module_type 641 642 if not module_name: 643 ### Global Constants 644 # Device configuration 645 self.use_cuda = torch.cuda.is_available() 646 add_pai_config_var_functions(self, "use_cuda", self.use_cuda) 647 self.device = torch.device("cuda" if self.use_cuda else "cpu") 648 add_pai_config_var_functions(self, "device", self.device) 649 650 self.save_name = "" 651 add_pai_config_var_functions(self, "save_name", self.save_name) 652 653 self.config_file = None 654 add_pai_config_var_functions(self, "config_file", self.config_file) 655 656 # Debug settings 657 self.debugging_output_dimensions = 0 658 add_pai_config_var_functions( 659 self, "debugging_output_dimensions", self.debugging_output_dimensions 660 ) 661 # Debugging input tensor sizes. 662 # This will slow things down very slightly and is not necessary but can help 663 # catch when dimensions were not filled in correctly. 664 self.confirm_correct_sizes = False 665 add_pai_config_var_functions( 666 self, "confirm_correct_sizes", self.confirm_correct_sizes 667 ) 668 669 # Confirmation flags for non-recommended options 670 self.unwrapped_modules_confirmed = False 671 add_pai_config_var_functions( 672 self, "unwrapped_modules_confirmed", self.unwrapped_modules_confirmed 673 ) 674 # We used to be worried this caused problems, but it is now accepted. 675 # Leaving code in case future changes require revisiting this decision. 676 self.weight_decay_accepted = True 677 add_pai_config_var_functions( 678 self, "weight_decay_accepted", self.weight_decay_accepted 679 ) 680 self.checked_skipped_modules = False 681 add_pai_config_var_functions( 682 self, "checked_skipped_modules", self.checked_skipped_modules 683 ) 684 self.configuration_confirmed = False 685 add_pai_config_var_functions( 686 self, "configuration_confirmed", self.configuration_confirmed 687 ) 688 # Analysis settings 689 self.save_old_graph_scores = True 690 add_pai_config_var_functions( 691 self, "save_old_graph_scores", self.save_old_graph_scores 692 ) 693 # Testing settings 694 self.testing_dendrite_capacity = True 695 add_pai_config_var_functions( 696 self, "testing_dendrite_capacity", self.testing_dendrite_capacity 697 ) 698 699 # File format settings 700 self.using_safe_tensors = True 701 add_pai_config_var_functions( 702 self, "using_safe_tensors", self.using_safe_tensors 703 ) 704 705 # Checkpoint loading settings 706 # Whether to use strict=True when loading state_dict 707 # Set to False if loading old checkpoints that are missing new fields 708 self.strict_loading = True 709 add_pai_config_var_functions( 710 self, "strict_loading", self.strict_loading 711 ) 712 713 # Graph and visualization settings 714 # A graph setting which can be set to false if you want to do your own 715 # training visualizations 716 self.drawing_pai = True 717 add_pai_config_var_functions(self, "drawing_pai", self.drawing_pai) 718 719 # Drawing extra graphs beyond the standard ones. 720 self.drawing_extra_graphs = True 721 add_pai_config_var_functions( 722 self, "drawing_extra_graphs", self.drawing_extra_graphs 723 ) 724 725 # Saving test intermediary models, good for experimentation, bad for memory 726 self.test_saves = True 727 add_pai_config_var_functions(self, "test_saves", self.test_saves) 728 # To be filled in later. pai_saves will remove some extra scaffolding for 729 # slight memory and speed improvements 730 self.pai_saves = False 731 add_pai_config_var_functions(self, "pai_saves", self.pai_saves) 732 # Improvement thresholds 733 # Percentage improvement increase needed to call a new best validation score 734 self.improvement_threshold = [0.001, 0.0001, 0.0] 735 add_pai_config_var_functions( 736 self, "improvement_threshold", self.improvement_threshold 737 ) 738 739 # Raw increase needed 740 self.improvement_threshold_raw = 1e-5 741 add_pai_config_var_functions( 742 self, "improvement_threshold_raw", self.improvement_threshold_raw 743 ) 744 # SWITCH MODE SETTINGS 745 746 # Add dendrites every time to debug implementation 747 self.DOING_SWITCH_EVERY_TIME = 0 748 749 # Switch when validation hasn't improved over x epochs 750 self.DOING_HISTORY = 1 751 # Epochs to try before deciding to load previous best and add dendrites 752 # Be sure this is higher than scheduler patience 753 self.n_epochs_to_switch = 10 754 add_pai_config_var_functions( 755 self, "n_epochs_to_switch", self.n_epochs_to_switch 756 ) 757 # Number to average validation scores over 758 self.history_lookback = 1 759 add_pai_config_var_functions( 760 self, "history_lookback", self.history_lookback 761 ) 762 # Amount of epochs to run after adding a new set of dendrites before checking 763 # to add more 764 self.initial_history_after_switches = 0 765 add_pai_config_var_functions( 766 self, 767 "initial_history_after_switches", 768 self.initial_history_after_switches, 769 ) 770 771 # Switch after a fixed number of epochs 772 self.DOING_FIXED_SWITCH = 2 773 # Number of epochs to complete before switching 774 self.fixed_switch_num = 250 775 add_pai_config_var_functions( 776 self, "fixed_switch_num", self.fixed_switch_num 777 ) 778 # An additional flag if you want your first switch to occur later than all the 779 # rest for initial pretraining. This is a new minimum, if its lower than 780 # the above it will be ignored. 781 self.first_fixed_switch_num = -1 782 add_pai_config_var_functions( 783 self, "first_fixed_switch_num", self.first_fixed_switch_num 784 ) 785 786 # A setting to not add dendrites and just do regular training 787 # Warning, this will also never trigger training_complete 788 self.DOING_NO_SWITCH = 3 789 790 # Default switch mode 791 self.switch_mode = self.DOING_HISTORY 792 add_pai_config_var_functions(self, "switch_mode", self.switch_mode) 793 794 # Reset settings 795 # Resets score on switch 796 # This can be useful if you need many epochs to catch up to the best score 797 # from the previous version after adding dendrites 798 self.reset_best_score_on_switch = True 799 add_pai_config_var_functions( 800 self, "reset_best_score_on_switch", self.reset_best_score_on_switch 801 ) 802 803 # Advanced settings 804 # Not used in open source implementation, leave as default 805 self.learn_dendrites_live = False 806 add_pai_config_var_functions( 807 self, "learn_dendrites_live", self.learn_dendrites_live 808 ) 809 self.no_extra_n_modes = True 810 add_pai_config_var_functions( 811 self, "no_extra_n_modes", self.no_extra_n_modes 812 ) 813 814 # Data type for new modules and dendrite to dendrite / dendrite to neuron 815 # weights 816 self.d_type = torch.float 817 add_pai_config_var_functions(self, "d_type", self.d_type) 818 819 # Learning rate management 820 # A setting to automatically sweep over previously used learning rates when 821 # adding new dendrites 822 # Sometimes it's best to go back to initial LR, but often its best to start 823 # at a lower LR 824 self.find_best_lr = True 825 add_pai_config_var_functions(self, "find_best_lr", self.find_best_lr) 826 # Enforces the above even if the previous epoch didn't lower the learning rate 827 self.dont_give_up_unless_learning_rate_lowered = True 828 add_pai_config_var_functions( 829 self, 830 "dont_give_up_unless_learning_rate_lowered", 831 self.dont_give_up_unless_learning_rate_lowered, 832 ) 833 # Whether a higher validation score is better (True) or lower is better (False). 834 self.maximizing_score = True 835 add_pai_config_var_functions( 836 self, "maximizing_score", self.maximizing_score 837 ) 838 839 # Dendrite attempt settings 840 # Set to 1 if you want to quit as soon as one dendrite fails 841 # Higher values will try new random dendrite weights this many times before 842 # accepting that more dendrites don't improve 843 self.max_dendrite_tries = 2 844 add_pai_config_var_functions( 845 self, "max_dendrite_tries", self.max_dendrite_tries 846 ) 847 848 # Scheduler parameter settings 849 # Have learning rate params be by total epoch 850 self.PARAM_VALS_BY_TOTAL_EPOCH = 0 851 # Reset the params at every switch 852 self.PARAM_VALS_BY_UPDATE_EPOCH = 1 853 # Reset params for dendrite starts but not for normal restarts 854 # Not used for open source version 855 self.PARAM_VALS_BY_NEURON_EPOCH_START = 2 856 # Default setting 857 self.param_vals_setting = self.PARAM_VALS_BY_UPDATE_EPOCH 858 add_pai_config_var_functions( 859 self, "param_vals_setting", self.param_vals_setting 860 ) 861 # Lists for module types and names to add dendrites to 862 # For these lists no specifier means type, name is module name 863 # and ids is the individual modules id, eg. model.conv2 864 self.modules_to_perforate = [] 865 add_pai_config_var_functions( 866 self, "modules_to_perforate", self.modules_to_perforate, list_type=True 867 ) 868 self.module_names_to_perforate = [ 869 "PAISequential", 870 "Conv1d", 871 "Conv2d", 872 "Conv3d", 873 "Linear", 874 ] 875 add_pai_config_var_functions( 876 self, 877 "module_names_to_perforate", 878 self.module_names_to_perforate, 879 list_type=True, 880 ) 881 self.module_ids_to_perforate = [] 882 add_pai_config_var_functions( 883 self, 884 "module_ids_to_perforate", 885 self.module_ids_to_perforate, 886 list_type=True, 887 ) 888 889 # All modules should either be perforated or tracked to ensure all modules 890 # are accounted for 891 self.modules_to_track = [] 892 add_pai_config_var_functions( 893 self, "modules_to_track", self.modules_to_track, list_type=True 894 ) 895 self.module_names_to_track = [] 896 add_pai_config_var_functions( 897 self, 898 "module_names_to_track", 899 self.module_names_to_track, 900 list_type=True, 901 ) 902 # IDs are for if you want to pass only a single module by its assigned ID rather than the module type by name 903 self.module_ids_to_track = [] 904 add_pai_config_var_functions( 905 self, "module_ids_to_track", self.module_ids_to_track, list_type=True 906 ) 907 908 # Parameter IDs to track as neuron parameters without recursive behavior 909 # (e.g., [".my_custom_parameter"]). 910 self.parameter_ids_to_track = [] 911 add_pai_config_var_functions( 912 self, 913 "parameter_ids_to_track", 914 self.parameter_ids_to_track, 915 list_type=True, 916 ) 917 918 # Replacement modules happen before the conversion, 919 # so replaced modules will then also be run through the conversion steps 920 # These are for modules that need to be replaced before addition of dendrites 921 # See the resnet example in models_perforatedai 922 self.modules_to_replace = [] 923 add_pai_config_var_functions( 924 self, "modules_to_replace", self.modules_to_replace, list_type=True 925 ) 926 # Modules to replace the above modules with 927 self.replacement_modules = [] 928 add_pai_config_var_functions( 929 self, "replacement_modules", self.replacement_modules, list_type=True 930 ) 931 932 # Dendrites default to modules which are one tensor input and one tensor 933 # output in forward() 934 # Other modules require to be labeled as modules with processing and assigned 935 # processing classes 936 # This can be done by module type or module name see customization.md in API 937 # for example 938 self.modules_with_processing = [] 939 add_pai_config_var_functions( 940 self, 941 "modules_with_processing", 942 self.modules_with_processing, 943 list_type=True, 944 ) 945 self.modules_processing_classes = [] 946 add_pai_config_var_functions( 947 self, 948 "modules_processing_classes", 949 self.modules_processing_classes, 950 list_type=True, 951 ) 952 self.module_names_with_processing = [] 953 add_pai_config_var_functions( 954 self, 955 "module_names_with_processing", 956 self.module_names_with_processing, 957 list_type=True, 958 ) 959 self.module_by_name_processing_classes = [] 960 add_pai_config_var_functions( 961 self, 962 "module_by_name_processing_classes", 963 self.module_by_name_processing_classes, 964 list_type=True, 965 ) 966 967 # Similarly here as above. Some huggingface models have multiple pointers to 968 # the same modules which cause problems 969 # If you want to only save one of the multiple pointers you can set which ones 970 # not to save here 971 self.module_names_to_not_save = [".base_model"] 972 add_pai_config_var_functions( 973 self, 974 "module_names_to_not_save", 975 self.module_names_to_not_save, 976 list_type=True, 977 ) 978 979 # Perforated Backpropagation settings 980 self.perforated_backpropagation = False 981 add_pai_config_var_functions( 982 self, "perforated_backpropagation", self.perforated_backpropagation 983 ) 984 985 # This is specifically a workaround for weight tying 986 # Not to be used for a duplicate pointer that isn't actually run twice 987 self.weight_tying_experimental = False 988 add_pai_config_var_functions( 989 self, "weight_tying_experimental", self.weight_tying_experimental 990 ) 991 992 # Dashboard event streaming settings 993 self.dashboard_events_enabled = False 994 add_pai_config_var_functions( 995 self, "dashboard_events_enabled", self.dashboard_events_enabled 996 ) 997 self.dashboard_url = "http://localhost:3002" 998 add_pai_config_var_functions( 999 self, "dashboard_url", self.dashboard_url 1000 ) 1001 self.dashboard_debug = False 1002 add_pai_config_var_functions( 1003 self, "dashboard_debug", self.dashboard_debug 1004 ) 1005 1006 # These are settings where libraries must be doing the scoring adding to 1007 # message from your main script what metric to use 1008 self.library_validation_score = "" 1009 add_pai_config_var_functions( 1010 self, "library_validation_score", self.library_validation_score 1011 ) 1012 self.library_extra_scores = [] 1013 add_pai_config_var_functions( 1014 self, 1015 "library_extra_scores", 1016 self.library_extra_scores, 1017 list_type=True, 1018 ) 1019 self.library_extra_scores_without_graphing = [] 1020 add_pai_config_var_functions( 1021 self, 1022 "library_extra_scores_without_graphing", 1023 self.library_extra_scores_without_graphing, 1024 list_type=True, 1025 ) 1026 1027 # Input dimensions needs to be set every time. It is set to what format of 1028 # planes you are expecting. 1029 # Neuron index should be set to 0, variable indexes should be set to -1. 1030 # For example, if your format is [batchsize, nodes, x, y] 1031 # output_dimensions is [-1, 0, -1, -1]. 1032 # if your format is, [batchsize, time index, nodes] output_dimensions is 1033 # [-1, -1, 0] 1034 self.output_dimensions = [-1, 0, -1, -1] 1035 add_pai_config_var_functions( 1036 self, "output_dimensions", self.output_dimensions, list_type=True 1037 ) 1038 # Verbosity settings 1039 self.verbose = False 1040 add_pai_config_var_functions(self, "verbose", self.verbose) 1041 self.extra_verbose = False 1042 add_pai_config_var_functions(self, "extra_verbose", self.extra_verbose) 1043 # Suppress all PAI prints 1044 self.silent = False 1045 add_pai_config_var_functions(self, "silent", self.silent) 1046 1047 # In place for future implementation options of adding multiple candidate 1048 # dendrites together 1049 self.global_candidates = 1 1050 add_pai_config_var_functions(self, "global_candidates", self.global_candidates) 1051 1052 # Weight initialization settings 1053 # Multiplier when randomizing dendrite weights 1054 self.candidate_weight_initialization_multiplier = 0.01 1055 add_pai_config_var_functions( 1056 self, 1057 "candidate_weight_initialization_multiplier", 1058 self.candidate_weight_initialization_multiplier, 1059 ) 1060 # Multiplier when randomizing dendrite weights 1061 self.candidate_weight_init_by_main = False 1062 add_pai_config_var_functions( 1063 self, 1064 "candidate_weight_init_by_main", 1065 self.candidate_weight_init_by_main, 1066 ) 1067 1068 # Dendrite retention settings 1069 # A setting to keep dendrites even if they do not improve scores 1070 self.retain_all_dendrites = False 1071 add_pai_config_var_functions( 1072 self, "retain_all_dendrites", self.retain_all_dendrites 1073 ) 1074 1075 # Max dendrites to add even if they do continue improving scores 1076 self.max_dendrites = 100 1077 add_pai_config_var_functions(self, "max_dendrites", self.max_dendrites) 1078 1079 # Activation function settings 1080 # The activation function to use for dendrites 1081 self.pai_forward_function = torch.sigmoid 1082 add_pai_config_var_functions( 1083 self, "pai_forward_function", self.pai_forward_function 1084 ) 1085 1086 # Apply per-module overrides after defaults are defined so loaded values 1087 # win over initializer defaults. 1088 if module_name is not None: 1089 import os 1090 1091 global_pc = globals().get("pc") 1092 source_paths = [] 1093 if global_pc is not None: 1094 config_file = getattr(global_pc, "__dict__", {}).get("_config_file") 1095 run_config = global_pc.get_run_config_path() 1096 if config_file: 1097 source_paths.append(config_file) 1098 if run_config: 1099 source_paths.append(run_config) 1100 1101 for source_path in source_paths: 1102 if source_path and os.path.exists(source_path): 1103 self.load_config( 1104 source_path, 1105 module_name=module_name, 1106 module_type=module_type, 1107 ) 1108 break 1109 1110 # ------------------------------------------------------------------ 1111 # Config file will be set when save_name is assigned (in perforate_model) 1112 # ------------------------------------------------------------------ 1113 # _config_file stays None until save_name is set to a non-empty value 1114 1115 # ------------------------------------------------------------------ 1116 1117 def save_config(self, filename): 1118 """Save the current PAIConfig state to a JSON file. 1119 1120 Parameters 1121 ---------- 1122 filename : str 1123 Destination file path (created or overwritten). 1124 1125 Notes 1126 ----- 1127 Values that are not natively JSON-serialisable (torch.device, 1128 torch.dtype, nn.Module subclasses, callables) are stored as their 1129 dotted-string representations so they can be round-tripped by 1130 :py:meth:`load_config`. 1131 1132 Returns 1133 ------- 1134 None 1135 This function does not return a value. 1136 """ 1137 import json 1138 1139 config_dict = {} 1140 1141 # Private-storage vars added by add_pai_config_var_functions 1142 # e.g. self._module_ids_to_perforate → key 'module_ids_to_perforate' 1143 for key, val in sorted(self.__dict__.items()): 1144 if not (key.startswith("_") and not key.startswith("__")): 1145 continue 1146 # Skip internal bookkeeping keys that must not round-trip through JSON 1147 if key in ( 1148 "_config_file", 1149 "_module_name", 1150 "_module_type", 1151 "_manually_set_keys", 1152 "_loading_config_values", 1153 "_auto_persist_config", 1154 ): 1155 continue 1156 clean_key = key[1:] 1157 # Keep callable config fields (e.g. pai_forward_function) but skip 1158 # any other private callable values that are not typed as callable. 1159 if callable(val) and PAIConfig._TYPES.get(clean_key) is not callable: 1160 continue 1161 try: 1162 config_dict[clean_key] = _serialize_pai_value(val) 1163 except Exception: 1164 config_dict[clean_key] = str(val) 1165 1166 # Plain constants (DOING_*, PARAM_VALS_BY_*, etc.) 1167 for key, val in self.__dict__.items(): 1168 if key.startswith("_") or callable(val): 1169 continue 1170 if key not in config_dict: 1171 # Keep config_file runtime-only: do not persist pointer paths in 1172 # saved JSON snapshots. 1173 if key == "config_file": 1174 continue 1175 try: 1176 config_dict[key] = _serialize_pai_value(val) 1177 except Exception: 1178 config_dict[key] = str(val) 1179 1180 # Merge short class names from modules_to_perforate into module_names_to_perforate 1181 # so the UI (and JS) only needs to check one array. 1182 type_short_names = [ 1183 cls.__name__ 1184 for cls in self.__dict__.get("_modules_to_perforate", []) 1185 if isinstance(cls, type) 1186 ] 1187 existing = config_dict.get("module_names_to_perforate", []) 1188 config_dict["module_names_to_perforate"] = existing + [ 1189 n for n in type_short_names if n not in existing 1190 ] 1191 1192 # Preserve any per-module settings written by the Studio frontend. 1193 _existing_ms: dict = {} 1194 try: 1195 with open(filename, "r") as _f: 1196 _existing_ms = json.load(_f).get("module_settings", {}) 1197 except Exception: 1198 pass 1199 config_dict["module_settings"] = _existing_ms 1200 1201 # Publish customizable field type names so the Studio frontend can 1202 # render appropriate editors without needing to import the library. 1203 config_dict["_customizable_fields"] = { 1204 k: (v.__name__ if hasattr(v, "__name__") else str(v)) 1205 for k, v in PAIConfig._CUSTOMIZABLE.items() 1206 } 1207 1208 # Ensure the directory exists before saving 1209 import os 1210 1211 directory = os.path.dirname(filename) 1212 if directory: 1213 os.makedirs(directory, exist_ok=True) 1214 1215 with open(filename, "w") as f: 1216 json.dump(config_dict, f, indent=2) 1217 print(f"[PAI Config] Saved {len(config_dict)} variables \u2192 {filename}") 1218 1219 def load_config(self, filename, module_name=None, module_type=None, skip_keys=None): 1220 """Load PAIConfig state from a JSON file produced by :py:meth:`save_config`. 1221 1222 If *module_name* is ``None`` (default) every serialisable variable in 1223 the file is restored on this instance. 1224 1225 If *module_name* is given the lookup priority is: 1226 1. ``module_settings[module_name]`` (exact name / id match) 1227 2. ``module_settings[module_type]`` (type-level fallback) 1228 3. No-op — the defaults already set by ``__init__`` are kept. 1229 1230 Parameters 1231 ---------- 1232 filename : str 1233 Path to the JSON file to read. 1234 module_name : str, optional 1235 Display name (id) of the module whose custom settings should be loaded. 1236 module_type : str, optional 1237 Short class name of the module type, used as a fallback key. 1238 1239 skip_keys : set, optional 1240 Variable names to ignore while loading (manual overrides). 1241 1242 Returns 1243 ------- 1244 None 1245 This function does not return a value. 1246 """ 1247 import json 1248 1249 with open(filename, "r") as f: 1250 config_dict = json.load(f) 1251 1252 if module_name is not None: 1253 # ── Per-module load ────────────────────────────────────────────── 1254 module_settings = config_dict.get("module_settings", {}) 1255 # Priority: exact name → type fallback → no-op 1256 if module_name in module_settings: 1257 custom = module_settings[module_name] 1258 resolved_key = module_name 1259 elif module_type and module_type in module_settings: 1260 custom = module_settings[module_type] 1261 resolved_key = module_type 1262 else: 1263 # No custom settings for this module or type — keep defaults. 1264 return 1265 loaded = 0 1266 skipped = 0 1267 for key, json_val in custom.items(): 1268 if key not in PAIConfig._CUSTOMIZABLE: 1269 continue 1270 type_hint = PAIConfig._TYPES.get(key) 1271 private_key = f"_{key}" 1272 # Write directly to __dict__ so we bypass any setter guards and also 1273 # correctly handle vars that are not pre-initialised on per-module 1274 # configs (e.g. output_dimensions, which only lives inside the 1275 # ``if not module_name:`` block of __init__). 1276 try: 1277 self.__dict__[private_key] = ( 1278 _deserialize_pai_value(json_val, type_hint) 1279 if type_hint is not None 1280 else json_val 1281 ) 1282 loaded += 1 1283 except Exception as exc: 1284 print( 1285 f"[PAI Config] Warning: could not load '{key}' for '{resolved_key}': {exc}" 1286 ) 1287 skipped += 1 1288 print( 1289 f"[PAI Config] Loaded {loaded} custom vars for '{resolved_key}' from {filename}" 1290 + (f" ({skipped} skipped)" if skipped else "") 1291 ) 1292 return 1293 1294 # ── Global load: every variable in the JSON ────────────────────────── 1295 skip_keys = set(skip_keys or []) 1296 loaded = 0 1297 skipped = 0 1298 self.__dict__["_loading_config_values"] = True 1299 try: 1300 for key, json_val in config_dict.items(): 1301 # Skip internal bookkeeping and Studio-only metadata keys. 1302 # 'module_name' and 'module_type' must never overwrite the 1303 # instance's _module_name/_module_type (they are internal only). 1304 if key in ( 1305 "config_file", 1306 "module_settings", 1307 "module_name", 1308 "module_type", 1309 ) or key.startswith("_"): 1310 continue 1311 if key in skip_keys: 1312 continue 1313 type_hint = PAIConfig._TYPES.get(key) 1314 private_key = f"_{key}" 1315 if hasattr(self, private_key): 1316 try: 1317 setattr( 1318 self, 1319 private_key, 1320 ( 1321 _deserialize_pai_value(json_val, type_hint) 1322 if type_hint is not None 1323 else json_val 1324 ), 1325 ) 1326 loaded += 1 1327 except Exception as exc: 1328 print(f"[PAI Config] Warning: could not load '{key}': {exc}") 1329 skipped += 1 1330 elif hasattr(self, key) and not callable(getattr(self, key, None)): 1331 try: 1332 setattr(self, key, json_val) 1333 loaded += 1 1334 except Exception: 1335 skipped += 1 1336 finally: 1337 self.__dict__["_loading_config_values"] = False 1338 1339 print( 1340 f"[PAI Config] Loaded {loaded} variables from {filename}" 1341 + (f" ({skipped} skipped)" if skipped else "") 1342 ) 1343 1344 def get_run_config_path(self): 1345 """Get the run-specific config path derived from save_name. 1346 1347 Returns 1348 ------- 1349 str or None 1350 Path to the run config file, or None when save_name is unset. 1351 """ 1352 import os 1353 1354 save_name = self.__dict__.get("_save_name") 1355 if not save_name: 1356 return None 1357 return os.path.join(os.getcwd(), save_name, f"{save_name}_config.json") 1358 1359 def sync_config_sources(self): 1360 """Load config values from configured local JSON sources. 1361 1362 Load order: 1363 1. config_file pointer (if set) 1364 2. run config derived from save_name (if present) 1365 1366 Keys set manually in this process are never overwritten. 1367 """ 1368 import os 1369 1370 if self.__dict__.get("_module_name") is not None: 1371 return 1372 1373 skip_keys = self.__dict__.get("_manually_set_keys", set()) 1374 config_file = self.__dict__.get("_config_file") 1375 run_config = self.get_run_config_path() 1376 1377 if config_file and os.path.exists(config_file): 1378 self.load_config(config_file, skip_keys=skip_keys) 1379 if run_config and os.path.exists(run_config): 1380 self.load_config(run_config, skip_keys=skip_keys) 1381 1382 def persist_config_outputs(self, overwrite_config_file=False): 1383 """Persist current config state to local JSON outputs. 1384 1385 Parameters 1386 ---------- 1387 overwrite_config_file : bool 1388 When True, also writes the configured config_file pointer target. 1389 """ 1390 if self.__dict__.get("_module_name") is not None: 1391 return 1392 1393 run_config = self.get_run_config_path() 1394 if run_config: 1395 self.save_config(run_config) 1396 1397 if overwrite_config_file: 1398 config_file = self.__dict__.get("_config_file") 1399 if config_file: 1400 self.save_config(config_file)
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.
621 def __init__(self, module_name=None, module_type=None): 622 """Initialize PAIConfig with default settings. 623 624 module_name=None means this is the main global config. 625 If module_name is set this is a per-module config that loads 626 custom settings from module_settings[module_name] (by id) or 627 module_settings[module_type] (by type) in the JSON file. 628 """ 629 # Must be first: prevents __getattr__ from firing for _config_file 630 # during construction (before add_pai_config_var_functions sets it). 631 # Also disables auto-save in setters until the end of __init__. 632 self.__dict__["_config_file"] = None 633 self.__dict__["_manually_set_keys"] = set() 634 self.__dict__["_loading_config_values"] = False 635 self.__dict__["_auto_persist_config"] = True 636 # None = global config; any string = per-module config 637 self.__dict__["_module_name"] = module_name 638 # Short class name of the wrapped module (e.g. 'Conv2d'), used as 639 # a fallback lookup key when the specific name has no saved settings. 640 self.__dict__["_module_type"] = module_type 641 642 if not module_name: 643 ### Global Constants 644 # Device configuration 645 self.use_cuda = torch.cuda.is_available() 646 add_pai_config_var_functions(self, "use_cuda", self.use_cuda) 647 self.device = torch.device("cuda" if self.use_cuda else "cpu") 648 add_pai_config_var_functions(self, "device", self.device) 649 650 self.save_name = "" 651 add_pai_config_var_functions(self, "save_name", self.save_name) 652 653 self.config_file = None 654 add_pai_config_var_functions(self, "config_file", self.config_file) 655 656 # Debug settings 657 self.debugging_output_dimensions = 0 658 add_pai_config_var_functions( 659 self, "debugging_output_dimensions", self.debugging_output_dimensions 660 ) 661 # Debugging input tensor sizes. 662 # This will slow things down very slightly and is not necessary but can help 663 # catch when dimensions were not filled in correctly. 664 self.confirm_correct_sizes = False 665 add_pai_config_var_functions( 666 self, "confirm_correct_sizes", self.confirm_correct_sizes 667 ) 668 669 # Confirmation flags for non-recommended options 670 self.unwrapped_modules_confirmed = False 671 add_pai_config_var_functions( 672 self, "unwrapped_modules_confirmed", self.unwrapped_modules_confirmed 673 ) 674 # We used to be worried this caused problems, but it is now accepted. 675 # Leaving code in case future changes require revisiting this decision. 676 self.weight_decay_accepted = True 677 add_pai_config_var_functions( 678 self, "weight_decay_accepted", self.weight_decay_accepted 679 ) 680 self.checked_skipped_modules = False 681 add_pai_config_var_functions( 682 self, "checked_skipped_modules", self.checked_skipped_modules 683 ) 684 self.configuration_confirmed = False 685 add_pai_config_var_functions( 686 self, "configuration_confirmed", self.configuration_confirmed 687 ) 688 # Analysis settings 689 self.save_old_graph_scores = True 690 add_pai_config_var_functions( 691 self, "save_old_graph_scores", self.save_old_graph_scores 692 ) 693 # Testing settings 694 self.testing_dendrite_capacity = True 695 add_pai_config_var_functions( 696 self, "testing_dendrite_capacity", self.testing_dendrite_capacity 697 ) 698 699 # File format settings 700 self.using_safe_tensors = True 701 add_pai_config_var_functions( 702 self, "using_safe_tensors", self.using_safe_tensors 703 ) 704 705 # Checkpoint loading settings 706 # Whether to use strict=True when loading state_dict 707 # Set to False if loading old checkpoints that are missing new fields 708 self.strict_loading = True 709 add_pai_config_var_functions( 710 self, "strict_loading", self.strict_loading 711 ) 712 713 # Graph and visualization settings 714 # A graph setting which can be set to false if you want to do your own 715 # training visualizations 716 self.drawing_pai = True 717 add_pai_config_var_functions(self, "drawing_pai", self.drawing_pai) 718 719 # Drawing extra graphs beyond the standard ones. 720 self.drawing_extra_graphs = True 721 add_pai_config_var_functions( 722 self, "drawing_extra_graphs", self.drawing_extra_graphs 723 ) 724 725 # Saving test intermediary models, good for experimentation, bad for memory 726 self.test_saves = True 727 add_pai_config_var_functions(self, "test_saves", self.test_saves) 728 # To be filled in later. pai_saves will remove some extra scaffolding for 729 # slight memory and speed improvements 730 self.pai_saves = False 731 add_pai_config_var_functions(self, "pai_saves", self.pai_saves) 732 # Improvement thresholds 733 # Percentage improvement increase needed to call a new best validation score 734 self.improvement_threshold = [0.001, 0.0001, 0.0] 735 add_pai_config_var_functions( 736 self, "improvement_threshold", self.improvement_threshold 737 ) 738 739 # Raw increase needed 740 self.improvement_threshold_raw = 1e-5 741 add_pai_config_var_functions( 742 self, "improvement_threshold_raw", self.improvement_threshold_raw 743 ) 744 # SWITCH MODE SETTINGS 745 746 # Add dendrites every time to debug implementation 747 self.DOING_SWITCH_EVERY_TIME = 0 748 749 # Switch when validation hasn't improved over x epochs 750 self.DOING_HISTORY = 1 751 # Epochs to try before deciding to load previous best and add dendrites 752 # Be sure this is higher than scheduler patience 753 self.n_epochs_to_switch = 10 754 add_pai_config_var_functions( 755 self, "n_epochs_to_switch", self.n_epochs_to_switch 756 ) 757 # Number to average validation scores over 758 self.history_lookback = 1 759 add_pai_config_var_functions( 760 self, "history_lookback", self.history_lookback 761 ) 762 # Amount of epochs to run after adding a new set of dendrites before checking 763 # to add more 764 self.initial_history_after_switches = 0 765 add_pai_config_var_functions( 766 self, 767 "initial_history_after_switches", 768 self.initial_history_after_switches, 769 ) 770 771 # Switch after a fixed number of epochs 772 self.DOING_FIXED_SWITCH = 2 773 # Number of epochs to complete before switching 774 self.fixed_switch_num = 250 775 add_pai_config_var_functions( 776 self, "fixed_switch_num", self.fixed_switch_num 777 ) 778 # An additional flag if you want your first switch to occur later than all the 779 # rest for initial pretraining. This is a new minimum, if its lower than 780 # the above it will be ignored. 781 self.first_fixed_switch_num = -1 782 add_pai_config_var_functions( 783 self, "first_fixed_switch_num", self.first_fixed_switch_num 784 ) 785 786 # A setting to not add dendrites and just do regular training 787 # Warning, this will also never trigger training_complete 788 self.DOING_NO_SWITCH = 3 789 790 # Default switch mode 791 self.switch_mode = self.DOING_HISTORY 792 add_pai_config_var_functions(self, "switch_mode", self.switch_mode) 793 794 # Reset settings 795 # Resets score on switch 796 # This can be useful if you need many epochs to catch up to the best score 797 # from the previous version after adding dendrites 798 self.reset_best_score_on_switch = True 799 add_pai_config_var_functions( 800 self, "reset_best_score_on_switch", self.reset_best_score_on_switch 801 ) 802 803 # Advanced settings 804 # Not used in open source implementation, leave as default 805 self.learn_dendrites_live = False 806 add_pai_config_var_functions( 807 self, "learn_dendrites_live", self.learn_dendrites_live 808 ) 809 self.no_extra_n_modes = True 810 add_pai_config_var_functions( 811 self, "no_extra_n_modes", self.no_extra_n_modes 812 ) 813 814 # Data type for new modules and dendrite to dendrite / dendrite to neuron 815 # weights 816 self.d_type = torch.float 817 add_pai_config_var_functions(self, "d_type", self.d_type) 818 819 # Learning rate management 820 # A setting to automatically sweep over previously used learning rates when 821 # adding new dendrites 822 # Sometimes it's best to go back to initial LR, but often its best to start 823 # at a lower LR 824 self.find_best_lr = True 825 add_pai_config_var_functions(self, "find_best_lr", self.find_best_lr) 826 # Enforces the above even if the previous epoch didn't lower the learning rate 827 self.dont_give_up_unless_learning_rate_lowered = True 828 add_pai_config_var_functions( 829 self, 830 "dont_give_up_unless_learning_rate_lowered", 831 self.dont_give_up_unless_learning_rate_lowered, 832 ) 833 # Whether a higher validation score is better (True) or lower is better (False). 834 self.maximizing_score = True 835 add_pai_config_var_functions( 836 self, "maximizing_score", self.maximizing_score 837 ) 838 839 # Dendrite attempt settings 840 # Set to 1 if you want to quit as soon as one dendrite fails 841 # Higher values will try new random dendrite weights this many times before 842 # accepting that more dendrites don't improve 843 self.max_dendrite_tries = 2 844 add_pai_config_var_functions( 845 self, "max_dendrite_tries", self.max_dendrite_tries 846 ) 847 848 # Scheduler parameter settings 849 # Have learning rate params be by total epoch 850 self.PARAM_VALS_BY_TOTAL_EPOCH = 0 851 # Reset the params at every switch 852 self.PARAM_VALS_BY_UPDATE_EPOCH = 1 853 # Reset params for dendrite starts but not for normal restarts 854 # Not used for open source version 855 self.PARAM_VALS_BY_NEURON_EPOCH_START = 2 856 # Default setting 857 self.param_vals_setting = self.PARAM_VALS_BY_UPDATE_EPOCH 858 add_pai_config_var_functions( 859 self, "param_vals_setting", self.param_vals_setting 860 ) 861 # Lists for module types and names to add dendrites to 862 # For these lists no specifier means type, name is module name 863 # and ids is the individual modules id, eg. model.conv2 864 self.modules_to_perforate = [] 865 add_pai_config_var_functions( 866 self, "modules_to_perforate", self.modules_to_perforate, list_type=True 867 ) 868 self.module_names_to_perforate = [ 869 "PAISequential", 870 "Conv1d", 871 "Conv2d", 872 "Conv3d", 873 "Linear", 874 ] 875 add_pai_config_var_functions( 876 self, 877 "module_names_to_perforate", 878 self.module_names_to_perforate, 879 list_type=True, 880 ) 881 self.module_ids_to_perforate = [] 882 add_pai_config_var_functions( 883 self, 884 "module_ids_to_perforate", 885 self.module_ids_to_perforate, 886 list_type=True, 887 ) 888 889 # All modules should either be perforated or tracked to ensure all modules 890 # are accounted for 891 self.modules_to_track = [] 892 add_pai_config_var_functions( 893 self, "modules_to_track", self.modules_to_track, list_type=True 894 ) 895 self.module_names_to_track = [] 896 add_pai_config_var_functions( 897 self, 898 "module_names_to_track", 899 self.module_names_to_track, 900 list_type=True, 901 ) 902 # IDs are for if you want to pass only a single module by its assigned ID rather than the module type by name 903 self.module_ids_to_track = [] 904 add_pai_config_var_functions( 905 self, "module_ids_to_track", self.module_ids_to_track, list_type=True 906 ) 907 908 # Parameter IDs to track as neuron parameters without recursive behavior 909 # (e.g., [".my_custom_parameter"]). 910 self.parameter_ids_to_track = [] 911 add_pai_config_var_functions( 912 self, 913 "parameter_ids_to_track", 914 self.parameter_ids_to_track, 915 list_type=True, 916 ) 917 918 # Replacement modules happen before the conversion, 919 # so replaced modules will then also be run through the conversion steps 920 # These are for modules that need to be replaced before addition of dendrites 921 # See the resnet example in models_perforatedai 922 self.modules_to_replace = [] 923 add_pai_config_var_functions( 924 self, "modules_to_replace", self.modules_to_replace, list_type=True 925 ) 926 # Modules to replace the above modules with 927 self.replacement_modules = [] 928 add_pai_config_var_functions( 929 self, "replacement_modules", self.replacement_modules, list_type=True 930 ) 931 932 # Dendrites default to modules which are one tensor input and one tensor 933 # output in forward() 934 # Other modules require to be labeled as modules with processing and assigned 935 # processing classes 936 # This can be done by module type or module name see customization.md in API 937 # for example 938 self.modules_with_processing = [] 939 add_pai_config_var_functions( 940 self, 941 "modules_with_processing", 942 self.modules_with_processing, 943 list_type=True, 944 ) 945 self.modules_processing_classes = [] 946 add_pai_config_var_functions( 947 self, 948 "modules_processing_classes", 949 self.modules_processing_classes, 950 list_type=True, 951 ) 952 self.module_names_with_processing = [] 953 add_pai_config_var_functions( 954 self, 955 "module_names_with_processing", 956 self.module_names_with_processing, 957 list_type=True, 958 ) 959 self.module_by_name_processing_classes = [] 960 add_pai_config_var_functions( 961 self, 962 "module_by_name_processing_classes", 963 self.module_by_name_processing_classes, 964 list_type=True, 965 ) 966 967 # Similarly here as above. Some huggingface models have multiple pointers to 968 # the same modules which cause problems 969 # If you want to only save one of the multiple pointers you can set which ones 970 # not to save here 971 self.module_names_to_not_save = [".base_model"] 972 add_pai_config_var_functions( 973 self, 974 "module_names_to_not_save", 975 self.module_names_to_not_save, 976 list_type=True, 977 ) 978 979 # Perforated Backpropagation settings 980 self.perforated_backpropagation = False 981 add_pai_config_var_functions( 982 self, "perforated_backpropagation", self.perforated_backpropagation 983 ) 984 985 # This is specifically a workaround for weight tying 986 # Not to be used for a duplicate pointer that isn't actually run twice 987 self.weight_tying_experimental = False 988 add_pai_config_var_functions( 989 self, "weight_tying_experimental", self.weight_tying_experimental 990 ) 991 992 # Dashboard event streaming settings 993 self.dashboard_events_enabled = False 994 add_pai_config_var_functions( 995 self, "dashboard_events_enabled", self.dashboard_events_enabled 996 ) 997 self.dashboard_url = "http://localhost:3002" 998 add_pai_config_var_functions( 999 self, "dashboard_url", self.dashboard_url 1000 ) 1001 self.dashboard_debug = False 1002 add_pai_config_var_functions( 1003 self, "dashboard_debug", self.dashboard_debug 1004 ) 1005 1006 # These are settings where libraries must be doing the scoring adding to 1007 # message from your main script what metric to use 1008 self.library_validation_score = "" 1009 add_pai_config_var_functions( 1010 self, "library_validation_score", self.library_validation_score 1011 ) 1012 self.library_extra_scores = [] 1013 add_pai_config_var_functions( 1014 self, 1015 "library_extra_scores", 1016 self.library_extra_scores, 1017 list_type=True, 1018 ) 1019 self.library_extra_scores_without_graphing = [] 1020 add_pai_config_var_functions( 1021 self, 1022 "library_extra_scores_without_graphing", 1023 self.library_extra_scores_without_graphing, 1024 list_type=True, 1025 ) 1026 1027 # Input dimensions needs to be set every time. It is set to what format of 1028 # planes you are expecting. 1029 # Neuron index should be set to 0, variable indexes should be set to -1. 1030 # For example, if your format is [batchsize, nodes, x, y] 1031 # output_dimensions is [-1, 0, -1, -1]. 1032 # if your format is, [batchsize, time index, nodes] output_dimensions is 1033 # [-1, -1, 0] 1034 self.output_dimensions = [-1, 0, -1, -1] 1035 add_pai_config_var_functions( 1036 self, "output_dimensions", self.output_dimensions, list_type=True 1037 ) 1038 # Verbosity settings 1039 self.verbose = False 1040 add_pai_config_var_functions(self, "verbose", self.verbose) 1041 self.extra_verbose = False 1042 add_pai_config_var_functions(self, "extra_verbose", self.extra_verbose) 1043 # Suppress all PAI prints 1044 self.silent = False 1045 add_pai_config_var_functions(self, "silent", self.silent) 1046 1047 # In place for future implementation options of adding multiple candidate 1048 # dendrites together 1049 self.global_candidates = 1 1050 add_pai_config_var_functions(self, "global_candidates", self.global_candidates) 1051 1052 # Weight initialization settings 1053 # Multiplier when randomizing dendrite weights 1054 self.candidate_weight_initialization_multiplier = 0.01 1055 add_pai_config_var_functions( 1056 self, 1057 "candidate_weight_initialization_multiplier", 1058 self.candidate_weight_initialization_multiplier, 1059 ) 1060 # Multiplier when randomizing dendrite weights 1061 self.candidate_weight_init_by_main = False 1062 add_pai_config_var_functions( 1063 self, 1064 "candidate_weight_init_by_main", 1065 self.candidate_weight_init_by_main, 1066 ) 1067 1068 # Dendrite retention settings 1069 # A setting to keep dendrites even if they do not improve scores 1070 self.retain_all_dendrites = False 1071 add_pai_config_var_functions( 1072 self, "retain_all_dendrites", self.retain_all_dendrites 1073 ) 1074 1075 # Max dendrites to add even if they do continue improving scores 1076 self.max_dendrites = 100 1077 add_pai_config_var_functions(self, "max_dendrites", self.max_dendrites) 1078 1079 # Activation function settings 1080 # The activation function to use for dendrites 1081 self.pai_forward_function = torch.sigmoid 1082 add_pai_config_var_functions( 1083 self, "pai_forward_function", self.pai_forward_function 1084 ) 1085 1086 # Apply per-module overrides after defaults are defined so loaded values 1087 # win over initializer defaults. 1088 if module_name is not None: 1089 import os 1090 1091 global_pc = globals().get("pc") 1092 source_paths = [] 1093 if global_pc is not None: 1094 config_file = getattr(global_pc, "__dict__", {}).get("_config_file") 1095 run_config = global_pc.get_run_config_path() 1096 if config_file: 1097 source_paths.append(config_file) 1098 if run_config: 1099 source_paths.append(run_config) 1100 1101 for source_path in source_paths: 1102 if source_path and os.path.exists(source_path): 1103 self.load_config( 1104 source_path, 1105 module_name=module_name, 1106 module_type=module_type, 1107 ) 1108 break 1109 1110 # ------------------------------------------------------------------ 1111 # Config file will be set when save_name is assigned (in perforate_model) 1112 # ------------------------------------------------------------------ 1113 # _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.
1117 def save_config(self, filename): 1118 """Save the current PAIConfig state to a JSON file. 1119 1120 Parameters 1121 ---------- 1122 filename : str 1123 Destination file path (created or overwritten). 1124 1125 Notes 1126 ----- 1127 Values that are not natively JSON-serialisable (torch.device, 1128 torch.dtype, nn.Module subclasses, callables) are stored as their 1129 dotted-string representations so they can be round-tripped by 1130 :py:meth:`load_config`. 1131 1132 Returns 1133 ------- 1134 None 1135 This function does not return a value. 1136 """ 1137 import json 1138 1139 config_dict = {} 1140 1141 # Private-storage vars added by add_pai_config_var_functions 1142 # e.g. self._module_ids_to_perforate → key 'module_ids_to_perforate' 1143 for key, val in sorted(self.__dict__.items()): 1144 if not (key.startswith("_") and not key.startswith("__")): 1145 continue 1146 # Skip internal bookkeeping keys that must not round-trip through JSON 1147 if key in ( 1148 "_config_file", 1149 "_module_name", 1150 "_module_type", 1151 "_manually_set_keys", 1152 "_loading_config_values", 1153 "_auto_persist_config", 1154 ): 1155 continue 1156 clean_key = key[1:] 1157 # Keep callable config fields (e.g. pai_forward_function) but skip 1158 # any other private callable values that are not typed as callable. 1159 if callable(val) and PAIConfig._TYPES.get(clean_key) is not callable: 1160 continue 1161 try: 1162 config_dict[clean_key] = _serialize_pai_value(val) 1163 except Exception: 1164 config_dict[clean_key] = str(val) 1165 1166 # Plain constants (DOING_*, PARAM_VALS_BY_*, etc.) 1167 for key, val in self.__dict__.items(): 1168 if key.startswith("_") or callable(val): 1169 continue 1170 if key not in config_dict: 1171 # Keep config_file runtime-only: do not persist pointer paths in 1172 # saved JSON snapshots. 1173 if key == "config_file": 1174 continue 1175 try: 1176 config_dict[key] = _serialize_pai_value(val) 1177 except Exception: 1178 config_dict[key] = str(val) 1179 1180 # Merge short class names from modules_to_perforate into module_names_to_perforate 1181 # so the UI (and JS) only needs to check one array. 1182 type_short_names = [ 1183 cls.__name__ 1184 for cls in self.__dict__.get("_modules_to_perforate", []) 1185 if isinstance(cls, type) 1186 ] 1187 existing = config_dict.get("module_names_to_perforate", []) 1188 config_dict["module_names_to_perforate"] = existing + [ 1189 n for n in type_short_names if n not in existing 1190 ] 1191 1192 # Preserve any per-module settings written by the Studio frontend. 1193 _existing_ms: dict = {} 1194 try: 1195 with open(filename, "r") as _f: 1196 _existing_ms = json.load(_f).get("module_settings", {}) 1197 except Exception: 1198 pass 1199 config_dict["module_settings"] = _existing_ms 1200 1201 # Publish customizable field type names so the Studio frontend can 1202 # render appropriate editors without needing to import the library. 1203 config_dict["_customizable_fields"] = { 1204 k: (v.__name__ if hasattr(v, "__name__") else str(v)) 1205 for k, v in PAIConfig._CUSTOMIZABLE.items() 1206 } 1207 1208 # Ensure the directory exists before saving 1209 import os 1210 1211 directory = os.path.dirname(filename) 1212 if directory: 1213 os.makedirs(directory, exist_ok=True) 1214 1215 with open(filename, "w") as f: 1216 json.dump(config_dict, f, indent=2) 1217 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.
1219 def load_config(self, filename, module_name=None, module_type=None, skip_keys=None): 1220 """Load PAIConfig state from a JSON file produced by :py:meth:`save_config`. 1221 1222 If *module_name* is ``None`` (default) every serialisable variable in 1223 the file is restored on this instance. 1224 1225 If *module_name* is given the lookup priority is: 1226 1. ``module_settings[module_name]`` (exact name / id match) 1227 2. ``module_settings[module_type]`` (type-level fallback) 1228 3. No-op — the defaults already set by ``__init__`` are kept. 1229 1230 Parameters 1231 ---------- 1232 filename : str 1233 Path to the JSON file to read. 1234 module_name : str, optional 1235 Display name (id) of the module whose custom settings should be loaded. 1236 module_type : str, optional 1237 Short class name of the module type, used as a fallback key. 1238 1239 skip_keys : set, optional 1240 Variable names to ignore while loading (manual overrides). 1241 1242 Returns 1243 ------- 1244 None 1245 This function does not return a value. 1246 """ 1247 import json 1248 1249 with open(filename, "r") as f: 1250 config_dict = json.load(f) 1251 1252 if module_name is not None: 1253 # ── Per-module load ────────────────────────────────────────────── 1254 module_settings = config_dict.get("module_settings", {}) 1255 # Priority: exact name → type fallback → no-op 1256 if module_name in module_settings: 1257 custom = module_settings[module_name] 1258 resolved_key = module_name 1259 elif module_type and module_type in module_settings: 1260 custom = module_settings[module_type] 1261 resolved_key = module_type 1262 else: 1263 # No custom settings for this module or type — keep defaults. 1264 return 1265 loaded = 0 1266 skipped = 0 1267 for key, json_val in custom.items(): 1268 if key not in PAIConfig._CUSTOMIZABLE: 1269 continue 1270 type_hint = PAIConfig._TYPES.get(key) 1271 private_key = f"_{key}" 1272 # Write directly to __dict__ so we bypass any setter guards and also 1273 # correctly handle vars that are not pre-initialised on per-module 1274 # configs (e.g. output_dimensions, which only lives inside the 1275 # ``if not module_name:`` block of __init__). 1276 try: 1277 self.__dict__[private_key] = ( 1278 _deserialize_pai_value(json_val, type_hint) 1279 if type_hint is not None 1280 else json_val 1281 ) 1282 loaded += 1 1283 except Exception as exc: 1284 print( 1285 f"[PAI Config] Warning: could not load '{key}' for '{resolved_key}': {exc}" 1286 ) 1287 skipped += 1 1288 print( 1289 f"[PAI Config] Loaded {loaded} custom vars for '{resolved_key}' from {filename}" 1290 + (f" ({skipped} skipped)" if skipped else "") 1291 ) 1292 return 1293 1294 # ── Global load: every variable in the JSON ────────────────────────── 1295 skip_keys = set(skip_keys or []) 1296 loaded = 0 1297 skipped = 0 1298 self.__dict__["_loading_config_values"] = True 1299 try: 1300 for key, json_val in config_dict.items(): 1301 # Skip internal bookkeeping and Studio-only metadata keys. 1302 # 'module_name' and 'module_type' must never overwrite the 1303 # instance's _module_name/_module_type (they are internal only). 1304 if key in ( 1305 "config_file", 1306 "module_settings", 1307 "module_name", 1308 "module_type", 1309 ) or key.startswith("_"): 1310 continue 1311 if key in skip_keys: 1312 continue 1313 type_hint = PAIConfig._TYPES.get(key) 1314 private_key = f"_{key}" 1315 if hasattr(self, private_key): 1316 try: 1317 setattr( 1318 self, 1319 private_key, 1320 ( 1321 _deserialize_pai_value(json_val, type_hint) 1322 if type_hint is not None 1323 else json_val 1324 ), 1325 ) 1326 loaded += 1 1327 except Exception as exc: 1328 print(f"[PAI Config] Warning: could not load '{key}': {exc}") 1329 skipped += 1 1330 elif hasattr(self, key) and not callable(getattr(self, key, None)): 1331 try: 1332 setattr(self, key, json_val) 1333 loaded += 1 1334 except Exception: 1335 skipped += 1 1336 finally: 1337 self.__dict__["_loading_config_values"] = False 1338 1339 print( 1340 f"[PAI Config] Loaded {loaded} variables from {filename}" 1341 + (f" ({skipped} skipped)" if skipped else "") 1342 )
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:
module_settings[module_name](exact name / id match)module_settings[module_type](type-level fallback)- 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.
- skip_keys (set, optional): Variable names to ignore while loading (manual overrides).
Returns
- None: This function does not return a value.
1344 def get_run_config_path(self): 1345 """Get the run-specific config path derived from save_name. 1346 1347 Returns 1348 ------- 1349 str or None 1350 Path to the run config file, or None when save_name is unset. 1351 """ 1352 import os 1353 1354 save_name = self.__dict__.get("_save_name") 1355 if not save_name: 1356 return None 1357 return os.path.join(os.getcwd(), save_name, f"{save_name}_config.json")
Get the run-specific config path derived from save_name.
Returns
- str or None: Path to the run config file, or None when save_name is unset.
1359 def sync_config_sources(self): 1360 """Load config values from configured local JSON sources. 1361 1362 Load order: 1363 1. config_file pointer (if set) 1364 2. run config derived from save_name (if present) 1365 1366 Keys set manually in this process are never overwritten. 1367 """ 1368 import os 1369 1370 if self.__dict__.get("_module_name") is not None: 1371 return 1372 1373 skip_keys = self.__dict__.get("_manually_set_keys", set()) 1374 config_file = self.__dict__.get("_config_file") 1375 run_config = self.get_run_config_path() 1376 1377 if config_file and os.path.exists(config_file): 1378 self.load_config(config_file, skip_keys=skip_keys) 1379 if run_config and os.path.exists(run_config): 1380 self.load_config(run_config, skip_keys=skip_keys)
Load config values from configured local JSON sources.
Load order:
- config_file pointer (if set)
- run config derived from save_name (if present)
Keys set manually in this process are never overwritten.
1382 def persist_config_outputs(self, overwrite_config_file=False): 1383 """Persist current config state to local JSON outputs. 1384 1385 Parameters 1386 ---------- 1387 overwrite_config_file : bool 1388 When True, also writes the configured config_file pointer target. 1389 """ 1390 if self.__dict__.get("_module_name") is not None: 1391 return 1392 1393 run_config = self.get_run_config_path() 1394 if run_config: 1395 self.save_config(run_config) 1396 1397 if overwrite_config_file: 1398 config_file = self.__dict__.get("_config_file") 1399 if config_file: 1400 self.save_config(config_file)
Persist current config state to local JSON outputs.
Parameters
- overwrite_config_file (bool): When True, also writes the configured config_file pointer target.
1403class PAISequential(nn.Sequential): 1404 """Sequential module wrapper for PAI. 1405 1406 This wrapper takes an array of layers and creates a sequential container 1407 that is compatible with PAI's dendrite addition system. It should be used 1408 for normalization layers and can be used for final output layers. 1409 1410 Parameters 1411 ---------- 1412 layer_array : list 1413 List of PyTorch nn.Module objects to be executed sequentially. 1414 1415 Examples 1416 -------- 1417 >>> layers = [nn.Linear(2 * hidden_dim, seq_width), 1418 ... nn.LayerNorm(seq_width)] 1419 >>> sequential_block = PAISequential(layers) 1420 1421 Notes 1422 ----- 1423 This should be used for: 1424 - All normalization layers (LayerNorm, BatchNorm, etc.) 1425 This can be used for: 1426 - Final output layer and softmax combinations 1427 """ 1428 1429 def __init__(self, layer_array): 1430 """Initialize PAISequential with a list of layers. 1431 1432 Parameters 1433 ---------- 1434 layer_array : list 1435 List of PyTorch modules to execute in sequence. 1436 """ 1437 super(PAISequential, self).__init__() 1438 self.model = nn.Sequential(*layer_array) 1439 1440 def forward(self, *args, **kwargs): 1441 """Forward pass through the sequential layers. 1442 1443 Parameters 1444 ---------- 1445 *args 1446 Positional arguments passed to the first layer. 1447 **kwargs 1448 Keyword arguments passed to the layers. 1449 1450 Returns 1451 ------- 1452 torch.Tensor 1453 Output from the final layer in the sequence. 1454 """ 1455 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
1429 def __init__(self, layer_array): 1430 """Initialize PAISequential with a list of layers. 1431 1432 Parameters 1433 ---------- 1434 layer_array : list 1435 List of PyTorch modules to execute in sequence. 1436 """ 1437 super(PAISequential, self).__init__() 1438 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.
1440 def forward(self, *args, **kwargs): 1441 """Forward pass through the sequential layers. 1442 1443 Parameters 1444 ---------- 1445 *args 1446 Positional arguments passed to the first layer. 1447 **kwargs 1448 Keyword arguments passed to the layers. 1449 1450 Returns 1451 ------- 1452 torch.Tensor 1453 Output from the final layer in the sequence. 1454 """ 1455 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.
Global PAIConfig instance.
This is the primary configuration object used throughout the PAI system. Modify settings through this instance to control PAI behavior.