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