diff --git a/.github/workflows/test_tutorials.yml b/.github/workflows/test_tutorials.yml index 41ecbfee..b484b25c 100644 --- a/.github/workflows/test_tutorials.yml +++ b/.github/workflows/test_tutorials.yml @@ -23,4 +23,5 @@ jobs: pip install .[all] # Install all dependencies, including dev - name: Test tutorials notebooks - run: pytest --nbmake tutorials/notebooks --nbmake-timeout=600 \ No newline at end of file + run: pytest --nbmake tutorials/notebooks --nbmake-timeout=600 + --ignore tutorials/notebooks/intro_torchgfn_performance_tuning.ipynb diff --git a/docs/source/guides/states_actions_containers.md b/docs/source/guides/states_actions_containers.md index ea231ae1..e15d0bc7 100644 --- a/docs/source/guides/states_actions_containers.md +++ b/docs/source/guides/states_actions_containers.md @@ -14,6 +14,18 @@ Because multiple trajectories can have different lengths, batching requires appe For discrete environments, the action set is represented with the set $\{0, \dots, n_{actions} - 1\}$, where the $(n_{actions})$-th action always corresponds to the exit or terminate action, i.e. that results in a transition of the type $s \rightarrow s_f$, but not all actions are possible at all states. For discrete environments, each `States` object is endowed with two extra attributes: `forward_masks` and `backward_masks`, representing which actions are allowed at each state and which actions could have led to each state, respectively. Such states are instances of the `DiscreteStates` abstract subclass of `States`. The `forward_masks` tensor is of shape `(*batch_shape, n_{actions})`, and `backward_masks` is of shape `(*batch_shape, n_{actions} - 1)`. Each subclass of `DiscreteStates` needs to implement the `update_masks` function, that uses the environment's logic to define the two tensors. +### Debug guards and factory signatures + +To keep compiled hot paths fast, `States`/`DiscreteStates`/`GraphStates` expect a `debug` flag passed at construction time. When `debug=False` (default) no Python-side checks run in hot paths; when `debug=True`, shape/device/type guards run to catch silent bugs. Environments carry an env-level `debug` and pass it when they instantiate `States`. + +When defining your own `States` subclass or environment factories, make sure all state factories accept `debug`: + +- Constructors: `__init__(..., debug: bool = False, ...)` should store `self.debug` and pass it along when cloning or slicing. +- Factory classmethods: `make_random_states`, `make_initial_states`, `make_sink_states` (and any overrides) **must** accept `debug` (or `**kwargs`) and forward it to `States(...)`. The base class enforces this and will raise a clear `TypeError` otherwise. +- Env helpers: if you override `states_from_tensor` or `reset` in an environment, thread `self.debug` into state construction so all emitted states share the env-level setting. + +This pattern avoids graph breaks in `torch.compile` by letting you keep `debug=False` in compiled runs while still enabling strong checks in development and tests. + ## Actions Actions should be though of as internal actions of an agent building a compositional object. They correspond to transitions $s \rightarrow s'$. An abstract `Actions` class is provided. It is automatically subclassed for discrete environments, but needs to be manually subclassed otherwise. @@ -24,6 +36,14 @@ Additionally, each subclass needs to define two more class variable tensors: - `dummy_action`: A tensor that is padded to sequences of actions in the shorter trajectories of a batch of trajectories. It is `[-1]` for discrete environments. - `exit_action`: A tensor that corresponds to the termination action. It is `[n_{actions} - 1]` fo discrete environments. +### Debug guards and factory signatures + +`Actions` mirrors the `States` pattern: constructors and factories accept `debug: bool = False`. Keep `debug=False` in compiled/hot paths to avoid Python-side asserts; flip it on in development/tests to run shape/device validations. When defining custom subclasses, ensure: + +- `__init__(..., debug: bool = False, ...)` stores `self.debug` and only runs validations when `debug` is True. +- Factory classmethods (`make_dummy_actions`, `make_exit_actions`, helpers like `from_tensor_dict`) accept `debug` (or `**kwargs`) and forward it to the constructor. +- Environment helpers (`actions_from_tensor`, `actions_from_batch_shape`) should thread the env-level `debug` so all emitted actions share the setting. + ## Containers Containers are collections of `States`, along with other information, such as reward values, or densities $p(s' \mid s)$. Three containers are available: diff --git a/src/gfn/actions.py b/src/gfn/actions.py index 520189a1..b356eb78 100644 --- a/src/gfn/actions.py +++ b/src/gfn/actions.py @@ -39,18 +39,22 @@ class Actions(ABC): # The following class variable corresponds to $s \rightarrow s_f$ transitions. exit_action: ClassVar[torch.Tensor] # Action to exit the environment. - def __init__(self, tensor: torch.Tensor): + def __init__(self, tensor: torch.Tensor, debug: bool = False): """Initializes an Actions object with a batch of actions. Args: tensor: Tensor of shape (*batch_shape, *action_shape) representing a batch of actions. """ - assert ( - tensor.shape[-len(self.action_shape) :] == self.action_shape - ), f"Batched actions tensor has shape {tensor.shape}, but the expected action shape is {self.action_shape}." + # Debug-only validation keeps hot paths tensor-only when debug is False. + if debug: + if tensor.shape[-len(self.action_shape) :] != self.action_shape: + raise ValueError( + f"Batched actions tensor has shape {tensor.shape}, expected {self.action_shape}." + ) self.tensor = tensor + self.debug = debug @property def device(self) -> torch.device: @@ -72,13 +76,17 @@ def batch_shape(self) -> tuple[int, ...]: @classmethod def make_dummy_actions( - cls, batch_shape: tuple[int, ...], device: torch.device | None = None + cls, + batch_shape: tuple[int, ...], + device: torch.device | None = None, + debug: bool = False, ) -> Actions: """Creates an Actions object filled with dummy actions. Args: batch_shape: Shape of the batch dimensions. device: The device to create the actions on. + debug: Whether to run debug validations on the constructed Actions. Returns: An Actions object with the specified batch shape filled with dummy actions. @@ -87,17 +95,21 @@ def make_dummy_actions( tensor = cls.dummy_action.repeat(*batch_shape, *((1,) * action_ndim)) if device is not None: tensor = tensor.to(device) - return cls(tensor) + return cls(tensor, debug=debug) @classmethod def make_exit_actions( - cls, batch_shape: tuple[int, ...], device: torch.device | None = None + cls, + batch_shape: tuple[int, ...], + device: torch.device | None = None, + debug: bool = False, ) -> Actions: """Creates an Actions object filled with exit actions. Args: batch_shape: Shape of the batch dimensions. device: The device to create the actions on. + debug: Whether to run debug validations on the constructed Actions. Returns: An Actions object with the specified batch shape filled with exit actions. @@ -106,7 +118,7 @@ def make_exit_actions( tensor = cls.exit_action.repeat(*batch_shape, *((1,) * action_ndim)) if device is not None: tensor = tensor.to(device) - return cls(tensor) + return cls(tensor, debug=debug) def __len__(self) -> int: """Returns the number of actions in the batch. @@ -142,7 +154,7 @@ def __getitem__( A new Actions object with the selected actions. """ actions = self.tensor[index] - return self.__class__(actions) + return self.__class__(actions, debug=self.debug) def __setitem__( self, @@ -158,7 +170,7 @@ def __setitem__( self.tensor[index] = actions.tensor @classmethod - def stack(cls, actions_list: List[Actions]) -> Actions: + def stack(cls, actions_list: List[Actions], debug: bool | None = None) -> Actions: """Stacks a list of Actions objects along a new dimension (0). The individual actions need to have the same batch shape. An example application @@ -173,8 +185,13 @@ def stack(cls, actions_list: List[Actions]) -> Actions: Returns: A new Actions object with the stacked actions. """ + if debug is None: + # Reuse caller-provided debug setting when available to keep behavior consistent. + debug = getattr(actions_list[0], "debug", False) if actions_list else False + debug = bool(debug) + actions_tensor = torch.stack([actions.tensor for actions in actions_list], dim=0) - return cls(actions_tensor) + return cls(actions_tensor, debug=debug) def extend(self, other: Actions) -> None: """Concatenates another Actions object along the final batch dimension. @@ -215,7 +232,7 @@ def extend_with_dummy_actions(self, required_first_dim: int) -> None: return n = required_first_dim - self.batch_shape[0] dummy_actions = self.__class__.make_dummy_actions( - (n, self.batch_shape[1]), device=self.device + (n, self.batch_shape[1]), device=self.device, debug=self.debug ) self.tensor = torch.cat((self.tensor, dummy_actions.tensor), dim=0) else: @@ -234,21 +251,41 @@ def _compare(self, other: torch.Tensor) -> torch.Tensor: equal. """ n_batch_dims = len(self.batch_shape) - if n_batch_dims == 1: - assert (other.shape == self.action_shape) or ( - other.shape == self.batch_shape + self.action_shape - ), f"Expected shape {self.action_shape} or {self.batch_shape + self.action_shape}, got {other.shape}." - else: - assert ( - other.shape == self.batch_shape + self.action_shape - ), f"Expected shape {self.batch_shape + self.action_shape}, got {other.shape}." + if self.debug: + if n_batch_dims == 1: + # other.shape can either have only the action shape, or the + # flattened batch_shape + action_shape. + if other.shape not in ( + self.action_shape, + self.batch_shape + self.action_shape, + ): + raise ValueError( + ( + f"Expected shape {self.action_shape} or " + f"{self.batch_shape + self.action_shape}, got {other.shape}." + ) + ) + else: + # other.shape must have the full batch and action shape. + if other.shape != self.batch_shape + self.action_shape: + raise ValueError( + ( + f"Expected shape {self.batch_shape + self.action_shape}, " + f"got {other.shape}." + ) + ) out = self.tensor == other if len(self.action_shape) > 1: out = out.flatten(start_dim=n_batch_dims) out = out.all(dim=-1) - assert out.shape == self.batch_shape + if self.debug: + if out.shape != self.batch_shape: + raise ValueError( + f"Comparison output has shape {out.shape}, expected {self.batch_shape}." + ) + return out @property @@ -287,7 +324,7 @@ def clone(self) -> Actions: Returns: A new Actions object with the same tensor. """ - return self.__class__(self.tensor.clone()) + return self.__class__(self.tensor.clone(), debug=self.debug) class GraphActionType(enum.IntEnum): @@ -329,19 +366,28 @@ class GraphActions(Actions): EDGE_INDEX_KEY: 4, } - def __init__(self, tensor: torch.Tensor): + # Required by the Actions base class for DB/SubTB style algorithms. + action_shape = (5,) + dummy_action = torch.tensor( + [GraphActionType.DUMMY, -2, -2, -2, -2], dtype=torch.long + ) + exit_action = torch.tensor([GraphActionType.EXIT, -1, -1, -1, -1], dtype=torch.long) + + def __init__(self, tensor: torch.Tensor, debug: bool = False): """Initializes a GraphActions object. Args: tensor: A tensor of shape (*batch_shape, 5) containing the action type, node class, edge class, and edge index components. """ - if tensor.shape[-1] != 5: - raise ValueError( - f"Expected tensor of shape (*batch_shape, 5), got {tensor.shape}.\n" - "The last dimension should contain the action type, node class, node index, edge class, and edge index." - ) + if debug: + if tensor.shape[-1] != 5: + raise ValueError( + f"Expected tensor of shape (*batch_shape, 5), got {tensor.shape}.\n" + "The last dimension should contain the action type, node class, node index, edge class, and edge index." + ) self.tensor = tensor + self.debug = debug @property def batch_shape(self) -> tuple[int, ...]: @@ -350,11 +396,14 @@ def batch_shape(self) -> tuple[int, ...]: Returns: The batch shape as a tuple. """ - assert self.tensor.shape[-1] == 5 + if self.debug: + assert self.tensor.shape[-1] == 5 return self.tensor.shape[:-1] @classmethod - def from_tensor_dict(cls, tensor_dict: TensorDict) -> GraphActions: + def from_tensor_dict( + cls, tensor_dict: TensorDict, debug: bool = False + ) -> GraphActions: """Creates a GraphActions object from a tensor dict. Args: @@ -374,7 +423,8 @@ def from_tensor_dict(cls, tensor_dict: TensorDict) -> GraphActions: return cls( torch.cat( [action_type, node_class, node_index, edge_class, edge_index], dim=-1 - ) + ), + debug=debug, ) def __repr__(self): @@ -450,7 +500,10 @@ def edge_index(self) -> torch.Tensor: @classmethod def make_dummy_actions( - cls, batch_shape: tuple[int], device: torch.device + cls, + batch_shape: tuple[int], + device: torch.device | None = None, + debug: bool = False, ) -> GraphActions: """Creates a GraphActions object filled with dummy actions. @@ -462,13 +515,20 @@ def make_dummy_actions( A GraphActions object with the specified batch shape filled with dummy actions. """ - tensor = torch.zeros(batch_shape + (5,), dtype=torch.long, device=device) + tensor = torch.zeros( + batch_shape + (5,), + dtype=torch.long, + device=device, + ) tensor[..., cls.ACTION_INDICES[cls.ACTION_TYPE_KEY]] = GraphActionType.DUMMY - return cls(tensor) + return cls(tensor, debug=debug) @classmethod def make_exit_actions( - cls, batch_shape: tuple[int], device: torch.device + cls, + batch_shape: tuple[int], + device: torch.device | None = None, + debug: bool = False, ) -> GraphActions: """Creates a GraphActions object filled with exit actions. @@ -479,9 +539,13 @@ def make_exit_actions( Returns: A GraphActions object with the specified batch shape filled with exit actions. """ - tensor = torch.zeros(batch_shape + (5,), dtype=torch.long, device=device) + tensor = torch.zeros( + batch_shape + (5,), + dtype=torch.long, + device=device, + ) tensor[..., cls.ACTION_INDICES[cls.ACTION_TYPE_KEY]] = GraphActionType.EXIT - return cls(tensor) + return cls(tensor, debug=debug) @classmethod def edge_index_action_to_src_dst( diff --git a/src/gfn/containers/replay_buffer.py b/src/gfn/containers/replay_buffer.py index 3b6cd49a..f2ff6f03 100644 --- a/src/gfn/containers/replay_buffer.py +++ b/src/gfn/containers/replay_buffer.py @@ -97,7 +97,7 @@ def device(self) -> torch.device: assert self.training_container is not None, "Buffer is empty, it has no device!" return self.training_container.device - def add(self, training_container: ContainerUnion) -> float | None: + def add(self, training_container: ContainerUnion) -> dict[str, float] | None: """Adds a training container to the buffer. The type of the training container is dynamically set based on the type of the @@ -107,18 +107,16 @@ def add(self, training_container: ContainerUnion) -> float | None: training_container: The Trajectories, Transitions, or StatesContainer object to add. """ - if not isinstance(training_container, ContainerUnion): - raise TypeError("Must be a container type") - + assert isinstance(training_container, ContainerUnion), "Must be a container type" self._add_objs(training_container) - # Handle remote buffer communication + # Handle remote buffer communication. if self.remote_manager_rank is not None: self._add_counter += 1 if self._add_counter % self.remote_buffer_freq == 0: return self._send_objs(training_container) - def _send_objs(self, training_container: ContainerUnion) -> float: + def _send_objs(self, training_container: ContainerUnion) -> dict[str, float]: """Sends a training container to the remote manager.""" msg = Message(MessageType.DATA, training_container) msg_tensor = msg.serialize() @@ -130,11 +128,17 @@ def _send_objs(self, training_container: ContainerUnion) -> float: # Now send the actual content dist.send(msg_tensor, dst=self.remote_manager_rank) - # Receive a dummy score back - score = torch.zeros(1, dtype=torch.float32) - dist.recv(score, src=self.remote_manager_rank) + # Receive the length of the score dictionary + length_tensor = torch.zeros(1, dtype=torch.int32) + dist.recv(length_tensor, src=self.remote_manager_rank) + length = length_tensor.item() - return score.item() + # Receive the actual score dictionary + score_tensor = torch.ByteTensor(length) + dist.recv(score_tensor, src=self.remote_manager_rank) + score_dict = Message.deserialize(score_tensor).message_data + + return score_dict def __repr__(self) -> str: """Returns a string representation of the ReplayBuffer. @@ -291,6 +295,8 @@ def __init__( capacity: int = 1000, cutoff_distance: float = 0.0, p_norm_distance: float = 1.0, + remote_manager_rank: int | None = None, + remote_buffer_freq: int = 1, ): """Initializes a NormBasedDiversePrioritizedReplayBuffer instance. @@ -300,8 +306,18 @@ def __init__( cutoff_distance: Threshold used to determine whether a new terminating state is different enough from those already in the buffer. p_norm_distance: p-norm value for distance calculation (used in torch.cdist). + remote_manager_rank: Rank of the assigned remote replay buffer manager, or + None if no remote manager is assigned. + remote_buffer_freq: Frequency (in number of add() calls) at which to contact + the remote buffer manager. """ - super().__init__(env, capacity, prioritized_capacity=True) + super().__init__( + env, + capacity, + prioritized_capacity=True, + remote_manager_rank=remote_manager_rank, + remote_buffer_freq=remote_buffer_freq, + ) self.cutoff_distance = cutoff_distance self.p_norm_distance = p_norm_distance diff --git a/src/gfn/containers/replay_buffer_manager.py b/src/gfn/containers/replay_buffer_manager.py index fc2783cd..d37d0cef 100644 --- a/src/gfn/containers/replay_buffer_manager.py +++ b/src/gfn/containers/replay_buffer_manager.py @@ -20,7 +20,7 @@ def __init__( env: Env, rank: int, num_training_ranks: int, - scoring_function: Optional[Callable[[ContainerUnion], float]] = None, + scoring_function: Optional[Callable[[ContainerUnion], dict[str, float]]] = None, diverse_replay_buffer: bool = False, capacity: int = 10000, remote_manager_rank: int | None = None, @@ -48,14 +48,14 @@ def __init__( self.replay_buffer = ReplayBuffer( env, capacity=self.capacity, - prioritized_capacity=False, + prioritized_capacity=True, # Always prioritize high reward items. remote_manager_rank=self.remote_manager_rank, remote_buffer_freq=1, ) - def default_scoring_function(self, obj) -> float: + def default_scoring_function(self, obj) -> dict[str, float]: """Default score function if none provided, placeholder.""" - return math.inf + return {"score": math.inf} def _compute_metadata(self) -> dict: raise NotImplementedError( @@ -63,17 +63,21 @@ def _compute_metadata(self) -> dict: ) def run(self): - """Runs on remote buffer manager ranks. Waits for training data, computes dummy reward, sends back.""" + """Runs on remote buffer manager ranks. Waits for training data, computes reward, sends back.""" while self.is_running: # Receive data sender_rank, msg, msg_data_len = self._recv_object() + # Recieved some data to add to the buffer. if msg.message_type == MessageType.DATA: - score = self.scoring_function(msg.message_data) - score_tensor = torch.tensor([score], dtype=torch.float32) - dist.send(score_tensor, dst=sender_rank) self.replay_buffer.add(msg.message_data) + score_dict = self.scoring_function(msg.message_data) + message = Message(message_type=MessageType.DATA, message_data=score_dict) + message_tensor = message.serialize() + length_message_tensor = torch.IntTensor([len(message_tensor)]) + dist.send(length_message_tensor, dst=sender_rank) + dist.send(message_tensor, dst=sender_rank) elif msg.message_type == MessageType.GET_METADATA: metadata = self._compute_metadata() @@ -88,25 +92,25 @@ def run(self): if self.exit_counter == self.num_training_ranks: self.is_running = False print( - f"Replay buffer manager {self.rank} received exit signals from all training ranks. Exiting." + f"Manager - Replay buffer {self.rank} received exit signals from all training ranks. Exiting." ) else: raise ValueError( - f"Rank {self.rank} received unknown message type: {msg.message_type}" + f"Manager - Rank {self.rank} received unknown message type: {msg.message_type}" ) def _recv_object(self): - # Receive the length + # Receive the length. length_tensor = torch.IntTensor([0]) sender_rank = dist.recv(length_tensor) length = length_tensor.item() - # Receive the actual serialized data + # Receive the actual serialized data. byte_tensor = torch.ByteTensor(length) dist.recv(byte_tensor, src=sender_rank) - # Deserialize back into object - # obj_bytes = bytes(byte_tensor.tolist()) + # Deserialize back into object. + # obj_bytes = bytes(byte_tensor.tolist()). # TODO -- Remove? msg = Message.deserialize(byte_tensor) return sender_rank, msg, length @@ -128,12 +132,16 @@ def get_metadata(manager_rank: int) -> dict: """Sends a get metadata signal to the replay buffer manager.""" msg = Message(message_type=MessageType.GET_METADATA, message_data=None) msg_bytes = msg.serialize() + length_tensor = torch.IntTensor([len(msg_bytes)]) dist.send(length_tensor, dst=manager_rank) + dist.send(msg_bytes, dst=manager_rank) length_metadata_tensor = torch.IntTensor([0]) + dist.recv(length_metadata_tensor, src=manager_rank) metadata_tensor = torch.ByteTensor(length_metadata_tensor.item()) + dist.recv(metadata_tensor, src=manager_rank) metadata = Message.deserialize(metadata_tensor) return metadata.message_data diff --git a/src/gfn/env.py b/src/gfn/env.py index 646094f0..fc88ddf3 100644 --- a/src/gfn/env.py +++ b/src/gfn/env.py @@ -45,7 +45,7 @@ def __init__( dummy_action: torch.Tensor, exit_action: torch.Tensor, sf: Optional[torch.Tensor | GeometricData] = None, - check_action_validity: bool = True, + debug: bool = False, ): """Initializes an environment. @@ -58,7 +58,8 @@ def __init__( exit_action: Tensor of shape (*action_shape) representing the exit action. sf: (Optional) Tensor of shape (*state_shape) or GeometricData representing the sink (final) state. - check_action_validity: Whether to check the action validity. + debug: If True, States/Actions created by this env will run runtime guards + (not torch.compile friendly). Keep False in compiled runs. """ if isinstance(s0.device, str): # This can happen when s0 is a GeometricData. s0.device = torch.device(s0.device) @@ -79,7 +80,7 @@ def __init__( self.action_shape = action_shape self.dummy_action = dummy_action.to(s0.device) self.exit_action = exit_action.to(s0.device) - self.check_action_validity = check_action_validity + self.debug = debug # Warning: don't use self.States or self.Actions to initialize an instance of # the class. Use self.states_from_tensor or self.actions_from_tensor instead. @@ -104,7 +105,7 @@ def states_from_tensor(self, tensor: torch.Tensor) -> States: Returns: A States instance. """ - return self.States(tensor) + return self.States(tensor, debug=self.debug) def states_from_batch_shape( self, batch_shape: Tuple, random: bool = False, sink: bool = False @@ -120,7 +121,7 @@ def states_from_batch_shape( A batch of random, initial, or sink states. """ return self.States.from_batch_shape( - batch_shape, random=random, sink=sink, device=self.device + batch_shape, random=random, sink=sink, device=self.device, debug=self.debug ) def actions_from_tensor(self, tensor: torch.Tensor) -> Actions: @@ -132,7 +133,7 @@ def actions_from_tensor(self, tensor: torch.Tensor) -> Actions: Returns: An Actions instance. """ - return self.Actions(tensor) + return self.Actions(tensor, debug=self.debug) def actions_from_batch_shape(self, batch_shape: Tuple) -> Actions: """Returns a batch of dummy actions with the supplied batch shape. @@ -143,7 +144,9 @@ def actions_from_batch_shape(self, batch_shape: Tuple) -> Actions: Returns: A batch of dummy actions. """ - return self.Actions.make_dummy_actions(batch_shape, device=self.device) + return self.Actions.make_dummy_actions( + batch_shape, device=self.device, debug=self.debug + ) @abstractmethod def step(self, states: States, actions: Actions) -> States: @@ -271,7 +274,7 @@ def reset( assert not (random and sink) if random and seed is not None: - set_seed(seed, performance_mode=True) + set_seed(seed, deterministic_mode=False) # TODO: configurable? if isinstance(batch_shape, int): batch_shape = (batch_shape,) @@ -295,14 +298,19 @@ def _step(self, states: States, actions: Actions) -> States: Returns: A batch of next states. """ - assert states.batch_shape == actions.batch_shape - assert len(states.batch_shape) == 1, "Batch shape must be 1 for the step method." + if self.debug: + # Debug-only guards to avoid graph breaks in compiled runs. + assert states.batch_shape == actions.batch_shape + assert ( + len(states.batch_shape) == 1 + ), "Batch shape must be 1 for the step method." valid_states_idx: torch.Tensor = ~states.is_sink_state - assert valid_states_idx.shape == states.batch_shape - assert valid_states_idx.dtype == torch.bool + if self.debug: + assert valid_states_idx.shape == states.batch_shape + assert valid_states_idx.dtype == torch.bool - if self.check_action_validity: + # Action validity checks only when debug is enabled to keep compiled hot paths lean. valid_actions = actions[valid_states_idx] valid_states = states[valid_states_idx] @@ -353,7 +361,8 @@ def _backward_step(self, states: States, actions: Actions) -> States: Returns: A batch of previous states. """ - assert states.batch_shape == actions.batch_shape + if self.debug: + assert states.batch_shape == actions.batch_shape # IMPORTANT: states.clone() is used to ensure that the new states are a # distinct object from the old states. This is important for the sampler to @@ -363,12 +372,13 @@ def _backward_step(self, states: States, actions: Actions) -> States: new_states = states.clone() valid_states_idx: torch.Tensor = ~new_states.is_initial_state - assert valid_states_idx.shape == new_states.batch_shape - assert valid_states_idx.dtype == torch.bool + if self.debug: + assert valid_states_idx.shape == new_states.batch_shape + assert valid_states_idx.dtype == torch.bool valid_actions = actions[valid_states_idx] valid_states = new_states[valid_states_idx] - if self.check_action_validity and not self.is_action_valid( + if self.debug and not self.is_action_valid( valid_states, valid_actions, backward=True ): raise NonValidActionsError( @@ -465,7 +475,7 @@ def __init__( dummy_action: Optional[torch.Tensor] = None, exit_action: Optional[torch.Tensor] = None, sf: Optional[torch.Tensor] = None, - check_action_validity: bool = True, + debug: bool = False, ): """Initializes a discrete environment. @@ -478,7 +488,8 @@ def __init__( exit_action: (Optional) Tensor of shape (1,) representing the exit action. sf: (Optional) Tensor of shape (*state_shape) representing the final state. - check_action_validity: Whether to check the action validity. + debug: If True, States created by this env will run runtime guards + (not torch.compile friendly). Keep False in compiled runs. """ # Add validation/warnings for advanced usage if dummy_action is not None or exit_action is not None or sf is not None: @@ -526,7 +537,7 @@ def __init__( dummy_action, exit_action, sf, - check_action_validity, + debug=debug, ) def states_from_tensor(self, tensor: torch.Tensor) -> DiscreteStates: @@ -538,7 +549,7 @@ def states_from_tensor(self, tensor: torch.Tensor) -> DiscreteStates: Returns: An instance of DiscreteStates. """ - states_instance = self.make_states_class()(tensor) + states_instance = self.make_states_class()(tensor, debug=self.debug) self.update_masks(states_instance) return states_instance @@ -636,8 +647,14 @@ def is_action_valid( backward: If True, checks validity for backward actions. Returns: - True if all actions are valid in the given states, False otherwise. + True if all actions are valid in the given states, False otherwise. When + `debug` is False, returns True without checking to keep hot paths + compile-friendly. """ + if not self.debug: + # Skip costly validity checks in production/compiled runs. + return True + assert states.forward_masks is not None and states.backward_masks is not None masks_tensor = states.backward_masks if backward else states.forward_masks return bool(torch.gather(masks_tensor, 1, actions.tensor).all().item()) @@ -921,7 +938,7 @@ def __init__( num_node_classes: int, num_edge_classes: int, is_directed: bool, - check_action_validity: bool = True, + debug: bool = False, ): """Initializes a graph-based environment. @@ -931,7 +948,8 @@ def __init__( num_node_classes: Number of node classes. num_edge_classes: Number of edge classes. is_directed: Whether the graph is directed. - check_action_validity: Whether to check the action validity. + debug: Kept for consistency with the other environments. Currently does not + optimize runtime. """ assert s0.x is not None and sf.x is not None assert s0.edge_attr is not None and sf.edge_attr is not None @@ -945,7 +963,7 @@ def __init__( self.num_node_classes = num_node_classes self.num_edge_classes = num_edge_classes self.is_directed = is_directed - self.check_action_validity = check_action_validity + self.debug = debug assert s0.x is not None assert sf.x is not None diff --git a/src/gfn/gym/bayesian_structure.py b/src/gfn/gym/bayesian_structure.py index 6ce52c0d..3d9704ce 100644 --- a/src/gfn/gym/bayesian_structure.py +++ b/src/gfn/gym/bayesian_structure.py @@ -31,7 +31,7 @@ def __init__( n_nodes: int, state_evaluator: Callable[[GraphStates], torch.Tensor], device: Literal["cpu", "cuda"] | torch.device = "cpu", - check_action_validity: bool = True, + debug: bool = False, ): if isinstance(device, str): device = torch.device(device) @@ -68,7 +68,7 @@ def __init__( device=device, s0=s0, sf=sf, - check_action_validity=check_action_validity, + debug=debug, ) def make_states_class(self) -> type[GraphStates]: diff --git a/src/gfn/gym/bitSequence.py b/src/gfn/gym/bitSequence.py index 2aca72bf..ed0ecb95 100644 --- a/src/gfn/gym/bitSequence.py +++ b/src/gfn/gym/bitSequence.py @@ -36,6 +36,7 @@ def __init__( length: Optional[torch.Tensor] = None, forward_masks: Optional[torch.Tensor] = None, backward_masks: Optional[torch.Tensor] = None, + debug: bool = False, ) -> None: """Initializes the BitSequencesStates object. @@ -44,9 +45,13 @@ def __init__( length: The tensor representing the length of each bit sequence. forward_masks: The tensor representing the forward masks. backward_masks: The tensor representing the backward masks. + debug: If True, enable runtime guards in the parent class (not compile-friendly). """ super().__init__( - tensor, forward_masks=forward_masks, backward_masks=backward_masks + tensor, + forward_masks=forward_masks, + backward_masks=backward_masks, + debug=debug, ) if length is None: length = torch.zeros(self.batch_shape, dtype=torch.long, device=self.device) @@ -67,6 +72,7 @@ def clone(self) -> BitSequenceStates: self.length.detach().clone(), self.forward_masks.detach().clone(), self.backward_masks.detach().clone(), + debug=self.debug, ) def _check_both_forward_backward_masks_exist(self): @@ -89,6 +95,7 @@ def __getitem__( self.length[index], self.forward_masks[index], self.backward_masks[index], + debug=self.debug, ) def __setitem__( @@ -114,7 +121,9 @@ def flatten(self) -> BitSequenceStates: self._check_both_forward_backward_masks_exist() forward_masks = self.forward_masks.view(-1, self.forward_masks.shape[-1]) backward_masks = self.backward_masks.view(-1, self.backward_masks.shape[-1]) - return self.__class__(states, length, forward_masks, backward_masks) + return self.__class__( + states, length, forward_masks, backward_masks, debug=self.debug + ) def extend(self, other: BitSequenceStates) -> None: """Extends the current BitSequencesStates object with another BitSequencesStates object. @@ -214,7 +223,7 @@ def __init__( H: Optional[torch.Tensor] = None, device_str: str = "cpu", seed: int = 0, - check_action_validity: bool = True, + debug: bool = False, ): """Initializes the BitSequence environment. @@ -226,7 +235,7 @@ def __init__( H: A tensor used to create the modes. device_str: The device to run the computations on ("cpu" or "cuda"). seed: The seed for the random number generator. - check_action_validity: Whether to check the action validity. + debug: If True, emit States with debug guards (not compile-friendly). """ assert seq_size % word_size == 0, "word_size must divide seq_size." self.words_per_seq: int = seq_size // word_size @@ -250,7 +259,7 @@ def __init__( dummy_action, exit_action, sf, - check_action_validity=check_action_validity, + debug=debug, ) self.H = H self.modes = self.make_modes_set(seed) # set of modes written as binary @@ -290,7 +299,9 @@ def states_from_tensor( if length is None: mask = tensor != -1 length = mask.long().sum(dim=-1) - states_instance = self.make_states_class()(tensor, length=length) + states_instance = self.make_states_class()( + tensor, length=length, debug=self.debug + ) self.update_masks(states_instance) return states_instance diff --git a/src/gfn/gym/box.py b/src/gfn/gym/box.py index 583d0b90..37f4c7c3 100644 --- a/src/gfn/gym/box.py +++ b/src/gfn/gym/box.py @@ -28,7 +28,7 @@ def __init__( R2: float = 2.0, epsilon: float = 1e-4, device: Literal["cpu", "cuda"] | torch.device = "cpu", - check_action_validity: bool = True, + debug: bool = False, ): """Initializes the Box environment. @@ -39,7 +39,7 @@ def __init__( R2: The reward for being inside the second box. epsilon: A small value to avoid numerical issues. device: The device to use. - check_action_validity: Whether to check the action validity. + debug: If True, emit States with debug guards (not compile-friendly). """ assert 0 < delta <= 1, "delta must be in (0, 1]" self.delta = delta @@ -59,11 +59,14 @@ def __init__( action_shape=(2,), dummy_action=dummy_action, exit_action=exit_action, - check_action_validity=check_action_validity, + debug=debug, ) def make_random_states( - self, batch_shape: Tuple[int, ...], device: torch.device | None = None + self, + batch_shape: Tuple[int, ...], + device: torch.device | None = None, + debug: bool = False, ) -> States: """Generates random states tensor of shape (*batch_shape, 2). @@ -75,7 +78,7 @@ def make_random_states( A States object with random states. """ device = self.device if device is None else device - return self.States(torch.rand(batch_shape + (2,), device=device)) + return self.States(torch.rand(batch_shape + (2,), device=device), debug=debug) def step(self, states: States, actions: Actions) -> States: """Step function for the Box environment. diff --git a/src/gfn/gym/diffusion_sampling.py b/src/gfn/gym/diffusion_sampling.py index 72e5975e..68fc5385 100644 --- a/src/gfn/gym/diffusion_sampling.py +++ b/src/gfn/gym/diffusion_sampling.py @@ -678,7 +678,6 @@ class DiffusionSampling(Env): Attributes: target: The target distribution. device: The device to use. - check_action_validity: Whether to check the action validity. """ # Registry of available targets. @@ -697,7 +696,7 @@ def __init__( target_kwargs: dict[str, Any] | None, num_discretization_steps: float, device: torch.device = torch.device("cpu"), - check_action_validity: bool = False, + debug: bool = False, ) -> None: """Initialize the DiffusionSampling environment. @@ -707,7 +706,7 @@ def __init__( defaults. num_discretization_steps: The number of discretization steps. device: The device to use. - check_action_validity: Whether to check the action validity. + debug: If True, emit States with debug guards (not compile-friendly). """ # Initalize the target. @@ -746,7 +745,7 @@ def __init__( device=device, dtype=torch.get_default_dtype(), ), - check_action_validity=check_action_validity, + debug=debug, ) def make_states_class(self) -> type[States]: diff --git a/src/gfn/gym/discrete_ebm.py b/src/gfn/gym/discrete_ebm.py index 3ed1c5eb..e691ab6e 100644 --- a/src/gfn/gym/discrete_ebm.py +++ b/src/gfn/gym/discrete_ebm.py @@ -84,7 +84,7 @@ def __init__( energy: EnergyFunction | None = None, alpha: float = 1.0, device: Literal["cpu", "cuda"] | torch.device = "cpu", - check_action_validity: bool = True, + debug: bool = False, ): """Discrete EBM environment. @@ -94,7 +94,7 @@ def __init__( Identity matrix is used. alpha: interaction strength the EBM. Defaults to 1.0. device: Device to use for the environment. - check_action_validity: Whether to check the action validity. + debug: If True, emit States with debug guards (not compile-friendly). """ self.ndim = ndim @@ -116,7 +116,7 @@ def __init__( # exit_action=, n_actions=n_actions, sf=sf, - check_action_validity=check_action_validity, + debug=debug, ) self.States: type[DiscreteStates] = self.States @@ -133,7 +133,7 @@ def update_masks(self, states: DiscreteStates) -> None: states.backward_masks[..., self.ndim : 2 * self.ndim] = states.tensor == 1 def make_random_states( - self, batch_shape: Tuple, device: torch.device | None = None + self, batch_shape: Tuple, device: torch.device | None = None, debug: bool = False ) -> DiscreteStates: """Generates random states tensor of shape `(*batch_shape, ndim)`. @@ -146,7 +146,7 @@ def make_random_states( """ device = self.device if device is None else device tensor = torch.randint(-1, 2, batch_shape + (self.ndim,), device=device) - return self.States(tensor) + return self.States(tensor, debug=debug) def is_exit_actions(self, actions: torch.Tensor) -> torch.Tensor: """Determines if the actions are exit actions. diff --git a/src/gfn/gym/graph_building.py b/src/gfn/gym/graph_building.py index 061d0f97..c438f826 100644 --- a/src/gfn/gym/graph_building.py +++ b/src/gfn/gym/graph_building.py @@ -37,7 +37,7 @@ def __init__( device: Literal["cpu", "cuda"] | torch.device = "cpu", s0: GeometricData | None = None, sf: GeometricData | None = None, - check_action_validity: bool = True, + debug: bool = False, ): """Initializes the GraphBuilding environment. @@ -51,7 +51,7 @@ def __init__( device: The device to run computations on. s0: The initial state. sf: The sink state. - check_action_validity: Whether to check the action validity. + debug: If True, emit States with debug guards (not compile-friendly). """ if s0 is None: s0 = GeometricData( @@ -82,7 +82,7 @@ def __init__( num_node_classes=num_node_classes, num_edge_classes=num_edge_classes, is_directed=is_directed, - check_action_validity=check_action_validity, + debug=debug, ) def step(self, states: GraphStates, actions: GraphActions) -> GraphStates: @@ -290,13 +290,17 @@ def reward(self, final_states: GraphStates) -> torch.Tensor: return self.state_evaluator(final_states) def make_random_states( - self, batch_shape: Tuple, device: torch.device | None = None + self, + batch_shape: Tuple, + device: torch.device | None = None, + debug: bool = False, ) -> GraphStates: """Generates random states. Args: batch_shape: The shape of the batch. device: The device to use. + debug: If True, emit States with debug guards (not compile-friendly). Returns: A `GraphStates` object with random states. @@ -338,7 +342,7 @@ def make_random_states( ) data_array.flat[i] = data - return self.States(data_array, device=device) + return self.States(data_array, device=device, debug=debug) def make_states_class(self) -> type[GraphStates]: """Creates a `GraphStates` class for this environment.""" @@ -375,6 +379,29 @@ def make_actions_class(self) -> type[GraphActions]: env = self class GraphBuildingActions(GraphActions): + # Required by the Actions base class for DB/SubTB style algorithms. + action_shape = (5,) + dummy_action = torch.tensor( + [ + GraphActionType.DUMMY, + -2, + -2, + -2, + -2, + ], + dtype=torch.long, + ) + exit_action = torch.tensor( + [ + GraphActionType.EXIT, + -1, + -1, + -1, + -1, + ], + dtype=torch.long, + ) + @classmethod def edge_index_action_to_src_dst( cls, edge_index_action: torch.Tensor, n_nodes: int @@ -413,7 +440,7 @@ def __init__( state_evaluator: callable, directed: bool, device: Literal["cpu", "cuda"] | torch.device, - check_action_validity: bool = True, + debug: bool = False, ): """Initializes the `GraphBuildingOnEdges` environment. @@ -422,7 +449,6 @@ def __init__( state_evaluator: A function that evaluates a state and returns a reward. directed: Whether the graph should be directed. device: The device to use. - check_action_validity: Whether to check the action validity. """ self.n_nodes = n_nodes if directed: @@ -459,7 +485,7 @@ def __init__( device=device, s0=s0, sf=sf, - check_action_validity=check_action_validity, + debug=debug, ) def make_states_class(self) -> type[GraphStates]: @@ -558,13 +584,17 @@ def is_initial_state(self) -> torch.Tensor: return GraphBuildingOnEdgesStates def make_random_states( - self, batch_shape: Tuple, device: torch.device | None = None + self, + batch_shape: Tuple, + device: torch.device | None = None, + debug: bool = False, ) -> GraphStates: """Makes a batch of random graph states with fixed number of nodes. Args: batch_shape: Shape of the batch dimensions. device: The device to use. + debug: If True, emit States with debug guards (not compile-friendly). Returns: A `GraphStates` object containing random graph states. @@ -598,7 +628,7 @@ def make_random_states( data = GeometricData(x=x, edge_index=edge_index, edge_attr=edge_attr) data_array.flat[i] = data - return self.States(data_array, device=device) + return self.States(data_array, device=device, debug=debug) def is_action_valid( self, states: GraphStates, actions: GraphActions, backward: bool = False diff --git a/src/gfn/gym/hypergrid.py b/src/gfn/gym/hypergrid.py index 1215b5f5..406ffde8 100644 --- a/src/gfn/gym/hypergrid.py +++ b/src/gfn/gym/hypergrid.py @@ -71,7 +71,7 @@ def __init__( device: Literal["cpu", "cuda"] | torch.device = "cpu", calculate_partition: bool = False, store_all_states: bool = False, - check_action_validity: bool = True, + debug: bool = False, ): """Initializes the HyperGrid environment. @@ -84,7 +84,7 @@ def __init__( calculate_partition: Whether to calculate the log partition function. store_all_states: Whether to store all states. If True, the true distribution can be accessed via the `true_dist` property. - check_action_validity: Whether to check the action validity. + debug: If True, emit States with debug guards (not compile-friendly). """ if height <= 4: warnings.warn("+ Warning: height <= 4 can lead to unsolvable environments.") @@ -140,7 +140,7 @@ def __init__( s0=s0, state_shape=state_shape, sf=sf, - check_action_validity=check_action_validity, + debug=debug, ) self.States: type[DiscreteStates] = self.States # for type checking @@ -160,7 +160,10 @@ def update_masks(self, states: DiscreteStates) -> None: states.backward_masks = states.tensor != 0 def make_random_states( - self, batch_shape: Tuple[int, ...], device: torch.device | None = None + self, + batch_shape: Tuple[int, ...], + device: torch.device | None = None, + debug: bool = False, ) -> DiscreteStates: """Creates a batch of random states. @@ -175,7 +178,7 @@ def make_random_states( tensor = torch.randint( 0, self.height, batch_shape + self.s0.shape, device=device ) - return self.States(tensor) + return self.States(tensor, debug=debug) def step(self, states: DiscreteStates, actions: Actions) -> DiscreteStates: """Performs a step in the environment. diff --git a/src/gfn/gym/line.py b/src/gfn/gym/line.py index e4b34b8c..5ac9faf6 100644 --- a/src/gfn/gym/line.py +++ b/src/gfn/gym/line.py @@ -28,7 +28,7 @@ def __init__( n_sd: float = 4.5, n_steps_per_trajectory: int = 5, device: Literal["cpu", "cuda"] | torch.device = "cpu", - check_action_validity: bool = True, + debug: bool = False, ): """Initializes the Line environment. @@ -39,7 +39,7 @@ def __init__( n_sd: The number of standard deviations to consider for the bounds. n_steps_per_trajectory: The number of steps per trajectory. device: The device to use. - check_action_validity: Whether to check the action validity. + debug: If True, emit States with debug guards (not compile-friendly). """ assert len(mus) == len(sigmas) device = torch.device(device) @@ -64,7 +64,7 @@ def __init__( action_shape=(1,), # [x_pos] dummy_action=dummy_action, exit_action=exit_action, - check_action_validity=check_action_validity, + debug=debug, ) # sf is -inf by default. def step(self, states: States, actions: Actions) -> States: diff --git a/src/gfn/gym/perfect_tree.py b/src/gfn/gym/perfect_tree.py index 7c3512da..6035b99a 100644 --- a/src/gfn/gym/perfect_tree.py +++ b/src/gfn/gym/perfect_tree.py @@ -42,16 +42,19 @@ def __init__( self, reward_fn: Callable, depth: int = 4, - device: Literal["cpu", "cuda"] | torch.device = "cpu", - check_action_validity: bool = True, + device: Literal["cpu", "cuda"] | torch.device | None = None, + debug: bool = False, ): """Initializes the PerfectBinaryTree environment. Args: reward_fn: A function that computes the reward for a given state. depth: The depth of the tree. - check_action_validity: Whether to check the action validity. + debug: If True, emit States with debug guards (not compile-friendly). """ + if device is None: + device = torch.get_default_device() + device = torch.device(device) self.reward_fn = reward_fn self.depth = depth @@ -66,7 +69,7 @@ def __init__( self.s0, (1,), sf=self.sf, - check_action_validity=check_action_validity, + debug=debug, ) self.States: type[DiscreteStates] = self.States @@ -125,11 +128,9 @@ def backward_step(self, states: DiscreteStates, actions: Actions) -> DiscreteSta next_states_tns = [ self.inverse_transition_table.get(tuple(tuple_)) for tuple_ in tuples ] - next_states_tns = ( - torch.tensor(next_states_tns, device=states.tensor.device) - .reshape(-1, 1) - .long() - ) + next_states_tns = torch.tensor( + next_states_tns, device=states.tensor.device, dtype=torch.int64 + ).reshape(-1, 1) return self.States(next_states_tns) def step(self, states: DiscreteStates, actions: Actions) -> DiscreteStates: @@ -145,11 +146,9 @@ def step(self, states: DiscreteStates, actions: Actions) -> DiscreteStates: tuples = torch.hstack((states.tensor, actions.tensor)).tolist() tuples = tuple(tuple(tuple_) for tuple_ in tuples) next_states_tns = [self.transition_table.get(tuple_) for tuple_ in tuples] - next_states_tns = ( - torch.tensor(next_states_tns, device=states.tensor.device) - .reshape(-1, 1) - .long() - ) + next_states_tns = torch.tensor( + next_states_tns, device=states.tensor.device, dtype=torch.int64 + ).reshape(-1, 1) return self.States(next_states_tns) def update_masks(self, states: DiscreteStates) -> None: diff --git a/src/gfn/gym/set_addition.py b/src/gfn/gym/set_addition.py index 65f0707a..04fef178 100644 --- a/src/gfn/gym/set_addition.py +++ b/src/gfn/gym/set_addition.py @@ -28,8 +28,8 @@ def __init__( max_items: int, reward_fn: Callable, fixed_length: bool = False, - device: Literal["cpu", "cuda"] | torch.device = "cpu", - check_action_validity: bool = True, + device: Literal["cpu", "cuda"] | torch.device | None = None, + debug: bool = False, ): """Initializes the SetAddition environment. @@ -38,8 +38,11 @@ def __init__( max_items: The maximum number of items that can be added to the set. reward_fn: The reward function. fixed_length: Whether the trajectories have a fixed length. - check_action_validity: Whether to check the action validity. + debug: If True, emit States with debug guards (not compile-friendly). """ + if device is None: + device = torch.get_default_device() + device = torch.device(device) self.n_items = n_items self.reward_fn = reward_fn @@ -53,7 +56,7 @@ def __init__( n_actions, s0, state_shape, - check_action_validity=check_action_validity, + debug=debug, ) self.States: type[DiscreteStates] = self.States diff --git a/src/gfn/states.py b/src/gfn/states.py index 47b5b7c8..932edc90 100644 --- a/src/gfn/states.py +++ b/src/gfn/states.py @@ -1,5 +1,6 @@ from __future__ import annotations # This allows to use the class name in type hints +import inspect from abc import ABC from math import prod from typing import ( @@ -24,6 +25,25 @@ from gfn.utils.graphs import GeometricBatch, get_edge_indices +def _assert_factory_accepts_debug(factory: Callable, factory_name: str) -> None: + """Ensure the factory can accept a debug kwarg (explicit or via **kwargs).""" + try: + sig = inspect.signature(factory) + except (TypeError, ValueError): + return + + params = sig.parameters + if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()): + return + debug_param = params.get("debug") + if debug_param is not None: + return + raise TypeError( + f"{factory_name} must accept a `debug` keyword argument (or **kwargs) " + "to support debug-gated States construction." + ) + + class States(ABC): r"""Base class for states, representing nodes in the DAG of a GFlowNet. @@ -50,6 +70,12 @@ class States(ABC): trajectory. This dummy state should never be processed, and is used to pad the batch of states only. + Compile-related expectations: + - Hot paths should be called with tensors already on the target device and with + correct shapes; debug guards can be enabled during development/tests to validate. + - Set `debug=False` inside torch.compile regions to avoid Python-side graph breaks; + enable `debug=True` only when running eager checks. + Attributes: tensor: Tensor of shape (*batch_shape, *state_shape) representing a batch of states. @@ -66,26 +92,42 @@ class States(ABC): s0: ClassVar[torch.Tensor | GeometricData] sf: ClassVar[torch.Tensor | GeometricData] - make_random_states: Callable = lambda *x: (_ for _ in ()).throw( - NotImplementedError( - "The environment does not support initialization of random states." + make_random_states: Callable = staticmethod( + lambda *args, **kwargs: (_ for _ in ()).throw( + NotImplementedError( + "The environment does not support initialization of random states." + ) ) ) - def __init__(self, tensor: torch.Tensor, device: torch.device | None = None) -> None: + def __init__( + self, + tensor: torch.Tensor, + device: torch.device | None = None, + debug: bool = False, + ) -> None: """Initializes a States object with a batch of states. Args: tensor: Tensor of shape (*batch_shape, *state_shape) representing a batch of states. + debug: If True, keep runtime guards active for safety; keep False in + compiled regions to avoid graph breaks when using torch.compile. + Preconditions when debug is False: `tensor` is already on the intended + device and its trailing dimensions equal `state_shape`. """ - assert self.s0.shape == self.state_shape - assert self.sf.shape == self.state_shape - assert tensor.shape[-len(self.state_shape) :] == self.state_shape + if debug: + # Keep shape validations in debug so compiled graphs avoid Python asserts. + assert self.s0.shape == self.state_shape + assert self.sf.shape == self.state_shape + assert ( + tensor.shape[-len(self.state_shape) :] == self.state_shape + ) # noqa: E203 # Per-instance device resolution: prefer explicit device, else infer from tensor resolved_device = device if device is not None else tensor.device self.tensor = tensor.to(resolved_device) + self.debug = debug @property def device(self) -> torch.device: @@ -112,6 +154,7 @@ def from_batch_shape( random: bool = False, sink: bool = False, device: torch.device | None = None, + debug: bool = False, ) -> States: r"""Creates a States object with the given batch shape. @@ -126,6 +169,7 @@ def from_batch_shape( random: If True, initialize states randomly. sink: If True, initialize states as sink states ($s_f$). device: The device to create the states on. + debug: If True, keeps compile graph-breaking checks in the logic for safety. Returns: A States object with the specified batch shape and initialization. @@ -137,15 +181,21 @@ def from_batch_shape( raise ValueError("Only one of `random` and `sink` should be True.") if random: - return cls.make_random_states(batch_shape, device=device) + _assert_factory_accepts_debug(cls.make_random_states, "make_random_states") + return cls.make_random_states(batch_shape, device=device, debug=debug) elif sink: - return cls.make_sink_states(batch_shape, device=device) + _assert_factory_accepts_debug(cls.make_sink_states, "make_sink_states") + return cls.make_sink_states(batch_shape, device=device, debug=debug) else: - return cls.make_initial_states(batch_shape, device=device) + _assert_factory_accepts_debug(cls.make_initial_states, "make_initial_states") + return cls.make_initial_states(batch_shape, device=device, debug=debug) @classmethod def make_initial_states( - cls, batch_shape: tuple[int, ...], device: torch.device | None = None + cls, + batch_shape: tuple[int, ...], + device: torch.device | None = None, + debug: bool = False, ) -> States: r"""Creates a States object with all states set to $s_0$. @@ -160,7 +210,10 @@ def make_initial_states( assert cls.s0 is not None and state_ndim is not None device = cls.s0.device if device is None else device if isinstance(cls.s0, torch.Tensor): - return cls(cls.s0.repeat(*batch_shape, *((1,) * state_ndim)).to(device)) + return cls( + cls.s0.repeat(*batch_shape, *((1,) * state_ndim)).to(device), + debug=debug, + ) else: raise NotImplementedError( f"make_initial_states is not implemented by default for {cls.__name__}" @@ -168,7 +221,10 @@ def make_initial_states( @classmethod def make_sink_states( - cls, batch_shape: tuple[int, ...], device: torch.device | None = None + cls, + batch_shape: tuple[int, ...], + device: torch.device | None = None, + debug: bool = False, ) -> States: r"""Creates a States object with all states set to $s_f$. @@ -183,7 +239,10 @@ def make_sink_states( assert cls.sf is not None and state_ndim is not None device = cls.sf.device if device is None else device if isinstance(cls.sf, torch.Tensor): - return cls(cls.sf.repeat(*batch_shape, *((1,) * state_ndim)).to(device)) + return cls( + cls.sf.repeat(*batch_shape, *((1,) * state_ndim)).to(device), + debug=debug, + ) else: raise NotImplementedError( f"make_sink_states is not implemented by default for {cls.__name__}" @@ -222,7 +281,7 @@ def __getitem__( Returns: A new States object with the selected states. """ - return self.__class__(self.tensor[index]) + return self.__class__(self.tensor[index], debug=self.debug) def __setitem__( self, @@ -243,7 +302,7 @@ def clone(self) -> States: Returns: A new States object with the same data. """ - return self.__class__(self.tensor.clone()) + return self.__class__(self.tensor.clone(), debug=self.debug) def flatten(self) -> States: """Flattens the batch dimension of the states. @@ -254,7 +313,7 @@ def flatten(self) -> States: A new States object with the batch dimension flattened. """ states = self.tensor.view(-1, *self.state_shape) - return self.__class__(states) + return self.__class__(states, debug=self.debug) def extend(self, other: States) -> None: """Concatenates another States object along the final batch dimension. @@ -325,21 +384,31 @@ def _compare(self, other: torch.Tensor) -> torch.Tensor: equal to `other`. """ n_batch_dims = len(self.batch_shape) - if n_batch_dims == 1: - assert (other.shape == self.state_shape) or ( - other.shape == self.batch_shape + self.state_shape - ), f"Expected shape {self.state_shape} or {self.batch_shape + self.state_shape}, got {other.shape}." + if self.debug: + full_shape = self.batch_shape + self.state_shape + if not ( + other.shape == self.state_shape or other.shape == full_shape # type: ignore[misc] + ): + raise ValueError( + f"Expected shape {self.state_shape} or {full_shape}, got {other.shape}." + ) + + # Broadcast single-state inputs instead of branching on shape at runtime. + if other.shape == self.state_shape: + other_expanded = other.view( + *((1,) * n_batch_dims), *self.state_shape + ).expand(*self.batch_shape, *self.state_shape) else: - assert ( - other.shape == self.batch_shape + self.state_shape - ), f"Expected shape {self.batch_shape + self.state_shape}, got {other.shape}." + other_expanded = other - out = self.tensor == other + out = self.tensor == other_expanded if len(self.__class__.state_shape) > 1: out = out.flatten(start_dim=n_batch_dims) out = out.all(dim=-1) - assert out.shape == self.batch_shape + if self.debug: + assert out.shape == self.batch_shape + return out @property @@ -349,23 +418,22 @@ def is_initial_state(self) -> torch.Tensor: Returns: A boolean tensor of shape (*batch_shape,) that is True for initial states. """ - if isinstance(self.__class__.s0, torch.Tensor): - if len(self.batch_shape) == 1: - try: - ensure_same_device(self.device, self.__class__.s0.device) - source_states_tensor = self.__class__.s0 - except ValueError: - source_states_tensor = self.__class__.s0.to(self.device) - else: - source_states_tensor = self.__class__.s0.repeat( - *self.batch_shape, *((1,) * len(self.__class__.state_shape)) - ).to(self.device) - else: + if not isinstance(self.__class__.s0, torch.Tensor): raise NotImplementedError( "is_initial_state is not implemented by default " f"for {self.__class__.__name__}" ) - return self._compare(source_states_tensor) + # We do not cast devices here to avoid breaking the graph when using + # torch.compile. We use `ensure_same_device` to catch silent device drift + # during testing. + if self.debug: + ensure_same_device(self.device, self.__class__.s0.device) + if self.__class__.s0.shape != self.state_shape: + raise ValueError( + f"s0 must have shape {self.state_shape}; got {self.__class__.s0.shape}" + ) + + return self._compare(self.__class__.s0) @property def is_sink_state(self) -> torch.Tensor: @@ -374,23 +442,23 @@ def is_sink_state(self) -> torch.Tensor: Returns: A boolean tensor of shape (*batch_shape,) that is True for sink states. """ - if isinstance(self.__class__.sf, torch.Tensor): - if len(self.batch_shape) == 1: - try: - ensure_same_device(self.device, self.__class__.sf.device) - sink_states = self.__class__.sf - except ValueError: - sink_states = self.__class__.sf.to(self.device) - else: - sink_states = self.__class__.sf.repeat( - *self.batch_shape, *((1,) * len(self.__class__.state_shape)) - ).to(self.device) - else: + if not isinstance(self.__class__.sf, torch.Tensor): raise NotImplementedError( "is_sink_state is not implemented by default " f"for {self.__class__.__name__}" ) - return self._compare(sink_states) + + # We do not cast devices here to avoid breaking the graph when using + # torch.compile. We use `ensure_same_device` to catch silent device drift + # during testing. + if self.debug: + ensure_same_device(self.device, self.__class__.sf.device) + if self.__class__.sf.shape != self.state_shape: + raise ValueError( + f"sf must have shape {self.state_shape}; got {self.__class__.sf.shape}" + ) + + return self._compare(self.__class__.sf) def sample(self, n_samples: int) -> States: """Randomly samples a subset of states from the batch. @@ -450,6 +518,12 @@ class DiscreteStates(States, ABC): device: The device on which the states are stored. forward_masks: Boolean tensor indicating forward actions allowed at each state. backward_masks: Boolean tensor indicating backward actions allowed at each state. + + Compile-related expectations: + - Inputs (state tensor and masks) should already be on the target device with + correct shapes; debug can be used to validate during development/tests. + - Mask helpers reset masks before applying new conditions; rely on this behavior + to avoid cross-step leakage. """ n_actions: ClassVar[int] @@ -460,6 +534,7 @@ def __init__( forward_masks: Optional[torch.Tensor] = None, backward_masks: Optional[torch.Tensor] = None, device: torch.device | None = None, + debug: bool = False, ) -> None: """Initializes a DiscreteStates container with a batch of states and masks. @@ -470,9 +545,12 @@ def __init__( indicating forward actions allowed at each state. backward_masks: Optional boolean tensor of shape (*batch_shape, n_actions - 1) indicating backward actions allowed at each state. + debug: If True, run mask/state validations even in compiled contexts. """ - super().__init__(tensor, device=device) - assert tensor.shape == self.batch_shape + self.state_shape + super().__init__(tensor, device=device, debug=debug) + if debug: + # Keep shape validation in debug to avoid graph breaks in compiled regions. + assert tensor.shape == self.batch_shape + self.state_shape # In the usual case, no masks are provided and we produce these defaults. # Note: this **must** be updated externally by the env. @@ -509,10 +587,16 @@ def clone(self) -> DiscreteStates: self.tensor.clone(), self.forward_masks.clone(), self.backward_masks.clone(), + debug=self.debug, ) def _check_both_forward_backward_masks_exist(self): - assert self.forward_masks is not None and self.backward_masks is not None + # Only validate in debug to avoid graph breaks in compiled regions. + if self.debug: + if not torch.is_tensor(self.forward_masks): + raise TypeError("forward_masks must be tensors") + if not torch.is_tensor(self.backward_masks): + raise TypeError("backward_masks must be tensors") def __repr__(self) -> str: """Returns a detailed string representation of the DiscreteStates object. @@ -545,7 +629,7 @@ def __getitem__( self._check_both_forward_backward_masks_exist() forward_masks = self.forward_masks[index] backward_masks = self.backward_masks[index] - out = self.__class__(states, forward_masks, backward_masks) + out = self.__class__(states, forward_masks, backward_masks, debug=self.debug) return out def __setitem__( @@ -572,7 +656,7 @@ def flatten(self) -> DiscreteStates: self._check_both_forward_backward_masks_exist() forward_masks = self.forward_masks.view(-1, self.forward_masks.shape[-1]) backward_masks = self.backward_masks.view(-1, self.backward_masks.shape[-1]) - return self.__class__(states, forward_masks, backward_masks) + return self.__class__(states, forward_masks, backward_masks, debug=self.debug) def extend(self, other: DiscreteStates) -> None: """Concatenates another DiscreteStates object along the batch dimension. @@ -668,14 +752,35 @@ def set_nonexit_action_masks( times, cond might be state.tensor > 5 (assuming count starts at 0). allow_exit: sets whether exiting can happen at any point in the trajectory - if so, it should be set to True. + + Notes: + - Always resets `forward_masks` to all True before applying the new mask + so updates do not leak across steps. + - Works for 1D or 2D batch shapes; cond must match `batch_shape`. + - Debug guards validate shape/dtype but should be off in compiled regions. """ + if self.debug: + # Validate mask shape/dtype to catch silent misalignment during testing. + expected_shape = self.batch_shape + (self.n_actions - 1,) + if cond.shape != expected_shape: + raise ValueError( + f"cond must have shape {expected_shape}; got {cond.shape}" + ) + if cond.dtype is not torch.bool: + raise ValueError(f"cond must be boolean; got {cond.dtype}") + # Resets masks in place to prevent side-effects across steps. self.forward_masks[:] = True - if allow_exit: - exit_idx = torch.zeros(self.batch_shape + (1,)).to(cond.device) - else: - exit_idx = torch.ones(self.batch_shape + (1,)).to(cond.device) - self.forward_masks[torch.cat([cond, exit_idx], dim=-1).bool()] = False + exit_mask = torch.zeros( + self.batch_shape + (1,), device=cond.device, dtype=cond.dtype + ) + + if not allow_exit: + exit_mask.fill_(True) + + # Concatenate and mask in a single tensor op to stay torch.compile friendly. + # Sets the forward mask to be False where this concatenated mask is True. + self.forward_masks[torch.cat([cond, exit_mask], dim=-1)] = False def set_exit_masks(self, batch_idx: torch.Tensor) -> None: """Sets forward masks such that the only allowable next action is to exit. @@ -685,16 +790,26 @@ def set_exit_masks(self, batch_idx: torch.Tensor) -> None: Args: batch_idx: A boolean index along the batch dimension, along which to enforce exits. + + Notes: + - Works for 1D or 2D batch shapes; `batch_idx` must match `batch_shape`. + - Clears all actions for the selected batch entries, then sets only the + exit action True via masked_fill to stay torch.compile friendly. + - Does not move devices; expects masks/tensors already on the target device. """ - self.forward_masks[batch_idx, :] = torch.cat( - [ - torch.zeros([int(torch.sum(batch_idx).item()), *self.s0.shape]).to( - self.device - ), - torch.ones([int(torch.sum(batch_idx).item()), 1]).to(self.device), - ], - dim=-1, - ).bool() + if self.debug: + if batch_idx.shape != self.batch_shape: + raise ValueError( + f"batch_idx must have shape {self.batch_shape}; got {batch_idx.shape}" + ) + if batch_idx.dtype is not torch.bool: + raise ValueError(f"batch_idx must be boolean; got {batch_idx.dtype}") + + # Avoid Python .item() to stay torch.compile friendly. For any True entry in + # batch_idx (1D or 2D), zero all actions then set only the exit action True. + self.forward_masks[batch_idx] = False + # Use masked_fill on the last action slice to avoid advanced indexing graph breaks. + self.forward_masks[..., -1].masked_fill_(batch_idx, True) def init_forward_masks(self, set_ones: bool = True) -> None: """Initalizes forward masks. @@ -756,6 +871,7 @@ def __init__( categorical_node_features: bool = False, categorical_edge_features: bool = False, device: torch.device | None = None, + debug: bool = False, ) -> None: """Initializes the GraphStates with a numpy array of `GeometricData` objects. @@ -764,11 +880,15 @@ def __init__( categorical_node_features: Whether the node features are categorical. categorical_edge_features: Whether the edge features are categorical. device: The device to store the graphs on (optional). + debug: If True, keep runtime validations enabled; stored for parity with + tensor-based States. """ assert isinstance(data, np.ndarray), "data must be a numpy array" self.categorical_node_features = categorical_node_features self.categorical_edge_features = categorical_edge_features self.data = data + # Keep a debug flag for interface consistency and future guarded checks. + self.debug = debug # Resolve device per instance: prefer explicit, else infer, else default @@ -870,7 +990,10 @@ def batch_shape(self) -> tuple[int, ...]: @classmethod def make_initial_states( - cls, batch_shape: int | Tuple, device: torch.device | None = None + cls, + batch_shape: int | Tuple, + device: torch.device | None = None, + debug: bool = False, ) -> GraphStates: r"""Creates a numpy array of graphs consisting of initial states ($s_0$). @@ -898,11 +1021,15 @@ def make_initial_states( categorical_node_features=cls.s0.x.dtype == torch.long, categorical_edge_features=cls.s0.edge_attr.dtype == torch.long, device=device, + debug=debug, ) @classmethod def make_sink_states( - cls, batch_shape: int | Tuple, device: torch.device | None = None + cls, + batch_shape: int | Tuple, + device: torch.device | None = None, + debug: bool = False, ) -> GraphStates: r"""Creates a numpy array of graphs consisting of sink states ($s_f$). @@ -933,6 +1060,7 @@ def make_sink_states( categorical_node_features=cls.sf.x.dtype == torch.long, categorical_edge_features=cls.sf.edge_attr.dtype == torch.long, device=device, + debug=debug, ) @property @@ -984,7 +1112,7 @@ def forward_masks(self) -> TensorDict: node_index_masks[i, graph.x.size(0)] = True ei0, ei1 = get_edge_indices(graph.x.size(0), self.is_directed, self.device) - edge_masks[i, len(ei0) :] = False + edge_masks[i, len(ei0) :] = False # noqa: E203 if graph.edge_index is not None and graph.edge_index.size(1) > 0: edge_idx = torch.logical_and( @@ -1157,7 +1285,7 @@ def __getitem__( selected_graphs_array[0] = selected_graphs selected_graphs = selected_graphs_array.squeeze() - out = self.__class__(selected_graphs, device=self.device) + out = self.__class__(selected_graphs, device=self.device, debug=self.debug) return out def __setitem__( @@ -1212,7 +1340,7 @@ def clone(self) -> GraphStates: for i, graph in enumerate(self.data.flat): cloned_graphs.flat[i] = graph.clone() - return self.__class__(cloned_graphs, device=self.device) + return self.__class__(cloned_graphs, device=self.device, debug=self.debug) def extend(self, other: GraphStates): """Concatenates another GraphStates object along the batch dimension. diff --git a/src/gfn/utils/common.py b/src/gfn/utils/common.py index 6f4601ee..5c7fd44d 100644 --- a/src/gfn/utils/common.py +++ b/src/gfn/utils/common.py @@ -145,12 +145,12 @@ def filter_kwargs_for_callable( # ----------------------------------------------------------------------------- -def set_seed(seed: int, performance_mode: bool = False) -> None: +def set_seed(seed: int, deterministic_mode: bool = False) -> None: """Used to control randomness for both single and distributed training. Args: seed: The seed to use for all random number generators - performance_mode: If True, disables deterministic behavior for better performance. + deterministic_mode: If True, uses deterministic behavior for better performance. In multi-GPU settings, this only affects cuDNN. In multi-CPU settings, this allows parallel processing in NumPy. """ @@ -186,19 +186,22 @@ def set_seed(seed: int, performance_mode: bool = False) -> None: # Set device-specific environment variables if torch.cuda.is_available(): - # For GPU training, we can use multiple threads for CPU operations - if performance_mode: - os.environ["OMP_NUM_THREADS"] = str(num_cpus) - os.environ["MKL_NUM_THREADS"] = str(num_cpus) - else: + if deterministic_mode: # For reproducibility in GPU training, we still want deterministic # CPU operations os.environ["OMP_NUM_THREADS"] = "1" os.environ["MKL_NUM_THREADS"] = "1" + else: + # For GPU training, we can use multiple threads for CPU operations + os.environ["OMP_NUM_THREADS"] = str(num_cpus) + os.environ["MKL_NUM_THREADS"] = str(num_cpus) else: # For CPU-only training, we need to be more careful with threading - if performance_mode: - + if deterministic_mode: + # For perfect reproducibility in CPU training, disable parallel processing + os.environ["OMP_NUM_THREADS"] = "1" + os.environ["MKL_NUM_THREADS"] = "1" + else: # Allow parallel processing but with controlled number of threads # Different backends might handle threading differently if backend in ["mpi", "ccl"]: @@ -211,10 +214,6 @@ def set_seed(seed: int, performance_mode: bool = False) -> None: os.environ["OMP_NUM_THREADS"] = str(num_threads) os.environ["MKL_NUM_THREADS"] = str(num_threads) - else: - # For perfect reproducibility in CPU training, disable parallel processing - os.environ["OMP_NUM_THREADS"] = "1" - os.environ["MKL_NUM_THREADS"] = "1" else: # Non-distributed training - use the global seed @@ -237,7 +236,7 @@ def set_seed(seed: int, performance_mode: bool = False) -> None: threading.current_thread()._seed = seed # These are only set when we care about reproducibility over performance - if not performance_mode: + if deterministic_mode: # GPU-specific settings if torch.cuda.is_available(): torch.backends.cudnn.deterministic = True @@ -314,9 +313,15 @@ def temporarily_set_seed(seed): def make_dataloader_seed_fns( base_seed: int, + deterministic_mode: bool = False, ) -> Tuple[Callable[[int], None], torch.Generator]: """Return `(worker_init_fn, generator)` for DataLoader reproducibility. + Args: + base_seed: The base seed to use for the DataLoader. + deterministic_mode: If True, uses deterministic behavior for better + reproducibility at the cost of performance. + Example ------- >>> w_init, g = make_dataloader_seed_fns(process_seed) @@ -332,7 +337,7 @@ def make_dataloader_seed_fns( def _worker_init_fn(worker_id: int) -> None: # pragma: no cover # Each worker gets a distinct seed in the same pattern used for ranks. - set_seed(base_seed + worker_id, performance_mode=False) + set_seed(base_seed + worker_id, deterministic_mode=deterministic_mode) gen = torch.Generator() gen.manual_seed(base_seed) diff --git a/src/gfn/utils/compile.py b/src/gfn/utils/compile.py new file mode 100644 index 00000000..d772a503 --- /dev/null +++ b/src/gfn/utils/compile.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from typing import Iterable + +import torch + + +def try_compile_gflownet( + gfn, + *, + mode: str = "default", + components: Iterable[str] = ("pf", "pb", "logZ", "logF"), +) -> None: + """Best-effort compilation of estimator modules attached to a GFlowNet. + Args: + gfn: The GFlowNet instance to compile. + mode: Compilation mode forwarded to ``torch.compile``. + components: Attribute names to attempt compilation on (e.g., ``pf``). + Returns: + Mapping from component name to compilation success status. + """ + results: dict[str, bool] = {} + + if not hasattr(torch, "compile"): + results = {name: False for name in components} + + else: + for name in components: + + # If the estimator does not exist, we cannot compile it. + if not hasattr(gfn, name): + results[name] = False + continue + + estimator = getattr(gfn, name) + module = getattr(estimator, "module", None) + + # If the estimator does not have a module, we cannot compile it. + if module is None: + results[name] = False + continue + + try: + # Attempt to compile the module. + assert isinstance(estimator.module, torch.nn.Module) + estimator.module = torch.compile(module, mode=mode) + results[name] = True + except Exception: + results[name] = False + + # Print the results. + formatted = ", ".join( + f"{name}:{'✓' if success else 'x'}" for name, success in results.items() + ) + print(f"[compile] {formatted}") diff --git a/src/gfn/utils/modules.py b/src/gfn/utils/modules.py index f0248224..72697457 100644 --- a/src/gfn/utils/modules.py +++ b/src/gfn/utils/modules.py @@ -765,6 +765,58 @@ def forward(self, states_tensor: GeometricBatch) -> TensorDict: ) +class GraphScalarMLP(nn.Module): + """Graph encoder that maps adjacency structure to n scalar output.""" + + def __init__( + self, + n_nodes: int, + directed: bool, + embedding_dim: int = 128, + n_hidden_layers: int = 2, + n_outputs: int = 1, + ) -> None: + super().__init__() + assert n_nodes > 0, "n_nodes must be positive" + assert embedding_dim > 0, "embedding_dim must be positive" + assert n_hidden_layers >= 0, "n_hidden_layers must be non-negative" + self.n_nodes = n_nodes + self.is_directed = directed + self.input_dim = n_nodes**2 + + self.backbone = MLP( + input_dim=n_nodes**2, + output_dim=embedding_dim, + hidden_dim=embedding_dim, + n_hidden_layers=n_hidden_layers, + add_layer_norm=True, + ) + self.head = MLP( + input_dim=embedding_dim, + output_dim=n_outputs, + hidden_dim=embedding_dim, + n_hidden_layers=1, + add_layer_norm=True, + ) + + def forward(self, states_tensor: GeometricBatch) -> torch.Tensor: + """Encode graphs into a scalar per element of the batch.""" + batch_size = len(states_tensor) + device = states_tensor.x.device + adj = torch.zeros((batch_size, self.n_nodes, self.n_nodes), device=device) + + if states_tensor.edge_index.numel() > 0: + # Vectorized over the batch dim. + B = torch.arange(batch_size) + edges = states_tensor.edge_index + adj[B, edges[:, 0], edges[:, 1]] = 1 + if not self.is_directed: + adj[B, edges[:, 1], edges[:, 0]] = 1 + + embedding = self.backbone(adj.view(batch_size, -1)) + return self.head(embedding) + + class GraphActionUniform(nn.Module): """Implements a uniform distribution over discrete actions given a graph state. @@ -809,11 +861,16 @@ def forward(self, states_tensor: GeometricBatch) -> TensorDict: ingestion by the uniform distribution. Returns: - A TensorDict containing logits for each action component, with all values set to 1 to represent a uniform distribution: - - GraphActions.ACTION_TYPE_KEY: Tensor of shape [*batch_shape, 3] for the 3 possible action types - - GraphActions.EDGE_CLASS_KEY: Tensor of shape [*batch_shape, num_edge_classes] for edge class logits - - GraphActions.NODE_CLASS_KEY: Tensor of shape [*batch_shape, num_node_classes] for node class logits - - GraphActions.EDGE_INDEX_KEY: Tensor of shape [*batch_shape, edges_dim] for edge index logits + A TensorDict containing logits for each action component, with all values + set to 1 to represent a uniform distribution: + - GraphActions.ACTION_TYPE_KEY: Tensor of shape [*batch_shape, 3] for the 3 + possible action types + - GraphActions.EDGE_CLASS_KEY: Tensor of shape [*batch_shape, + num_edge_classes] for edge class logits + - GraphActions.NODE_CLASS_KEY: Tensor of shape [*batch_shape, + num_node_classes] for node class logits + - GraphActions.EDGE_INDEX_KEY: Tensor of shape [*batch_shape, edges_dim] + for edge index logits """ device = states_tensor.x.device max_nodes = int(torch.max(states_tensor.ptr[1:] - states_tensor.ptr[:-1])) @@ -1371,9 +1428,8 @@ def forward( raise ValueError( "Value carry shape is incompatible with the provided tokens." ) - if ( - key_carry.size(-1) != self.head_dim - or value_carry.size(-1) != self.head_dim + if (key_carry.size(-1) != self.head_dim) or ( + value_carry.size(-1) != self.head_dim ): raise ValueError("Key/value carry head dimension mismatch detected.") if key_carry.device != device or value_carry.device != device: diff --git a/src/gfn/utils/prob_calculations.py b/src/gfn/utils/prob_calculations.py index 5f1a75e8..1047dc98 100644 --- a/src/gfn/utils/prob_calculations.py +++ b/src/gfn/utils/prob_calculations.py @@ -34,9 +34,6 @@ def get_trajectory_pfs_and_pbs( Returns: ``(log_pf[T,N], log_pb[T,N])`` """ - # TODO: Remove this assertion and move to a test. - # fill value is the value used for invalid states (sink state usually) - assert trajectories.states.is_sink_state[:-1].equal(trajectories.actions.is_dummy) log_pf_trajectories = get_trajectory_pfs( pf, diff --git a/testing/test_actions.py b/testing/test_actions.py index 54590cdb..373d6996 100644 --- a/testing/test_actions.py +++ b/testing/test_actions.py @@ -67,3 +67,23 @@ def test_action(action_fixture, request): extended_actions[0] = extended_actions[BATCH] is_exit_extended[0] = False assert torch.all(extended_actions.is_exit == is_exit_extended) + + +def test_debug_shape_guard(): + class SmallAction(Actions): + action_shape = (2,) + dummy_action = torch.zeros(2) + exit_action = torch.ones(2) + + # Debug mode enforces shape checks. + with pytest.raises(ValueError): + SmallAction(torch.zeros(3), debug=True) + + # Non-debug mode keeps hot path free of Python checks. + SmallAction(torch.zeros(3)) + + +def test_stack_preserves_debug_flag(): + base = ContinuousActions(torch.arange(0, 10), debug=True) + stacked = ContinuousActions.stack([base, base]) + assert getattr(stacked, "debug", False) is True diff --git a/testing/test_adaptor_estimator_gflownet_integration.py b/testing/test_adaptor_estimator_gflownet_integration.py index eb943228..dc064671 100644 --- a/testing/test_adaptor_estimator_gflownet_integration.py +++ b/testing/test_adaptor_estimator_gflownet_integration.py @@ -26,7 +26,6 @@ def _make_bitsequence_env( H=H, device_str=str(device), seed=0, - check_action_validity=True, ) return env diff --git a/testing/test_environments.py b/testing/test_environments.py index 42abc54f..81f67428 100644 --- a/testing/test_environments.py +++ b/testing/test_environments.py @@ -43,7 +43,7 @@ def test_HyperGrid_preprocessors( ND_BATCH_SHAPE = (4, 2) SEED = 1234 - env = HyperGrid(ndim=NDIM, height=ENV_HEIGHT) + env = HyperGrid(ndim=NDIM, height=ENV_HEIGHT, debug=True) if preprocessor_name == "Identity": preprocessor = IdentityPreprocessor(output_dim=NDIM) @@ -72,7 +72,7 @@ def test_HyperGrid_fwd_step(): NDIM = 2 ENV_HEIGHT = BATCH_SIZE = 3 - env = HyperGrid(ndim=NDIM, height=ENV_HEIGHT) + env = HyperGrid(ndim=NDIM, height=ENV_HEIGHT, debug=True) states = env.reset(batch_shape=BATCH_SIZE) # Instantiate a batch of initial states assert (states.batch_shape[0], states.state_shape[0]) == (BATCH_SIZE, NDIM) @@ -104,7 +104,7 @@ def test_HyperGrid_bwd_step(): SEED = 1234 # Testing the backward method from a batch of random (seeded) state. - env = HyperGrid(ndim=NDIM, height=ENV_HEIGHT) + env = HyperGrid(ndim=NDIM, height=ENV_HEIGHT, debug=True) states = env.reset(batch_shape=(NDIM, ENV_HEIGHT), random=True, seed=SEED) passing_actions_lists = [ @@ -137,7 +137,7 @@ def test_DiscreteEBM_fwd_step(): NDIM = 2 BATCH_SIZE = 4 - env = DiscreteEBM(ndim=NDIM) + env = DiscreteEBM(ndim=NDIM, debug=True) states = env.reset( batch_shape=BATCH_SIZE, seed=1234 ) # Instantiate a batch of initial states @@ -173,7 +173,7 @@ def test_DiscreteEBM_bwd_step(): SEED = 1234 # Testing the backward method from a batch of random (seeded) state. - env = DiscreteEBM(ndim=NDIM) + env = DiscreteEBM(ndim=NDIM, debug=True) states = env.reset(batch_shape=BATCH_SIZE, random=True, seed=SEED) passing_actions_lists = [ @@ -195,7 +195,7 @@ def test_DiscreteEBM_bwd_step(): @pytest.mark.parametrize("delta", [0.1, 0.5, 1.0]) def test_box_fwd_step(delta: float): - env = Box(delta=delta) + env = Box(delta=delta, debug=True) BATCH_SIZE = 3 states = env.reset(batch_shape=BATCH_SIZE) # Instantiate a batch of initial states @@ -261,11 +261,11 @@ def test_states_getitem(ndim: int, env_name: str): ND_BATCH_SHAPE = (2, 3) if env_name == "HyperGrid": - env = HyperGrid(ndim=ndim, height=8) + env = HyperGrid(ndim=ndim, height=8, debug=True) elif env_name == "DiscreteEBM": - env = DiscreteEBM(ndim=ndim) + env = DiscreteEBM(ndim=ndim, debug=True) elif env_name == "Box": - env = Box(delta=1.0 / ndim) + env = Box(delta=1.0 / ndim, debug=True) else: raise ValueError(f"Unknown env_name {env_name}") @@ -298,7 +298,11 @@ def test_get_grid(): NDIM = 2 env = HyperGrid( - height=HEIGHT, ndim=NDIM, store_all_states=True, calculate_partition=True + height=HEIGHT, + ndim=NDIM, + store_all_states=True, + calculate_partition=True, + debug=True, ) all_states = env.all_states assert all_states is not None @@ -329,6 +333,7 @@ def test_graph_env(): state_evaluator=lambda s: torch.zeros(s.batch_shape), num_node_classes=10, num_edge_classes=10, + debug=True, ) states = env.reset(batch_shape=BATCH_SIZE) assert states.batch_shape == (BATCH_SIZE,) @@ -598,7 +603,7 @@ def test_set_addition_fwd_step(): BATCH_SIZE = 2 env = SetAddition( - n_items=N_ITEMS, max_items=MAX_ITEMS, reward_fn=lambda s: s.sum(-1) + n_items=N_ITEMS, max_items=MAX_ITEMS, reward_fn=lambda s: s.sum(-1), debug=True ) states = env.reset(batch_shape=BATCH_SIZE) assert states.tensor.shape == (BATCH_SIZE, N_ITEMS) @@ -654,7 +659,7 @@ def test_set_addition_bwd_step(): BATCH_SIZE = 2 env = SetAddition( - n_items=N_ITEMS, max_items=MAX_ITEMS, reward_fn=lambda s: s.sum(-1) + n_items=N_ITEMS, max_items=MAX_ITEMS, reward_fn=lambda s: s.sum(-1), debug=True ) # Start from a state with 3 items @@ -700,6 +705,7 @@ def test_perfect_binary_tree_fwd_step(): env = PerfectBinaryTree( depth=DEPTH, reward_fn=lambda s: s.to(torch.get_default_dtype()) + 1, + debug=True, ) states = env.reset(batch_shape=BATCH_SIZE) assert states.tensor.shape == (BATCH_SIZE, 1) @@ -743,7 +749,7 @@ def test_perfect_binary_tree_fwd_step(): def test_perfect_binary_tree_bwd_step(): DEPTH = 3 - env = PerfectBinaryTree(depth=DEPTH, reward_fn=lambda s: s.float() + 1) + env = PerfectBinaryTree(depth=DEPTH, reward_fn=lambda s: s.float() + 1, debug=True) # Start from leaf nodes 8 and 12 initial_tensor = torch.tensor([[8], [12]], dtype=torch.long) @@ -815,6 +821,7 @@ def test_env_default_sf_float_dtypes(dtype: torch.dtype): dummy_action=dummy_action, exit_action=exit_action, sf=None, + debug=True, ) assert env.sf.dtype == dtype assert isinstance(env.sf, torch.Tensor) @@ -835,6 +842,7 @@ def test_env_default_sf_complex_dtypes(dtype: torch.dtype): dummy_action=dummy_action, exit_action=exit_action, sf=None, + debug=True, ) assert env.sf.dtype == dtype assert isinstance(env.sf, torch.Tensor) @@ -860,6 +868,7 @@ def test_env_default_sf_integer_dtypes(dtype: torch.dtype): dummy_action=dummy_action, exit_action=exit_action, sf=None, + debug=True, ) assert env.sf.dtype == dtype assert isinstance(env.sf, torch.Tensor) @@ -879,6 +888,7 @@ def test_env_default_sf_bool_dtype(): dummy_action=dummy_action, exit_action=exit_action, sf=None, + debug=True, ) assert env.sf.dtype == torch.bool assert isinstance(env.sf, torch.Tensor) diff --git a/testing/test_estimators.py b/testing/test_estimators.py index d0e99fc4..ca28c08a 100644 --- a/testing/test_estimators.py +++ b/testing/test_estimators.py @@ -476,7 +476,7 @@ def test_uniform_log_probs_method(): def test_mix_with_uniform_in_log_space(): """Test the _mix_with_uniform_in_log_space static method.""" batch_size, n_actions = 3, 4 - set_seed(123) + set_seed(123, deterministic_mode=True) # Create log-softmax values logits = torch.randn(batch_size, n_actions) diff --git a/testing/test_probability_calculations.py b/testing/test_probability_calculations.py index ce4613ac..430099fd 100644 --- a/testing/test_probability_calculations.py +++ b/testing/test_probability_calculations.py @@ -413,3 +413,25 @@ def test_get_transition_pbs_matches_legacy_with_default_adapter(): transitions, ) torch.testing.assert_close(modern, legacy) + + +def test_trajectory_states_sink_consistency(): + """Test that sink states and dummy actions are consistent in trajectories. + + This test verifies that for valid trajectories, sink states (excluding the last one) + should correspond to dummy actions. This was previously an assertion in + get_trajectory_pfs_and_pbs function. + """ + env, pf_estimator, _, pf_sampler = _build_env_pf_pb() + trajectories = pf_sampler.sample_trajectories( + env, + n=5, + save_estimator_outputs=False, + save_logprobs=False, + ) + + # Test the consistency between sink states and dummy actions + # sink states (excluding the last one) should equal dummy actions + assert trajectories.states.is_sink_state[:-1].equal( + trajectories.actions.is_dummy + ), "Sink states (excluding last) should correspond to dummy actions" diff --git a/testing/test_replay_buffer.py b/testing/test_replay_buffer.py index 054b55ac..fb28fdea 100644 --- a/testing/test_replay_buffer.py +++ b/testing/test_replay_buffer.py @@ -194,7 +194,7 @@ def test_type_error(simple_env): buffer = ReplayBuffer(simple_env) # Try to add an invalid type - with pytest.raises(TypeError, match="Must be a container type"): + with pytest.raises(AssertionError, match="Must be a container type"): buffer.add("not a container") # type: ignore diff --git a/testing/test_states.py b/testing/test_states.py index c68382d6..43d3f5a5 100644 --- a/testing/test_states.py +++ b/testing/test_states.py @@ -691,6 +691,215 @@ def test_discrete_masks_device_consistency_after_mask_ops(simple_discrete_state) assert state.forward_masks.device == state.device +def _make_discrete(batch_shape: tuple[int, ...]) -> DiscreteStates: + class SimpleDiscreteStates(DiscreteStates): + state_shape = (2,) + n_actions = 4 + s0 = torch.zeros(2) + sf = torch.ones(2) + + tensor = torch.zeros(batch_shape + SimpleDiscreteStates.state_shape) + fm = torch.ones(batch_shape + (SimpleDiscreteStates.n_actions,), dtype=torch.bool) + bm = torch.ones( + batch_shape + (SimpleDiscreteStates.n_actions - 1,), dtype=torch.bool + ) + return SimpleDiscreteStates(tensor, fm, bm, debug=True) + + +def test_set_nonexit_action_masks_resets_each_call_1d(): + state = _make_discrete((2,)) + + cond1 = torch.tensor([[False, True, False], [True, False, False]], dtype=torch.bool) + state.set_nonexit_action_masks(cond=cond1, allow_exit=True) + expected1 = torch.tensor( + [[True, False, True, True], [False, True, True, True]], dtype=torch.bool + ) + assert torch.equal(state.forward_masks, expected1) + + # Second call should start from all True because set_nonexit_action_masks resets. + cond2 = torch.zeros_like(cond1, dtype=torch.bool) + state.set_nonexit_action_masks(cond=cond2, allow_exit=True) + expected2 = torch.ones_like(expected1) + assert torch.equal(state.forward_masks, expected2) + + +def test_set_nonexit_action_masks_resets_each_call_2d(): + state = _make_discrete((2, 2)) + + cond1 = torch.tensor( + [ + [[False, True, False], [True, False, False]], + [[False, False, True], [False, True, True]], + ], + dtype=torch.bool, + ) + state.set_nonexit_action_masks(cond=cond1, allow_exit=False) + # When allow_exit is False, exit column is also masked to False. + expected1 = torch.tensor( + [ + [[True, False, True, False], [False, True, True, False]], + [[True, True, False, False], [True, False, False, False]], + ], + dtype=torch.bool, + ) + assert torch.equal(state.forward_masks, expected1) + + # Second call should reset masks before applying the new condition. + cond2 = torch.tensor( + [ + [[True, False, True], [False, False, False]], + [[False, True, False], [True, True, False]], + ], + dtype=torch.bool, + ) + state.set_nonexit_action_masks(cond=cond2, allow_exit=True) + expected2 = torch.tensor( + [ + [[False, True, False, True], [True, True, True, True]], + [[True, False, True, True], [False, False, True, True]], + ], + dtype=torch.bool, + ) + assert torch.equal(state.forward_masks, expected2) + + +def test_set_exit_masks_exit_only_1d(): + state = _make_discrete((3,)) + state.init_forward_masks(set_ones=True) + + batch_idx = torch.tensor([True, False, True], dtype=torch.bool) + state.set_exit_masks(batch_idx) + + expected = torch.tensor( + [ + [False, False, False, True], + [True, True, True, True], + [False, False, False, True], + ], + dtype=torch.bool, + ) + assert torch.equal(state.forward_masks, expected) + + # Running again with a different mask after re-init should not leak previous masks. + state.init_forward_masks(set_ones=True) + batch_idx2 = torch.tensor([False, True, False], dtype=torch.bool) + state.set_exit_masks(batch_idx2) + expected2 = torch.tensor( + [ + [True, True, True, True], + [False, False, False, True], + [True, True, True, True], + ], + dtype=torch.bool, + ) + assert torch.equal(state.forward_masks, expected2) + + +def test_set_exit_masks_exit_only_2d(): + state = _make_discrete((2, 2)) + state.init_forward_masks(set_ones=True) + + batch_idx = torch.tensor([[True, False], [False, True]], dtype=torch.bool) + state.set_exit_masks(batch_idx) + + expected = torch.tensor( + [ + [[False, False, False, True], [True, True, True, True]], + [[True, True, True, True], [False, False, False, True]], + ], + dtype=torch.bool, + ) + assert torch.equal(state.forward_masks, expected) + + # Re-init and apply a different mask to ensure previous updates don't leak. + state.init_forward_masks(set_ones=True) + batch_idx2 = torch.tensor([[False, False], [True, False]], dtype=torch.bool) + state.set_exit_masks(batch_idx2) + expected2 = torch.tensor( + [ + [[True, True, True, True], [True, True, True, True]], + [[False, False, False, True], [True, True, True, True]], + ], + dtype=torch.bool, + ) + assert torch.equal(state.forward_masks, expected2) + + +def test_states_factory_requires_debug(): + class NoDebugStates(States): + state_shape = (1,) + s0 = torch.tensor([0.0]) + sf = torch.tensor([1.0]) + + @classmethod + def make_random_states(cls, batch_shape, device=None): + return cls(torch.zeros(batch_shape + cls.state_shape, device=device)) + + with pytest.raises(TypeError, match="must accept a `debug`"): + NoDebugStates.from_batch_shape((2,), random=True, debug=True) + + +def test_discrete_states_factory_requires_debug(): + class NoDebugDiscreteStates(DiscreteStates): + state_shape = (1,) + s0 = torch.tensor([0.0]) + sf = torch.tensor([1.0]) + n_actions = 2 + + @classmethod + def make_random_states(cls, batch_shape, device=None): + t = torch.zeros(batch_shape + cls.state_shape, device=device) + fm = torch.ones( + batch_shape + (cls.n_actions,), dtype=torch.bool, device=device + ) + bm = torch.ones( + batch_shape + (cls.n_actions - 1,), dtype=torch.bool, device=device + ) + return cls(t, fm, bm) + + with pytest.raises(TypeError, match="must accept a `debug`"): + NoDebugDiscreteStates.from_batch_shape((2,), random=True, debug=True) + + +def test_graph_states_factory_requires_debug(): + class NoDebugGraphStates(GraphStates): + num_node_classes = 1 + num_edge_classes = 1 + is_directed = True + max_nodes = 2 + + s0 = GeometricData( + x=torch.zeros((1, 1)), + edge_index=torch.zeros((2, 0), dtype=torch.long), + edge_attr=torch.zeros((0, 1)), + ) + sf = s0.clone() + + @classmethod + def make_random_states(cls, batch_shape, device=None): + batch_shape = ( + batch_shape if isinstance(batch_shape, tuple) else (batch_shape,) + ) + data_array = np.empty(batch_shape, dtype=object) + for i in range(np.prod(batch_shape)): + data_array.flat[i] = cls.s0.clone() + + if device is not None: + dev = device + else: + dev = cls.s0.x.device # pyright: ignore[reportOptionalMemberAccess] + + return cls( + data_array, + categorical_node_features=True, + categorical_edge_features=True, + device=dev, + ) + + with pytest.raises(TypeError, match="must accept a `debug`"): + NoDebugGraphStates.from_batch_shape((2,), random=True, debug=True) + + def normalize_device(device): """Normalize device to use index form (cuda:0 instead of cuda)""" device = torch.device(device) diff --git a/tutorials/README.md b/tutorials/README.md new file mode 100644 index 00000000..18932e45 --- /dev/null +++ b/tutorials/README.md @@ -0,0 +1,7 @@ +tutorials +--------- + ++ `examples/` contains self-contained training scripts for various gflownet use-cases. ++ `misc/` contains example utilities that might be useful for things such as performance benchmarking. ++ `notebooks/` contains didactic, self-contained training materials on both gflownet and `torchgfn` basics. + diff --git a/tutorials/examples/multinode/install_multinode_dependencies b/tutorials/examples/multinode/install_multinode_dependencies index 5c869af7..3a3cce73 100644 --- a/tutorials/examples/multinode/install_multinode_dependencies +++ b/tutorials/examples/multinode/install_multinode_dependencies @@ -1,7 +1,7 @@ #!/bin/bash # Remove old version of torchgfn -pip uninstall torch +#pip uninstall torch # Install the relevant OneCCL, mpiexec.hydra, Torch, and Compilers. conda config --set channel_priority strict @@ -11,9 +11,12 @@ conda config --append channels 'https://software.repos.intel.com/python/conda/' conda config --append channels pytorch conda config --append channels conda-forge conda config --append channels defaults -conda install gcc_linux-64 gxx_linux-64 gfortran_linux-64 \ - impi_rt impi-devel intel-opencl-rt intel-openmp mpi4py \ - 'oneccl_bind_pt=*=py310_cpu_*' oneccl-devel \ - 'intel-extension-for-pytorch=*=py310_cpu_*' setuptools \ - 'pytorch==2.2' cpuonly \ - mkl -y + +conda install gcc_linux-64 gxx_linux-64 gfortran_linux-64 impi_rt impi-devel intel-opencl-rt intel-openmp mpi4py oneccl_bind_pt oneccl intel-extension-for-pytorch + +#conda install gcc_linux-64 gxx_linux-64 gfortran_linux-64 \ +# impi_rt impi-devel intel-opencl-rt intel-openmp mpi4py \ +# 'oneccl_bind_pt=*=py310_cpu_*' oneccl-devel \ +# 'intel-extension-for-pytorch=*=py310_cpu_*' setuptools \ +# 'pytorch==2.2' cpuonly \ +# mkl -y diff --git a/tutorials/examples/multinode/spawn_policy.py b/tutorials/examples/multinode/spawn_policy.py index 277ecf5a..c7ce36e6 100644 --- a/tutorials/examples/multinode/spawn_policy.py +++ b/tutorials/examples/multinode/spawn_policy.py @@ -82,12 +82,16 @@ def __init__( averaging_strategy: str = "mean", momentum: float = 0.0, poll_interval_s: float = 0.01, + threshold: Optional[float] = None, + cooldown: int = 200, ) -> None: super().__init__(average_every) self.replacement_ratio = float(replacement_ratio) self.averaging_strategy = str(averaging_strategy) self.momentum = float(momentum) self.poll_interval_s = float(poll_interval_s) + self.threshold: Optional[float] = threshold + self.cooldown: int = int(cooldown) self._model_builder = model_builder self._initialized = False @@ -96,6 +100,7 @@ def __init__( self._bg_thread: Optional[threading.Thread] = None self._pending_lock = threading.Lock() self._last_iter_sent: int = -1 + self._last_trigger_iter: int = -self.cooldown # When rebuilding a fresh model + optimizer is desired, we store the # averaged parameters and construct the new model at the next safe call. @@ -117,7 +122,11 @@ def _ensure_initialized(self, model: GFlowNet) -> None: self._initialized = True return self._validate_params( - self.replacement_ratio, self.averaging_strategy, self.momentum + self.replacement_ratio, + self.averaging_strategy, + self.momentum, + self.threshold, + self.cooldown, ) self._model = model self._initialized = True @@ -217,15 +226,31 @@ def _rank0_record_metric( all_metrics = torch.zeros(world_size, dtype=torch.float32) for r, m in bucket.items(): all_metrics[r] = m - ranks_to_replace, ranks_to_average = self._determine_ranks_for_averaging( - all_metrics, world_size, self.replacement_ratio, self.averaging_strategy - ) - weights = self._compute_averaging_weights( - all_metrics, ranks_to_average, self.averaging_strategy - ) - self._rank0_dispatch_controls( - iteration, ranks_to_replace, ranks_to_average, weights - ) + # Gate dispatch using threshold and cooldown if configured + should_dispatch = True + if self.threshold is not None: + # Cooldown window + if iteration - self._last_trigger_iter < self.cooldown: + should_dispatch = False + else: + # Trigger only if any metric falls below threshold + if torch.min(all_metrics).item() >= float(self.threshold): + should_dispatch = False + + if should_dispatch: + ranks_to_replace, ranks_to_average = self._determine_ranks_for_averaging( + all_metrics, + world_size, + self.replacement_ratio, + self.averaging_strategy, + ) + weights = self._compute_averaging_weights( + all_metrics, ranks_to_average, self.averaging_strategy + ) + self._rank0_dispatch_controls( + iteration, ranks_to_replace, ranks_to_average, weights + ) + self._last_trigger_iter = int(iteration) del self._rank0_buckets[iteration] def _rank0_dispatch_controls( @@ -413,7 +438,11 @@ def _background_loop(self) -> None: # ---------------- Local helpers (copied from selective policy to avoid lints) ---------------- @staticmethod def _validate_params( - replacement_ratio: float, averaging_strategy: str, momentum: float + replacement_ratio: float, + averaging_strategy: str, + momentum: float, + threshold: Optional[float], + cooldown: int, ) -> None: if not 0.0 <= replacement_ratio <= 1.0: raise ValueError( @@ -428,6 +457,8 @@ def _validate_params( raise ValueError(f"Unknown averaging_strategy: {averaging_strategy}") if not 0.0 <= momentum <= 1.0: raise ValueError(f"momentum must be between 0 and 1, got {momentum}") + if cooldown < 0: + raise ValueError(f"cooldown must be non-negative, got {cooldown}") @staticmethod def _determine_ranks_for_averaging( diff --git a/tutorials/examples/test_scripts.py b/tutorials/examples/test_scripts.py index ecfc9031..1b1b71ee 100644 --- a/tutorials/examples/test_scripts.py +++ b/tutorials/examples/test_scripts.py @@ -19,10 +19,16 @@ if str(_repo_root) not in sys.path: sys.path.insert(0, str(_repo_root)) +# Ensure we run with Python debug mode enabled (no -O) so envs use debug guards. +assert __debug__, "Tests must run without -O so __debug__ stays True." + from tutorials.examples.train_bayesian_structure import ( main as train_bayesian_structure_main, ) from tutorials.examples.train_bit_sequences import main as train_bitsequence_main +from tutorials.examples.train_bitsequence_recurrent import ( + main as train_bitsequence_recurrent_main, +) from tutorials.examples.train_box import main as train_box_main from tutorials.examples.train_conditional import main as train_conditional_main from tutorials.examples.train_diffusion_sampler import ( @@ -43,6 +49,9 @@ from tutorials.examples.train_hypergrid_simple import main as train_hypergrid_simple_main from tutorials.examples.train_ising import main as train_ising_main from tutorials.examples.train_line import main as train_line_main +from tutorials.examples.train_with_example_modes import ( + main as train_with_example_modes_main, +) @dataclass @@ -201,10 +210,15 @@ class BoxArgs(CommonArgs): @dataclass class BitSequenceArgs(CommonArgs): - n_iterations: int = 1000 + n_iterations: int = 5000 word_size: int = 1 seq_size: int = 4 n_modes: int = 2 + temperature: float = 1.0 + lr: 1e-4 + lr_Z: 1e-2 + seed: int = 0 + batch_size: int = 32 @dataclass @@ -639,20 +653,18 @@ def test_diffusion_sampler_smoke(): @pytest.mark.parametrize("seq_size", [4, 8]) @pytest.mark.parametrize("n_modes", [2, 4]) def test_bitsequence(seq_size: int, n_modes: int): - n_iterations = 1000 - args = BitSequenceArgs( - seq_size=seq_size, n_modes=n_modes, n_iterations=n_iterations, seed=0 - ) + args = BitSequenceArgs(seq_size=seq_size, n_modes=n_modes) final_l1_dist = train_bitsequence_main(args) assert final_l1_dist is not None + # print(f"[DEBUG] BitSequence seq_size={seq_size}, n_modes={n_modes}, l1={final_l1_dist}") if seq_size == 4 and n_modes == 2: - assert final_l1_dist <= 1e-4 + assert final_l1_dist <= 9e-5 if seq_size == 4 and n_modes == 4: - assert final_l1_dist <= 1e-4 + assert final_l1_dist <= 1e-5 if seq_size == 8 and n_modes == 2: assert final_l1_dist <= 1e-3 if seq_size == 8 and n_modes == 4: - assert final_l1_dist <= 1e-3 + assert final_l1_dist <= 2e-4 @pytest.mark.parametrize("gflownet", ["tb", "db", "subtb", "fm"]) @@ -789,5 +801,55 @@ def test_hypergrid_exploration_smoke(): train_hypergrid_exploration_main(namespace_args) # Runs without errors. +def test_bitsequence_recurrent_smoke(): + """Smoke test for the recurrent BitSequence training script.""" + args = BitSequenceArgs( + n_iterations=50, + word_size=1, + seq_size=4, + n_modes=2, + seed=0, + batch_size=4, + ) + args_dict = asdict(args) + # Added (not needed for non-recurrent script). + args_dict.update( + embedding_dim=4, + hidden_size=8, + num_layers=2, + rnn_type="gru", + dropout=0.1, + lr_logz=1e-1, + lr=1e-3, + epsilon=0.1, + ) + + train_bitsequence_recurrent_main(Namespace(**args_dict)) + + +def test_with_example_modes_smoke(): + """Smoke test for example modes graph-building script.""" + # Keep small for speed. + args = Namespace( + n_nodes=4, + max_rings=20, + device="cpu", + seed=0, + embedding_dim=16, + num_conv_layers=1, + lr=1e-3, + lr_Z=1e-3, + use_lr_scheduler=False, + n_iterations=2, + replay_buffer_max_size=50, + use_expert_data=False, + action_type_epsilon=0.0, + edge_index_epsilon=0.0, + batch_size=2, + plot=False, + ) + train_with_example_modes_main(args) # Runs without errors. + + if __name__ == "__main__": test_conditional_basic("tb") diff --git a/tutorials/examples/train_bayesian_structure.py b/tutorials/examples/train_bayesian_structure.py index a71ea3ce..1a617006 100644 --- a/tutorials/examples/train_bayesian_structure.py +++ b/tutorials/examples/train_bayesian_structure.py @@ -433,6 +433,7 @@ def main(args: Namespace): n_nodes=args.num_nodes, state_evaluator=scorer.state_evaluator, device=device, + debug=__debug__, ) if args.module == "mlp": diff --git a/tutorials/examples/train_bit_sequences.py b/tutorials/examples/train_bit_sequences.py index beb76bc6..12ad27eb 100644 --- a/tutorials/examples/train_bit_sequences.py +++ b/tutorials/examples/train_bit_sequences.py @@ -33,7 +33,7 @@ def main(args): H = torch.randint( 0, 2, (args.n_modes, args.seq_size), dtype=torch.long, device=device ) - env = BitSequence(args.word_size, args.seq_size, args.n_modes, H=H) + env = BitSequence(args.word_size, args.seq_size, args.n_modes, H=H, debug=__debug__) if args.loss == "TB": pf = MLP(env.words_per_seq, env.n_actions) diff --git a/tutorials/examples/train_bitsequence_recurrent.py b/tutorials/examples/train_bitsequence_recurrent.py index e766160c..243cb8a8 100644 --- a/tutorials/examples/train_bitsequence_recurrent.py +++ b/tutorials/examples/train_bitsequence_recurrent.py @@ -32,7 +32,6 @@ def estimated_dist(gflownet: PFBasedGFlowNet, env: BitSequence): pf=gflownet.pf, trajectories=trajectories, recalculate_all_logprobs=True, - adapter=gflownet.pf_adapter, ) pf = torch.exp(log_pf_trajectories.sum(dim=0)) @@ -59,7 +58,7 @@ def main(args): H=H, device_str=str(device), seed=args.seed, - check_action_validity=__debug__, + debug=__debug__, ) # Model + Estimator @@ -79,8 +78,7 @@ def main(args): is_backward=False, ).to(device) - # GFlowNet (Trajectory Balance), tree DAG -> pb=None, constant_pb=True, - # Use a recurrent adapter for the PF. + # GFlowNet (Trajectory Balance), tree DAG -> pb=None, constant_pb=True. gflownet = TBGFlowNet( pf=pf_estimator, pb=None, diff --git a/tutorials/examples/train_box.py b/tutorials/examples/train_box.py index 8f15a28f..773345ef 100644 --- a/tutorials/examples/train_box.py +++ b/tutorials/examples/train_box.py @@ -105,7 +105,7 @@ def main(args: Namespace) -> float: # noqa: C901 n_iterations = args.n_trajectories // args.batch_size # 1. Create the environment - env = Box(delta=args.delta, epsilon=1e-10, device=device) + env = Box(delta=args.delta, epsilon=1e-10, device=device, debug=__debug__) preprocessor = IdentityPreprocessor(output_dim=env.state_shape[-1]) # 2. Create the gflownet. diff --git a/tutorials/examples/train_conditional.py b/tutorials/examples/train_conditional.py index b2f1b3a5..716e923e 100644 --- a/tutorials/examples/train_conditional.py +++ b/tutorials/examples/train_conditional.py @@ -641,6 +641,7 @@ def main(args): device=device, calculate_partition=True, # Need this for validation store_all_states=True, # Need this for validation + debug=__debug__, ) seed = int(args.seed) if args.seed is not None else DEFAULT_SEED diff --git a/tutorials/examples/train_diffusion_sampler.py b/tutorials/examples/train_diffusion_sampler.py index 69ccdf5e..b7e4553e 100644 --- a/tutorials/examples/train_diffusion_sampler.py +++ b/tutorials/examples/train_diffusion_sampler.py @@ -133,7 +133,7 @@ def main(args): target_kwargs=target_kwargs, num_discretization_steps=args.num_steps, device=device, - check_action_validity=False, + debug=__debug__, ) # Build forward/backward modules and estimators diff --git a/tutorials/examples/train_discreteebm.py b/tutorials/examples/train_discreteebm.py index cc44f2da..b79b828d 100644 --- a/tutorials/examples/train_discreteebm.py +++ b/tutorials/examples/train_discreteebm.py @@ -43,7 +43,7 @@ def main(args): # noqa: C901 wandb.config.update(args) # 1. Create the environment. - env = DiscreteEBM(ndim=args.ndim, alpha=args.alpha, device=device) + env = DiscreteEBM(ndim=args.ndim, alpha=args.alpha, device=device, debug=__debug__) # 2. Create the gflownet. # We need a LogEdgeFlowEstimator diff --git a/tutorials/examples/train_graph_ring.py b/tutorials/examples/train_graph_ring.py index 3bde1d82..9df9b61d 100644 --- a/tutorials/examples/train_graph_ring.py +++ b/tutorials/examples/train_graph_ring.py @@ -328,6 +328,7 @@ def init_env(n_nodes: int, directed: bool, device: torch.device) -> GraphBuildin state_evaluator=state_evaluator, directed=directed, device=device, + debug=__debug__, ) diff --git a/tutorials/examples/train_graph_triangle.py b/tutorials/examples/train_graph_triangle.py index 2f40402d..73568d89 100644 --- a/tutorials/examples/train_graph_triangle.py +++ b/tutorials/examples/train_graph_triangle.py @@ -71,6 +71,7 @@ def init_env(device: torch.device) -> GraphBuilding: is_directed=False, device=device, max_nodes=3, + debug=__debug__, ) return env diff --git a/tutorials/examples/train_hypergrid.py b/tutorials/examples/train_hypergrid.py index e251fc70..0313eeb8 100644 --- a/tutorials/examples/train_hypergrid.py +++ b/tutorials/examples/train_hypergrid.py @@ -31,7 +31,7 @@ import time from argparse import ArgumentParser from math import ceil -from typing import Tuple, cast +from typing import Optional, Tuple, cast import matplotlib.pyplot as plt import torch @@ -41,7 +41,7 @@ from tqdm import trange from gfn.containers import NormBasedDiversePrioritizedReplayBuffer, ReplayBuffer -from gfn.containers.replay_buffer_manager import ReplayBufferManager +from gfn.containers.replay_buffer_manager import ContainerUnion, ReplayBufferManager from gfn.estimators import DiscretePolicyEstimator, Estimator, ScalarEstimator from gfn.gflownet import ( DBGFlowNet, @@ -73,6 +73,14 @@ def __init__( diverse_replay_buffer: bool = False, capacity: int = 10000, remote_manager_rank: int | None = None, + # Scoring config + w_retained: float = 1.0, + w_novelty: float = 0.1, + w_reward: float = 0.0, + w_mode_bonus: float = 10.0, + p_norm_novelty: float = 2.0, + cdist_max_bytes: int = 268435456, + ema_decay: float = 0.98, ): super().__init__( env, @@ -85,12 +93,145 @@ def __init__( ) self.discovered_modes = set() self.env = env + self._ema_decay: float = float(ema_decay) + self._score_ema: Optional[float] = None + # Scoring configuration parameters. + self.w_retained = w_retained + self.w_novelty = w_novelty + self.w_reward = w_reward + self.w_mode_bonus = w_mode_bonus + self.p_norm_novelty = p_norm_novelty + self.cdist_max_bytes = cdist_max_bytes + + def scoring_function(self, obj: ContainerUnion) -> dict[str, float]: + + # print("Score - Computing score for object:", obj) + # print("Score - Terminating states:", obj.terminating_states) + # print("Score - Log rewards:", obj.log_rewards) + + # A) Retention (usefulness) + if not self.replay_buffer.prioritized_capacity: + retained_count = 0 + + # If the buffer is empty, retain all the new objects. + if self.replay_buffer.training_container is None: + retained_count = len(obj) + + # If the buffer isn't full yet, we retain all the new objects. + elif ( + len(self.replay_buffer.training_container) + len(obj) + <= self.replay_buffer.capacity + ): + retained_count = len(obj) + + # If the buffer is full, we keep the high reward items only. + elif self.replay_buffer.prioritized_capacity: + assert self.replay_buffer.training_container.log_rewards is not None + assert obj.log_rewards is not None + + # The old log_rewards are already sorted in ascending order. + old_log_rewards = self.replay_buffer.training_container.log_rewards + + threshold = old_log_rewards.min() + new_log_rewards = obj.log_rewards + retained_new_log_rewards = new_log_rewards[new_log_rewards >= threshold] + retained_count = len(retained_new_log_rewards) + + print("Score - Retained count:", retained_count) + + # B) Novelty (sum of min-distances vs pre-add buffer). Higher min-distances are better. + if ( + self.replay_buffer.training_container is None + or len(self.replay_buffer.training_container) == 0 + ): + novelty_sum = float(len(obj)) # Placeholder value when the buffer is empty. + + else: + # Compute the batch x buffer distances of the terminating states. + batch = obj.terminating_states.tensor.to(torch.get_default_dtype()) + buf = self.replay_buffer.training_container.terminating_states.tensor.to( + torch.get_default_dtype() + ) + + m_ = batch.shape[0] + n_ = buf.shape[0] + + batch = batch.view(m_, -1) + buf = buf.view(n_, -1) + + # Compute the chunk size based on the max bytes per chunk. + bytes_per = 8 if batch.dtype == torch.float64 else 4 + chunk = max( + 1, + int(self.cdist_max_bytes // max(1, (m_ * bytes_per))), + ) + min_dist = torch.full( + (m_,), + torch.finfo(batch.dtype).max, + dtype=batch.dtype, + device=batch.device, + ) + for start in range(0, n_, chunk): + end = min(start + chunk, n_) + + # Loop over chunks of the buffer to compute batch x buffer distances. + distances = torch.cdist( + batch, + buf[start:end], + p=self.p_norm_novelty, + ) + min_dist = torch.minimum(min_dist, distances.min(dim=1).values) - def scoring_function(self, obj) -> float: + # Sum the minimum batch x buffer distances for each batch element. + novelty_sum = float(min_dist.sum().item()) + print("Score - Min distances:", min_dist) + + print("Score - Novelty sum:", novelty_sum) + + # C) High reward term (sum over batch) + assert ( + obj.log_rewards is not None + ), "log_rewards is None in submitted trajectories!" + reward_sum = float(obj.log_rewards.exp().sum().item()) + print("Score - Reward sum:", reward_sum) + + # D) Mode bonus + print("Score - Modes discovered before update:", self.discovered_modes) + + n_new_modes = 0.0 + assert isinstance(obj.terminating_states, DiscreteStates) modes_found = self.env.modes_found(obj.terminating_states) - score = len(modes_found - self.discovered_modes) - self.discovered_modes.update(modes_found) - return float(score) + if isinstance(modes_found, set): + new_modes = modes_found - self.discovered_modes + if new_modes: + n_new_modes = float(len(new_modes)) + self.discovered_modes.update(new_modes) + + print("Score - New modes found:", n_new_modes) + print("Score - Modes discovered after update:", self.discovered_modes) + + # Compute the final score. + final_score = self.w_retained * float(retained_count) + final_score += self.w_novelty * novelty_sum + final_score += self.w_reward * reward_sum + final_score += self.w_mode_bonus * n_new_modes + print("Score - Final score:", final_score) + # Update and return EMA of the score + if self._score_ema is None: + self._score_ema = final_score + else: + self._score_ema = self._ema_decay * self._score_ema + ( + 1.0 - self._ema_decay + ) * float(final_score) + print("Score - EMA score:", self._score_ema) + return { + "score": float(self._score_ema), + "score_before_ema": final_score, + "retained_count": retained_count, + "novelty_sum": novelty_sum, + "reward_sum": reward_sum, + "n_new_modes": n_new_modes, + } def _compute_metadata(self) -> dict: return {"n_modes_found": len(self.discovered_modes)} @@ -530,6 +671,7 @@ def main(args) -> dict: # noqa: C901 }, calculate_partition=args.calculate_partition, store_all_states=args.store_all_states, + debug=__debug__, ) if args.distributed and distributed_context.is_buffer_rank(): @@ -544,7 +686,7 @@ def main(args) -> dict: # noqa: C901 num_training_ranks=num_training_ranks, diverse_replay_buffer=args.diverse_replay_buffer, capacity=args.global_replay_buffer_size, - ) + ) # TODO: If the remote_manager_rank is set, does this produce an infinite loop? replay_buffer_manager.run() return {} @@ -589,7 +731,9 @@ def main(args) -> dict: # noqa: C901 else: group_name = wandb.util.generate_id() - wandb.init(project=args.wandb_project, group=group_name) + wandb.init( + project=args.wandb_project, group=group_name, entity=args.wandb_entity + ) wandb.config.update(args) # Initialize the preprocessor. @@ -622,6 +766,8 @@ def _model_builder() -> Tuple[GFlowNet, torch.optim.Optimizer]: capacity=args.replay_buffer_size, cutoff_distance=args.cutoff_distance, p_norm_distance=args.p_norm_distance, + remote_manager_rank=distributed_context.assigned_buffer, + remote_buffer_freq=1, ) else: replay_buffer = ReplayBuffer( @@ -689,6 +835,8 @@ def cleanup(): replacement_ratio=args.replacement_ratio, averaging_strategy=args.averaging_strategy, momentum=args.momentum, + threshold=args.performance_tracker_threshold, + cooldown=args.performance_tracker_cooldown, ) else: averaging_policy = AverageAllPolicy(average_every=args.average_every) @@ -736,10 +884,10 @@ def cleanup(): ) as to_train_samples_timer: training_samples = gflownet.to_training_samples(trajectories) - score = None + score_dict = None if replay_buffer is not None: with torch.no_grad(): - score = replay_buffer.add(training_samples) + score_dict = replay_buffer.add(training_samples) training_objects = replay_buffer.sample( n_samples=per_node_batch_size ) @@ -796,11 +944,12 @@ def cleanup(): timing, "averaging_model", enabled=args.timing ) as model_averaging_timer: if averaging_policy is not None: + assert score_dict is not None gflownet, optimizer, averaging_info = averaging_policy( iteration=iteration, model=gflownet, optimizer=optimizer, - local_metric=score if score is not None else -loss.item(), + local_metric=score_dict["score"], group=distributed_context.train_global_group, ) @@ -846,10 +995,11 @@ def cleanup(): "opt_time": opt_timer.elapsed, "model_averaging_time": model_averaging_timer.elapsed, "rest_time": rest_time, - "score": score, "l1_dist": None, # only logged if calculate_partition. } to_log.update(averaging_info) + if score_dict is not None: + to_log.update(score_dict) if log_this_iter: validation_info, all_visited_terminating_states = env.validate( @@ -867,7 +1017,9 @@ def cleanup(): metadata = ReplayBufferManager.get_metadata(manager_rank) to_log.update(metadata) else: - modes_found.update(env.modes_found(visited_terminating_states)) + modes_found.update( + env.modes_found(all_visited_terminating_states) + ) n_modes_found = len(modes_found) to_log["n_modes_found"] = n_modes_found @@ -979,7 +1131,7 @@ def cleanup(): parser.add_argument( "--global_replay_buffer_size", type=int, - default=10000, + default=8192, help="Global replay buffer size (only if using distributed computation)", ) parser.add_argument( @@ -1058,7 +1210,7 @@ def cleanup(): parser.add_argument( "--replay_buffer_size", type=int, - default=1000, + default=2048, help="If zero, no replay buffer is used. Otherwise, the replay buffer is used.", ) parser.add_argument( @@ -1170,6 +1322,12 @@ def cleanup(): default="torchgfn", help="Name of the wandb project. If empty, don't use wandb", ) + parser.add_argument( + "--wandb_entity", + type=str, + default="torchgfn", + help="Name of the wandb entity. If empty, don't use wandb", + ) parser.add_argument( "--wandb_local", action="store_true", @@ -1234,5 +1392,31 @@ def cleanup(): help="Use restarts.", ) + # Performance tracker settings. + parser.add_argument( + "--performance_tracker_decay", + type=float, + default=0.98, + help="Decay factor for the performance tracker.", + ) + parser.add_argument( + "--performance_tracker_warmup", + type=int, + default=100, + help="Warmup period for the performance tracker.", + ) + parser.add_argument( + "--performance_tracker_threshold", + type=float, + default=100, + help="Threshold for the performance tracker. If None, the performance tracker is not triggered.", + ) + parser.add_argument( + "--performance_tracker_cooldown", + type=int, + default=200, + help="Cooldown period for the performance tracker.", + ) + args = parser.parse_args() main(args) diff --git a/tutorials/examples/train_hypergrid_buffer.py b/tutorials/examples/train_hypergrid_buffer.py index 268ea82f..5a0b41e5 100644 --- a/tutorials/examples/train_hypergrid_buffer.py +++ b/tutorials/examples/train_hypergrid_buffer.py @@ -40,6 +40,7 @@ def main(args): device=device, calculate_partition=True, store_all_states=True, + debug=__debug__, ) preprocessor = KHotPreprocessor(height=env.height, ndim=env.ndim) diff --git a/tutorials/examples/train_hypergrid_exploration_examples.py b/tutorials/examples/train_hypergrid_exploration_examples.py index 67da4d5d..dec91012 100644 --- a/tutorials/examples/train_hypergrid_exploration_examples.py +++ b/tutorials/examples/train_hypergrid_exploration_examples.py @@ -311,7 +311,7 @@ def main(args): device=device, calculate_partition=True, store_all_states=True, - check_action_validity=__debug__, + debug=__debug__, ) preprocessor = KHotPreprocessor(height=env.height, ndim=env.ndim) _print_mode_stats(env) diff --git a/tutorials/examples/train_hypergrid_gafn.py b/tutorials/examples/train_hypergrid_gafn.py index 4df290d1..ccd5d2be 100644 --- a/tutorials/examples/train_hypergrid_gafn.py +++ b/tutorials/examples/train_hypergrid_gafn.py @@ -261,6 +261,7 @@ def main(args): device=device, calculate_partition=True, store_all_states=True, + debug=__debug__, ) preprocessor = KHotPreprocessor(height=env.height, ndim=env.ndim) diff --git a/tutorials/examples/train_hypergrid_local_search.py b/tutorials/examples/train_hypergrid_local_search.py index fdef6071..e0fba6a9 100644 --- a/tutorials/examples/train_hypergrid_local_search.py +++ b/tutorials/examples/train_hypergrid_local_search.py @@ -46,6 +46,7 @@ def main(args): device=device, calculate_partition=True, store_all_states=True, + debug=__debug__, ) preprocessor = KHotPreprocessor(height=env.height, ndim=env.ndim) diff --git a/tutorials/examples/train_hypergrid_simple.py b/tutorials/examples/train_hypergrid_simple.py index 10cd7a08..d2fb3ab0 100644 --- a/tutorials/examples/train_hypergrid_simple.py +++ b/tutorials/examples/train_hypergrid_simple.py @@ -50,7 +50,7 @@ def main(args): device=device, calculate_partition=True, store_all_states=True, - check_action_validity=__debug__, + debug=__debug__, ) preprocessor = KHotPreprocessor(height=env.height, ndim=env.ndim) diff --git a/tutorials/examples/train_ising.py b/tutorials/examples/train_ising.py index 4ec306e8..1595d778 100644 --- a/tutorials/examples/train_ising.py +++ b/tutorials/examples/train_ising.py @@ -60,7 +60,7 @@ def ising_n_to_ij(L, n): N = args.L**2 J = make_J(args.L, args.J) ising_energy = IsingModel(J) - env = DiscreteEBM(N, alpha=1, energy=ising_energy, device=device) + env = DiscreteEBM(N, alpha=1, energy=ising_energy, device=device, debug=__debug__) # Parametrization and losses pf_module = MLP( diff --git a/tutorials/examples/train_line.py b/tutorials/examples/train_line.py index 4a5d492d..60b273d2 100644 --- a/tutorials/examples/train_line.py +++ b/tutorials/examples/train_line.py @@ -290,6 +290,7 @@ def main(args): n_sd=4.5, n_steps_per_trajectory=5, device=device, + debug=__debug__, ) # Hyperparameters. diff --git a/tutorials/examples/train_with_compile.py b/tutorials/examples/train_with_compile.py new file mode 100644 index 00000000..c6a0f245 --- /dev/null +++ b/tutorials/examples/train_with_compile.py @@ -0,0 +1,1388 @@ +#!/usr/bin/env python +r""" +Benchmark the runtime impact of different `torch.compile` strategies on several +GFlowNet losses (Trajectory Balance, Detailed Balance, SubTB) and environments. + +Four compile modes are compared for each (env, loss) pair: + +0) Pure eager execution +1) Compile loss only +2) Compile estimator modules only (`try_compile_gflownet`) +3) Compile both the loss wrapper and estimator modules + +The script reuses the components defined in the example training scripts: +`train_hypergrid.py`, `train_line.py`, `train_bitsequence_recurrent.py`, +`train_bit_sequences.py`, `train_graph_ring.py`, and `train_diffusion_sampler.py`. +""" + +from __future__ import annotations + +import argparse +import statistics +import sys +import time +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Callable, Dict, Iterable, Literal + +import torch +from tqdm import tqdm + +try: # pragma: no cover - older PyTorch versions may lack torch._dynamo + import torch._dynamo as _torch_dynamo + + _torch_dynamo.config.capture_scalar_outputs = True +except Exception: # pragma: no cover + _torch_dynamo = None + +from gfn.actions import GraphActions +from gfn.env import Env +from gfn.estimators import ( + DiscreteGraphPolicyEstimator, + DiscretePolicyEstimator, + PinnedBrownianMotionBackward, + PinnedBrownianMotionForward, + RecurrentDiscretePolicyEstimator, + ScalarEstimator, +) +from gfn.gflownet import ModifiedDBGFlowNet, PFBasedGFlowNet, SubTBGFlowNet +from gfn.gflownet.trajectory_balance import TBGFlowNet +from gfn.gym import HyperGrid +from gfn.gym.bitSequence import BitSequence +from gfn.gym.diffusion_sampling import DiffusionSampling +from gfn.gym.graph_building import GraphBuildingOnEdges +from gfn.gym.line import Line +from gfn.preprocessors import IdentityPreprocessor, KHotPreprocessor +from gfn.samplers import Sampler +from gfn.utils.common import set_seed +from gfn.utils.compile import try_compile_gflownet +from gfn.utils.modules import ( + MLP, + DiffusionFixedBackwardModule, + DiffusionPISGradNetForward, + GraphActionGNN, + GraphEdgeActionMLP, + GraphScalarMLP, + RecurrentDiscreteSequenceModel, +) + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +from tutorials.examples.train_graph_ring import RingReward # noqa: E402 +from tutorials.examples.train_line import GaussianStepMLP, StepEstimator # noqa: E402 + +# --------------------------------------------------------------------------- +# Dataclasses and shared configuration +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class FlowVariant: + key: Literal["tb", "modified_dbg", "subtb"] + label: str + description: str + requires_logf: bool + + +FLOW_VARIANTS: dict[str, FlowVariant] = { + "tb": FlowVariant( + key="tb", + label="Trajectory Balance", + description="Standard TB loss with learnable log Z.", + requires_logf=False, + ), + "modified_dbg": FlowVariant( + key="modified_dbg", + label="Modified Detailed Balance", + description="Modified detailed balance variant without learned log-state flows.", + requires_logf=False, + ), + "subtb": FlowVariant( + key="subtb", + label="Sub-trajectory Balance", + description="SubTB with configurable weighting scheme.", + requires_logf=True, + ), +} +DEFAULT_FLOW_ORDER = ["tb", "modified_dbg", "subtb"] + + +@dataclass(frozen=True) +class CompileMode: + key: Literal["eager", "loss", "estimators", "both"] + label: str + description: str + compile_loss: bool + compile_estimators: bool + + +COMPILE_MODES: dict[str, CompileMode] = { + "eager": CompileMode( + key="eager", + label="Eager", + description="No compilation; reference runtime.", + compile_loss=False, + compile_estimators=False, + ), + "loss": CompileMode( + key="loss", + label="Loss Only", + description="Compile the loss wrapper while leaving estimators eager.", + compile_loss=True, + compile_estimators=False, + ), + "estimators": CompileMode( + key="estimators", + label="Estimators Only", + description="Compile estimator modules via try_compile_gflownet.", + compile_loss=False, + compile_estimators=True, + ), + "both": CompileMode( + key="both", + label="Loss + Estimators", + description="Compile estimator modules and the loss wrapper.", + compile_loss=True, + compile_estimators=True, + ), +} +DEFAULT_COMPILE_ORDER = ["eager", "loss", "estimators", "both"] +COMPILE_MODE_COLORS: dict[str, str] = { + "eager": "#000000", + "loss": "#1f77b4", + "estimators": "#d62728", + "both": "#ff7f0e", +} + + +@dataclass +class TrainingComponents: + env: Env + gflownet: PFBasedGFlowNet + optimizer: torch.optim.Optimizer + sampler: Sampler | None + use_training_samples: bool = False + sampler_kwargs: Dict[str, Any] = field(default_factory=dict) + recalc_logprobs: bool = True + notes: str = "" + + +@dataclass +class EnvironmentBenchmark: + key: Literal[ + "hypergrid", "line", "bitseq_recurrent", "bitseq_mlp", "diffusion", "graph_ring" + ] + label: str + description: str + color: str + builder: Callable[ + [argparse.Namespace, torch.device, FlowVariant], TrainingComponents + ] + supported_flows: list[str] + + +# Default hypergrid parameters (shared across builders + CLI defaults). +HYPERGRID_DEFAULTS: Dict[str, Any] = { + "ndim": 2, + "height": 32, + "reward_fn_str": "original", + "reward_fn_kwargs": {"R0": 0.1, "R1": 0.5, "R2": 2.0}, + "calculate_partition": False, + "store_all_states": False, + "check_action_validity": __debug__, +} + + +# --------------------------------------------------------------------------- +# CLI helpers +# --------------------------------------------------------------------------- + + +def _normalize_keys( + requested: list[str], valid: Dict[str, Any], label: str +) -> list[str]: + normalized: list[str] = [] + for key in requested: + alias = key.lower() + if alias not in valid: + raise ValueError( + f"Unsupported {label} '{key}'. Choose from {', '.join(sorted(valid))}." + ) + if alias not in normalized: + normalized.append(alias) + return normalized + + +def _mps_backend_available() -> bool: + backend = getattr(torch.backends, "mps", None) + return bool(backend and backend.is_available()) + + +def resolve_device(requested: str) -> torch.device: + if requested == "auto": + if torch.cuda.is_available(): + return torch.device("cuda") + if _mps_backend_available(): + return torch.device("mps") + return torch.device("cpu") + + device = torch.device(requested) + if device.type == "cuda" and not torch.cuda.is_available(): + raise RuntimeError("CUDA requested but not available.") + if device.type == "mps" and not _mps_backend_available(): + raise RuntimeError("MPS requested but not available.") + return device + + +def synchronize_if_needed(device: torch.device) -> None: + if device.type == "cuda" and torch.cuda.is_available(): + torch.cuda.synchronize() + elif device.type == "mps" and _mps_backend_available() and hasattr(torch, "mps"): + torch.mps.synchronize() + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Benchmark compile strategies across multiple GFlowNet workloads." + ) + parser.add_argument("--n-iterations", type=int, default=50, dest="n_iterations") + parser.add_argument("--batch-size", type=int, default=32, dest="batch_size") + parser.add_argument("--warmup-iters", type=int, default=5, dest="warmup_iters") + parser.add_argument("--seed", type=int, default=0) + parser.add_argument("--grad-clip", type=float, default=1.0, help="Grad clip value.") + parser.add_argument("--lr", type=float, default=1e-3, help="Base learning rate.") + parser.add_argument( + "--lr-logz", type=float, default=1e-1, dest="lr_logz", help="LogZ learning rate." + ) + parser.add_argument( + "--lr-logf", type=float, default=1e-3, dest="lr_logf", help="LogF learning rate." + ) + + parser.add_argument( + "--device", + choices=["auto", "cpu", "mps", "cuda"], + default="auto", + help="Device preference.", + ) + + parser.add_argument( + "--environments", + nargs="+", + default=[ + "hypergrid", + "line", + "bitseq_recurrent", + "bitseq_mlp", + "diffusion", + "graph_ring", + ], + help="Subset of environments to benchmark.", + ) + parser.add_argument( + "--gflownets", + nargs="+", + default=DEFAULT_FLOW_ORDER, + help="Loss variants to benchmark (tb, modified_dbg, subtb).", + ) + parser.add_argument( + "--compile-modes", + nargs="+", + default=DEFAULT_COMPILE_ORDER, + help="Compile modes to evaluate (eager, loss, estimators, both).", + ) + + parser.add_argument( + "--benchmark-output", + type=str, + default=str(Path.home() / "compile_benchmark.png"), + help="Path to save the optional benchmark plot.", + ) + parser.add_argument("--skip-plot", action="store_true", help="Disable plotting.") + parser.add_argument( + "--torch-compile-mode", + type=str, + default="reduce-overhead", + help="Mode passed to torch.compile.", + ) + parser.add_argument( + "--compile-fullgraph", + action="store_true", + help="Request `fullgraph=True` when compiling the loss wrapper.", + ) + parser.add_argument( + "--large-models", + action="store_true", + help="Double estimator widths/depths to stress-test large model builds.", + ) + + # Hypergrid knobs + parser.add_argument("--hypergrid-ndim", type=int, default=HYPERGRID_DEFAULTS["ndim"]) + parser.add_argument( + "--hypergrid-height", type=int, default=HYPERGRID_DEFAULTS["height"] + ) + parser.add_argument("--hidden-dim", type=int, default=256, help="MLP hidden dim.") + parser.add_argument("--n-hidden", type=int, default=2, help="#hidden layers.") + + # Line environment knobs + parser.add_argument("--line-n-steps", type=int, default=5) + parser.add_argument("--line-hidden-dim", type=int, default=64) + parser.add_argument("--line-n-hidden", type=int, default=2) + parser.add_argument("--line-std-min", type=float, default=0.1) + parser.add_argument("--line-std-max", type=float, default=1.0) + + # Bit sequence knobs + parser.add_argument("--bitseq-word-size", type=int, default=3) + parser.add_argument("--bitseq-seq-size", type=int, default=9) + parser.add_argument("--bitseq-n-modes", type=int, default=5) + parser.add_argument("--bitseq-temperature", type=float, default=1.0) + parser.add_argument("--bitseq-embedding-dim", type=int, default=64) + parser.add_argument("--bitseq-hidden-size", type=int, default=128) + parser.add_argument("--bitseq-num-layers", type=int, default=2) + parser.add_argument("--bitseq-dropout", type=float, default=0.0) + parser.add_argument("--bitseq-mlp-hidden-dim", type=int, default=128) + parser.add_argument("--bitseq-mlp-n-hidden", type=int, default=2) + + # Diffusion knobs + parser.add_argument("--diffusion-target", type=str, default="gmm2") + parser.add_argument("--diffusion-num-steps", type=int, default=32) + parser.add_argument("--diffusion-sigma", type=float, default=5.0) + parser.add_argument("--diffusion-hidden-dim", type=int, default=64) + parser.add_argument("--diffusion-joint-layers", type=int, default=2) + parser.add_argument("--diffusion-harmonics-dim", type=int, default=64) + parser.add_argument("--diffusion-t-emb-dim", type=int, default=64) + parser.add_argument("--diffusion-s-emb-dim", type=int, default=64) + parser.add_argument("--diffusion-zero-init", action="store_true") + parser.add_argument("--diffusion-dim", type=int, default=None) + parser.add_argument("--diffusion-num-components", type=int, default=None) + parser.add_argument("--diffusion-target-seed", type=int, default=2) + + # Graph ring knobs + parser.add_argument("--ring-n-nodes", type=int, default=4) + parser.add_argument("--ring-directed", action="store_true", default=True) + parser.add_argument("--ring-use-gnn", action="store_true", default=True) + parser.add_argument("--ring-embedding-dim", type=int, default=128) + parser.add_argument("--ring-num-conv-layers", type=int, default=1) + parser.add_argument("--ring-num-edge-classes", type=int, default=2) + parser.add_argument("--ring-reward", type=float, default=100.0) + parser.add_argument("--ring-reward-eps", type=float, default=1e-6) + + # SubTB knobs + parser.add_argument( + "--subtb-weighting", + type=str, + default="geometric_within", + help="Weighting scheme for SubTB.", + ) + parser.add_argument( + "--subtb-lambda", + type=float, + default=0.9, + dest="subtb_lamda", + help="Lambda parameter for SubTB.", + ) + + return parser.parse_args() + + +LARGE_MODEL_FIELDS = [ + "hidden_dim", + "n_hidden", + "line_hidden_dim", + "line_n_hidden", + "bitseq_embedding_dim", + "bitseq_hidden_size", + "bitseq_num_layers", + "bitseq_mlp_hidden_dim", + "bitseq_mlp_n_hidden", + "diffusion_hidden_dim", + "diffusion_joint_layers", + "diffusion_harmonics_dim", + "diffusion_t_emb_dim", + "diffusion_s_emb_dim", + "ring_embedding_dim", + "ring_num_conv_layers", +] + + +def _apply_large_model_scaling(args: argparse.Namespace) -> None: + if not getattr(args, "large_models", False): + return + for attr in LARGE_MODEL_FIELDS: + value = getattr(args, attr, None) + if value is None: + continue + if isinstance(value, int): + setattr(args, attr, max(1, value * 2)) + print("[config] Enabled large model mode: doubled estimator sizes.") + + +# --------------------------------------------------------------------------- +# Environment builders +# --------------------------------------------------------------------------- + + +def _build_hypergrid_components( + args: argparse.Namespace, device: torch.device, variant: FlowVariant +) -> TrainingComponents: + kwargs = dict(HYPERGRID_DEFAULTS) + kwargs["ndim"] = args.hypergrid_ndim + kwargs["height"] = args.hypergrid_height + kwargs["device"] = device + env = HyperGrid(**kwargs) + + preprocessor = KHotPreprocessor(height=env.height, ndim=env.ndim) + pf_module = MLP( + input_dim=preprocessor.output_dim, + output_dim=env.n_actions, + hidden_dim=args.hidden_dim, + n_hidden_layers=args.n_hidden, + ) + pb_module = MLP( + input_dim=preprocessor.output_dim, + output_dim=env.n_actions - 1, + hidden_dim=args.hidden_dim, + n_hidden_layers=args.n_hidden, + trunk=pf_module.trunk, + ) + + pf = DiscretePolicyEstimator( + module=pf_module, + n_actions=env.n_actions, + preprocessor=preprocessor, + ) + pb = DiscretePolicyEstimator( + module=pb_module, + n_actions=env.n_actions, + is_backward=True, + preprocessor=preprocessor, + ) + + logF: ScalarEstimator | None = None + if variant.requires_logf: + logF_module = MLP( + input_dim=preprocessor.output_dim, + output_dim=1, + hidden_dim=args.hidden_dim, + n_hidden_layers=args.n_hidden, + ) + logF = ScalarEstimator(module=logF_module, preprocessor=preprocessor) + + if variant.key == "tb": + gflownet = TBGFlowNet(pf=pf, pb=pb, init_logZ=0.0) + elif variant.key == "modified_dbg": + gflownet = ModifiedDBGFlowNet(pf=pf, pb=pb) + elif variant.key == "subtb": + assert logF is not None + gflownet = SubTBGFlowNet( + pf=pf, + pb=pb, + logF=logF, + weighting=args.subtb_weighting, + lamda=args.subtb_lamda, + ) + else: # pragma: no cover + raise ValueError(f"Unsupported FlowVariant '{variant.key}'") + + gflownet = gflownet.to(device) + optimizer = torch.optim.Adam(gflownet.pf_pb_parameters(), lr=args.lr) + + logz_params = getattr(gflownet, "logz_parameters", None) + if callable(logz_params): + params = logz_params() + if params: + optimizer.add_param_group({"params": params, "lr": args.lr_logz}) + logf_params = getattr(gflownet, "logF_parameters", None) + if callable(logf_params): + params = logf_params() + if params: + optimizer.add_param_group({"params": params, "lr": args.lr_logf}) + + sampler = Sampler(estimator=pf) + sampler_kwargs = { + "save_logprobs": False, + "save_estimator_outputs": False, + "epsilon": 0.05, + } + use_training_samples = variant.key in {"modified_dbg", "subtb"} + return TrainingComponents( + env=env, + gflownet=gflownet, + optimizer=optimizer, + sampler=sampler, + sampler_kwargs=sampler_kwargs, + recalc_logprobs=True, + use_training_samples=use_training_samples, + ) + + +def _build_line_components( + args: argparse.Namespace, device: torch.device, variant: FlowVariant +) -> TrainingComponents: + env = Line( + mus=[2, 5], + sigmas=[0.5, 0.5], + init_value=0, + n_sd=4.5, + n_steps_per_trajectory=args.line_n_steps, + device=device, + ) + + pf_module = GaussianStepMLP( + hidden_dim=args.line_hidden_dim, + n_hidden_layers=args.line_n_hidden, + policy_std_min=args.line_std_min, + policy_std_max=args.line_std_max, + ) + pb_module = GaussianStepMLP( + hidden_dim=args.line_hidden_dim, + n_hidden_layers=args.line_n_hidden, + policy_std_min=args.line_std_min, + policy_std_max=args.line_std_max, + ) + + pf = StepEstimator(env, pf_module, backward=False) + pb = StepEstimator(env, pb_module, backward=True) + + logF: ScalarEstimator | None = None + if variant.requires_logf: + logF_module = MLP( + input_dim=2, # [position, step counter] + output_dim=1, + hidden_dim=args.line_hidden_dim, + n_hidden_layers=args.line_n_hidden, + ) + logF = ScalarEstimator(module=logF_module) + + if variant.key == "tb": + gflownet = TBGFlowNet(pf=pf, pb=pb, init_logZ=0.0) + elif variant.key == "modified_dbg": + gflownet = ModifiedDBGFlowNet(pf=pf, pb=pb) + elif variant.key == "subtb": + assert logF is not None + gflownet = SubTBGFlowNet( + pf=pf, + pb=pb, + logF=logF, + weighting=args.subtb_weighting, + lamda=args.subtb_lamda, + ) + else: + raise ValueError(f"Unsupported FlowVariant '{variant.key}'") + + gflownet = gflownet.to(device) + optimizer = torch.optim.Adam(gflownet.pf_pb_parameters(), lr=args.lr) + logz_params = getattr(gflownet, "logz_parameters", None) + if callable(logz_params): + params = logz_params() + if params: + optimizer.add_param_group({"params": params, "lr": args.lr_logz}) + logf_params = getattr(gflownet, "logF_parameters", None) + if callable(logf_params): + params = logf_params() + if params: + optimizer.add_param_group({"params": params, "lr": args.lr_logf}) + + sampler = Sampler(estimator=pf) + sampler_kwargs = { + "save_logprobs": False, + "save_estimator_outputs": False, + } + use_training_samples = variant.key in {"modified_dbg", "subtb"} + return TrainingComponents( + env=env, + gflownet=gflownet, + optimizer=optimizer, + sampler=sampler, + sampler_kwargs=sampler_kwargs, + recalc_logprobs=True, + use_training_samples=use_training_samples, + ) + + +def _build_bitsequence_recurrent_components( + args: argparse.Namespace, device: torch.device, variant: FlowVariant +) -> TrainingComponents: + H = torch.randint( + 0, + 2, + (args.bitseq_n_modes, args.bitseq_seq_size), + dtype=torch.long, + device=device, + ) + env = BitSequence( + word_size=args.bitseq_word_size, + seq_size=args.bitseq_seq_size, + n_modes=args.bitseq_n_modes, + temperature=args.bitseq_temperature, + H=H, + device_str=str(device), + seed=args.seed, + debug=__debug__, + ) + + pf_module = RecurrentDiscreteSequenceModel( + vocab_size=env.n_actions, + embedding_dim=args.bitseq_embedding_dim, + hidden_size=args.bitseq_hidden_size, + num_layers=args.bitseq_num_layers, + dropout=args.bitseq_dropout, + ).to(device) + pf = RecurrentDiscretePolicyEstimator( + module=pf_module, + n_actions=env.n_actions, + is_backward=False, + ) + + if variant.key != "tb": + raise ValueError( + "BitSequence benchmark currently supports Trajectory Balance only." + ) + + gflownet = TBGFlowNet( + pf=pf, + pb=None, + init_logZ=0.0, + constant_pb=True, + ).to(device) + + optimizer = torch.optim.Adam(gflownet.pf_pb_parameters(), lr=args.lr) + optimizer.add_param_group({"params": gflownet.logz_parameters(), "lr": args.lr_logz}) + + sampler_kwargs = { + "n": args.batch_size, + "save_logprobs": True, + "save_estimator_outputs": False, + "epsilon": 0.05, + } + return TrainingComponents( + env=env, + gflownet=gflownet, + optimizer=optimizer, + sampler=None, # Use gflownet.sample_trajectories for recurrent adapter support. + sampler_kwargs=sampler_kwargs, + recalc_logprobs=False, + ) + + +def _build_bitsequence_mlp_components( + args: argparse.Namespace, device: torch.device, variant: FlowVariant +) -> TrainingComponents: + H = torch.randint( + 0, + 2, + (args.bitseq_n_modes, args.bitseq_seq_size), + dtype=torch.long, + device=device, + ) + env = BitSequence( + word_size=args.bitseq_word_size, + seq_size=args.bitseq_seq_size, + n_modes=args.bitseq_n_modes, + temperature=args.bitseq_temperature, + H=H, + device_str=str(device), + seed=args.seed, + debug=__debug__, + ) + + pf_module = MLP( + input_dim=env.words_per_seq, + output_dim=env.n_actions, + hidden_dim=args.bitseq_mlp_hidden_dim, + n_hidden_layers=args.bitseq_mlp_n_hidden, + ) + pb_module = MLP( + input_dim=env.words_per_seq, + output_dim=env.n_actions - 1, + hidden_dim=args.bitseq_mlp_hidden_dim, + n_hidden_layers=args.bitseq_mlp_n_hidden, + trunk=pf_module.trunk, + ) + pf_estimator = DiscretePolicyEstimator( + module=pf_module, + n_actions=env.n_actions, + ) + pb_estimator = DiscretePolicyEstimator( + module=pb_module, + n_actions=env.n_actions, + is_backward=True, + ) + + logF: ScalarEstimator | None = None + if variant.requires_logf: + logF_module = MLP( + input_dim=env.words_per_seq, + output_dim=1, + hidden_dim=args.bitseq_mlp_hidden_dim, + n_hidden_layers=args.bitseq_mlp_n_hidden, + trunk=pf_module.trunk, + ) + logF = ScalarEstimator(module=logF_module) + + if variant.key == "tb": + gflownet = TBGFlowNet(pf=pf_estimator, pb=pb_estimator, init_logZ=0.0) + elif variant.key == "modified_dbg": + gflownet = ModifiedDBGFlowNet(pf=pf_estimator, pb=pb_estimator) + elif variant.key == "subtb": + assert logF is not None + gflownet = SubTBGFlowNet( + pf=pf_estimator, + pb=pb_estimator, + logF=logF, + weighting=args.subtb_weighting, + lamda=args.subtb_lamda, + ) + else: + raise ValueError(f"Unsupported FlowVariant '{variant.key}' for bitseq_mlp") + + gflownet = gflownet.to(device) + optimizer = torch.optim.Adam(gflownet.pf_pb_parameters(), lr=args.lr) + + logz_params = getattr(gflownet, "logz_parameters", None) + if callable(logz_params): + params = logz_params() + if params: + optimizer.add_param_group({"params": params, "lr": args.lr_logz}) + logf_params = getattr(gflownet, "logF_parameters", None) + if callable(logf_params): + params = logf_params() + if params: + optimizer.add_param_group({"params": params, "lr": args.lr_logf}) + + sampler = Sampler(estimator=pf_estimator) + sampler_kwargs = { + "save_logprobs": False, + "save_estimator_outputs": False, + "epsilon": 0.05, + } + use_training_samples = variant.key in {"modified_dbg", "subtb"} + return TrainingComponents( + env=env, + gflownet=gflownet, + optimizer=optimizer, + sampler=sampler, + sampler_kwargs=sampler_kwargs, + recalc_logprobs=True, + use_training_samples=use_training_samples, + ) + + +def _build_diffusion_components( + args: argparse.Namespace, device: torch.device, variant: FlowVariant +) -> TrainingComponents: + target_kwargs: Dict[str, Any] = {"seed": args.diffusion_target_seed} + if args.diffusion_dim is not None: + target_kwargs["dim"] = args.diffusion_dim + if args.diffusion_num_components is not None: + target_kwargs["num_components"] = args.diffusion_num_components + + env = DiffusionSampling( + target_str=args.diffusion_target, + target_kwargs=target_kwargs, + num_discretization_steps=args.diffusion_num_steps, + device=device, + debug=False, + ) + + pf_module = DiffusionPISGradNetForward( + s_dim=env.dim, + harmonics_dim=args.diffusion_harmonics_dim, + t_emb_dim=args.diffusion_t_emb_dim, + s_emb_dim=args.diffusion_s_emb_dim, + hidden_dim=args.diffusion_hidden_dim, + joint_layers=args.diffusion_joint_layers, + zero_init=args.diffusion_zero_init, + ) + pb_module = DiffusionFixedBackwardModule(s_dim=env.dim) + + pf = PinnedBrownianMotionForward( + s_dim=env.dim, + pf_module=pf_module, + sigma=args.diffusion_sigma, + num_discretization_steps=args.diffusion_num_steps, + ) + pb = PinnedBrownianMotionBackward( + s_dim=env.dim, + pb_module=pb_module, + sigma=args.diffusion_sigma, + num_discretization_steps=args.diffusion_num_steps, + ) + + logF: ScalarEstimator | None = None + if variant.requires_logf: + logF_module = MLP( + input_dim=env.state_shape[-1], + output_dim=1, + hidden_dim=args.diffusion_hidden_dim, + n_hidden_layers=args.diffusion_joint_layers, + ) + preproc = IdentityPreprocessor(output_dim=env.state_shape[-1]) + logF = ScalarEstimator(module=logF_module, preprocessor=preproc) + + if variant.key == "tb": + gflownet = TBGFlowNet(pf=pf, pb=pb, init_logZ=0.0) + elif variant.key == "modified_dbg": + gflownet = ModifiedDBGFlowNet(pf=pf, pb=pb) + elif variant.key == "subtb": + assert logF is not None + gflownet = SubTBGFlowNet( + pf=pf, + pb=pb, + logF=logF, + weighting=args.subtb_weighting, + lamda=args.subtb_lamda, + ) + else: + raise ValueError(f"Unsupported FlowVariant '{variant.key}'") + + gflownet = gflownet.to(device) + optimizer = torch.optim.Adam(gflownet.pf_pb_parameters(), lr=args.lr) + + logz_params = getattr(gflownet, "logz_parameters", None) + if callable(logz_params): + params = logz_params() + if params: + optimizer.add_param_group({"params": params, "lr": args.lr_logz}) + logf_params = getattr(gflownet, "logF_parameters", None) + if callable(logf_params): + params = logf_params() + if params: + optimizer.add_param_group({"params": params, "lr": args.lr_logf}) + + sampler = Sampler(estimator=pf) + sampler_kwargs = { + "save_logprobs": True, + "save_estimator_outputs": False, + "n": args.batch_size, + } + use_training_samples = variant.key in {"modified_dbg", "subtb"} + return TrainingComponents( + env=env, + gflownet=gflownet, + optimizer=optimizer, + sampler=sampler, + sampler_kwargs=sampler_kwargs, + recalc_logprobs=False, + use_training_samples=use_training_samples, + ) + + +def _build_graph_ring_components( + args: argparse.Namespace, device: torch.device, variant: FlowVariant +) -> TrainingComponents: + state_evaluator = RingReward( + directed=args.ring_directed, + reward_val=args.ring_reward, + eps_val=args.ring_reward_eps, + device=device, + ) + env = GraphBuildingOnEdges( + n_nodes=args.ring_n_nodes, + state_evaluator=state_evaluator, + directed=args.ring_directed, + device=device, + ) + + num_node_classes = getattr(env, "num_node_classes", args.ring_n_nodes) + num_edge_classes = getattr(env, "num_edge_classes", args.ring_num_edge_classes) + + if args.ring_use_gnn: + module_pf = GraphActionGNN( + num_node_classes=num_node_classes, + directed=args.ring_directed, + num_conv_layers=args.ring_num_conv_layers, + num_edge_classes=num_edge_classes, + embedding_dim=args.ring_embedding_dim, + ) + module_pb = GraphActionGNN( + num_node_classes=num_node_classes, + directed=args.ring_directed, + is_backward=True, + num_conv_layers=args.ring_num_conv_layers, + num_edge_classes=num_edge_classes, + embedding_dim=args.ring_embedding_dim, + ) + else: + module_pf = GraphEdgeActionMLP( + args.ring_n_nodes, + args.ring_directed, + num_node_classes=num_node_classes, + num_edge_classes=num_edge_classes, + embedding_dim=args.ring_embedding_dim, + ) + module_pb = GraphEdgeActionMLP( + args.ring_n_nodes, + args.ring_directed, + is_backward=True, + num_node_classes=num_node_classes, + num_edge_classes=num_edge_classes, + embedding_dim=args.ring_embedding_dim, + ) + + pf_estimator = DiscreteGraphPolicyEstimator(module=module_pf) + pb_estimator = DiscreteGraphPolicyEstimator(module=module_pb, is_backward=True) + + logF: ScalarEstimator | None = None + if variant.requires_logf: + logF_module = GraphScalarMLP( + n_nodes=args.ring_n_nodes, + directed=args.ring_directed, + embedding_dim=args.ring_embedding_dim, + n_outputs=1, + n_hidden_layers=2, + ) + logF = ScalarEstimator(module=logF_module) + + if variant.key == "tb": + gflownet = TBGFlowNet(pf=pf_estimator, pb=pb_estimator, init_logZ=0.0) + elif variant.key == "modified_dbg": + gflownet = ModifiedDBGFlowNet(pf=pf_estimator, pb=pb_estimator) + elif variant.key == "subtb": + assert logF is not None + gflownet = SubTBGFlowNet( + pf=pf_estimator, + pb=pb_estimator, + logF=logF, + weighting=args.subtb_weighting, + lamda=args.subtb_lamda, + ) + else: + raise ValueError(f"Unsupported FlowVariant '{variant.key}' for graph ring") + + gflownet = gflownet.to(device) + optimizer = torch.optim.Adam(gflownet.pf_pb_parameters(), lr=args.lr) + logz_params = getattr(gflownet, "logz_parameters", None) + if callable(logz_params): + params = logz_params() + if params: + optimizer.add_param_group({"params": params, "lr": args.lr_logz}) + logf_params = getattr(gflownet, "logF_parameters", None) + if callable(logf_params): + params = logf_params() + if params: + optimizer.add_param_group({"params": params, "lr": args.lr_logf}) + + sampler = Sampler(estimator=pf_estimator) + epsilon_dict = { + GraphActions.ACTION_TYPE_KEY: 0.05, + GraphActions.EDGE_INDEX_KEY: 0.05, + GraphActions.NODE_CLASS_KEY: 0.05, + GraphActions.NODE_INDEX_KEY: 0.05, + GraphActions.EDGE_CLASS_KEY: 0.05, + } + sampler_kwargs = { + "save_logprobs": False, + "save_estimator_outputs": False, + "epsilon": epsilon_dict, + } + use_training_samples = variant.key in {"modified_dbg", "subtb"} + return TrainingComponents( + env=env, + gflownet=gflownet, + optimizer=optimizer, + sampler=sampler, + sampler_kwargs=sampler_kwargs, + recalc_logprobs=True, + use_training_samples=use_training_samples, + ) + + +ENVIRONMENT_BENCHMARKS: dict[str, EnvironmentBenchmark] = { + "hypergrid": EnvironmentBenchmark( + key="hypergrid", + label="HyperGrid", + description="High-dimensional discrete grid.", + color="#4a90e2", + builder=_build_hypergrid_components, + supported_flows=list(DEFAULT_FLOW_ORDER), + ), + "line": EnvironmentBenchmark( + key="line", + label="Line", + description="Continuous 1D environment with Gaussian rewards.", + color="#ffa600", + builder=_build_line_components, + supported_flows=list(DEFAULT_FLOW_ORDER), + ), + "bitseq_recurrent": EnvironmentBenchmark( + key="bitseq_recurrent", + label="BitSequence (Recurrent)", + description="Recurrent sequence generation benchmark.", + color="#2ca02c", + builder=_build_bitsequence_recurrent_components, + supported_flows=["tb"], # Recurrent path supports TB only. + ), + "bitseq_mlp": EnvironmentBenchmark( + key="bitseq_mlp", + label="BitSequence (MLP)", + description="Tabular/MLP bit sequence benchmark.", + color="#17becf", + builder=_build_bitsequence_mlp_components, + supported_flows=["tb"], # Only TB is stable for this environment. + ), + "diffusion": EnvironmentBenchmark( + key="diffusion", + label="Diffusion Sampler", + description="Pinned Brownian motion diffusion sampling.", + color="#a17be7", + builder=_build_diffusion_components, + supported_flows=list(DEFAULT_FLOW_ORDER), + ), + "graph_ring": EnvironmentBenchmark( + key="graph_ring", + label="Graph Ring", + description="Graph-building environment for ring structures.", + color="#ff7f0e", + builder=_build_graph_ring_components, + supported_flows=list(DEFAULT_FLOW_ORDER), + ), +} + + +# --------------------------------------------------------------------------- +# Training + benchmarking utilities +# --------------------------------------------------------------------------- + + +def prepare_loss_fn( + gflownet: PFBasedGFlowNet, + compile_mode: CompileMode, + variant: FlowVariant, + args: argparse.Namespace, +) -> Callable[[Env, Any, bool], torch.Tensor]: + def loss_wrapper(env: Env, training_batch: Any, recalc: bool) -> torch.Tensor: + return gflownet.loss( + env, + training_batch, + recalculate_all_logprobs=recalc, + ) + + if compile_mode.compile_loss: + if variant.requires_logf: + print( + "[compile loss] Skipping torch.compile for Detailed/SubTB " + "variants due to unsupported logF ops." + ) + return loss_wrapper + return torch.compile( + loss_wrapper, + mode=args.torch_compile_mode, + fullgraph=args.compile_fullgraph, + ) + return loss_wrapper + + +def maybe_compile_estimators( + components: TrainingComponents, + compile_mode: CompileMode, + dynamo_mode: str, +) -> bool: + if not compile_mode.compile_estimators: + return False + results = try_compile_gflownet(components.gflownet, mode=dynamo_mode) + if not isinstance(results, dict): + print("[compile estimators] try_compile_gflownet returned None/unknown result") + return False + joined = ", ".join( + f"{name}:{'✓' if success else 'x'}" for name, success in results.items() + ) + print(f"[compile estimators] {joined}") + return any(results.values()) + + +def sample_trajectories( + components: TrainingComponents, + batch_size: int, +) -> Any: + kwargs = dict(components.sampler_kwargs) + kwargs.setdefault("n", batch_size) + if components.sampler is None: + return components.gflownet.sample_trajectories(components.env, **kwargs) + return components.sampler.sample_trajectories(components.env, **kwargs) + + +def training_loop( + components: TrainingComponents, + loss_fn: Callable[[Env, Any, bool], torch.Tensor], + args: argparse.Namespace, + *, + n_iters: int, + track_time: bool, +) -> tuple[float | None, Dict[str, list[float]]]: + iterator: Iterable[int] = range(n_iters) + iterator = tqdm(iterator, dynamic_ncols=True) if n_iters > 1 else iterator + + history = {"losses": [], "iter_times": []} + start_time = time.perf_counter() if track_time else None + + for step in iterator: + iter_start = time.perf_counter() if track_time else None + trajectories = sample_trajectories(components, args.batch_size) + training_batch = ( + components.gflownet.to_training_samples(trajectories) + if components.use_training_samples + else trajectories + ) + + components.optimizer.zero_grad() + loss = loss_fn(components.env, training_batch, components.recalc_logprobs) + loss.backward() + torch.nn.utils.clip_grad_norm_(components.gflownet.parameters(), args.grad_clip) + components.optimizer.step() + + history["losses"].append(loss.item()) + if iter_start is not None: + history["iter_times"].append(time.perf_counter() - iter_start) + + if isinstance(iterator, tqdm): + iterator.set_postfix({"loss": loss.item(), "iter": step + 1}) + + elapsed = None + if track_time: + synchronize_if_needed(getattr(components.env, "device", torch.device("cpu"))) + elapsed = time.perf_counter() - start_time # type: ignore[arg-type] + return elapsed, history + + +def run_case( + args: argparse.Namespace, + device: torch.device, + env_cfg: EnvironmentBenchmark, + variant: FlowVariant, + compile_mode: CompileMode, +) -> dict[str, Any]: + set_seed(args.seed) + components = env_cfg.builder(args, device, variant) + loss_fn = prepare_loss_fn(components.gflownet, compile_mode, variant, args) + estimator_compiled = maybe_compile_estimators( + components, compile_mode, args.torch_compile_mode + ) + + if args.warmup_iters > 0: + training_loop( + components, + loss_fn, + args, + n_iters=args.warmup_iters, + track_time=False, + ) + + elapsed, history = training_loop( + components, + loss_fn, + args, + n_iters=args.n_iterations, + track_time=True, + ) + + return { + "env_key": env_cfg.key, + "env_label": env_cfg.label, + "gflownet_key": variant.key, + "gflownet_label": variant.label, + "compile_key": compile_mode.key, + "label": compile_mode.label, + "compile_description": compile_mode.description, + "elapsed": elapsed or 0.0, + "losses": history["losses"], + "iter_times": history["iter_times"], + "use_compile_estimator": estimator_compiled, + "use_compile_loss": compile_mode.compile_loss, + } + + +# --------------------------------------------------------------------------- +# Plotting helpers (adapted from the earlier sketch) +# --------------------------------------------------------------------------- + + +def _summarize_iteration_times(times: list[float]) -> tuple[float, float]: + if not times: + return 0.0, 0.0 + mean_time = statistics.fmean(times) + std_time = statistics.pstdev(times) if len(times) > 1 else 0.0 + return mean_time, std_time + + +VARIANT_COLORS: dict[str, str] = { + "tb": "#000000", + "modified_dbg": "#1f77b4", + "subtb": "#d62728", +} +LOSS_LINE_ALPHA = 0.5 + + +def summarize_results(results: list[dict[str, Any]]) -> None: + print("\nBenchmark summary:") + grouped: Dict[tuple[str, str], list[dict[str, Any]]] = {} + for res in results: + grouped.setdefault((res["env_key"], res["gflownet_key"]), []).append(res) + + for (env_key, flow_key), entries in grouped.items(): + env_label = entries[0]["env_label"] + flow_label = entries[0]["gflownet_label"] + print(f"\n[{env_label}] {flow_label}") + baseline = next((e for e in entries if e["compile_key"] == "eager"), entries[0]) + baseline_time = baseline["elapsed"] or 1.0 + for entry in entries: + elapsed = entry["elapsed"] + speedup = baseline_time / elapsed if elapsed else float("inf") + print( + f" - {entry['label']:<18} {elapsed:.2f}s " + f"({speedup:.2f}x vs eager) " + f"compile_loss={entry['use_compile_loss']} " + f"compile_estimators={entry['use_compile_estimator']}" + ) + + +def plot_benchmark( + results: list[dict[str, Any]], output_path: str, run_label: str | None = None +) -> None: + try: + import matplotlib.pyplot as plt + except ImportError as exc: # pragma: no cover + raise RuntimeError("matplotlib is required for plotting.") from exc + + env_keys = list({res["env_key"] for res in results}) + if not env_keys: + print("No results collected; skipping plot.") + return + + fig, axes = plt.subplots(len(env_keys), 3, figsize=(20, 5 * len(env_keys))) + if run_label: + fig.suptitle(f"Run label: {run_label}", fontsize=18) + if len(env_keys) == 1: + axes = [axes] # type: ignore[list-item] + + palette = ["#6c757d", "#1f77b4", "#2ca02c", "#d62728", "#9467bd", "#8c564b"] + + for row_idx, env_key in enumerate(env_keys): + env_results = [res for res in results if res["env_key"] == env_key] + env_label = env_results[0]["env_label"] + row_axes = axes[row_idx] + + labels = [f"{res['label']} [{res['gflownet_label']}]" for res in env_results] + times = [res["elapsed"] for res in env_results] + eager_baselines: dict[str, float] = { + res["gflownet_key"]: res["elapsed"] + for res in env_results + if res["compile_key"] == "eager" and (res["elapsed"] or 0.0) > 0.0 + } + fallback_baseline = min((t for t in times if t > 0), default=0.0) + colors = [ + COMPILE_MODE_COLORS.get(res["compile_key"], palette[i % len(palette)]) + for i, res in enumerate(env_results) + ] + bars = row_axes[0].bar(labels, times, color=colors) + row_axes[0].set_ylabel("Time (s)") + row_axes[0].set_title(f"{env_label} | Total time") + for bar, res in zip(bars, env_results): + value = res["elapsed"] + baseline = eager_baselines.get(res["gflownet_key"], fallback_baseline) + rel = value / baseline if baseline and baseline > 0 else 0.0 + row_axes[0].text( + bar.get_x() + bar.get_width() / 2, + value, + f"{rel:.2f}x", + ha="center", + va="bottom", + ) + + loss_ax = row_axes[1] + for idx, res in enumerate(env_results): + losses = res["losses"] + if not losses: + continue + color = VARIANT_COLORS.get(res["gflownet_key"], palette[idx % len(palette)]) + loss_ax.plot( + range(1, len(losses) + 1), + losses, + label=labels[idx], + color=color, + alpha=LOSS_LINE_ALPHA, + ) + loss_ax.set_title(f"{env_label} | Loss curves") + loss_ax.set_xlabel("Iteration") + loss_ax.set_ylabel("Loss") + if loss_ax.lines: + loss_ax.legend(fontsize="small") + + iter_ax = row_axes[2] + stats = [_summarize_iteration_times(res["iter_times"]) for res in env_results] + means = [mean * 1000.0 for mean, _ in stats] + stds = [std * 1000.0 for _, std in stats] + iter_ax.bar(labels, means, yerr=stds, capsize=6, color=colors) + iter_ax.set_ylabel("Iteration time (ms)") + iter_ax.set_title(f"{env_label} | Iter timing") + + for ax in row_axes: + for label in ax.get_xticklabels(): + label.set_rotation(25) + label.set_ha("right") + + output = Path(output_path) + output.parent.mkdir(parents=True, exist_ok=True) + if run_label: + fig.tight_layout(rect=(0, 0, 1, 0.96)) + else: + fig.tight_layout() + fig.savefig(output, dpi=150) + plt.close(fig) + if run_label: + print(f"Saved benchmark plot to {output} [{run_label}]") + else: + print(f"Saved benchmark plot to {output}") + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +def main() -> None: + args = parse_args() + _apply_large_model_scaling(args) + device = resolve_device(args.device) + + env_keys = _normalize_keys(args.environments, ENVIRONMENT_BENCHMARKS, "environment") + flow_keys = _normalize_keys(args.gflownets, FLOW_VARIANTS, "GFlowNet variant") + compile_keys = _normalize_keys(args.compile_modes, COMPILE_MODES, "compile mode") + + if not env_keys or not flow_keys or not compile_keys: + raise ValueError("Nothing to benchmark—please specify envs, flows, and modes.") + + results: list[dict[str, Any]] = [] + + for env_key in env_keys: + env_cfg = ENVIRONMENT_BENCHMARKS[env_key] + for flow_key in flow_keys: + if flow_key not in env_cfg.supported_flows: + print( + f"[skip] {env_cfg.label} does not support {FLOW_VARIANTS[flow_key].label}" + ) + continue + for compile_key in compile_keys: + compile_mode = COMPILE_MODES[compile_key] + print( + f"\n[run] Env={env_cfg.label}, Loss={FLOW_VARIANTS[flow_key].label}, " + f"Mode={compile_mode.label}" + ) + result = run_case( + args, + device, + env_cfg, + FLOW_VARIANTS[flow_key], + compile_mode, + ) + results.append(result) + + summarize_results(results) + if not args.skip_plot: + run_label = "large_model_run" if args.large_models else None + plot_benchmark(results, args.benchmark_output, run_label=run_label) + + +if __name__ == "__main__": + main() diff --git a/tutorials/examples/train_with_example_modes.py b/tutorials/examples/train_with_example_modes.py index d9c4c2cb..a35a1b89 100644 --- a/tutorials/examples/train_with_example_modes.py +++ b/tutorials/examples/train_with_example_modes.py @@ -35,7 +35,6 @@ from tensordict import TensorDict from torch.optim.lr_scheduler import LambdaLR from tqdm import trange -from train_graph_ring import RingReward, init_env, init_gflownet, render_states from gfn.actions import GraphActions, GraphActionType from gfn.containers.replay_buffer import ReplayBuffer @@ -46,6 +45,12 @@ from gfn.utils.common import set_seed from gfn.utils.graphs import from_edge_indices, hash_graph from gfn.utils.training import lr_grad_ratio +from tutorials.examples.train_graph_ring import ( + RingReward, + init_env, + init_gflownet, + render_states, +) def per_step_decay(num_steps: int, total_drop: float) -> float: @@ -89,7 +94,11 @@ def generate_all_rings( device = torch.device(device) state_evaluator = RingReward(directed=True, device=device) env = GraphBuildingOnEdges( - n_nodes=n_nodes, state_evaluator=state_evaluator, directed=True, device=device + n_nodes=n_nodes, + state_evaluator=state_evaluator, + directed=True, + device=device, + debug=__debug__, ) valid_rings = [] @@ -113,6 +122,12 @@ def generate_all_rings( GraphActions.NODE_CLASS_KEY: torch.zeros( 1, dtype=torch.long, device=device ), + # this node_index field isn't used, but is required for compatibility + # with the GraphActions.from_tensor_dict method while debug=True. + # TODO: is there a more elegant solution to this problem? + GraphActions.NODE_INDEX_KEY: torch.zeros( + 1, dtype=torch.long, device=device + ), GraphActions.EDGE_CLASS_KEY: torch.zeros( 1, dtype=torch.long, device=device ), @@ -292,12 +307,12 @@ def main(args: Namespace): training_samples.extend(replay_buffer_samples) if iteration % 100 == 0 or iteration == 0 or iteration == args.n_iterations - 1: - n_modes_in_buffer = sum( - env.reward( - replay_buffer.training_objects.terminating_states # type: ignore - ) - > 0.1 - ) + # Compute how many stored trajectories terminate in a true mode. + n_modes_in_buffer = 0 + if replay_buffer.training_container is not None: + terminating_states = replay_buffer.training_container.terminating_states # type: ignore[attr-defined] + if terminating_states is not None: + n_modes_in_buffer = int(torch.sum(env.reward(terminating_states) > 0.1)) # type: ignore optimizer.zero_grad() loss = gflownet.loss(env, training_samples, recalculate_all_logprobs=True) # type: ignore diff --git a/tutorials/misc/intro_torchgfn_performance_tuning.ipynb b/tutorials/misc/intro_torchgfn_performance_tuning.ipynb new file mode 100644 index 00000000..b119fc5e --- /dev/null +++ b/tutorials/misc/intro_torchgfn_performance_tuning.ipynb @@ -0,0 +1,253 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "cc9f1e1b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using device: cpu\n", + "Running hypergrid/TB/dbg/comp ...\n", + " done (compiled): elapsed=1.68s, mean_iter=33.59 ms, std=8.60 ms\n", + "Running hypergrid/TB/dbg/eager ...\n", + " done (eager): elapsed=1.42s, mean_iter=28.31 ms, std=3.87 ms\n", + "Running hypergrid/TB/nodbg/comp ...\n", + " done (compiled): elapsed=1.59s, mean_iter=31.74 ms, std=6.12 ms\n", + "Running hypergrid/TB/nodbg/eager ...\n", + " done (eager): elapsed=1.27s, mean_iter=25.32 ms, std=2.19 ms\n", + "Running hypergrid/SubTB/dbg/comp ...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/jdv/code/torchgfn/src/gfn/gflownet/sub_trajectory_balance.py:603: UserWarning: Mean reduction is not supported for SubTBGFlowNet with geometric weighting, using sum instead.\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " done (compiled): elapsed=2.68s, mean_iter=53.56 ms, std=20.77 ms\n", + "Running hypergrid/SubTB/dbg/eager ...\n", + " done (eager): elapsed=2.59s, mean_iter=51.77 ms, std=16.99 ms\n", + "Running hypergrid/SubTB/nodbg/comp ...\n", + " done (compiled): elapsed=1.57s, mean_iter=31.43 ms, std=20.19 ms\n", + "Running hypergrid/SubTB/nodbg/eager ...\n", + " done (eager): elapsed=1.81s, mean_iter=36.22 ms, std=18.88 ms\n", + "Running hypergrid/DBG/dbg/comp ...\n", + " done (compiled): elapsed=2.49s, mean_iter=49.84 ms, std=2.57 ms\n", + "Running hypergrid/DBG/dbg/eager ...\n", + " done (eager): elapsed=2.01s, mean_iter=40.15 ms, std=4.49 ms\n", + "Running hypergrid/DBG/nodbg/comp ...\n", + " done (compiled): elapsed=2.74s, mean_iter=54.90 ms, std=23.00 ms\n", + "Running hypergrid/DBG/nodbg/eager ...\n", + " done (eager): elapsed=2.28s, mean_iter=45.69 ms, std=6.59 ms\n", + "Running diffusion/TB/dbg/comp ...\n", + "DiffusionSampling:\n", + "+ Initalizing target SimpleGaussianMixture with kwargs: {'num_components': 2, 'seed': 2}\n", + "+ Gaussian Mixture Target initialization:\n", + "+ num_components: 2\n", + "+ mixture_weights: [0.4588415 0.5411585]\n", + "\tComponent 1: loc=[-4.77, -4.03], cov=[[7.25, 4.84], [4.84, 7.67]]\n", + "\tComponent 2: loc=[ 6.28, -8.16], cov=[[ 6.55, -1.42], [-1.42, 2. ]]\n", + " done (compiled): elapsed=0.83s, mean_iter=16.62 ms, std=0.53 ms\n", + "Running diffusion/TB/dbg/eager ...\n", + "DiffusionSampling:\n", + "+ Initalizing target SimpleGaussianMixture with kwargs: {'num_components': 2, 'seed': 2}\n", + "+ Gaussian Mixture Target initialization:\n", + "+ num_components: 2\n", + "+ mixture_weights: [0.4588415 0.5411585]\n", + "\tComponent 1: loc=[-4.77, -4.03], cov=[[7.25, 4.84], [4.84, 7.67]]\n", + "\tComponent 2: loc=[ 6.28, -8.16], cov=[[ 6.55, -1.42], [-1.42, 2. ]]\n", + " done (eager): elapsed=0.84s, mean_iter=16.83 ms, std=2.58 ms\n", + "Running diffusion/TB/nodbg/comp ...\n", + "DiffusionSampling:\n", + "+ Initalizing target SimpleGaussianMixture with kwargs: {'num_components': 2, 'seed': 2}\n", + "+ Gaussian Mixture Target initialization:\n", + "+ num_components: 2\n", + "+ mixture_weights: [0.4588415 0.5411585]\n", + "\tComponent 1: loc=[-4.77, -4.03], cov=[[7.25, 4.84], [4.84, 7.67]]\n", + "\tComponent 2: loc=[ 6.28, -8.16], cov=[[ 6.55, -1.42], [-1.42, 2. ]]\n", + " done (compiled): elapsed=0.87s, mean_iter=17.45 ms, std=1.90 ms\n", + "Running diffusion/TB/nodbg/eager ...\n", + "DiffusionSampling:\n", + "+ Initalizing target SimpleGaussianMixture with kwargs: {'num_components': 2, 'seed': 2}\n", + "+ Gaussian Mixture Target initialization:\n", + "+ num_components: 2\n", + "+ mixture_weights: [0.4588415 0.5411585]\n", + "\tComponent 1: loc=[-4.77, -4.03], cov=[[7.25, 4.84], [4.84, 7.67]]\n", + "\tComponent 2: loc=[ 6.28, -8.16], cov=[[ 6.55, -1.42], [-1.42, 2. ]]\n", + " done (eager): elapsed=0.73s, mean_iter=14.52 ms, std=0.61 ms\n", + "Running diffusion/SubTB/dbg/comp ...\n", + "DiffusionSampling:\n", + "+ Initalizing target SimpleGaussianMixture with kwargs: {'num_components': 2, 'seed': 2}\n", + "+ Gaussian Mixture Target initialization:\n", + "+ num_components: 2\n", + "+ mixture_weights: [0.4588415 0.5411585]\n", + "\tComponent 1: loc=[-4.77, -4.03], cov=[[7.25, 4.84], [4.84, 7.67]]\n", + "\tComponent 2: loc=[ 6.28, -8.16], cov=[[ 6.55, -1.42], [-1.42, 2. ]]\n", + " done (compiled): elapsed=1.40s, mean_iter=28.03 ms, std=1.14 ms\n", + "Running diffusion/SubTB/dbg/eager ...\n", + "DiffusionSampling:\n", + "+ Initalizing target SimpleGaussianMixture with kwargs: {'num_components': 2, 'seed': 2}\n", + "+ Gaussian Mixture Target initialization:\n", + "+ num_components: 2\n", + "+ mixture_weights: [0.4588415 0.5411585]\n", + "\tComponent 1: loc=[-4.77, -4.03], cov=[[7.25, 4.84], [4.84, 7.67]]\n", + "\tComponent 2: loc=[ 6.28, -8.16], cov=[[ 6.55, -1.42], [-1.42, 2. ]]\n", + " done (eager): elapsed=1.33s, mean_iter=26.57 ms, std=1.52 ms\n", + "Running diffusion/SubTB/nodbg/comp ...\n", + "DiffusionSampling:\n", + "+ Initalizing target SimpleGaussianMixture with kwargs: {'num_components': 2, 'seed': 2}\n", + "+ Gaussian Mixture Target initialization:\n", + "+ num_components: 2\n", + "+ mixture_weights: [0.4588415 0.5411585]\n", + "\tComponent 1: loc=[-4.77, -4.03], cov=[[7.25, 4.84], [4.84, 7.67]]\n", + "\tComponent 2: loc=[ 6.28, -8.16], cov=[[ 6.55, -1.42], [-1.42, 2. ]]\n", + " done (compiled): elapsed=1.32s, mean_iter=26.43 ms, std=1.12 ms\n", + "Running diffusion/SubTB/nodbg/eager ...\n", + "DiffusionSampling:\n", + "+ Initalizing target SimpleGaussianMixture with kwargs: {'num_components': 2, 'seed': 2}\n", + "+ Gaussian Mixture Target initialization:\n", + "+ num_components: 2\n", + "+ mixture_weights: [0.4588415 0.5411585]\n", + "\tComponent 1: loc=[-4.77, -4.03], cov=[[7.25, 4.84], [4.84, 7.67]]\n", + "\tComponent 2: loc=[ 6.28, -8.16], cov=[[ 6.55, -1.42], [-1.42, 2. ]]\n", + " done (eager): elapsed=1.25s, mean_iter=24.97 ms, std=1.03 ms\n", + "Running diffusion/DBG/dbg/comp ...\n", + "DiffusionSampling:\n", + "+ Initalizing target SimpleGaussianMixture with kwargs: {'num_components': 2, 'seed': 2}\n", + "+ Gaussian Mixture Target initialization:\n", + "+ num_components: 2\n", + "+ mixture_weights: [0.4588415 0.5411585]\n", + "\tComponent 1: loc=[-4.77, -4.03], cov=[[7.25, 4.84], [4.84, 7.67]]\n", + "\tComponent 2: loc=[ 6.28, -8.16], cov=[[ 6.55, -1.42], [-1.42, 2. ]]\n", + " done (compiled): elapsed=1.11s, mean_iter=22.29 ms, std=2.10 ms\n", + "Running diffusion/DBG/dbg/eager ...\n", + "DiffusionSampling:\n", + "+ Initalizing target SimpleGaussianMixture with kwargs: {'num_components': 2, 'seed': 2}\n", + "+ Gaussian Mixture Target initialization:\n", + "+ num_components: 2\n", + "+ mixture_weights: [0.4588415 0.5411585]\n", + "\tComponent 1: loc=[-4.77, -4.03], cov=[[7.25, 4.84], [4.84, 7.67]]\n", + "\tComponent 2: loc=[ 6.28, -8.16], cov=[[ 6.55, -1.42], [-1.42, 2. ]]\n", + " done (eager): elapsed=1.07s, mean_iter=21.49 ms, std=1.56 ms\n", + "Running diffusion/DBG/nodbg/comp ...\n", + "DiffusionSampling:\n", + "+ Initalizing target SimpleGaussianMixture with kwargs: {'num_components': 2, 'seed': 2}\n", + "+ Gaussian Mixture Target initialization:\n", + "+ num_components: 2\n", + "+ mixture_weights: [0.4588415 0.5411585]\n", + "\tComponent 1: loc=[-4.77, -4.03], cov=[[7.25, 4.84], [4.84, 7.67]]\n", + "\tComponent 2: loc=[ 6.28, -8.16], cov=[[ 6.55, -1.42], [-1.42, 2. ]]\n", + " done (compiled): elapsed=1.02s, mean_iter=20.47 ms, std=0.81 ms\n", + "Running diffusion/DBG/nodbg/eager ...\n", + "DiffusionSampling:\n", + "+ Initalizing target SimpleGaussianMixture with kwargs: {'num_components': 2, 'seed': 2}\n", + "+ Gaussian Mixture Target initialization:\n", + "+ num_components: 2\n", + "+ mixture_weights: [0.4588415 0.5411585]\n", + "\tComponent 1: loc=[-4.77, -4.03], cov=[[7.25, 4.84], [4.84, 7.67]]\n", + "\tComponent 2: loc=[ 6.28, -8.16], cov=[[ 6.55, -1.42], [-1.42, 2. ]]\n", + " done (eager): elapsed=0.97s, mean_iter=19.37 ms, std=1.28 ms\n", + "Running discrete_ebm/TB/dbg/comp ...\n", + " done (compiled): elapsed=0.48s, mean_iter=9.62 ms, std=0.33 ms\n", + "Running discrete_ebm/TB/dbg/eager ...\n", + " done (eager): elapsed=0.49s, mean_iter=9.89 ms, std=0.30 ms\n", + "Running discrete_ebm/TB/nodbg/comp ...\n", + " done (compiled): elapsed=0.57s, mean_iter=11.45 ms, std=0.58 ms\n", + "Running discrete_ebm/TB/nodbg/eager ...\n", + " done (eager): elapsed=0.52s, mean_iter=10.44 ms, std=0.42 ms\n", + "Running discrete_ebm/SubTB/dbg/comp ...\n", + " done (compiled): elapsed=0.78s, mean_iter=15.56 ms, std=0.50 ms\n", + "Running discrete_ebm/SubTB/dbg/eager ...\n", + " done (eager): elapsed=0.74s, mean_iter=14.85 ms, std=2.67 ms\n", + "Running discrete_ebm/SubTB/nodbg/comp ...\n", + " done (compiled): elapsed=1.21s, mean_iter=24.20 ms, std=31.62 ms\n", + "Running discrete_ebm/SubTB/nodbg/eager ...\n", + " done (eager): elapsed=0.79s, mean_iter=15.75 ms, std=5.99 ms\n", + "Running discrete_ebm/DBG/dbg/comp ...\n", + " done (compiled): elapsed=0.71s, mean_iter=14.10 ms, std=0.58 ms\n", + "Running discrete_ebm/DBG/dbg/eager ...\n", + " done (eager): elapsed=0.62s, mean_iter=12.41 ms, std=0.47 ms\n", + "Running discrete_ebm/DBG/nodbg/comp ...\n", + " done (compiled): elapsed=0.68s, mean_iter=13.51 ms, std=2.35 ms\n", + "Running discrete_ebm/DBG/nodbg/eager ...\n", + " done (eager): elapsed=1.03s, mean_iter=20.57 ms, std=14.47 ms\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABW4AAAU7CAYAAAC5dzxaAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjUsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvWftoOwAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3Qm8TYX3//9lnslUpowpKkMqNFMRUaQiZAqFDIUMGdNAhVQqyaxRA6mPIaIioZCkQckQ0WQuY+7/8V7f3z7/c0d34h7u6/l47M+9d599zt5n36PPue+z9loZoqKiogwAAAAAAAAAEDEypvUBAAAAAAAAAACiI7gFAAAAAAAAgAhDcAsAAAAAAAAAEYbgFgAAAAAAAAAiDMEtAAAAAAAAAEQYglsAAAAAAAAAiDAEtwAAAAAAAAAQYQhuAQAAAAAAACDCENwCAAAAAAAAQIQhuAUAAEihoUOHWoYMGeyvv/6y9Grz5s1+DqZMmZLo85XYx/zkk08s0jzxxBM2a9asWOt1rJF6zHGd25EjR9rp8nrS91qn285Ebdu2tdKlSyfrvrpfw4YNU/2YAABA2iK4BQAAQIoVLVrUvvjiC2vQoIGlB/EFt9WqVfPzoK9IXXpt6dzqtQYAAJAeENwCAACkU0ePHrVjx46l6DH+++8/O3z4sGXLls1q1qxphQsXtkhx8ODBU77PvHnz+nnQV6QuvbZ0bvVaQ2RTVX1yq4cjXfDfPAAATgWCWwAAgFTy+++/W/PmzS1fvnx2zjnn2D333GN79+4N3X7DDTdYhQoVLCoqKtr99PN5550XqlYNLhN/6qmn7PHHH7eSJUta9uzZ7bLLLrOPP/441n5/+ukna9GihZ199tkealWsWNFeeOGFOC/hnz59uvXq1cuKFy/u2/78889++yuvvGLnn3++r7vwwgvt9ddfj3XpdvhxPfbYY1amTBnffvHixfG2Svjf//5nVatW9e20/cm6ND+4VPy9996zSy65xM/XI488kmALB61XwBSzhcP69esT/D1qm3/++cemTp3q32upVatWvK0SdB5z585tP/zwg910002WK1curxodMWKE3758+XK7+uqrfb1+B3rcmHbu3Gn33XeflShRwrJmzernUs8vpcH78ePHT/ga02ukXbt2Vr58ecuZM6e/dm655RZbt25drMfS6+KCCy6wHDly2FlnnWWVK1e2Z599Nsmv17jE1SpB5/3iiy+2L7/80q655ho/vrJly/q51fGE27dvn/Xu3dvPnc6hnscDDzzgv8tTTc9F5yl4/tOmTYtzuyNHjvg51X83tK3Ca/0u/vzzzzi3nzlzpp9z/S51Hp577rlY+42r3URcr1v9d0mV5aVKlQq9NhYsWODnPHi9p1RiX9daV6NGDStQoIB/KKKK9okTJ8b6b6kCVf33rUiRIv5auPbaa23VqlX+3wf9O0zqvhP6bx4AAKdC5lOyFwAAgHTg9ttvt2bNmln79u091Orfv7+vnzRpkn/t0aOHNWrUyIOxG2+8MXS/uXPn2saNG2OFLGPHjvXQZMyYMR5CKTyoX7++ffrpp3bFFVf4Nt99951deeWVHryNGjXKA4v58+db9+7dvefukCFDoj2mjkn3HTdunGXMmNHDs/Hjx3uAoeN/5plnPKRUgBFfVZmOUwGjQliFKAr04qLnqeer/b355pteqabnoID7ZFi9erV9//33NnDgQA9YFISejN+jLte//vrrrXbt2jZo0CBfd6IKW1U3N2nSxDp16mQPPfSQB+N6XIWJ7777rvXt29cDpOeff94DJoWRl156aShgql69uv++Bg8ebOXKlfNjUJCkYGny5Mmh/ei+Cn43bdqUqIrHxLzGfvvtNytYsKCHoQoOd+3a5ftQkLZmzRoPIEX3Vfit86/ATM9ZYfWePXtC+0vq6zUxdH5atmzpgZ3ur/BS57ZYsWLWunVr3+bff/+16667zrZt22YPP/ywh5sK6HU+9TteuHBhgn2XdW5iBsFx0WNkypQpwW0Unip81b8NnQP9e9N50783/Y7D96ltlixZYn369PHztmXLFn+OCk6/+uorD8gDX3/9tQfReiyd19dee83/m6PwV4F1Ug0YMMCGDx9u9957r792f/31V+vQoYP/XvXvP6WS8rrWz/pvlF43wYcd3bp1s+3bt/t9Azqvb731lp8v/RvV6+22227zf2fJ3XdS/psHAECqiwIAAECKDBkyRGVfUU899VS09V26dInKnj171PHjx/3n//77L6ps2bJRjRo1irZd/fr1o8qVKxfabtOmTf54xYoVizp48GBou3379kUVKFAg6sYbbwytu+mmm6JKlCgRtXfv3miP2bVrV9/3rl27/OfFixf7Y1577bXRttMxFSlSJKpGjRrR1m/Z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unxc2rZt631E1eIiMeFaYl5jCrmmTJliFSpU8MB11apVHvYG+wyoF6v6yKrKsnDhwh7wjRkzxqtQy5cvnyqv1+RSePzuu+96uPfggw/689Dz1QcUH330kb8+EwpHFaimpLI5oH9P+nehNhPq9duxY0f/9zZ06NBYrQ7uuusue+2117z/b48ePfyDEFWebtu2zRYvXmyNGjXyxwg/RvVi1WPpNalKab1u9DvVhzxy+eWX++tH1f4KMxVazpw505YuXRpt32rhodfE8OHDfRvtR/tNjf8uJPV13aBBAxs9erRfmaAAXdWzI0eOjBWSq8dv8+bNbdSoUf66vv766z3M1s+60iH8mJPybwoAgLREcAsAAJAGmjVr5sFtfNW2otsUJihY0AAeBROqdAwfGqYqRg0eUhg0cOBA307VjgrKwodhJURhiALOp556ygMahWn9+vXzqtRgiFpyKBDU5cw6Lj1fBVMaXKVqucRcdp5aFNyInp/6tirQ0WXXKakaVACpy/0Vrv3777/e+/VEA66SS0HZV1995b9jBaYK0BQUq3JUwXh4xaCen1oCxNV6I7mvMT1XBYYK8fT4qmZUpa5+r+FUTatwVG1AFP7q963XwKBBg0K9RFPj9Zoc+rBgyZIlNmLECO+/qmBb50lD+DT0KjUqSBOrffv2/lWBqqpZtW+1lNBArPDXkMJHVcLq/Ku6VOdflacKFPV604cH4TS0rF27djZkyBAfHKcgV4Gngurwx1RArt97p06dPPzUa3js2LEekIZTGwGdN4WYaumi4P6ll16yAQMGJPr1lRqva/171QcvOl/6cEBtEBR4q+dwcC4DOk49rob0PfPMM35OdDWBHi/8mJPybwoAgLSUISoqKipNjwAAACAduv322/2yY1V4xRyQo3UKEBQoqDIuLagKUJXBjRs39qArLQTnQdWFGkyEE1NYqh6yeu0AqU2BtwJchcMKm08HakGhDyJUvayqXQAATidU3AIAAJwiGgikakNdqq/Lk1UNF99U81NJw4ZUXaeKSfUA1SXuqlbTJey6RBunB10Wrurfvn37pvWh4Aygfrnqxat2Annz5rUff/zRq9b1fcxK10ih9gdffPGFXXrppV5RreegKmtVdIf36gUA4HRBcAsAAHCK7NixIxSCqKdst27dLBLocmlVt6qNgXrPqh9mzZo1/RJpXTqP04N+V2pRAKQGtUlQOwG1HVAFvvrEqvJdH/Jo6F8k0n9b1bNYvZX1wVOhQoWsfv363mYiGMAHAMDphFYJAAAAiEi0SgAAAEB6RnALAAAAAAAAABEmY1ofAAAAAAAAAAAgOnrcIt06fvy4/fbbb5YnTx7LkCFDWh8OAAAAAAAAznBRUVHei71YsWKWMWPCNbUEt0i3FNqee+65aX0YAAAAAAAASGd+/fVXK1GiRILbENwi3VKlbfAPRRNoAQAAAAAAgJNp3759XkgY5FIJIbhFuhW0R1BoS3ALAAAAAACAUyUxbTsZTgYAAAAAAAAAEYbgFgAAAAAAAAAiDMEtAAAAAAAAAEQYglsAAAAAAAAAiDAEtwAAAAAAAAAQYQhuAQAAAAAAACDCENwCAAAAAAAAQIQhuAUAAAAAAACACENwCwAAAAAAAAARhuAWAAAAAAAAACIMwS0AAAAAAAAARBiCWwAAAAAAAACIMAS3AAAAAAAAABBhCG4BAAAA4DQ3duxYu+yyyyxbtmzWuHHjE24/e/Zsq1q1quXKlcuKFStm48aNC922fft2f4yCBQtaoUKF7M4777Tff//9JD8DAAAQU+ZYa4B0pk2bNpYlS5a0PgwAAAAgyWbMmOFfFb4OHDjQFi5caNu2bUvwPvPmzbMuXbrYq6++atdcc43t27cvWjCr2zJkyGBbtmyxqKgoa9mypfXo0cPefPPNk/58AADA/4+KWwAAAAA4zTVp0sSrZFUheyKDBg2ywYMHW61atSxTpkyWP39+q1ChQuj2TZs2WdOmTS137tyWJ08ea9asmX377beh20ePHm0lS5b020qXLm0TJkw4ac8LAID0jOAWAAAAANKJf/75x1atWuVVtgprixQp4sHszp07Q9v07NnT3n77bdu7d6/t2bPH3njjDWvQoIHftmHDBq/s/eijj2z//v22YsUKq169eho+IwAAzlwEtwAAAACQTuzevdvbH0yfPt3mz59vP//8s7cNa9WqVWibq666yv744w+vxC1QoIDt2rXLw1pRha7uv379ejt48KCdc845Vrly5TR8RgAAnLkIbgEAAAAgnVD7A+nevbuVKlXKf37kkUfs448/9mrc48ePW506dTy8PXDggC9XX3213XTTTX6/cuXK2dSpU30YmkLbunXr2tdff53GzwoAgDMTwS0AAAAApBNnnXWW96fV8LGYVEmr6loNJVOwmzNnTl+6detmX3zxhf3111++nfrfLl682AeaValSJVq1LgAASD0EtwAAAABwmjt27JgdOnTIv6pqVt8fOXIkzm3vvfdee+6552z79u3e7mDYsGF2ww03ePWthpudd9559sILL/hjaNH3JUqU8Nt+/PFHW7Bggd8va9asfp/MmTOf8ucLAEB6QHAbAdq2bWtDhw5N0n02b97sn5IHlyV98skn/rOGB0TC8QEAAAA4dR577DHLkSOHPf744/bBBx/492pjIJ06dfIl0K9fPw9qVS177rnn2r///us9bwPvv/++rV692ooXL25Fixa1lStX2uzZs/02hcGDBg3yNgkFCxa0RYsW2ZQpU9LgGQMAcObjo9FEiOsyonBt2rTxNyvh26lpf7FixeyOO+6w4cOHW7Zs2U7BkQIAAABIj1RoEV+xxbhx46L9rL9VRo0a5UtcLrzwQh9cFpdKlSrZ8uXLU+GIAQDAiRDcJsKOHTtC37/11ls2ePBgv0QooE+zA5MnT7Z69erZ0aNHbe3atdauXTvLlSuXPfroo6f8uAEAAAAAAACcnmiVkAhFihQJLfny5fPK2pjrwpv9a50uOWrYsKHdeuutfplRSunypEsuucSyZ89ul112ma1ZsybO7T7//HO/5Enb1ahRw9atWxft9ldeecWPTUMGbrvtNhs9erQfc0pt27bN7rrrLitQoIAH1TrGFStWhG5/6aWXfAKt+mBdcMEF0S7FEp3Tl19+2c+Zjq1ixYo+AOHnn3+2WrVq+WNeccUVtnHjxtB9VFFQtWpVv1/wnO68885420UcPnzY9u3bF20BAAAAAAAAIhHB7Um0YcMGn7aqADUl/vnnHw80FXiuWrXKA8vevXvHue1DDz1kI0eOtC+//NLOPvtsD45V/RuEuupt1aNHD++NW6dOHe+BlVIHDhyw6667zn777TfvfaVK4z59+vhQBJk5c6bvs1evXvbtt9/afffd55XIOjfhVJXcunVrP7YKFSpYixYtfNv+/fvbV1995dt07do12n0U7M6YMcP7eM2bN8/ve//998d5nGpZoZA9WBT2AgAAAAAAAJGIVgmprHnz5t4zStNcVeGpwFXBY0q89tpr9t9//9mkSZO8qvSiiy7yCtfOnTvH2nbIkCEeyMrUqVN9+quC06ZNm9rzzz9v9evXD4W+559/vi1b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FEJOnDjR+9MqpEyuhCYHJuU56HGS+nwSQ20Q5H//+18otNaioDXoP6uWCGprcM8993irB93erl07D6TjOta4nnti9hP+ewAAAAAAADhTffvtt17EWKhQIc9TNNPpRNSSsnz58t5GUzOQNAspKbcj7Zy2wW1MGrqlsFRU3Sk7duwI3R4+qCzc8uXLQ9/v3r3bNmzYYBUqVEjRsegfjsLjgwcPJvsxFAJnyZIlzuOLKXyb4OfgOeirKlxV2RpQP9/UON+qYNVjB6F1sAS9YzUg7Morr7QuXbrYJZdc4reFV8mqz+w555xjK1eujDbQbM2aNUnaDwAAAAAAQHqgrKhp06aJvupcOVLLli3tmWeesV27dtn1119vjRo18mHxibkdaSuznWb+/vtvu/POO72KUwOo8uTJY1999ZU99dRT/sKSHDlyWM2aNW3EiBFWunRp++uvv3zAVVyGDRvmQ8QUIGpolz6x0DCvpNCQtJ07d4bC1bFjx/qQsltuuSXZz1PtANq3b+8DysKPL6gmDqehXnr+Om4NJXv77be9QlVUlVyuXDlr06aNb7N///7QcLKUVOLqvPfu3dsralUVq09kNFhM1c86du1P4apaGsyfP9/KlClj06dP99BY3we6devmQ8O0rULm559/3s9hcGyJ2Q8AAAAAAMCZ4tdff/UCuDfeeMNzHV25rJxLuY+uPFcbzc2bNyfqsZTFqMVnw4YN/edBgwZ59qJiO60/0e1IW6ddcKuwrkaNGv5JgKo31QJBlZcdO3a0hx9+OLTdpEmTPNxV31O9oBVaqvdsTAp3e/ToYT/99JNVqVLF++ZmzZo1Scc0b948K1q0aChoVACp8LRWrVopeq5PP/20B8C33nqrP26vXr28L2xMWr9q1Sp75JFHfLtRo0Z52XzQM1Yl7x06dLDLL7/cypYt64+rUFntJlLi0UcftbPPPtuD119++cXOOuss7zUc/B46derklc7NmjXzILZ58+ZefTt37tzQY/Tt29dD79atW/ux3nvvvX7s+j6x+wEAAAAAADhTKOcaP368ZyVr1671PER5WFCIlxTffPONVa1aNVrFrq5u1noFsye6HWnslIxLQyyLFy+OKlWqVJrse+nSpT697ueff4644/vvv/+izj///KiBAweesil+LCwsLCwsLCwsLCwsLCwsLJGyBO69996oSpUqRRUoUCBq69at0TKNTZs2+ba7d+9OMPu4/vrro55++ulo626++eaoRx99NFG34+TlUfp6IqddxS2SbubMmf7JjBpN//zzz15hfNVVV3kLhbS2ZcsWH1x23XXXecsJtZnYtGmTtWjRIq0PDQAAAAAAIM3oqmVV3nbt2jXZc36UB8W8els/64rtxNyOtHXGDCdD/NTXVv/Y1cKhbdu23jLh/ffft0ignr1qqK1jUpi8bt06W7hwoVWsWDGtDw0AAAAAACBNqK+tWoAqx9H8ILXITA7Nh1Iby4Bajn733XdWqVKlRN2ONHYSKn6RCCppf+aZZ6IiVaQfX2qgVQILCwsLCwsLCwsLCwsLC0ukLfLggw9GXXvttVHHjh2Lev7556PKly8ftX///qjjx49HHTx4MOqHH37wbXfu3Ok/a31ctF3OnDmj/ve//0UdOnQoasiQIf5YR48eTdTtSNtWCRn0P2kdHgNpYd++fZYvX760PgwAAAAAAIAQDXVv2bKlV8IGLRIaN25s+fPntyFDhliZMmVi3UdtJ0uXLm1Lliyx+vXr+7D78Baaffr0sW3btvmw94kTJ/pV2Ym9HScnj1JLirx58ya4LcEt0q2k/EMBAAAAAAAATmUeRY9bAAAAAAAAAIgwBLcAAAAAAAAAEGEIbgEAAAAAAAAgwhDcAgAAAAAAAECEyZzWBwCktR0DBtiBbNnS+jAAAAAQAYqNHGljx461KVOm2Lp163wy96xZs+Ldvm3btvb6669b1qxZQ+sWLFhgV1xxhX+fO3fuaNsfPnzYKlasaN98881JfBYAAOBMQHALAAAAAGGKFStmAwcOtIULF9q2bdtOuH2XLl1szJgxcd524MCBaD9XrlzZ7rrrrlQ7VgAAcOaiVQIAAAAAhGnSpIk1btzYChUqlKqPu3LlSvvuu++8SjcwevRoK1mypOXJk8dKly5tEyZMSNV9AgCA0xfBLQAAAACkwLRp06xAgQJ20UUX2ahRo+z48eNxbjdx4kRvvaCKXtmwYYNX9n700Ue2f/9+W7FihVWvXv0UHz0AAIhUBLcAAAAAkEzdu3e3H3/80f78808PZp999llfYvr333/tzTfftA4dOoTWZcqUyaKiomz9+vV28OBBO+ecc7yVAgAAgBDcAgAAAEAyVatWzQoXLuwhbM2aNa1fv3721ltvxdpuxowZljNnTmvQoEFoXbly5Wzq1Kk+DE2hbd26de3rr78+xc8AAABEKoJbAAAAAEglGTPG/SeWete2adPGMmeOPh+6adOmtnjxYvv999+tSpUq1qpVq1N0pAAAINIR3AIAAABAmGPHjtmhQ4f8q/rV6vsjR47Eua0qafft2+ctD7766isbMWKE3X777dG2USuFZcuW2T333BNr/YIFC7xNQtasWS137tyxgl0AAJB+pVlw+8knn1iGDBlsz549abZ/TW1NC0OHDrWqVasmevvNmzf7uUqry6Zq1aplU6ZMSZN9AwAAAKfaY489Zjly5LDHH3/cPvjgA/9ebQykU6dOvgTU5qBkyZKWJ08ea9mypXXp0sV69eoV7fHU+/aaa66x888/P9p6hcGDBg3yNgkFCxa0RYsW8b4bAACkLLj9448/7L777vM3KNmyZbMiRYrYTTfdZF988YWllrZt23pYmdAS13Z6w1OvXj375ptvUu1YAAAAAKQfKrRQBW34osIPGTdunC+Bzz77zItRDhw44BW0ffr0idUu4amnnrJPP/001n4qVapky5cv94pdPYa2UbsEAACAZAe3uvRn7dq13kh/w4YNNnv2bK/K3LVrV6qdVU1i3bFjR2iRyZMnx1onCmqDdR9//LFfXtSwYcNUOxYAAAAAAAAAiOjgVp8EL1261J588kmrXbu2lSpVyqpXr279+/cPTUiN69J+3U/rgk+qA59//rl/qpw9e3arUaOGrVu3ztfny5fPK3mDRc4666xY6ySo+tWiFgR9+/a1X3/91f78888Utwjo3r27f2peoEABf3x9+h5u69at1qhRI+9HlTdvXh8uoMEC4dTnSpc/6fKp9u3be4+smBRKV6xY0c9DhQoV7MUXX4y1zQ8//GBXXnmlb3PRRRdFO5e6pErnJ9ysWbNClcnhl32dffbZfiwdOnTwqbdJadsQn/Xr1/vvX+dAj61LwTZu3Oi3qS/YsGHDrESJEv670v7mzZsXum/welF/MN1Pl6Jdfvnl/qHAl19+aZdddpmfXwX04b9TVVs3btzYHnnkEX9O2rcqwePrP3b48GGvZghfAAAAAAAAgDMiuFWApkWhoIKwlHrooYds5MiRHtApfLv11lvt6NGjyX48XaL02muv2XnnnedtE1JKVcW5cuWyFStW+CVOCiA1QEB0yZSCQ1Ua67ImrVdY2axZs9D9FUYOGTLE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AAAAIOJC2/vvv99bJHz00UeWL1++E95HvWnbtGljmTP//91hlyxZYn/++adddNFFVqRIEa/a3bdvn3+vx5amTZva4sWLvUVClSpVrFWrVhYJ6HELAAAAAAAAIKJ07drVPv/8c1u0aJHlz5//hNurKnfZsmU2adKkaOubNWtm9erVC/2sbdq1a+dVtQULFvT7bd261a6++mrLmjWr5c6dO1rwm5bSdcWtptJt3rz5lO/3k08+8X3v2bMn0ffRRLsxY8ZYWhg6dGisSXwAAAAAAADAybBlyxZ78cUXPVQtVaqUh6laOnXq5Lfra/B9+FCya665xs4///xo63PkyOHVtcFSoEABz+X0fZYsWezIkSM2aNAgb5OgIFdB8ZQpUywSnNL4WOGfekb4jjNn9hOlZr/Nmzf328L7VCio1C9JtF4nTxPh1I8iPGVXafPTTz9t7733nv3yyy/ex6Js2bJ25513+rS4xCTyAAAAAAAAACKDwtqoqKh4bx83blysdU899VSiHlsDysKLKStVqmTLly+3SHTKK25Vmrxjxw6vdJ07d67Vrl3bevToYQ0bNrRjx45F23bYsGG+rcqVX3vtNfvss8+se/fuodt37dplNWvWtMmTJ1vv3r1txYoVXkI9ZMgQL3d+/fXXT/XTAwAAAAAAAIDTL7jNli2blyIXL17cqlWrZg8//LC9//77HuLGLEPOkydPaFsFvK1bt7bVq1eHbtd9FeoqsFVvClXvVqhQwUNghbZdunRJcYuAqlWr2vTp070CWE2Q77rrLtu/f39om8OHD3uYfPbZZ1v27Nm9H8aXX34Z7XHmzJnjZdoqzdbziKs9g/prXHvttb7Nueee64/5zz//RNtG+23RooWXhhcrVsyef/750G16TJV5h0+906cHWqfWDIHZs2db+fLlQ8eiCuiktm2Ii+5/7733emW0zsPFF19sH374Yej2d99915tA6/evczlq1Kho99e6xx57zH/Hen76ZEWvCzWPbtSoka/TJyBfffVV6D56vZx11lk+VVDnV/utU6eO/frrr3Eeo35XqtAOXwAAAAAAAIBIFBE9bq+//nqf2KZ2B/HZvn27B4E1atTwn48fP25vvfWW3X333R7sxkWBZEpt3LjRg0HtW8unn35qI0aMCN3ep08fDyUVgCpUPu+88+ymm27yamBRiKhpdTfffLOHqh06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AAAAAEhrXbp08blYutJaYaqo8lahZzj9nCdPngQfS/1tdUV4eJuEmPQY2qdacarYM60R3KYzasegTwxUQq52C2q/oIpaAAAAAAAAIFIcOXLE7rnnHm+1OW3aNG8fKmr/qerbgNqGat6T2qcmRK01VeGqat2EqDnBoUOH/OrztJaug9shQ4Z4/4r0RBP3fvvtN38BqqeHeulmzpw5wfvUqlUrYqbphdM/3OS2SQAAAAAAAEDk6tevn1fXTpgwwVt9aq6SqmbvvvtuW7Rokc2ZM8cOHz7st2mQ2bXXXpvg402cONGzpJgztt566y2fWXX8+HHPmdTjVnO2NLMpraXrHrdI35LSUwQAAAAAAACnxrx586xly5ahvraiosL8+fPb5MmTvUdtnz59bNu2bR6wKpStUKGCb7dkyRKvqlXIG1BF7sUXX+wBbdmyZaPta8SIEfbyyy/7EDQFttWrV7fHHnss2gC0tMqjCG6RbhHcAgAAAAAA4FRiOBkAAAAAAAAAnMYIbgEAAAAAAAAgwhDcAgAAAAAAAECEyZzWBwCktefy5bPsaX0QAAAAAADEoXdUlB0+fNi6du1qCxcutL/++suKFy/ug5nuueeeWNsnZtvt27fb/fff70OcMmTIYLVr17axY8faOeecc4qfHYCEUHELAAAAAAAQwY4dO2ZFixb1MFaDjaZMmWK9evWyjz76KFnbdunSxb9u2bLFNm3a5GFvjx49TulzAnBiBLcAAAAAAAARLFeuXDZs2DArV66cV8jWrFnTq2SXLl2arG0V1jZt2tRy585tefLksWbNmtm3334bun306NFWsmRJv6106dI2YcKEU/ZcAfz/CG4BAAAAAABOI4cOHbKVK1da5cqVk7Vtz5497e2337a9e/fanj177I033rAGDRr4bRs2bLCBAwd6he7+/fttxYoVVr169ZP6fADEjeAWAAAAAADgNBEVFWUdOnSw8uXLW5MmTZK17VVXXWV//PGH5c+f3woUKGC7du3ysFYyZcrk91u/fr0dPHjQ+94mJiAGkPoIbgEAAAAAAE4DClQ7d+5sP/74o82aNcsyZsyY5G2PHz9uderU8fD2wIEDvlx99dV20003+e1qsTB16tTQsLK6deva119/fcqeI4D/X4Yo/UsG0iE1ac+XL589ambZ0/pgAAAAAACIQ+//F9sovrn//vtt+fLl9vHHH3u1bHwS2vavv/6ywoUL26+//molSpTwdfpePW3//PNPK1SoUGhbVdwOHjzY5s2bZ+vWrTupzxNIb3nU3r17LW/evAluS8UtAAAAAABAhOvatat9/vnntmDBggRD2xNtq2D2vPPOsxdeeMH732rR9wpxdZsqdHU/hbZZs2b1AWaZM2c+yc8OQFzSZXCrqYqbN28+5fv95JNPfN9q/J1Ymt44ZswYSwtDhw61tm3bpsm+AQAAAADA/9myZYu9+OKLHqqWKlXKw1QtnTp18tv1Nfj+RNvK+++/b6tXr7bixYtb0aJFfXjZ7Nmz/bYjR47YoEGDvE1CwYIFbdGiRTZlypQ0euZA+nZKPjJR+Kf+KL7DzJm98bUaWzdv3txvC+/JoqBS/5ERrdd/KOrXr28jR46M9imRyoqffvppe++99+yXX36xnDlzWtmyZe3OO++0jh07nvDTJwAAAAAAgNOBAtiEOl2OGzcu0dvKhRdeaPPnz4/ztkqVKnmLBQDpqOK2Xr16tmPHDq90nTt3rtWuXdt69OhhDRs2tGPHjkXbdtiwYb7t1q1b7bXXXrPPPvvMunfvHrpd0w5r1qxpkydPtt69e9uKFSv8EoAhQ4Z4w+zXX3/9VD0tRKijR4+m9SEAAAAAAAAAkR/cZsuWzYoUKeJl+NWqVbOHH37YS/MV4sYsuc+TJ09oWwW8rVu39hL+gO6rUFeBbbt27bx6t0KFCh4CK7Tt0qVLilsEVK1a1aZPn+4VwGoYfNddd9n+/ftD2xw+fNjD5LPPPtuyZ8/uExi//PLLaI8zZ84cO//88y1Hjhz+POJqz7Bs2TK79tprfZtzzz3XH/Off/6Jto3226JFC7+0oVixYvb888+HbtNjqv1C+IRHtWLQOrVmCOiSh/Lly4eORRXQSW3bEBc1Ur733nv9PKih8vXXX29r164N3b5x40Zr1KiRV07r+C+//HJbuHBhtMdQSN+gQQM/tjJlyvjvMGaLiBPtJ/idTZo0ySuv9XqL+Qmjfmeq1A5fAAAAAAAAgEiUpj1uFb5VqVLF2x3EZ/v27fbhhx9ajRo1/Ofjx4/bW2+9ZXfffbcHu3FRIJlSChxnzZrl+9by6aef2ogRI0K39+nTx959910PQBUqq7H3TTfd5NXAwUTGJk2a2M033+yhaocOHaxfv37R9qGJjLqPtvvmm2/8eS1dutSbiIdTSwiF09pP//797cEHH/RG4YmlcPeOO+6wxo0b+7Hcd999NmDAgBSfIwWjClx37tzpIfWqVas8lL/hhhtC5+HAgQN+DhTWrlmzxp/vLbfc4sF7QMH8b7/95kGzzun48ePtjz/+SNJ+5Oeff7YZM2b4Y4QH2YHhw4d7CB8sCsoBAAAAAACASJTmw8lUKRuzErVv375enakKTE01VBA7evRov+3PP//0KtELLrgg2n0uvfTSUMNt9c5NKQXEqgS++OKL7Zr/j707gbOp8P8//rGTXfaQkqIiSZQ2UrYUqagkCi1CQvatEirRok2WaFFaLC0UUZGiIomkxVq02ctu/o/35/c993/vLMw+d8zr+XgcM/ecc+89986dMfO+n/P5XHKJtW3b1j7++GPfporY5557zgNV9d9Vb5gXX3zRj3fChAm+j7ar8nPMmDF+rG3atIkz6EvXVyVt9+7dvRq2bt269tRTT9mUKVN8qmPgoosu8tBX1btdu3b1EFa3m1jqdaNj0P3po6qHU2Po2IIFCzx8fvPNN61WrVr+GNSLuEiRIvbWW2/5PgrmFRSrR462Dxs2zJ+XoOn5mjVrPNTV86dwXoHs+PHjfXplUu4naKCuKulzzz3Xg+7YAb5Cb1XuBovCdQAAAAAAACAaZXhwq2rK2AHb/fff7xWTqkINwlJVXB4+fDi0T+zrTJ8+3a+jis7w0C+5dKq+WjYENGUxqAJVNa56qCpQDeTKlctq165tP/zwg1/WR/XhDT/OCy+8MOI+VDmqcDgInLXo+BUar1u3LsHr6XJwP4mhSZJqURBOx5pSOn5V1GrKZPhj0LHrOQpCblUnK9xW0KrtCmuDilsdmwbWKbANqHo5fLhcYu4naMBeokSJBI9X7RPUZiF8AQAAAAAAAKJRzow+AAWQ6msarnjx4h7eiaor1etUYaUqL9VeQQGgwr9wFSpU8I8KW1PatzUIYsMpgFWgKkHv1NjhcXgIfawJjqLbUzVq+OC12I8nIcH9ZM+ePc79xR7MFV84npjjS8zxK9AO76Ub0NcoCOE1qVIVsvqaqipZFcOqjj3acYSvT8z9SP78+VP8mAAAAAAAyEzU5lF/e6vVZHAWq85uTsjDDz/sLQq3b9/uZ8SqLaQGyid2O4AsUnE7f/58PwX+uuuuO+p+OXLk8I+qpFVQ2apVK3vllVf8h1JGUACZO3du70cbHpZ+/fXXVrVqVb+sCtMvv/wy4nqxL+sH6qpVq/z2Yi+6/YSup8vBD+GgwlQDvgKx+7tq39iD03SsKaXjV99ZVczGPn6F77Jw4UJvy3Dttdd6uwQNnQtvjaFjO3TokPe/De9VGx6+J+Z+AAAAAADIatauXeutGdVOUTNgVOymAeH6Ozs+OltZhVWa5aMWgj179vRMJpgfc6ztAI7T4Hb//v0evils1ZCt4cOH+w+TZs2a+XCqcLt37/Z9FUYuXbrU3zlSQKcesKLrajCZeqJOnDjRWyrolHn9gPniiy9CQW9aUWXn3Xff7cc1Z84cW716tXXq1Mn+++8/69Chg+9z1113+TH16NHD2wG89tpr3hYhdi9fHe8999zjYetPP/3kvV/Vxzbc559/bo8++qj/QH7mmWe81+u9997r21TBqpYMegdMx/HZZ5/ZwIEDI66vql5VKOv+dBsa4BUcS0oGuV1xxRVeCa2hZ6qqVSC7ePFiv/8gGFa4quFzenwrVqzwnr5B5XIQ3Op27rjjDv9aK8DV53pcwbEl5n4AAAAAADgeaT6LMpFgSLnOYFWB04MPPuhzXurXr+/ZSt68eW3QoEHe5lFFVPFRy0G1UlRhlf7m1jwfFaL9+uuvidoO4DgNbhVw6nR39Y5Vib3aHmgQ18yZM+MErYMHD/Z9y5Yt6z98FJTqB5R6nIo+KuRT4KuBW+rXqh8qQ4cOtdatW/ugq7SmoFTvOumHmH5gqkpUoWLQm1WtDt5++2179913fUCXBoQpcA6nAVqffvqpB7YagKahWvohq8ceTu9wqc+rtj/00EP2+OOPey/cgMJr/SDV4C4FuhoAFk6tKDTESwGq7lOD0wYMGBDq+5pc+iH+wQcf2KWXXmq33367D0/T4DMFq6VKlfJ99K6fnhOF7ldffbUfd3g/W9EwNu2v21FlrkJwtbzQfzqJvR8AAAAAAI5H5cuX99YFykAUygYD3fV3vQrZatSoEdH2UWcAa318lJmoUE5FU5ojNGnSJM9ezjrrrERtB5C+ssWkRrPTTEZBoN5FUoicValnjcJkvXOXEAXhCkdjVwqntc2bN/t/TPPmzbMGDRqk2f3s2rXLChcubA+Z2f9FxAAAAAAARJde/4ttdDatztrVmcw6q1V/N+tv5iZNmlivXr1C+2u4u85ajX02rqjoSwVjKoJTNqIAWEVearGQmO0AUi+PUjuSQoUKRW+PW6SfZ5991vvc6vQGnUqhH8Lt2rWzaKBex2oRoTBdLRBUTatQXRW2AAAAAADArHPnzj4nSC0IFdqKglWFP+F0WWexxkftFWbPnu1tFNVyQYPNVGWr1oaJ2Q4gfRHcZhFqx6CewjplQu0W1H5BFbXRQO/o9e/f30+9UKsEDVz75JNP/BQPAAAAAACyOoWoah2o4d9qN6h2iqJ2iOEDyvX3tebfqJ1kfNQC4YYbbrBKlSr58Pd69er5bQT9c4+1HUD6ypLB7ZAhQ6xIkSKWlajX7O+//2779u3zd8506kPOnDmPeh39gNZAsLSmvrfff/+9D3f7448/fMjcySefnOb3CwAAAABAZtC3b1+vrh0/fry3Przppptsz549dsstt/hZrJoLo6Hw2qZBZgmdwaoWCpqBs2HDBlPnTA1D1wyhoE/usbYDSF9ZssctkNSeIgAAAAAAZAQNe2/Tpk2or62oyEqDwDU8TMVPvXv39nkxGgY+YcIEq1Kliu+3cOFC74GrkDeoyFUIPG3aNNuxY4cPR+/atasvidkOIH3zKIJbZFkEtwAAAAAAAEhPDCcDAAAAAAAAgEyM4BYAAAAAAAAAogzBLQAAAAAAAABEGYJbAAAAAAAAAIgyBLcAAAAAAAAAEGUIbgEAAAAAAAAgyhDcAgAAAAAAAECUIbgFAAAAAAAAgChDcAsAAAAAAAAAUYbgFgAAAAAAAACiDMEtAAAAAAAAAEQZglsAAAAAAAAAiDIEtwAAAAAAAAAQZQhuAQAAAAAAACDK5MzoAwAySkxMjH/ctWtXRh8KAAAAAAAAsoBd/8uhglzqaAhukWX9888//rF8+fIZfSgAAAAAAADIQnbv3m2FCxc+6j4Et8iyihUr5h83btx4zG8UAP//nUG92bFp0yYrVKhQRh8OkGnwvQMkHd83QNLxfQMkHd83SG+qtFVoW7Zs2WPuS3CLLCt79v9r8azQlh/OQNLoe4bvGyDp+N4Bko7vGyDp+L4Bko7vG6SnxBYQMpwMAAAAAAAAAKIMwS0AAAAAAAAARBmCW2RZefLksSFDhvhHAInD9w2QPHzvAEnH9w2QdHzfAEnH9w2iWbYYdcQFAAAAAAAAAEQNKm4BAAAAAAAAIMoQ3AIAAAAAAABAlCG4BQAAAAAAAIAoQ3ALAAAAAAAAAFGG4BbHtWeffdZOOeUUy5s3r5133nm2cOHCo+7/6aef+n7a/9RTT7Xnn38+3Y4VyIzfN5988olly5YtzrJmzZp0PWYgI3322Wd29dVXW9myZf31P2PGjGNeh/9vkNUl9fuG/2+Q1Y0YMcLOP/98K1iwoJUsWdJatGhhP/744zGvx/83yOqS873D/zmIJgS3OG698cYb1r17dxswYIAtX77cLrnkEmvSpIlt3Lgx3v3XrVtnTZs29f20f//+/a1bt2729ttvp/uxA5nl+yagX362bNkSWipXrpxuxwxktH///dfOOeccGzt2bKL25/8bIOnfNwH+v0FWpQD2nnvusS+//NLmzp1rhw4dsoYNG/r3UkL4/wZI3vdOgP9zEA2yxcTExGT0QQBpoU6dOlazZk177rnnQuuqVq3q77DpXbfY+vTpY7NmzbIffvghtO6uu+6yFStW2BdffJFuxw1kpu8bvRtdv3592759uxUpUiSdjxaIPqrGmD59un/PJIT/b4Ckf9/w/w0Q6a+//vLqQYVSl156abz78P8NkLzvHf7PQTSh4hbHpQMHDtg333zj76SF0+XFixfHex398hJ7/0aNGtnXX39tBw8eTNPjBTLr903g3HPPtTJlyliDBg1swYIFaXykQObG/zdA8vH/DfB/du7c6R+LFSuW4D78fwMk73snwP85iAYEtzgu/f3333b48GErVapUxHpd3rp1a7zX0fr49tepFLo94HiXnO8b/SIzbtw4P+XunXfesTPOOMN/sVHvQgDx4/8bIOn4/wb4/3TSbI8ePeziiy+2s88+O8H9+P8GSN73Dv/nIJrkzOgDANL61LvYP6hjrzvW/vGtB45nSfm+0S8xWgIXXnihbdq0yUaNGpXgqUcA+P8GSCr+vwH+vy5duth3331nixYtOua+/H8DJP17h/9zEE2ouMVxqXjx4pYjR444VYJ//vlnnHedA6VLl453/5w5c9qJJ56YpscLZNbvm/hccMEF9tNPP6XBEQLHB/6/AVIH/98gK+ratav3rdVp2+XKlTvqvvx/AyTveyc+/J+DjEJwi+NS7ty57bzzzvOpkeF0uW7duvFeR++ixd7/o48+slq1almuXLnS9HiBzPp9Ex9NLdbpRQDix/83QOrg/xtkJaqUVbWgTtueP3++nXLKKce8Dv/fAMn73okP/+cgo9AqAcct9a5p27at/2KiX1rUo2bjxo0+SVX69etnv/32m02ZMsUva/3YsWP9ep06dfJm/hMmTLCpU6dm8CMBovf75oknnrCKFSvaWWed5cPNXnnlFe8FpQXIKvbs2WM///xz6PK6devs22+/9aEXFSpU4P8bIBW+b/j/BlndPffcY6+99prNnDnTChYsGKqkLVy4sOXLl88/5/8bIHW+d/g/B9GE4BbHrdatW9s///xjDz74oG3ZssWbj3/wwQd28skn+3atUyAV0Dtv2n7ffffZM888Y2XLlrWnnnrKrrvuugx8FEB0f9/oF5levXr5Lzr6xUe/3Lz//vvWtGnTDHwUQPrSdO769euHLusPZGnXrp299NJL/H8DpML3Df/fIKt77rnn/GO9evUi1k+aNMnat2/vn/P/DZA63zv8n4Noki0m6E4OAAAAAAAAAIgK9LgFAAAAAAAAgChDcAsAAAAAAAAAUYbgFgAAAAAAAACiDMEtAAAAAAAAAEQZglsAAAAAAAAAiDIEtwAAAAAAAAAQZQhuAQAAAAAAACDKENwCAAAAAAAAQJQhuAUAAACyqIoVK9oTTzyRpOusX7/esmXLZt9++61lhB9//NFKly5tu3fvPua+K1eutHLlytm///6bLscGAACQmghuAQAAgAymIPRoS/v27Y95/RkzZqT6cel+W7RoEbGufPnytmXLFjv77LMtIwwYMMDuueceK1iw4DH3rVatmtWuXdvGjBmTLscGAACQmghuAQAAgAymIDRYVAFbqFChiHVPPvmkRYscOXJ4xWvOnDnT/b43b95ss2bNsttuuy3R19G+zz33nB0+fDhNjw0AACC1EdwCAAAAGUxBaLAULlzYK2jD17322mtWqVIly507t51xxhn28ssvR7Q7kGuvvdavF1z+5ZdfrHnz5laqVCkrUKCAnX/++TZv3rxEH9PQoUNt8uTJNnPmzFDl7yeffBKnVYLW6fKHH35o5557ruXLl88uv/xy+/PPP2327NlWtWpVD6Jvuukm+++//0K3HxMTY48++qideuqpfp1zzjnH3nrrraMe07Rp03w/tT8IbNiwwa6++morWrSo5c+f38466yz74IMPQtsbNWpk//zzj3366aeJfuwAAADRIP3fJgcAAACQaNOnT7d7773XK3GvuOIKe++997yKVOFl/fr17auvvrKSJUvapEmTrHHjxl4RK3v27LGmTZvasGHDLG/evB7CKuBUj9gKFSoc83579eplP/zwg+3atctvW4oVK2a///57gkHv2LFj7YQTTrBWrVr5kidPHg+ddSwKlp9++mnr06eP7z9w4EB75513vBq2cuXK9tlnn9ktt9xiJUqUsMsuuyze+9A+tWrVilintgkHDhzwbQpuV69e7UF1QGG3wt6FCxd6oAwAAJBZENwCAAAAUWzUqFHea7Zz585+uUePHvbll1/6egW3CjqlSJEiXp0bUFipJaAAVyGwWg106dLlmPer8FOVsPv374+43YTo9i+66CL/vEOHDtavXz+v+lVFrVx//fW2YMECD241LGz06NE2f/58u/DCC3279lu0aJG98MILCQa3qvY977zzItZt3LjRrrvuOu9nG9xObCeddJJfFwAAIDOhVQIAAAAQxVT1GgSiAV3W+qNRONq7d28788wzPdRVELtmzRoPOtNC9erVQ5+rPYMqb8NDVK1T+wRRVey+ffvsyiuv9OMKlilTpnjYm5C9e/d69XC4bt26hULjIUOG2HfffRfnegqgw9s0AAAAZAZU3AIAAABRTj1kw6k/bOx1sd1///3ed1aVuaeddpqHl6p6VVuBtJArV66I4w2/HKw7cuSIfx58fP/9970aNpzaKySkePHitn379oh1HTt29D62uq2PPvrIRowYYY8//rh17do1tM+2bdu8RzAAAEBmQsUtAAAAEMU03EstBMItXrzY1wcUkh4+fDhiH/V0VYsF9ZZVGwG1O0hquwD1h419u6lBVcAKaFX9q1A5fClfvnyC19PwM1Xrxqbr3HXXXd4zt2fPnvbiiy9GbP/+++/9ugAAAJkJFbcAAABAFFPlrAZ91axZ0xo0aGDvvvuuB5Tz5s0L7VOxYkX7+OOPvV2AAtGiRYt6CKr9NJBM1a6DBg0KVbomlm5XVbsaaHbiiSda4cKFU+UxFSxY0Ief3XfffX5MF198sQ9BUyCtlgnt2rWL93qqrFWFrcLkYAhb9+7drUmTJnb66ad7Na765oaH2gqrf/vtNx/sBgAAkJlQcQsAAABEsRYtWtiTTz5pjz32mJ111lk+vGvSpElWr1690D5qDTB37lyvPA0qS8eMGeMBbt26dT28Veip8DcpOnXqZGeccYbVqlXLh6B9/vnnqfa4HnroIRs8eLC3NlDQquNTKH3KKackeJ2mTZt6dXF4aK0Q95577vHbaNy4sR/vs88+G9o+depUa9iwoZ188smpduwAAADpIVuMGmQBAAAAQCagUHbmzJleCXws+/fvt8qVK3t4G3vAGwAAQLSjVQIAAACATOOOO+7wlgi7d+/2lgtHs2HDBhswYAChLQAAyJSouAUAAAAAAACAKEOPWwAAAAAAAACIMgS3AAAAAAAAABBlCG4BAAAAAAAAIMoQ3AIAAAAAAABAlCG4BQAAAAAAAIAoQ3ALAAAAAAAAAFGG4BYAAAAAAAAAogzBLQAAAAAAAABEGYJbAAAAAAAAAIgyBLcAAAAAAAAAEGUIbgEAAAAAAAAgyhDcAgAAAAAAAECUIbgFAAAAAAAAgChDcAsAAAAAAAAAUYbgFgAAAAAAAACiDMEtAABAOho6dKhly5YtYl3FihWtffv2EeuWL19ul112mRUuXNj3f+KJJ3z9xx9/bLVq1bL8+fP7+hkzZqTq8a1fv95v96WXXrKMkJH3fTTPPvtsvMeV0c9XUug4u3TpYtF8fPr+CHzyySe+Th+zys+CxKpXr56dffbZqX5MAAAguuTM6AMAAADI6qZPn26FChWKWHf77bfbv//+a6+//roVLVrUw92YmBhr1aqVnX766TZr1iwPb88444xUPZYyZcrYF198YZUqVUrV283sFNwWL148TsDO85V2atas6c/tmWeemdGHAgAAkCEIbgEAADLYueeeG2fd999/b506dbImTZqE1v3222+2bds2u/baa61BgwZpcix58uSxCy64wDKj//77z0444YR0vc/M/HxFO72ZwXObOaji/LbbbvM3l443ekz79u2zfPnyZfShAACyIFolAAAApJH333/fatSo4eHeKaecYqNGjYp3v/BWCQpAdPr0oUOH7LnnnvPPg1PIy5Ur5/v06dPH1+l6ousGnx/rVOw333zT6tSp4y0YFHKeeuqpXt17rFP/Fy1a5GFxwYIF/Xp169b1xxcuOPYFCxbY3Xff7RWqJ554orVs2dJ+//13S03BqeKfffaZH4uOKXgcsU+5T6glRWKPV9dbtWqVffrpp6GvR/B8x/d8Bc/7d999ZzfccIM/18WKFbMePXr41/XHH3+0xo0b+3Op23n00UfjHOuuXbusV69e/rrJnTu3nXTSSda9e3evwk6pF154wau29bpUNauqusP99ddf1rlzZ99WoEABK1mypF1++eW2cOHCOLel1+g555zj++nxVKlSxfr37x+xz9atW+3OO+/0168eix7TAw884M/F0cTXKkFfP93Xzz//bE2bNvXPy5cvbz179rT9+/dHXP/AgQM2bNgwPyY91hIlSni4qMcXrT8LFBKqulv7KihUtf31119vv/76a7z762uicFv76jUyaNAgO3z48DHbTST0ff7iiy9GvDZee+21BH++JEdiX9fPPPOMXXrppf7a05kF1apV8++TgwcPxnm+hg8fbieffLLlzZvX28jMnTvXfz5oSc59By1Fnn/+eatatao/F5MnT06Vxw8AQFJRcQsAAJAG1Iu2efPmduGFF3owpjBFwcMff/xx1OtdddVVfnq4rqfARoGUKPRSQKZQsWvXrnbzzTd7oJAUut3WrVv7onBRQceGDRts/vz5R72eAssrr7zSqlevbhMmTPD7Vbh09dVX29SpU/32wnXs2NEfh0KfTZs22f3332+33HLLMe8nqbZs2eK327t3bw9vsmdPXk3CsY5XrSz0tVAAq8ctiXnu1dZCt6PQUmFSEDzNmzfPg1GFSLpPBfGnnXaaf22DymH1N968ebOHoHreFRwPHjzYVq5c6dcPAnl9HRWCKnyOHVTFRy02tO+DDz7ogZgez0033WQ5c+b0xyiq6pYhQ4ZY6dKlbc+ePf4c6Pb1ug7uR69rPQ69HhVE6vlXoLp69eqI0LZ27dq+TcevlhJ6HSpQVXg4adKkJH+99Bxec8011qFDB//+UHj/0EMP+ddH9yFHjhzx7z8Fm3p9KNzXa12PScf/9ddfH7WCUoFgeAB6NHruUutngV4rClO7detmjzzyiH8t9LXS8a9YscJKlSoV8dzeeOON1rdvX99H4bCe1+3bt9vYsWMtqcaNG+f3f91119mYMWNs586d/tqKHYgnV1Je17/88ov/jAtCVj32hx9+2NasWWMTJ04M3eaAAQNsxIgRdscdd/j3j75/9f2s14gC6OTct6h3uF472q7vAQXIAABkiBgAAACkujp16sSULVs2Zu/evaF1u3btiilWrJjOJY7Y9+STT45p165dxDrtc88990SsW7duna9/7LHHItbrurqN2IYMGRJxX6NGjfLLO3bsSPC4g/uYNGlSaN0FF1wQU7JkyZjdu3eH1h06dCjm7LPPjilXrlzMkSNHfJ2uo+t2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mj3zzDO2b98+X/S5gl9tUwXupEmTvPetWiKsXLnShg0bFqrI/fHHH23u3LneJiF37txe7ZszZ84MeMQAACA5CG6jgE5lGjp0aJKus379esuWLVvonfpPPvnEL+uXtmg4PgAAAADxGzRokF144YXe2kBL3bp1rX///r5NPWq1BGbOnGnLli2zk046ycqUKWNLly61WbNm+Tb9/v/aa695SwS1WWjevLkPLnviiSd8+4EDB/y+1CbhxBNPtPnz59tLL72UQY8aAAAkFW+3JoJ+ITqadu3a+S9A4ftpEEDZsmXt+uuvtxEjRliePHnS4UgBAAAARLtcuXJ55ayW2J5//vmIy2eeeaZ9+OGH8d5O/vz5vaI2IdWqVbMvv/wyFY4YAABkBILbRNiyZUvo8zfeeMMGDx7spx0F8uXLF/pcpyo1btzYe0utWLHCT4PSL1QPPfRQuh83AAAAAAAAgMyJVgmJULp06dCigQGqrI29LlCkSBFfp+ECzZo1s2uuucZPbUopnRJ17rnnWt68ea1WrVq2fPnyePf7/PPPfeiA9qtTp473uQr34osv+rFpsMG1115ro0eP9mNOqc2bN9uNN95oxYoV86Bax7hkyZLQ9ueee85P4VJvrTPOOMNefvnliOvrOX3hhRf8OdOx6ZQxDV34+eefrV69en6bOp3sl19+CV1H7Rtq1Kjh1wse0w033JBgu4j9+/fbrl27IhYAAAAAAAAgGhHcpqG1a9f6BFcFqCnx77//eqCpwPObb77xwLJXr17x7nv//ffbqFGj7KuvvrKSJUt6cKzq3yDUVb+se++913vjXnnllT4UIaU0FOGyyy6z33//3fttqdK4d+/eduTIEd8+ffp0v8+ePXva999/b3feeadXIuu5Caeq5FtvvdWPrUqVKnbzzTf7vv369bOvv/7a9+nSpUvEdRTsTps2zd59912bM2eOX/eee+6J9zjVskIhe7Ao7AUAAAAAAACiEa0SUtlNN93k/W0PHTrkFZ4KXBU8psSrr75qhw8ftokTJ3pV6VlnneUVrnfffXecfYcMGeKBrEyePNknzio4bdWqlT399NPWpEmTUOh7+umn2+LFi+29995L0fFpIMJff/3lYbEqbkXTbwMKkjXgrHPnzn65R48e3mtL6+vXrx/aT2GujlP69OnjFbYaphBMxVX4q33CabJu8DhFj1ED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V8vFF19sjRo18u2//PKLV9qOGjUq3vuqVKmSTZ48OTTMrGHDhvbtt9+m4aMDAABJQXALAAAAAFFClbMaIhYeoOrz8uXLx2mToOpaDSXr1q2bnXDCCb507drVvvjiC/v77799qNlff/1lZ511lpUuXdpatmxpu3bt8s+XLl3qt6H+uAsWLPAWCeecc461bds23R8zAACIH8EtAAAAAESR2267zR5++GHbunWrL8OHD7eOHTvG2a948eJ22mmneVXtvn37fNHnCn61TYPKNLxMwa8WDR5T1a4+P/fcc+3HH3+0uXPnepuE3Llze7VvzpyMQQEAIFoQ3Gag9u3b29ChQ5N0nfXr11u2bNlC78B/8sknfnnHjh1RcXwAAAAAUmbQoEF24YUXWtWqVX2pW7eu9e/f37fdddddvgRmzpzpw8VOOukkK1OmjFfSzpo1y7fly5fPq2uDRT1w9beDPs+VK5cdOHDA70ttEk488USbP3++vfTSSxn2uAEAQCTeTj0K/VJzNO3atfNfbML3U4P/smXL2vXXX28jRoywPHnypMORAgAAADheKFRV5ayW2J5//vmIy2eeeaZ9+OGHibpdDSgLL/ioVq2affnll6lwxAAAIC0Q3B7Fli1bQp+/8cYbNnjwYD+dKKB3sAOTJk2yxo0b28GDB23FihV+elP+/PntoYceSvfjhnn1gE73AgAAAAAAADIjWiUcRfhpRRoEEJxWFL4uUKRIEV+noQHNmjWza665xk9ZSimd6qT+U3nz5rVatWrZ8uXL493v888/92EC2q9OnTq2cuXKiO0vvviiH5sGFlx77bU2evRoP+aU+u2337x3loYo6PSq5s2bezuHwFdffeWTbtVjS8/XZZddFud5WbNmjU+/1bGrYmDevHn+XM+YMSPR96O2Di1atPAqZ1U8n3766XGOdf/+/T6MIXwBAAAAAAAAohHBbRpYu3atT2ZVgJoS//77r4fAZ5xxhn3zzTfeb7ZXr17x7nv//ffbqFGjPCgtWbKkB8eq/g1CXfXBuvfee703roJUDTtIqf/++8/q16/vQww+++wzW7RokX+uymNVvMru3bu9pYQm2uo0rMqVK1vTpk19vRw5csQDVwXKS5YssXHjxtmAAQOSfD/y8ccf2w8//OADFt577704x6tQV+FxsCjIBgAAAAAAAKIRrRJSyU033eT9bQ8dOuSVnQpc+/Xrl6LbfPXVV+3w4cM2ceJEDzbPOuss27x5s919991x9h0yZIgHsjJ58mSfJDt9+nRr1aqVPf3009akSZNQ6Ktq1MWLF8cbbibF66+/btmzZ/fptEGfX7WMUCWvhqY1bNjQLr/88ojrvPDCC141++mnn/pz9NFHH9kvv/zi+6tiWRQqB48lsfcjak2hfRJqkaCvR48ePUKXVXFLeAsAAAAAAIBoRMVtKhkzZoxXs6q/rQJRVd22bds2Rbep6lG1P1BoG9B02fiEr9e0WFXp6vqivry1a9eO2D/25eRQFfDPP/9sBQsW9ApYLbrvffv2eRgrf/75p1f7KiwOKl337NljGzduDB2bwtMgtI3v2BJzP8FwhaP1tdWguEKFCkUsAAAAAAAAQDSi4jaVKHg87bTT/HOFpmoFoCrcYcOGhdYnVUxMTIqOKahO1e0En6fWbQdtDs477zyvDI6tRIkSod6zf/31lz3xxBN28skne3iqkDlocRDfsSXnfoKKWwAAAOB49f3331vPnj29sOGff/6x7du3H3NuheZGqK2aZkbUrFnTz1CrUqVKorcDAICMQ8VtGlHbBNm7d2+yb0ODulTBG34b6hMbn/D1+gVOFb/BL1z6qCFn4b7++mtLKf1i99NPP3lPXYXT4UswuE29bbt16+Z9bdXqQcHt33//HboNHZuqb//444/QOvXpTer9AAAAAMe7XLlyeSu0l156KVH762+CNm3a+NmB27Zt8zZmGvKr9m6J2Q4AADIWwW0q2bFjh23dutV+//1379/64IMPenuAqlWrJvs2b775Zu/t2qFDB1u9erV98MEHPoAsPro/DefSu/Cqci1evLgP/ZKuXbv6dUePHu0BqPrMzp49+5iVrseiX/J0P/rlTgHtunXr/LFrCJp68YrC1ZdfftnbNmj4mK6TL1++0G2ol22lSpV8gNl3333ng9SC4WTB8SXmfgAAAIDjwaZNm/x3Xw3cFZ2ppkIG/b6vM/v0t8HZZ5+dqNvS7+Ea8qvZEnnz5rVBgwZ5KzP9Tp2Y7QAAIGMR3KaS2267zcqUKeNDwdQiQdWlCkdz5kx+Nwr1cn333Xc9tD333HM90HzkkUfi3XfkyJEeZKqlwJYtW2zWrFmhfq8XXXSRPf/88x7cqmfunDlz7L777vNfzlJCvXc/++wzq1ChgrVs2dJD6ttvv90rhIP+sRqspgpgHb96/qr6VpWz4ZXJOj1LfW/PP/9869ixow0cONC3BceXmPsBAAAAjgea/zBu3Di79dZbPUTt06eP/10QFDckhQojatSoEVGxq7P6tD4x2wEAQMaix20iqYpVS3xSo19sQi644AIfepbQ/dWrVy90We+UJ6RTp06+hF9Obu/d2L19J0+enOB2BbaxWx9cf/31EZfVLmHRokWhy6q6lfDjO9b9JPZ0MQAAACDaqVjhww8/tCuuuMJ7z+rvgaAVW1KoOCJ2D1xd1jyOxGwHAAAZi+A2i1CLBbUl0AAvVQIrBH322WctGkyfPt2rCCpXrmw///yzVw6rSlgtFAAAAICsqHPnzl5526VLF6/CTQ79jr1z586IdbpcsGDBRG0HAAAZi1YJWYSGkym4rVatmrdNeOqpp7wtQTTQO/r6xVSVt6pqVsuEmTNnZvRhAQAAABlCfW3VGky/G0+ZMsW++eabZN1O9erVI87eO3jwoLdh098EidkOAAAyFhW3GUjDw2KfmpRWpk2bFrXHp/5dWgAAAACY9e3b16thx48f7zMsNENj2bJlfvbc/v37fRF93Ldvn+XJkyfewcO33HKLz7nQoOIGDRrYiBEjfPDZpZdemqjtAAAgY2WLScsGrUAU27VrlxUuXNhPB2PIGQAAAKKBBgm3adPGK2GDFgkqqChatKgNGTLETjnllDjXWbdunVWsWNEWLlxoTZo08d614W3JevfubZs3b7aaNWvahAkT/Ey3xG4HAAAZl0cR3CLLIrgFAAAAAABAtOZR9LgFAAAAAAAAgChDcAsAAAAAAAAAUYbgFgAAAAAAAACiTM6MPgAgo20ZMMD25MmT0YcBAAAApIuyo0b5x4MHD9p9991nr732ml/WULQxY8ZYzpxx/0z87bff7J577vEBaNmyZbP69evb2LFjrVSpUr69QIECEfvv37/fqlatat999126PCYAAI5HVNwCAAAAQBY0bNgwW7Roka1atcoXhbLDhw+Pd9/OnTv7xw0bNti6des8mL333ntD2/fs2ROxKLS98cYb0+2xAABwPCK4BQAAAIAsaOLEiTZw4EArU6aMLwMGDLAJEybEu6/C2latWnllbcGCBa1169b2/fffx7vv0qVLbfXq1da+ffvQutGjR1uFChX8uhUrVrTx48en2eMCAOB4QXALAAAAAFnM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MHToUB8aFr6owFCef/55XxIqdoxdoKkAV31wt2/f7ov62QZVtdWqVbMvv/zS2yvoep9++qm3S4gGmTq4ve6662zFihXeQHjt2rU2a9Ys/+Js27Yt1e7jySeftC1btoQWmTRpUpx1oqA2WPfxxx/7C6BZs2apdiwAAAAAAAAAsoZMG9wqAV+0aJE98sgjVr9+fTv55JOtdu3a1q9fP7vqqqt8n/Xr13v1a3jzYV1P64KEPvD55597mp43b16rU6eOrVy50terSXF4ubUUKVIkzjoJqn611KhRw/r06WObNm3yBskp8e+//9qtt97qPTbKlCljjz/+eLz77d69226++Wbfr2zZsvb0009HbF+zZo3369BjPPPMM23evHn+XMyYMSNFx6c+I/o6nHbaaf4cqAJaZewBPZeXX365l66rEvmOO+6wPXv2hLarWrlFixbeX0Rl6Xp+H3jgAe9hcv/993uJu5pIT5w4MXSd4Gv7+uuve3m7HpOaUMf+uobbv3+/v3sSvgAAAAAAAADRKNMGtwontSh0VCCXUgoIR40aZV999ZWVLFnSrrnmGjt48GCyb0/B5KuvvuphZtAIOSXHpsl206dPt48++sjDyW+++SbOfo899phVr17dli1b5gH2fffdZ3Pnzg2FqwpHTzjhBFuyZImNGzfOBgwYYKlB96XgVv1AVq9eba+99poHsPLff/95JbIm+Om5ffPNNz0w7tKlS8RtqH/I77//bp999pmNHj3ay+FVrazr6XiD3iUKwmM/Nz179rTly5d7gKuv2z///BPvcY4YMcKD+GDR9EAAAAAAAAAgGmXa4FZtCNQoWG0SVKGpqXBqKpzcnrJDhgyxK6+80vta6Db/+OMPD0qT4r333gsFygULFvTWDW+88YZPvEtJADxhwgQPlcOP7/Dhw3H21XPQt29fO/30061r1652/fXX25gxY3ybAt9ffvnFpkyZ4pXFqrwNr4pNLlX5qp3Eo48+au3atbNKlSr5bQeT+RRe79271+/37LPP9spb9RF5+eWX/TkOqKr2qaeesjPOOMNuv/12/6jQV1/TypUrezisyX6qjA6nAFgtM9RY+rnnnvNAVs9XfHQbO3fuDC2xQ2AAAAAAAAAgWmTa4FYU2KlKUwGphpKpErVmzZrJmvymyXLhIaKCwx9++CFJt6GWDWrLoEVVopp016RJE9uw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kiVL2ooVK7w9QmK2r1271itvP/roI9u9e7ctWbLEateunUGPIusiuAUAAAAAAACOQ3379vXgdfXq1XbXXXdZ6dKlE7U9R44c3p5h1apVtnfvXitVqpRVr149gx5F1kVwCwAAAAAAABzH1BbhnHPOsfbt2ydqe6VKlWzy5Mk+DE2hbcOGDe3bb79N56MGwS0AAAAAAABwnDt48GBED9tjbdewsgULFtgff/zhoW7btm3T6UgRILgFAAAAAAAAMoFDhw7Zvn37/OORI0f88wMHDsTZb8+ePTZp0iTbsWOHtzxYuXKlDRs2zBo1apSo7T/++KPNnTvX2yTkzp3bChQoYDlz5kz3x5vVZdng9pNPPrFs2bL5CzSj7l8T+ZKqXr161r1799Bl3cYTTzwRNccHAAAAAACAtKFwNV++fPbwww/bu+++65+rjYGoR60WUeb12muvecsDDR9r3ry5XXXVVaEM6VjbFQYPGjTI2ySceOKJNn/+fHvppZcy8JFnTZkyKv/zzz/9xTN79mwv1y5atKiXbA8dOtQuvPDCVLkP9fRQL4+j0TsSsfcrVqyYnX/++fboo4/StBkAAAAAAACpRtmXlvg8//zzoc/z58/vFbMJOdb2atWq2ZdffpnCo0WWrLi97rrrbMWKFR6Yrl271mbNmuWVqNu2bUu1+3jyySdty5YtoUVUQh57nTRu3Di07uOPP/bS8WbNmqXasSBpFKjrlAEAAAAAAAAgs8p0wa1aGyxatMgeeeQRq1+/vp188slWu3Zt69evn5d0y/r1673kO3zana6ndWoBEO7zzz/3at28efNanTp1vKeHFC5c2EqXLh1apEiRInHWSZ48eULratSoYX369LFNmzbZX3/9laLH+u+//9qtt97qfUTKlCljjz/+eLz77d69226++Wbfr2zZsvb0009HbF+zZo1dfPHF/hjPPPNMmzdvnj8XM2bMSHFAqsriU0891Uvz9Ty+9dZboe2HDx+2Dh062CmnnOLbzzjjDA/Ewylg7datmz+3Kr3Xc9euXTtr0aJFou8naHvx4YcfWq1atfzrsXDhwjjHu3//ftu1a1fEAgAAAAAAAESjTBfcKpzUotBRQVxK3X///TZq1Cj76quvrGTJknbNNdf4FL3kUnPnV1991U477TQPIlN6bJreN336dPvoo488oPzmm2/i7PfYY495W4Zl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c+d6m4TcuXN7BW/sYBcAog3BbQbKli2brV+/PknXGTp0aEQzdk3I1ATOaDk+AAAAAAASa9iwYZYvXz4PZN99913/XG0M5K677vIlMGjQILvwwgutatWqvtStW9f69+8fZyjZJZdcYqeffnrEeoXBur7aJJx44ok2f/58e+mll9LpUQJA8mSLUXfuLEIhp3raiN5ZK1asmDcjv+mmm3xb9uz/P8euWLGibdiwwT/Xev1w10TKUaNG+SkagV27dtljjz1m77zzjv3666/eR+fUU0+1G264wTp16hSxb3zB6Lp16/y+khLczpgxI3R6iI57x44dvi61Jef4MhN97WKfUgMAAAAASHtZKIoAgHjzKPXpLlSokB1Nlqu4bdy4sW3ZssUrSWfPnm3169e3e++915o1a+anZoR78MEHfd+NGzfaq6++ap999pl169YttH3btm12wQUX2KRJk6xXr162ZMkS+/zzz23IkCEerL722msZ8AghBw8ezOhDAAAAAAAAAJItywW3efLksdKlS9tJJ51kNWvW9NMqZs6c6SFu7NMkNJky2FcB76233mrLli0Lbdd1FeoqsFWvHVXvVqlSxUNghbadO3dO8fGOHDnSq311LB06dPB+P/F54IEHrGTJkp7U33nnnRE9gXbv3m1t2rSx/PnzW5kyZWzMmDFWr1496969e4qPT6eynHfeeZY3b16vNNZxhAfgo0ePtmrVqvl9q2m8npM9e/ZE3MaLL77o21StfO211/p1ihQpkqT7UXXw888/b82bN/f70uk2se3fv9/f1QhfAAAAAAAAgGiU5YLb+Fx++eU+UVLtDhLy22+/2XvvvWd16tTxy2qa/sYbb9gtt9ziwW58FCamxLRp07x6V71+vv76aw9dn3322Tj7ffzxx/bDDz/4dMypU6fa9OnTPdgM9OjRwyuBZ82a5c3YFy5cGBFAJ9eHH37oj19VyKtXr7YXXnjBw28db0BtJp566in7/vvvvU2F+gj17t07tF3HpZ5FqnpWlfKVV14Zcf3E3o/ouVJwu3LlyjiN6GXEiBFeih4sCosBAAAAAACAqBSThbRr1y6mefPm8W5r3bp1TNWqVUOXTz755JjcuXPH5M+fPyZv3rxqvhNTp06dmO3bt/v2rVu3+rrRo0dH3E7NmjX9OlpuvPHGox6Prr9u3boEt1944YUxd911V8Q6HcM555wT8ZiKFSsW8++//4bWPffcczEFChSIOXz4cMyuXbticuXKFfPmm2+Gtu/YsSPmhBNOiLn33ntTdHyXXHJJzPDhwyPWvfzyyzFlypRJ8DrTpk2LOfHEEyOe96uuuipinzZt2sQULlw4SfejY+3evftRH8++fftidu7cGVo2bdrk12NhYWFhYWFhYWFhYWFJ3wUAsqqdO3f6z0F9PBYqbv9H2V/sCtn777/fq0C/++47r2qVq666yg4fPhzaJ/Z1VO2q6zRq1Mj27t2bomNSFa0mZoaLfVlULaw2A+H7qB3Bpk2bfGCa+r3Wrl07tF3VpmeccYal1DfffON9gAsUKBBaNJBNfYH/++8/30dVwKqiVVWy2j2o3cQ///xj//77r2//8ccfI45NYl9OzP1IrVq1jtkmQ60kwhcAAAAAAAAgGhHchoWkp5xySsS64sWL22mnnWaVK1f2dgpPPPGELV682MPIEiVKeB/WNWvWRFynQoUKfh2FlBlNoXIwqTN2wJwaEzzVLkItGRRUB4vaFPz000/ei3bDhg3WtGlTO/vss+3tt9/2APaZZ56JGB4WX2Ae+9iOdT8B9bYFAAAAAGR+arengij9Xa6/GXfs2HHM68yYMcP/fldh08UXXxzn7/VjbQeAaENwa+Z9VxUEXnfddUfdL0eOHP5RlbTq3dqqVSt75ZVXvP9tWqhatap9+eWXEetiX5YVK1ZEVPdqH1WllitXzipVqmS5cuWypUuXhrZrKJdCz5TScDdVzCqojr3o+VFfXg0Qe/zxx+2CCy6w008/3X7//feI29Awt/BjE10vKfcDAAAAADi+6O9Y/c0de4h4QtauXetDuTWMe9u2bV58pRkowVDrY20HgGiU5VKv/fv329atWz1s1YCu4cOH+w/rZs2a+Wn84Xbv3u376pR8hYtqnaB3++rWrevbdV21ANDAsokTJ3pLhV9++cXbJXzxxRehoDe5NLBLt6tF/8lo+NaqVavi7HfgwAHr0KGDD+6aPXu279elSxcPNVX5265dOz92VQrr+hrcpW0pHZ42ePBgmzJlig0dOtRvV1XLGtg2cOBA367QWP8JPv30096y4eWXX7bnn38+4ja6du1qH3zwgY0ePdrDZA0e02MIP7Zj3Q8AAAAAIPNRez/9ja0h2sHftircUas8tffT37k6gzMx9Pdm/fr1/W97nZk5aNAg+/PPP304d2K2A0BUislCNMgraISeM2fOmBIlSsRcccUVMRMnTvRBXuE0nCy8cbr2bdq0aczy5csj9tOgr379+sVUqVIlJk+ePDH58uWLqV69esygQYNi/vnnn6Mez7GGf8nDDz8cU7x4cR82puPv3bt3nOFkGrg2ePBgH/ql/Tp27OiDuAIaUHbzzTf7QLLSpUv7QLXatWvH9O3bN8XHN2fOnJi6dev64y5UqJDf7rhx40LbdV8aIqbtjRo1ipkyZYrfbjDkTbT/SSed5Pu0aNEiZtiwYX6cSbkf3eb06dNjktMMmoWFhYWFhYWFhYWFhSV9l8Dbb7/tf//98ccfPnBaw6kPHToU2q6/SWP/DRmfa665JmbAgAER6/Q35BNPPJGo7QAQjcPJsumfjA6PsypVla5bt84qVqyYrverwWCqFFYLA72DGW3Hp8Fj6jWU1u98qmWEBrUBAAAAANJXeBRx5513+lmrOjNWM03Kly8f2rZ+/XqfR7N9+3afM5OQBg0aWJMmTaxXr16hdRouruHdOlvzWNsBIL0EedTOnTutUKFCR903y7VKyIqWL19uU6dO9TYOag+hvj6iFhHRYNSoUd6n9+eff/a2CpMnT/b2DgAAAACA41/nzp197szNN98cEdomhea8KAQJp8vB4PBjbQeAaERwm0UoHD3nnHPsiiuu8IpbVbOql1A0UP/gK6+80qpVq+Y9cJ966inr2LFjRh8WAAAAACCNqa+t5rC0b9/eZ5t88803ybqd6tWre7Vu4ODBgz4HRn9nJmY7AESjnBl9AFmZhogd7VSP1HLuuecm6z+/9Dq+adOmpfl9AAAAAACiT9++fb0advz48XbeeefZTTfd5GeK5s+f34eLaxF93Ldvn+XJkyfeQdu33HKLD73W8Gu1RRgxYoQXK1166aWJ2g4A0Yget8iy6HELAAAAABlDUcScOXO8lV94X9sWLVpY0aJFvZBIvW1jC+aw6CxS9azds2dPaNv06dOtd+/etnnzZqtZs6ZNmDDBqlT5f+zdCZyN9f///5fsZCdL1iQkSyoqLbSItEhFkS20yJIUilCJFkuLNllSHyotWpWIyq4oiUrJWkKRJXvmf3u+vr/r/M+cmWH2OTPzuN9uV3POda5zbXNG5zzP63q9ayT6cQCIth63BLfItpLyhwIAAAAAAACkFIOTAQAAAAAAAEAmRnALAAAAAAAAAFGG4BYAAAAAAAAAogzBLQAAAAAAAABEmVwZvQNARtsycKDtzZs3o3cDAAAAAFJFuZEj/efYsWPtlVdesZUrV1rz5s3tvffeS/A5hw8ftj59+tjUqVP9frt27WzMmDGWK1cuO3jwoPXo0cNmz55tf/31l5188snWr18/u/XWW9PtmAAgO6LiFgAAAACALKhcuXI2aNAg69at23GXHTZsmM2fP99WrVrl07x582z48OH+2JEjR6xs2bIe3Go0dIXBffv2tc8++ywdjgIAsi+CWwAAAAAAsqBWrVpZy5YtrWTJksddduLEiR7yKqDVNHDgQJswYYI/VrBgQXv44YetatWqliNHDjv33HOtSZMmHvQGRo8ebRUrVrRChQpZ5cqVbfz48Wl6bACQHRDcAgAAAACQje3cudM2b95s9erVC83T7Y0bN9quXbviLH/gwAFbunSp1alTx++vWbPGQ19V4O7Zs8eWLFliDRo0SNdjAICsiOAWAAAAAIBsbO/evf6zaNGioXnBbQWx4WJiYqxr165WrVo1r+iVnDlz+ny1WNi/f7+VLl06FOoCAJKP4BYAAAAAgGzsxBNP9J/h1bXBbbU+CCicvfPOO+3nn3/2gc5OOOH/IgW1UJg8ebIPhqbQtmnTpvbdd9+l+3EAQFZDcAsAAAAAQDZWrFgxK1++fKywVbcrVKhgRYoUCYW2d911l7dIUEuEYH6gdevWNnfuXNu6davVrVvX2rdvn+7HAQBZDcEtAAAAAABZ0JEjR7wfrX4ePXrUbx86dCjeZTt37myPPvqo/fnnnz4NHz7cWyIEevToYQsWLLBZs2Z50BtOFbiarzYJefLk8QreXLlypfnxAUBWl62DW42GuX79+nTf7hdffOHb/ueffxL9HI3K+dRTT1lGGDp0qHXq1ClDtg0AAAAASJ5hw4ZZ/vz5PZD98MMP/bbaGMgdd9zhU+DBBx+08847z2rWrOnT+eefbw888IA/tmHDBnv++ec9oK1UqZIHs5qC5ysM1vPVJqFEiRI2Z84ce+WVVzLoqAEg60jXr8AU/qnvjW84Vy4rXry4Nyy/+eab/bGgP04QVOp/DqL5+h9A8+bNbeTIkbG+3du9e7c9+eST9u6779pvv/1mBQoUsFNOOcVuvPFG69atW5xvAgEAAAAAyA5UhKMpPi+++GKs+7lz57bnnnvOp0gKa9UqISG1a9e2xYsXp8IeAwAytOK2WbNmtmXLFq90/eSTT6xJkybWu3dvu+qqq/zyjXAPP/ywL7tx40abMmWKffXVV9arV6/Q4zt27LBzzz3XJk2aZPfee68tWbLEL90YMmSI9+OZOnVqeh8eAAAAAAAAAGS+4DZv3rxWpkwZO/nkk61+/fp+6cX777/vIW7kpRQavTJYVgFvhw4dbPny5aHH9VyFugps1Y9H1bs1atTwEFihbffu3VO0r/pmsl69evbaa695BbCar9900022Z8+e0DIHDx70MPmkk06yfPny2QUXXGBff/11rPXMmDHDTjvtNL8sRccRX3uGhQsX2kUXXeTLqAG81vnvv//GWkbbbdu2rV+SUq5cOXv22WdDj2mdar8Q3kxerRg0T60ZAh988IFVq1YttC+qgE5q24b46Pm33XabV0brPJxxxhn20UcfhR5/5513rFatWv7717kcNWpUrOdrni7j0e9Yx6dvdPW62L59u1177bU+T9/ifvPNN6Hn6PVStGhRH81U51fbvfzyy23Tpk3x7qN+V6rQDp8AAAAAAACAaBQVPW4vueQSH3VS7Q4S8vvvv3sQ2LBhQ7+vxupvvvmm3XLLLR7sxkeBZEqtXbvWg0FtW9OXX35pjz32WOjxfv36eSipAFSh8qmnnmpXXHGFVwOLQsRWrVrZlVde6aGqmrsPGDAg1jZWrlzpz9Fy33//vR/X/Pnzvfl7OLWEUDit7dx///3Wp08fbwCfWAp3b7jhBmvZsqXvy+23324DBw5M8TnS70JtLBQ+/+9//7PVq1f7OcqZM6c/vmzZMh9hVKG3jlWBuPofRQb1Y8aMsUaNGtm3335rLVq08FFIFeTqdxycW90Pv0Rn37593q9J51/V1gpjtZ34jBgxwsP3YFJADgAAAAAAAESjqAhuRZWykZWo/fv390pLVYeWL1/eg9jRo0f7Y6rEVJVn9erVYz3nrLPOCjVKV+/c1AglFTCqgvTCCy/0MPHzzz/3x1QR+8ILL3igquDy9NNPt5dfftn3d8KECb6MHlfPXYWS2td27drFGehLz1cl7d133+3VsGoC/8wzz9irr77qo34GFGoq9FV1ac+ePT2E1XoTSz2MtA/ann4q4EyNQcdmz55tS5cu9eBdFa86XlU965yIfmeXXnqph7Xad21TobT2I5zCbYXJOgeDBw/2CuNzzjnH+xXreXo9/Pjjj7Z169bQcw4fPmxjx471Jvr63SvAVYCs/YmksHvXrl2hKaHKXAAAAAAAACCjRU1wqyrKyArZ++67zytDVYUahKWqxPzvv/9Cy0Q+Z/r06f4cVbDu378/xfulS/jVsiFQtmxZ27ZtW6gaV8GhAtXwhu4NGjTwgFH0U314w/dTIWM4VaQqHA4CZ03af4XG69atS/B5uh9sJzE0AqiC0HDa15TS+VawrnA1PtrH8HMkuv/LL7/E+l2qmjiglgui9giR84LzHwxyd/bZZ8f6AkDtE+I7L2rTULhw4VgTAAAAAAAAEI2iJrhV0FalSpVY80qWLOmXx6sCU+0UnnrqKa+mnDt3rpUqVcoDup9++inWcypWrOjPCQ9bU0JBbDgFsApUJbhkPzI8Dg+hjzXyZkDrU6WpAtBgWrFihQebVatWPeZzg+2ccMIJcbanUDmh/Qqfl1KqMD6WxG43/FwHy8c3Lzj/kfOPNw8AAAAAYPbDDz94sZA+cyd2zBO1ENRn8wIFCvjYLuGfxTWuitYTXowU2foPAJBJg9s5c+Z479Prr7/+mMsFPVNVSaugUn1T1VNV/W8zggLiPHnyeD/a8LBUA2jVrFnT76t9wuLFi2M9L/K+BmlbtWqVry9y0voTep7uq8JUFGTLli1bQo+HD1QmWjZy4LTwwb6SS5WymzdvtjVr1sT7uM5B+DkSBfCq0A1+p8l15MiRWMegqmK96QjOCwAAAAAgNhXI6PN05LgjCdFnPbX9U6s+jeeiwioNIq3PYwGNI7J3797QpJZ2AIBMFtwePHjQ/vzzTw9bNeDU8OHD/R989UTVwFPh1ONUyyqMVM9StU7QN4LqASt6rgYm04BlEydO9JYKal+gdgmLFi1KcSh4PAULFrQ777zT9+vTTz/1Qbm6devmA2Z16dLFl7njjjt8n+655x4PFadOnRrnf47q3ar9veuuuzxsVaXtBx984H1sw2nwrSeeeML/p/ncc8/ZW2+9Zb179w5VvaolgwYF03589dVXNmjQoFjPV1WvvhXV9rSOadOmhfYlJRWqF198sV100UUevGuwNLV3+OSTT/ycSN++fb3VxSOPPOLbVR9a/U/83nvvtdR4w6HztGTJEn89de7c2c9DarSAAAAAAIDMSmN66PNzMKD1oUOHvGjo4Ycf9jFP9JlVY7kkxmuvvWZNmjTxz+358uXz8UvUwm7evHlpfBQAkL2le3CrME99YtU7tlmzZt72QANxvf/++3GCVg1QpWXLlSvn/4NQUKr/6ZQoUcIf108Fugp8NdCVwjr1RB06dKi1adPGBwpLawpKFVhq0DL9T/DXX3+1mTNnWrFixUKtG9555x378MMPrW7duj5AmALnyIrVL7/80gNbDYB25pln+v8IdezhFICqH64eVwg6atQov7wloPBaFb/q+apAd9iwYbGer1YUb7/9tg8ipm1q4LSBAweG+r+mhI5R/XM1IJwqbPv16xfqX6vzopD4jTfe8DcG+r3qzUJqDIymy3QURGtwN/X8VYCt7QAAAABAdlahQgUbN26cf15WyBoM/h18BkwKFUnVq1cvVgGNPvdpfkBVtvrsrvFPVJ2bUVfGAkBWkiMmNZqcZlKqMlV1qELk7OrRRx/1MFnfxiZEQfj69esTfRlNetH+3H333YnqxxSf3bt3++U8P/XoYYVSGFwDAAAAQLQoN3JkrCsvdYWnglRd4alAN6DPeSrw2blzp48hk5BLL73UmjdvHuuqSQ0cruIZXempK2X/+usvbxm4fft2v+JUV3uqrV0wHgsAIHYetWvXLitcuLAdC/+CZjPPP/+897n97bff/HIXVSp37Ngxo3cLAAAAAJAGunfv7mPK6CrF8NA2KVSpq4AhnO4Hg4KXKVPGr67UVbS6rUpfDbid0DgoAIDEIbjNZtSOQT2FdVmL2i2o/YIqagEAAAAAWYv62t56663epu7VV1/11nvJoVZ74YNfq0WfxlZRq8L4pGQMFQDA/y9bB7dDhgw55uUgWZFGAf3jjz/swIED/u2neunmypXrmM9p3LixtWzZ0qKN3nwkt00CAAAAAGR1AwYM8GrZ8ePHe5s8jUmiXrTqmKjPhBo8XPRT9xPqpHjLLbfYnDlzbMaMGb6s1qWBzzRItWjsGrUh1PP//vtvH8S7Vq1aVq1atXQ9XgDIarJ1j1tkb0npKQIAAAAAmYkGBtcgYeF9bVWQo4G0VcSk3raRgjFg5s2b5z1tFfIGpk+f7oNQb9682QegnjBhgtWoUcMfGz16tBcJ7dixwz9bqfjn8ccf98G6AQDJz6MIbpFtEdwCAAAAAAAgPTE4GQAAAAAAAABkYgS3AAAAAAAAABBlCG4BAAAAAAAAIMrkyugdADLaM0WKWL6M3gkAAAAAUeve/zc0zOHDh61Pnz42depUv6/BvzQoV65ccT9ar1271nr06GGLFy+2AgUKWO/evX1wr8CJJ54Ya/mDBw9azZo17fvvv0/z4wEAZA5U3AIAAAAAkAjDhg2z+fPn26pVq3yaN2+eDR8+PM5y//33n11zzTVWv35927Ztm82ZM8fGjh0bCnxl7969sSaFtjfddFM6HxEAIJoR3AIAAAAAkAgTJ060QYMGWdmyZX0aOHCgTZgwIc5yP//8s09Dhgyx3LlzW/Xq1a1Lly42bty4eNe7dOlSW716tXXq1Ck0b/To0VaxYkUrVKiQVa5c2caPH5+mxwYAiD60SgAAAAAA4Dh27txpmzdvtnr16oXm6fbGjRtt165dVqRIkdD8o0eP+s+Y/9diIZiXUBsEhb/Nmze3cuXK+f01a9Z4QLx8+XKrUaOGbd261ScAQPZCxS0AAAAAAMehdgZStGjR0Lzg9p49e2ItqwrbKlWq2ODBg713rdoqqFp39+7dcda7b98+e+ONN6xr166heTlz5vTQV8/bv3+/lS5d2urUqZOGRwcAiEYEtwAAAAAAHEcwmJiqawPBbbUzCKf2CB988IF99913Vr58eR/ErHPnzlaiRIk46502bZoPXtaiRYvQvKpVq9rkyZO9L65C26ZNm/q6AADZC8EtAAAAAADHUaxYMQ9hwwNU3a5QoUKsNgkBDTY2c+ZM2759uy+nytuLL744znLqXduxY0fLlSt2J8PWrVvb3LlzvUVC3bp1rX379ml0ZACAaEWPWwAAAAAAEkFVs48++qg1atTI7w8fPjxWi4Nw6meryllV33700UfeKuHzzz+PtYwGMFu4cKE/FjlfvXMvuOACy5Mnj1f7Rga7AICsL1tW3ObIkcPWr1+f7tv94osvfNv//PNPop+j0UOfeuopywhDhw6NNaopAAAAAGRnDz74oJ133nleTavp/PPPtwceeMAfu+OOO3wKb4GgalxV6o4cOdLee++9OH1qNSjZhRdeaKeddlqs+YcOHfJtqU2C2ivMmTPHXnnllXQ6SgBAtEiXr+wU/qk/j28wVy4rXry4/w/r5ptv9sdOOOGEWEHlhg0b/Lbm639UGl1T/6PT//ACaur+5JNP2rvvvmu//fab9wQ65ZRT7MYbb7Ru3brFWhYAAAAAgJRS9exzzz3nU6QXX3wx1v1hw4b5dCxPPPFEvPNr165tixcvTuHeAgAyu3SruG3WrJlt2bLFK10/+eQTa9KkifXu3duuuuoqO3LkSKxlH374YV9Wl4ZMmTLFvvrqK+vVq1fo8R07dti5555rkyZNsnvvvdeWLFliCxYssCFDhnjvoKlTp6bXYSFKHT58OKN3AQAAAAAAAIj+4DZv3rxWpkwZO/nkk61+/fp+Ocn777/vIW7kJR8akTNYVgFvhw4dbPny5aHH9VyFugps1WNI1bs1atTwEFihbffu3VPcIqBevXr22muveQWwGs3fdNNNtmfPntAyaiyvMPmkk06yfPnyee+hr7/+OtZ6ZsyY4Ze85M+f348jvvYM6md00UUX+TK6jEbr/Pfff2Mto+22bdvW+xqVK1fOnn322dBjWqfaL4Q3yFcrBs1Ta4aARjStVq1aaF9UAZ3Utg3x0Siqt912m5+HwoUL2yWXXGIrVqwIPb527Vq79tprvXJa+3/OOefY7NmzY61DIb1GUNW+ValSxX+HkS0ijred4Hem3lCqvNbrLSYmJtZ29DtTpXb4BAAAAAAAAESjDO1xq/BNo2Oq3UFCfv/9d2/k3rBhQ79/9OhRe/PNN+2WW27xYDc+CiRTSoGjehBp25q+/PJLe+yxx0KP9+vXz9555x0PQBUqn3rqqXbFFVd4NbBs2rTJWrVqZVdeeaWHqmpYP2DAgFjbWLlypT9Hy6lxvY5r/vz51qNHj1jLqSWEwmlt5/7777c+ffrYrFmzEn0sCndvuOEGa9mype/L7bffbgMHDkzxOVIwqsD1zz//9JB62bJlHspfeumlofOwd+9ePwcKa7/99ls/3quvvtqD94CC+T/++MODZp3TcePG2bZt25K0Hfn111+9j5TWER5kB0aMGOEhfDApKAcAAAAAAACiUYYPTqZK2chK1P79+3t1piowy5cv70Hs6NGj/bHt27d7lWj16tVjPeess87y52hS79yUUkCsSuAzzjjDm8W3b98+NAKoKmJfeOEFD1TVf/f000+3l19+2fdXzeVFj6vyc8yYMb6v7dq1izPQl56vStq7777bq2HV2P6ZZ56xV1991Q4cOBBaTiOWKvRV9W7Pnj09hNV6E0u9lrQP2p5+qno4NQYdmzt3rofPb731lp199tl+DOpFXLRoUXv77bd9GQXzCorVo0mPq8eTzosqgOWnn37yUFfnT+G8Atnx48fb/v37k7SdoIG/qqTPPPNMD7ojA3yF3qrcDSaF6wAAAAAAAEA0yvDgVtWUkQHbfffd5xWTqkINwlJVXP7333+hZSKfM336dH+OKjrDQ7/k0qX6atkQKFu2bKgKVNW46qGqQDW8SX2DBg3sxx9/9Pv6qT684fup0UfDqXJU4XAQOGvS/is0XrduXYLP0/1gO4nx888/e4uCcNrXlNL+q6JWo5yGH4P2XecoCLlVnaxwW0GrHldYG1Tcat80YJ0C24Cql8MHl0vMdqRSpUpWqlSpBPdX7RPUZiF8AgAAAAAAAKJRrozeAQWQ6msarmTJkh7eiaor1etUYaUqL9VeQQGgwr9wFStW9J8KW1PatzUIYsMpgFWgKkHv1MjwODyEjuyvGh+tT9Wo4QOvRR5PQoLtnHDCCXG2FzkwV3zheGL2LzH7r0A7vJduQL+jIISfOXOmV8jqd6qqZFUMqzr2WPsRPj8x25GCBQum+JgAAAAAICl++OEH69u3rxec/P3337Zz585Yn1OSunxS1wcAyLoytOJ2zpw5fgn89ddff8zlcubM6T9VSaugsnXr1va///3P+99mBAWQefLk8X604WHpN998YzVr1vT7qjBdvHhxrOdF3leV6apVq3x9kZPWn9DzdF8tJiSoMNUAX4HI/q5aNnLgNO1rSmn/1XdWFbOR+6/wXebNm+dtGa677jpvl6BB58JbY2jfjhw54v1vw3vVhofvidkOAAAAAGQEFf3oM2rkoNvJXT6p6wMAZF3pFtwePHjQwzeFrRpka/jw4XbttdfaVVdd5YNThduzZ48vqzBy6dKlXrWpgE49YEXP1cBk6ok6ceJEb6mgS+bVLmHRokWhoDetqLLzzjvv9P369NNPbfXq1datWzfbt2+fdenSxZe54447fJ/uuecebwcwderUOP/jVS9f7e9dd93lYesvv/zivV/VxzbcggUL7IknnrA1a9bYc889571ee/fu7Y+pglUtGTRwmvbjq6++skGDBsV6vqp6VaGs7WkdGsAr2JeUDOR22WWXeSW0Bj1TVa0C2YULF/r2g2BY4aoGn9PxrVixwnv6BpXLQXCr9dx2223+u1aAq9s6rmDfErMdAAAAAEgrGh9Dn0mDQaJ1BaEKTB5++GEfR0SfAzU+SmIcb/mkrg8AkHWlW3CrgFOXu6t3bLNmzbztgQbiev/99+MErYMHD/Zly5Ur58GuglL9D1I9TkU/FfIp8NWAW+rXqmrOoUOHWps2bXygq7SmoFSVwhq0TP/DVpWoQsWgN6taHbzzzjv24Ycf+gBdGiBMgXM4DaD15ZdfemCrAdA0qNaDDz7oxx4uuExGjz/yyCM2atQo74UbUHitil8N3KVAVwOAhVMrCg3ipQBV29TAaQMHDgz1fU0uBaszZsywiy66yG699VYfPE0DnylYLV26tC+jQdR0ThS6X3311b7f4f1sRYOxaXmtR5W5CsHV8iJfvnyJ3g4AAAAApJUKFSrYuHHj/DOoxj4JBtQOPlcBAJAWcsSkRrPTTEZBoAa2UoicXT366KMeJuub44QoCFc4mt6X6GzevNnfGM2ePdsuvfTSNNvO7t27rUiRIvaImf1fRAwAAAAAcd37/z4262pGXTWpK0l1VaE+twT02UlFM4ntSXu85ZO6PgBA5hDkUbt27bLChQtH9+BkSB/PP/+8nXPOOV6trNYLqlTu0aOHRQP1Ot67d69XTas9Rr9+/TxUV4UtAAAAAESL7t27e+WtPkuFh7YAAGS5wcmQftSOQT2FNWia2i2o/YIqaqOB2jw88MADVqtWLW+VoAHXvvjiC2/KDwAAAADRQH1t1bpNgy+r3Zva2QEAkJayZcXtkCFDst2lJuo1qykpGjdubP/884+lNfW9De/ZCwAAAADRZsCAAd7Xdvz48XbWWWfZzTff7ANva0wWDcatSfTzwIEDPp5IfINBq1vhsZY/3uMAgOwjW/a4BZLaUwQAAABA9qXBttu1axerr23Lli19IGYVBqkXbaRgXJV58+ZZ8+bNvT1ceO/ahJY/3uMAgOyTRxHcItsiuAUAAAAAAEC05lH0uAUAAAAAAACAKENwCwAAAAAAAABRhuAWAAAAAAAAAKIMwS0AAAAAAAAARBmCWwAAAAAAAACIMgS3AAAAAAAAABBlCG4BAAAAAAAAIMoQ3AIAAAAAAABAlCG4BQAAAAAAAIAoQ3ALAAAAAAAAAFGG4BYAAAAAAAAAogzBLQAAAAAAAABEGYJbAAAAAAAAAIgyBLcAAAAAAAAAEGVyZfQOABklJibGf+7evTujdwUAAAAAAADZwO7/l0MFudSxENwi2/r777/9Z4UKFTJ6VwAAAAAAAJCN7Nmzx4oUKXLMZQhukW0VL17cf27cuPG4fyhAWn7Tpi8PNm3aZIULF87o3UE2xGsQ0YDXITIar0FkNF6DiAa8DpHRsstrMCYmxkPbcuXKHXdZgltkWyec8H8tnhXaZuV/EJA56DXI6xAZidcgogGvQ2Q0XoPIaLwGEQ14HSKjZYfXYJFEFhAyOBkAAAAAAAAARBmCWwAAAAAAAACIMgS3yLby5s1rQ4YM8Z9ARuF1iIzGaxDRgNchMhqvQWQ0XoOIBrwOkdF4DcaVI0YdcQEAAAAAAAAAUYOKWwAAAAAAAACIMgS3AAAAAAAAABBlCG4BAAAAAAAAIMoQ3AIAAAAAAABAlCG4RZb2/PPPW5UqVSxfvnx21lln2bx58465/JdffunLaflTTjnFXnzxxXTbV2RdSXkdvvvuu3b55ZdbqVKlrHDhwnbeeefZzJkz03V/kfUk9d/CwIIFCyxXrlxWr169NN9HZG1JfQ0ePHjQBg4caJUqVfJRhatWrWoTJ05Mt/1F1pTU1+GUKVOsbt26VqBAAStbtqx17tzZ/v7773TbX2QtX331lV199dVWrlw5y5Ejh7333nvHfQ6fTZCRr0E+lyBa/i3M7p9NCG6RZb355pt29913+we/b7/91i688EJr3ry5bdy4Md7l161bZ1deeaUvp+UfeOAB69Wrl73zzjvpvu/Ivq9D/Y9Mb5BmzJhhy5YtsyZNmvj/2PRcID1eg4Fdu3ZZhw4d7NJLL023fUXWlJzXYOvWre3zzz+3CRMm2M8//2yvv/661ahRI133G9n7dTh//nz/N7BLly62atUqe+utt+zrr7+2rl27pvu+I2v4999//YuAsWPHJmp5Ppsgo1+DfC5BNLwOA9n5s0mOmJiYmIzeCSAtNGzY0OrXr28vvPBCaF7NmjWtZcuWNmLEiDjL9+/f3z744AP78ccfQ/PuuOMOW7FihS1atCjd9hvZ+3UYn1q1almbNm1s8ODBabinyKqS+xq86aabrFq1apYzZ07/Jvy7775Lpz1Gdn8Nfvrpp/76++2336x48eLpvLfIqpL6Ohw5cqQvu3bt2tC8Z5991p544gnbtGlTuu03siZVmU2fPt1ffwnhswky+jUYHz6XIKNehzdl488mVNwiSzp06JB/K9i0adNY83V/4cKF8T5Hb4Ail7/iiivsm2++scOHD6fp/iJrSs7rMNLRo0dtz549hBdI19fgpEmTPKwYMmRIOuwlsrLkvAYVVJx99tkekJ188sl22mmn2b333mv79+9Pp71GVpOc1+H5559vmzdv9koz1bls3brV3n77bWvRokU67TWyOz6bINrwuQQZZVI2/2ySK6N3AEgLf/31l/33339WunTpWPN1/88//4z3OZof3/JHjhzx9am3GZDWr8NIo0aN8stJdNkwkB6vwV9++cUGDBjgvR/VQwpI79egKm11mbp6OqoKQ+vo3r277dixgz63SLfXoYJb9bhVZdmBAwf8/eA111zjVbdAeuCzCaINn0uQEX7hswkVt8j6pffhVDEROe94y8c3H0jL12FAPR2HDh3qfflOOumkNNxDZHWJfQ0q2Gjbtq099NBDXuUIZMS/g6ro0WMKzRo0aOA9HkePHm2vvPIKVbdIt9fh6tWrvZ+oLgdWta5aeKjnqC5VB9ILn00QLfhcgozAZ5P/kz3jamR5JUuW9N4nkVUU27Zti/PNdaBMmTLxLq9vdUqUKJGm+4usKTmvw4DeFGlAFA2Gctlll6XxniKrSuprUJe/6RJMDTrRo0ePUIimD4r6t/Czzz6zSy65JN32H9nz30FVkalFQpEiRWL1ItXrUJeuq78ZkNavQ/W9bdSokd13331+v06dOlawYEEfKGrYsGFUOyLN8dkE0YLPJcgofDb5P1TcIkvKkyePnXXWWTZr1qxY83Vfl77F57zzzouzvP4hUJ+93Llzp+n+ImtKzusw+Ea7U6dONnXqVHrpIV1fg4ULF7aVK1d6s/9gUnVZ9erV/bYG9wHS+t9BhWV//PGH7d27NzRvzZo1dsIJJ1j58uXTfJ+R9STndbhv3z5/zYVT+CuM7Yz0wGcTRAM+lyAj8dnk/4kBsqg33ngjJnfu3DETJkyIWb16dczdd98dU7BgwZj169f74wMGDIhp3759aPnffvstpkCBAjF9+vTx5fU8Pf/tt9/OwKNAdnsdTp06NSZXrlwxzz33XMyWLVtC0z///JOBR4Hs9BqMNGTIkJi6deum4x4ju78G9+zZE1O+fPmYG264IWbVqlUxX375ZUy1atViunbtmoFHgez2Opw0aZL///j555+PWbt2bcz8+fNjzj777JgGDRpk4FEgM9O/bd9++61P+hg+evRov71hwwZ/nM8miLbXIJ9LEA2vw0jZ8bMJwS2yNP1PplKlSjF58uSJqV+/vn/4C3Ts2DHm4osvjrX8F198EXPmmWf68pUrV4554YUXMmCvkZ1fh7qt/4FFTloOSK9/C7P7myNk/Gvwxx9/jLnsssti8ufP7yHuPffcE7Nv374M2HNk59fhM888E3P66af767Bs2bIx7dq1i9m8eXMG7Dmygrlz5x7zPR6fTRBtr0E+lyBa/i3M7p9Ncug/QfUtAAAAAAAAACDj0eMWAAAAAAAAAKIMwS0AAAAAAAAARBmCWwAAAAAAAACIMgS3AAAAAAAAABBlCG4BAAAAAAAAIMoQ3AIAAAAAAABAlCG4BQAAAAAAAIAoQ3ALAAAAAAAAAFGG4BYAAAAAAAAAogzBLQAAAJBNVa5c2Z566qkkPWf9+vWWI0cO++677ywj/Pzzz1amTBnbs2fPcZdduXKllS9f3v7999902TcAAIDURHALAAAAZDAFoceaOnXqdNznv/fee6m+X9puy5YtY82rUKGCbdmyxc444wzLCAMHDrS77rrLChUqdNxla9eubQ0aNLAxY8aky74BAACkJoJbAAAAIIMpCA0mVcAWLlw41rynn37aokXOnDm94jVXrlzpvu3NmzfbBx98YJ07d070c7TsCy+8YP/991+a7hsAAEBqI7gFAAAAMpiC0GAqUqSIV9CGz5s6dapVrVrV8uTJY9WrV7fXXnstVrsDue666/x5wf21a9fatddea6VLl7YTTzzRzjnnHJs9e3ai92no0KE2efJke//990OVv1988UWcVgmap/szZ860M8880/Lnz2+XXHKJbdu2zT755BOrWbOmB9E333yz7du3L7T+mJgYe+KJJ+yUU07x59StW9fefvvtY+7TtGnTfDm1Pwhs2LDBrr76aitWrJgVLFjQatWqZTNmzAg9fsUVV9jff/9tX375ZaKPHQAAIBqk/9fkAAAAABJt+vTp1rt3b6/Eveyyy+yjjz7yKlKFl02aNLGvv/7aTjrpJJs0aZI1a9bMK2Jl7969duWVV9qwYcMsX758HsIq4FSP2IoVKx53u/fee6/9+OOPtnv3bl+3FC9e3P74448Eg96xY8dagQIFrHXr1j7lzZvXQ2fti4LlZ5991vr37+/LDxo0yN59912vhq1WrZp99dVXdsstt1ipUqXs4osvjncbWubss8+ONU9tEw4dOuSPKbhdvXq1B9UBhd0Ke+fNm+eBMgAAQGZBcAsAAABE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auvQsmVLr3JWxfNpp50WZ1810IkGQwmfAAAAAAAAgGhEcJsG1qxZ4yMfK0BNiX///ddD4OrVq9uyZcu83+y9994b77L33Xef94RTUHrSSSd5cKzq3yDUVZ+53r17e29cBakaTCSl9u3bZ02aNPFBQr766iubP3++31blsSpeZc+ePd5SQiNK6zLHatWq2ZVXXunz5ejRox64KlBesmSJjRs3zgYOHJjk7YgGMfnxxx9t1qxZPrBJJIW6Co+DSUE2AAAAAAAAEI1olZBKbr75Zu9ve+TIEa/sVOB6//33p2idU6ZMsf/++89HVlawWatWLdu8ebPdeeedcZYdMmSIB7IyefJkK1++vE2fPt1at25tzz77rDVv3jwU+qoaVSM6xxduJsUbb7xhJ5xwgo0fPz7U51ctI1TJq0HTmjZtapdcckms57z00kteNfvll1/6Ofrss89s7dq1vrwqlkWhcnAsid2OqDWFlkmoRYJ+H/fcc0/ovipuCW8BAAAAAAAQjai4TSVjxozxalb1t1Ugqqrb9u3bp2idqh5V+wOFtgGN3hyf8PkarVlVunq+qC9vgwYNYi0feT85VAX866+/WqFChbwCVpO2feDAAQ9jZdu2bV7tq7A4qHTdu3evbdy4MbRvCk+D0Da+fUvMdoLBS47V11YDxRUuXDjWBAAAAAAAAEQjKm5TiYLHU0891W8rNFUrAFXhaoTkYH5SxcTEpGifgupUrSe4nVrrDtocnHXWWV4ZHKlUqVKh3rPbt2+3p556yipVquThqULmoMVBfPuWnO0EFbcAACDj/PDDD9a3b1//0vXvv/+2nTt3Hrenvnraq+WT+tnXr1/fr56pUaNGoh8HAAAAsioqbtOI2ibI/v37k70ODdSlCt7wdahPbHzC5+tDkip+gw81+qlBzsJ98803llL68PTLL794T12F0+FTMHCbetv26tXL+9qq1YOC27/++iu0Du2bqm+3bt0amqc+vUndDgAAyHi5c+f2Nk2vvPJKopbX+5V27dr5lUs7duzwFksagFStpxLzOAAAAJCVEdymkn/++cf+/PNP++OPP7x/68MPP+ztAWrWrJnsdbZt29Z7u3bp0sVWr15tM2bM8AHI4qPtaXAuVbqoyrVkyZI+6Jf07NnTnzt69GgPQNVn9pNPPjlupevx6IOUtqMPUApo161b58euQdDUi1cU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cAsAAAAASDVB64OiRYuG5gW3Fa4mZN++fd7+oGvXrrHmN2rUyLZt22bFihXzfrk7duzwsFZy5szp7RPUkmH//v1WunTpWP1xAQDIzAhuAQAAAACpRi0NJLy6NritdgbH6o9boECBUDWtHD161C6//HIPbxUIa7rgggvsiiuu8MerVq1qkydPtrFjx3po27RpU/vuu+/S8OgAAEg/BLcAAAAAgFSjytjy5cvHClB1u0KFClakSJEEn6fetBo4OFeu/38MbVXXalCyXr16eairqWfPnrZo0SL766+/fBn1v507d65t3brV6tata+3bt0/jIwQAIH0Q3AIAAAAAUlXnzp3t0UcftT///NOn4cOHx2mBEO7nn3+2hQsX2q233hprfsmSJe3UU0+15557zg4cOOCTbisY1mN63qxZs7xNQp48ebzaNzz4BQAgM8v2we0XX3xhOXLk8Cb3GbV9jXyaVI0bN7a77747dF/reOqpp6Jm/wAAAABkXw8++KCdd955VrNmTZ/OP/98e+CBB/yxO+64w6fIQckuvPBCO+200+Ks6/3337fly5fbySefbGXLlrWlS5faBx984I8dOnTIt6U2CSVKlLA5c+bYK6+8kk5HCQBA2srUX0WqQb3+J/3JJ5/4ZTG6JEeXxgwdOtTfJKSGTp06ec+kY1Ez/Mjl1DT/nHPOsSeeeILm+AAAAACyldy5c3tlrKZIL774Ypx5+tyUkNNPP91mzpwZ72O1a9e2xYsXp3BvAQCITpm64vb666+3FStWeGC6Zs0a/9ZVlajqg5Rann76aduyZUtokkmTJsWZJ82aNQvN+/zzz/0SnauuuirV9gUAAAAAAABA9pBpg1u1Npg/f749/vjj1qRJE6tUqZI1aNDA7r///tAopOvXr/c2COFN8fU8zVMLgHALFizwat18+fJZw4YNbeXKlT5fzfPLlCkTmqRo0aJx5knevHlD8+rVq2f9+/e3TZs22fbt21N0rP/++6916NDB+zXp0qBRo0bFu9yePXusbdu2vly5cuXs2WefjfX4Tz/95COw6hj1rfXs2bP9XLz33nsp2j+N9Krfg3pP6RxUrFjR+1kFdC4vueQSy58/v1++dNttt/losAFVK7ds2dL7XukSJ53fhx56yI4cOWL33XefVy+rh9XEiRNDzwl+t2+88YZfdqVjqlWrVpzfa7iDBw/a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nXvBBRdYnjx5vNo3PPgFAAApk20rbr/44gvLkSOH/fPPPxm2fY26mlTqLXX33XeH7msdTz31VNTsHwAAAICM9eCDD9p5551nNWvW9On888+3Bx54wB+74447fIoclOzCCy+00047Ldb8/Pnze3VtMBUvXtw/Q+l27ty57dChQ74ttUlQkDtnzhx75ZVX0vVYAQDIyjJlcLtt2za7/fbbrWLFipY3b15/43DFFVfYokWLUm0bnTp18jclx5riW05vWPSt9Pfff59q+wIAAAAAiaVQVb1pd+7c6ZN60AaVsC+++KJP4Z544gn78ssvE1VEEl74Urt2bVu8eLG3T9B8rUPtEgAAQDYObq+//npbsWKFN8Jfs2aNffDBB/4mYseOHam2jaefftq2bNkSmmTSpElx5omC2mDe559/7m+KrrrqqlTbFySNRrY9cuRIRu8GAAAAAAAAkH2CW32TO3/+fHv88cetSZMmVqlSJWvQoIHdf//9oRFQ169f79Wv4Q359TzNUwuAcAsWLPBvhfPly2cNGza0lStX+nw17g+/LEiKFi0aZ54EVb+a6tWrZ/3797dNmzZ5I/+U+Pfff61Dhw7eK6ps2bI2atSoeJfbs2ePtW3b1pcrV66cPfvss7Ee/+mnn7zvlI7x9NNPt9mzZ/u5eO+991IckOrb+VNOOcUvo9J5fPvtt0OP//fff9alSxerUqWKP169enUPxMMpYO3Vq5efW1Ur69xpUISWLVsmejtB24uZM2fa2Wef7b8PDaQQ6eDBg14NED4BAAAAAAAA0SjTBbcKJzUpdFQQl1L33XefjRw50r7++ms76aST7JprrrHDhw8ne3179+61KVOm2KmnnupBZEr3TSO0Tp8+3T777DMPKJctWxZnuSeffNLq1Kljy5cv9wC7T58+NmvWLH/s6NGjHoIWKFDAlixZYuPGjbOBAwdaahg0aJBXIb/wwgu2atUq3+4tt9wSusxK29aIttOmTbPVq1fb4MGDvbeW7gcUwOt8aT0K0RWmRgbKx9tOoF+/fjZixAj78ccf/XxE0mMK5INJI+sCAAAAAAAAUSkmE3r77bdjihUrFpMvX76Y888/P+b++++PWbFiRejxdevWxejQvv3229C8nTt3+ry5c+f6ff3U/TfeeCO0zN9//x2TP3/+mDfffDPONrXs9OnT48zv2LFjTM6cOWMK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bOXKlda8eXN777334l3+4MGD1qNHD5s9e7b99ddfdvLJJ1u/fv3s1ltvDS2T2HVFG4JbAAAAAAAAAFGlXLlyNmjQIA9kN2/enOByR44csbJly/pyp5xyii1ZssTD2fLly1vTpk2TtK5oQ6sEAAAAAAAAAFGlVatW1rJlSytZsuQxlytYsKA9/PDDVrVqVcuRI4ede+651qRJE5s/f36i1zV69GirWLGiFSpUyCpXrmzjx4+3aEDFLQAAAAAAAIAs4cCBA7Z06VJr27ZtopZfs2aNV+MuX77catSoYVu3bvUpGhDcAgAAAAAAAMj0YmJirGvXrlatWjWvsk2MnDlz+vNWrVpllSpVstKlS/sUDWiVAAAAAAAAACBTi4mJsTvvvNN+/vlnH3zshBMSF3uqxcLkyZN9ADMFtuqL+91331k0ILgFAAAAAAAAkKlD27vuustbJHz22WdWpEiRJD2/devWNnfuXG+RULduXWvfvr1FA4JbAAAAAAAAAFHlyJEj3q9WP48ePeq3Dx06FO+yPXr0sAULFtisWbOsWLFiSVqXKnT1vP3791uePHnsxBNPtFy5cmXv4PaLL77wkd7++eefDNu+RonLCEOHDrV69eolevn169f7ucqoMu3GjRvbK6+8kiHbBgAAAAAAQPYzbNgwy58/vz366KP24Ycf+m21MZA77rjDJ9mwYYM9//zzHsCqR62CV03B48dblwLcBx980NsklChRwubMmRM1OViygttt27bZ7bffbhUrVrS8efNamTJl7IorrrBFixal2o516tTJw8pjTfEtpxPcrFkz+/7771NtXwAAAAAAAACkb+FjTExMrEmFmPLiiy/6JApr9ZiqaPfu3RuagsePt67atWvb4sWLbffu3V5g+uWXX3q7hEwb3F5//fW2YsUKb9y7Zs0a++CDD7wqc8eOHam2Y08//bRt2bIlNMmkSZPizBMFtcG8zz//3MuZr7rqqlTbFwAAAAAAAACI6uBWyfP8+fPt8ccftyZNmniq3aBBA7v//vutRYsWCV7ar+dpXpBmB9R/Qil2vnz5rGHDhrZy5UqfrybCquQNJilatGiceRJU/WpSC4L+/fvbpk2bbPv27SluEdCrVy/r16+fFS9e3NevhD7cxo0b7dprr/US7MKFC3szYzUyDvfYY495uXWhQoWsS5cu/g1AJIXSNWvW9PNQ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pyFDhnglbvXq1a1Lly42bty40HNGjx5tFStWtEKFClnlypVt/PjxaXpsAACkFipuAQAAAADpZufOnbZ582arV69eaJ5ub9y40Xbt2mVFihRJ8LkKd1WdW65cOb9/9OhR/xkTExNaRvO+//57v71mzRoPiJcvX241atSwrVu3+gQAQGZAxS0AAAAAIN0ErQ+KFi0amhfc3rNnT4LP27dvn73xxhvWtWvX0DxV2FapUsVbLBw8eNBbL6iad/fu3f54zpw5PdTV/P3791vp0qVj9ccFACCaEdwCAAAAANKNBhgTVdcGgttqZ5CQadOmWYECBaxFixaheWqPoN623333nfe1bdeunXXu3NlKlCjhj1etWtUmT55sY8eO9dC2adOmviwAAJkBwS0AAAAAIN0UK1bMQ9bwAFW3K1SocMw2CepN27FjR8uVK3bHv5o1a9rMmTNt+/btvh5V3l588cWhx1u3bm1z5871Fgl169a19u3bp9GRAQCQuuhxCwAAAABIV6qKffTRR61Ro0Z+f/jw4bFaIETSAGQLFy70NgiR1M9WlbWqvv3oo498mc8//zz0PPXOveCCCyxPnjxe7RsZ/AIAEK3SveL2iy++sBw5ctg///yT3psObV8jiWaEoUOHxmrAfzzr16/3c5VRl/I0btzYXnnllQzZNgAAAICs68EHH7TzzjvPq2U1nX/++fbAAw/4Y3fccYdPkYOSXXjhhXbaaafF20JB1bqq5B05cqS99957oT62hw4d8m2pTYLaJ8yZM4fPOACArBncbtu2zW6//XarWLGi5c2b18qUKWNXXHGFLVq0KNV2qFOnTh5WHmuKbzn9T7hZs2ah0UMBAAAAANFJ1bHPPfec7dy50yf1oA0qYV988UWfwj3xxBP25ZdfxruuYcOG2Y4dO+zff//1qtygildq165tixcv9sHKVDykdahdAgAAWS64vf76623FihXe3H3NmjXeBF5VmfqfZGp5+umnbcuWLaFJJk2aFGeeKKgN5ulSGP2P/qqrrkq1fUHmpW/WAQAAAAAAgCwf3Orbyfnz59vjjz9uTZo0sUqVKlmDBg3s/vvvD43qGd+l/Xqe5qlFQbgFCxb4N5358uWzhg0b2sqVK32+mtGrkjeYpGjRonHmSVD1q0ktCPr372+bNm3ypvQpoTC6V69e1q9fPytevLivX20OwqlP0rXX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Vs1GjRplFSpUSPV6AACAzOpoetwS3CJhHc0/FAAAAAAAACCtGE4GAAAAAAAAACcxglsAAAAAAAAAiDMEtwAAAAAAAAAQZwhuAQAAAAAAACDOZM/oAwAy2saePW1HrlwZfRgAAAA4DkoMGuRf9+/fb127drVx48b55RYtWtiQIUMse/bkPwJNmTLF+vTpYz/++KMPDNH37du393W//vqr3X///TZ37lzLkiWL1alTx4YNG2ZnnHFGOj4yAACQaKi4BQAAAJDp9O/f3+bNm2fLli3zRaHrgAEDkt32448/tg4dOtjzzz/vk561/VVXXRVer3Wybt06W7Nmje3du9c6d+6cbo8FAAAkJoJbAAAAAJnO6NGjrVevXla8eHFfevbsaaNGjUp22969e3uFrcLabNmyWcGCBa1ChQrh9QprmzVrZqeeeqqddtpp1rx5c/v3v/8dXj948GArXbq0rytbtqyNHDkyXR4jAADI3AhuAQAAAGQqW7ZssQ0bNljVqlXD1+n79evX27Zt26K23blzpy1atMgrbRXWFitWzIPZTZs2hbd58MEH7d133/V9t27dam+99ZY1atTI161atcoD4k8++cSSkpJs4cKFVrNmzXR8tAAAILMiuAUAAACQqezYscO/FihQIHxd8L3C1diQNxQK2dixY2369On2008/WY4cOaxly5bhbWrXrm1//PGHV+IWKlTINm/e7GGtqEJX+6u9wu7du73vbeXKldPpkQIAgMyM4BYAAABApqKWBhJZXRt8r3YGyW37wAMPWJkyZfzy448/bjNmzPBq3L/++suuvfZaD28VCGu5/PLLrV69er5fuXLl7LXXXgsPK7vuuuvsu+++S8dHCwAAMiuCWwAAAACZiipjS5YsGRWg6vtSpUpZ/vz5o7ZVJa7602bJkuWQ21ElraprNZRMwW6ePHl86dSpk3355Zf23//+17dT/9tZs2bZ77//blWqVImq1gUAADhWBLcAAAAAMp277rrLnnrqKe9Vq2XAgAHWtm3bZLe95557bOjQofbrr796u4MnnnjC6tat69W3RYoUsXPOOceGDx9ue/bs8UXfKxjWupUrV9qnn37q++XMmdP3yZ49e7o/XgAAkPkkdHCrv6qvXbs23e/3888/9/vWYIPU0nTa559/3jJCv379rHXr1hly3wAAAMCx6N27t9WqVcsqVqzo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vXv3tlq1ankbBC2XXXaZPfbYY75OGce4ceO8JYJaJ9xwww3WqFGjcI6hMFj7q01C4cKFbebMmfbqq69m0KMGTj7p8mcOhX/qaeJ3mD27FSpUyJtR33bbbb4ua9asUUHlunXr/Htdr3/cDRo0sEGDBnmJfmD79u327LPP2vvvv2+rV6+2PHny2Nlnn2233HKLtWvXLmpbAAAAAAAA/P+FWlqSM2LEiKjLOXLksOHDh/sSK2/evF5Rm5JKlSrZggULjsMRA4kp3Spu69evbxs3bvRK12nTplmdOnWsc+fO1rhxYy/Nj/TEE0/4tuvXr7c333zT5syZYw888EB4/ebNm+3SSy/1cvxu3brZwoUL7YsvvrC+fft6rxX9tQeJTT11AAAAAAAAgJNVugW3uXLlsmLFitmZZ55p1apV87L6yZMne4gbWyav8vpgWwW8d955p3377bfh9dpXoa4CW/VaUfVuhQoVPARWaNuhQ4c0Hav+6lS1alUbO3asVwCr4fatt95qSUlJ4W327t3rYXLRokUtd+7cdvnll9vXX38ddTtTp061c88910850ONIrj3D/Pnz7corr/Rt1Nxbt7lz586obXS/t99+u/eCKVGihL3wwgvhdbpNnZoQ2ShcrRh0nVozBKZMmWLly5cPH4sqoI+2bUNyNE3ynnvu8echX758dvXVV9uSJUvC63/++Wc/VUKV0zr+iy++2D777LOo21BIr1MpdGxnnXWW/wxjW0Qc6X6Cn9no0aO98lqvN/XXiaSfmSq1IxcAAAAAAAAgHmVoj1uFb5ooqHYHKfn111/tww8/tEsuucQvq2n2O++8Y3fccYcHu8lRIJlWChwnTZrk961l9uzZ9vTTT4fXd+/e3d577z0PQBUqn3POOd58W9XA8ssvv1jTpk2tYcOGHqqqcXePHj2i7kNNu7WPtvv+++/9cc2bN886duwYtZ1aQiic1v08+uij1rVr18OeihBL4e7NN99sTZo08WO59957rWfPnml+jhSMKnBVc3KF1IsWLfJQvm7duuHnQY3K9RworF28eLE/3uuvv96D94CC+d9++82DZj2nL7/8sv3xxx9HdT/y008/2fjx4/02IoPswMCBAz2EDxYF5QAAAAAAAEA8yvDhZKqUja1EfeSRR7w6UxWYJUuW9CB28ODBvu4///mPV4med955UftUr17d99Gi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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Intro to torchgfn performance tuning\n", + "import sys\n", + "from pathlib import Path\n", + "\n", + "# Ensure local import of performance_tuning.py in this folder\n", + "NOTEBOOK_DIR = Path(__file__).parent if \"__file__\" in globals() else Path.cwd()\n", + "if str(NOTEBOOK_DIR) not in sys.path:\n", + " sys.path.insert(0, str(NOTEBOOK_DIR))\n", + "\n", + "import performance_tuning as pt\n", + "\n", + "# Quick demo settings (adjust as needed)\n", + "settings = pt.BenchmarkSettings(\n", + " batch_size=32,\n", + " warmup_iters=50,\n", + " n_iters=200,\n", + " device=\"cpu\", # change to \"cuda\" or \"mps\" if available\n", + " output_path=None, # None => render inline, no file saved\n", + " deterministic_mode=False,\n", + ")\n", + "\n", + "results, fig = pt.run_benchmarks(settings=settings, return_fig=True, verbose=True)\n", + "# fig # display inline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c9414c1e", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "torchgfn", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/misc/performance_tuning.py b/tutorials/misc/performance_tuning.py new file mode 100644 index 00000000..fd731e1d --- /dev/null +++ b/tutorials/misc/performance_tuning.py @@ -0,0 +1,537 @@ +""" +Quick performance benchmark for GFlowNet training. + +Runs per-iteration timing across environments (HyperGrid, DiffusionSampling, +DiscreteEBM), losses (TB, SubTB, ModifiedDB), debug on/off, and torch.compile +on/off (compiling the standard sampling loop when possible). Produces a bar plot +saved to ~/performance_tuning.png. +""" + +from __future__ import annotations + +import argparse +import statistics +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Callable + +import matplotlib.pyplot as plt +import torch + +from gfn.estimators import ( + DiscretePolicyEstimator, + PinnedBrownianMotionBackward, + PinnedBrownianMotionForward, + ScalarEstimator, +) +from gfn.gflownet.detailed_balance import DBGFlowNet, ModifiedDBGFlowNet +from gfn.gflownet.sub_trajectory_balance import SubTBGFlowNet +from gfn.gflownet.trajectory_balance import TBGFlowNet +from gfn.gym import DiscreteEBM, HyperGrid +from gfn.gym.diffusion_sampling import DiffusionSampling +from gfn.preprocessors import IdentityPreprocessor, KHotPreprocessor +from gfn.utils.common import set_seed +from gfn.utils.modules import ( + MLP, + DiffusionFixedBackwardModule, + DiffusionPISGradNetForward, +) + + +@dataclass +class BenchmarkSettings: + batch_size: int = 32 + warmup_iters: int = 50 + n_iters: int = 200 + device: str = "cpu" + output_path: Path | None = Path.home() / "performance_tuning.png" + deterministic_mode: bool = False + + +LR = 1e-3 +LR_LOGZ = 1e-1 +LR_LOGF = 1e-3 + + +@dataclass +class BenchmarkConfig: + env_name: str + loss_name: str + debug: bool + use_compile: bool + + @property + def label(self) -> str: + dbg = "dbg" if self.debug else "nodbg" + comp = "comp" if self.use_compile else "eager" + return f"{self.env_name}/{self.loss_name}/{dbg}/{comp}" + + +@dataclass +class BenchmarkResult: + config: BenchmarkConfig + env_name: str + elapsed: float + mean_iter_ms: float + std_iter_ms: float + compiled: bool + skipped: bool + reason: str | None = None + + +def resolve_device(requested: str = "auto") -> torch.device: + if requested == "auto": + if torch.cuda.is_available(): + return torch.device("cuda") + mps_backend = getattr(torch.backends, "mps", None) + if mps_backend is not None and mps_backend.is_available(): + return torch.device("mps") + return torch.device("cpu") + device = torch.device(requested) + if device.type == "cuda" and not torch.cuda.is_available(): + raise RuntimeError("CUDA requested but not available.") + if device.type == "mps": + mps_backend = getattr(torch.backends, "mps", None) + if mps_backend is None or not mps_backend.is_available(): + raise RuntimeError("MPS requested but not available.") + return device + + +def build_components(env_name: str, loss_name: str, debug: bool, device: torch.device): + """Create env, gflownet, optimizer for a given setup.""" + # Diffusion branch first (no n_actions attribute). + if env_name == "diffusion": + env = DiffusionSampling( + target_str="gmm2", + target_kwargs={"seed": 2}, + num_discretization_steps=32, + device=device, + debug=debug, + ) + s_dim = env.dim + pf_module = DiffusionPISGradNetForward( + s_dim=s_dim, + harmonics_dim=64, + t_emb_dim=64, + s_emb_dim=64, + hidden_dim=64, + joint_layers=2, + zero_init=False, + ) + pb_module = DiffusionFixedBackwardModule(s_dim=s_dim) + pf = PinnedBrownianMotionForward( + s_dim=s_dim, + pf_module=pf_module, + sigma=5.0, + num_discretization_steps=32, + ) + pb = PinnedBrownianMotionBackward( + s_dim=s_dim, + pb_module=pb_module, + sigma=5.0, + num_discretization_steps=32, + ) + logF = ScalarEstimator( + module=MLP(input_dim=env.state_shape[-1], output_dim=1), + preprocessor=IdentityPreprocessor(output_dim=env.state_shape[-1]), + ) + elif env_name == "hypergrid": + env = HyperGrid( + ndim=2, + height=32, + reward_fn_str="original", + reward_fn_kwargs={"R0": 0.1, "R1": 0.5, "R2": 2.0}, + calculate_partition=False, + store_all_states=False, + device=device, + debug=debug, + ) + preprocessor = KHotPreprocessor(height=env.height, ndim=env.ndim) + assert isinstance(preprocessor.output_dim, int) + out_dim = preprocessor.output_dim + pf_module = MLP(input_dim=out_dim, output_dim=env.n_actions) + pb_module = MLP(input_dim=out_dim, output_dim=env.n_actions - 1) + pf = DiscretePolicyEstimator( + module=pf_module, n_actions=env.n_actions, preprocessor=preprocessor + ) + pb = DiscretePolicyEstimator( + module=pb_module, + n_actions=env.n_actions, + preprocessor=preprocessor, + is_backward=True, + ) + logF = ScalarEstimator( + module=MLP(input_dim=out_dim, output_dim=1), preprocessor=preprocessor + ) + elif env_name == "discrete_ebm": + env = DiscreteEBM(ndim=6, device=device, debug=debug) + preprocessor = IdentityPreprocessor(output_dim=env.state_shape[-1]) + assert isinstance(preprocessor.output_dim, int) + out_dim = preprocessor.output_dim + pf_module = MLP(input_dim=out_dim, output_dim=env.n_actions) + pb_module = MLP(input_dim=out_dim, output_dim=env.n_actions - 1) + pf = DiscretePolicyEstimator( + module=pf_module, n_actions=env.n_actions, preprocessor=preprocessor + ) + pb = DiscretePolicyEstimator( + module=pb_module, + n_actions=env.n_actions, + preprocessor=preprocessor, + is_backward=True, + ) + logF = ScalarEstimator( + module=MLP(input_dim=out_dim, output_dim=1), preprocessor=preprocessor + ) + else: + raise ValueError(f"Unknown environment {env_name}") + + if loss_name == "TB": + gflownet = TBGFlowNet(pf=pf, pb=pb, init_logZ=0.0) + elif loss_name == "SubTB": + gflownet = SubTBGFlowNet( + pf=pf, pb=pb, logF=logF, weighting="ModifiedDB", lamda=0.9 + ) + elif loss_name == "DBG": + gflownet = DBGFlowNet(pf=pf, pb=pb, logF=logF) + elif loss_name == "ModifiedDB": + gflownet = ModifiedDBGFlowNet(pf=pf, pb=pb) + else: + raise ValueError(f"Unknown loss {loss_name}") + + gflownet = gflownet.to(device) + optimizer = torch.optim.Adam(gflownet.pf_pb_parameters(), lr=LR) + + if hasattr(gflownet, "logz_parameters") and callable(gflownet.logz_parameters): + params = gflownet.logz_parameters() + if params: + optimizer.add_param_group({"params": params, "lr": LR_LOGZ}) + + if hasattr(gflownet, "logF_parameters") and callable(gflownet.logF_parameters): + params = gflownet.logF_parameters() + if params: + optimizer.add_param_group({"params": params, "lr": LR_LOGF}) + + return env, gflownet, optimizer + + +def make_step_fn( + env, gflownet, optimizer, batch_size: int, device: torch.device +) -> Callable[[], float]: + def step(): + trajectories = gflownet.sample_trajectories( + env, + n=batch_size, + save_logprobs=False, + save_estimator_outputs=False, + ) + training_samples = gflownet.to_training_samples(trajectories) + + # Simulated off-policy sampling. + loss = gflownet.loss(env, training_samples, recalculate_all_logprobs=True) + + optimizer.zero_grad(set_to_none=True) + loss.backward() + torch.nn.utils.clip_grad_norm_(gflownet.parameters(), 1.0) + optimizer.step() + return float(loss.detach()) + + return step + + +def maybe_compile(fn: Callable[[], float]) -> tuple[Callable[[], float], bool]: + if not hasattr(torch, "compile"): + return fn, False + try: + compiled = torch.compile(fn, mode="reduce-overhead") # type: ignore[arg-type] + # One dry run to populate graph / catch failures early. + compiled() + return compiled, True + except Exception: + return fn, False + + +def synchronize(device: torch.device) -> None: + if device.type == "cuda" and torch.cuda.is_available(): + torch.cuda.synchronize() + elif device.type == "mps": + mps_backend = getattr(torch, "mps", None) + if mps_backend is not None and hasattr(mps_backend, "synchronize"): + # Ensure queued MPS work completes before timing. + mps_backend.synchronize() + + +def time_iterations( + step_fn: Callable[[], float], + device: torch.device, + warmup: int, + n_iters: int, +) -> tuple[float, list[float]]: + + for _ in range(warmup): + step_fn() + + iter_times: list[float] = [] + start = time.perf_counter() + + for _ in range(n_iters): + iter_start = time.perf_counter() + step_fn() + synchronize(device) # Measure actual kernel + overhead per step. + iter_times.append((time.perf_counter() - iter_start) * 1000.0) + + synchronize(device) + total = time.perf_counter() - start + + return total, iter_times + + +def run_benchmark( + config: BenchmarkConfig, device: torch.device, settings: BenchmarkSettings +) -> BenchmarkResult: + # if config.loss_name == "ModifiedDB" and config.env_name != "hypergrid": + # return BenchmarkResult( + # config=config, + # elapsed=0.0, + # mean_iter_ms=0.0, + # std_iter_ms=0.0, + # compiled=False, + # skipped=True, + # reason="ModifiedDB supported only on HyperGrid", + # ) + try: + env, gflownet, optimizer = build_components( + config.env_name, config.loss_name, config.debug, device + ) + except Exception as exc: + return BenchmarkResult( + config=config, + env_name=config.env_name, + elapsed=0.0, + mean_iter_ms=0.0, + std_iter_ms=0.0, + compiled=False, + skipped=True, + reason=f"init failed: {exc}", + ) + + step_fn = make_step_fn(env, gflownet, optimizer, settings.batch_size, device) + compiled = False + if config.use_compile: + step_fn, compiled = maybe_compile(step_fn) + + try: + elapsed, iter_times = time_iterations( + step_fn, + device=device, + warmup=settings.warmup_iters, + n_iters=settings.n_iters, + ) + mean_ms = statistics.fmean(iter_times) if iter_times else 0.0 + std_ms = statistics.pstdev(iter_times) if len(iter_times) > 1 else 0.0 + return BenchmarkResult( + config=config, + env_name=config.env_name, + elapsed=elapsed, + mean_iter_ms=mean_ms, + std_iter_ms=std_ms, + compiled=compiled, + skipped=False, + reason=None, + ) + except Exception as exc: + return BenchmarkResult( + config=config, + env_name=config.env_name, + elapsed=0.0, + mean_iter_ms=0.0, + std_iter_ms=0.0, + compiled=compiled, + skipped=True, + reason=f"run failed: {exc}", + ) + + +def plot_results( + results: list[BenchmarkResult], + output_path: Path | None = None, + return_fig: bool = False, +): + ok_results = [r for r in results if not r.skipped] + if not ok_results: + print("No successful runs to plot.") + return + + # Preserve environment order of appearance. + env_order: list[str] = [] + for res in ok_results: + if res.env_name not in env_order: + env_order.append(res.env_name) + + n_rows = max(1, len(env_order)) + fig, axes = plt.subplots(n_rows, 1, figsize=(14, 4.5 * n_rows)) + if n_rows == 1: + axes = [axes] # type: ignore[list-item] + + # Fixed colors by condition for easy cross-env comparison. + condition_colors = { + (True, False): "#000000", # debug, eager + (False, False): "#8b0000", # nodebug, eager (dark red) + (True, True): "#555555", # debug, compiled (dark grey) + (False, True): "#e57373", # nodebug, compiled (light red) + } + + for row_idx, env_key in enumerate(env_order): + row_ax = axes[row_idx] + env_results = [r for r in ok_results if r.env_name == env_key] + if not env_results: + continue + + # Baselines per loss: debug=True & use_compile=False when available. + baselines: dict[str, float] = {} + for res in env_results: + if res.config.debug and not res.config.use_compile: + baselines[res.config.loss_name] = res.elapsed or 1.0 + for res in env_results: + baselines.setdefault(res.config.loss_name, res.elapsed or 1.0) + + labels: list[str] = [] + times: list[float] = [] + colors: list[str] = [] + speeds: list[float] = [] + + for res in env_results: + labels.append( + f"{res.config.loss_name} | {'dbg' if res.config.debug else 'nodebug'} | " + f"{'comp' if res.config.use_compile else 'eager'}" + ) + times.append(res.elapsed) + colors.append( + condition_colors.get( + (res.config.debug, res.config.use_compile), + "#6c757d", # fallback neutral + ) + ) + base = baselines.get(res.config.loss_name, res.elapsed or 1.0) + speeds.append(base / res.elapsed if res.elapsed else 0.0) + + bars = row_ax.barh(labels, times, color=colors) + row_ax.set_xlabel("Total time (s)") + row_ax.set_title(f"{env_key} | runtime; baseline = debug+eager") + + for bar, res, speed in zip(bars, env_results, speeds): + row_ax.text( + bar.get_width(), + bar.get_y() + bar.get_height() / 2, + f"{res.elapsed:.2f}s\nx{speed:.2f}", + va="center", + ha="left", + fontsize=9, + ) + + row_ax.invert_yaxis() + for label in row_ax.get_yticklabels(): + label.set_rotation(0) + label.set_ha("right") + + fig.tight_layout() + if output_path is not None: + output_path.parent.mkdir(parents=True, exist_ok=True) + fig.savefig(output_path, dpi=150, bbox_inches="tight") + if return_fig: + return fig + plt.close(fig) + + +def run_benchmarks( + settings: BenchmarkSettings, + *, + return_fig: bool = False, + verbose: bool = False, +): + # Reduce CPU scheduling noise for more stable comparisons. + try: + torch.set_num_threads(1) + except Exception: + pass + + set_seed(0, deterministic_mode=settings.deterministic_mode) + device = resolve_device(settings.device) + if verbose: + print(f"Using device: {device}") + + configs: list[BenchmarkConfig] = [] + for env_name in ["hypergrid", "diffusion", "discrete_ebm"]: + for loss_name in ["TB", "SubTB", "DBG"]: + for debug in [True, False]: + for use_compile in [True, False]: + configs.append( + BenchmarkConfig( + env_name=env_name, + loss_name=loss_name, + debug=debug, + use_compile=use_compile, + ) + ) + + results: list[BenchmarkResult] = [] + for cfg in configs: + if verbose: + print(f"Running {cfg.label} ...") + res = run_benchmark(cfg, device, settings) + if verbose: + status = ( + "skipped" + if res.skipped + else f"done ({'compiled' if res.compiled else 'eager'})" + ) + msg = ( + f" {status}: elapsed={res.elapsed:.2f}s, " + f"mean_iter={res.mean_iter_ms:.2f} ms, std={res.std_iter_ms:.2f} ms" + ) + if res.reason: + msg += f" | reason: {res.reason}" + print(msg) + results.append(res) + + fig = plot_results(results, output_path=settings.output_path, return_fig=return_fig) + return results, fig + + +def main() -> None: + parser = argparse.ArgumentParser(description="GFlowNet performance benchmark") + parser.add_argument( + "--device", + default="cpu", + choices=["auto", "cpu", "cuda", "mps"], + help="Device to run on; default cpu (auto prefers cuda>mps>cpu).", + ) + args = parser.parse_args() + + settings = BenchmarkSettings( + batch_size=32, + warmup_iters=50, + n_iters=200, + device=args.device, + output_path=Path.home() / "performance_tuning.png", + deterministic_mode=False, + ) + + results, _ = run_benchmarks(settings=settings, return_fig=False, verbose=True) + if settings.output_path is not None: + print(f"Saved plot to {settings.output_path}") + + print("\nSummary (successful runs):") + for res in results: + if res.skipped: + print(f"- {res.config.label}: skipped ({res.reason})") + continue + print( + f"- {res.config.label}: {res.elapsed:.2f}s total | " + f"{res.mean_iter_ms:.2f}±{res.std_iter_ms:.2f} ms/iter | " + f"{'compiled' if res.compiled else 'eager'}" + ) + + +if __name__ == "__main__": + main()