Spaces:
Running on Zero
Running on Zero
Download src/gem/sampler.py from hugging-apps/graph-energy-matching: direct link, hf CLI and curl.
- Browser
- Download file 38 kB
-
https://huggingface.co/spaces/hugging-apps/graph-energy-matching/resolve/main/src/gem/sampler.py
- Command line
-
hf download hf://spaces/hugging-apps/graph-energy-matching/src/gem/sampler.py
-
curl -L -o sampler.py https://huggingface.co/spaces/hugging-apps/graph-energy-matching/resolve/main/src/gem/sampler.py
38 kB
| from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple | |
| from contextlib import contextmanager, nullcontext | |
| from dataclasses import dataclass | |
| import torch | |
| from gem.flow_matching.noise_distribution import NoiseDistribution | |
| from gem.flow_matching import flow_matching_utils | |
| # Re-export energy utilities for external use | |
| from .sampler_energy import ( | |
| energy_and_grads_batch, | |
| energy_batch, | |
| register_property_time_override, | |
| ) | |
| # Proposals | |
| from .proposals.base import Proposal | |
| from .proposals.random_proposal import RandomProposal | |
| from .proposals.gwd_proposal import GWDProposal | |
| from .proposals.gwg_block_proposal import GWGBlockProposal | |
| from .proposals.dlangevin_proposal import ( | |
| DLangevinProposal, | |
| DLangevinVectorizedProposal, | |
| DLangevinNoMHProposal, | |
| DLangevinMTProposal, | |
| DLangevinAnnealingProposal, | |
| DLangevinTwoBetasProposal, | |
| DLangevinTwoBetasVectorizedProposal, | |
| DLangevinTwoBetasAnnealingProposal, | |
| DLangevinTwoBetasAnnealingVectorizedProposal, | |
| DLangevinTwoBetasAnnealingVectorizedNoOriginProposal, | |
| ) | |
| from .proposals.simple_proposal import ( | |
| SimpleProposal, | |
| SimpleProposalV2, | |
| SimpleProposalV2GuidedStrong, | |
| run_simple_v2_warmup_vectorized, | |
| ) | |
| from .dlangevin_utils import ( | |
| TWO_BETA_PROPOSALS, | |
| TWO_BETA_SCALAR_PROPOSALS, | |
| TWO_BETA_VECTOR_PROPOSALS, | |
| TWO_BETA_ANNEALING_PROPOSALS, | |
| TWO_BETA_ANNEALING_VECTOR_PROPOSALS, | |
| ) | |
| _VECTORIZED_SIMPLE_WARMUP_PROPOSALS = frozenset({"simple_ver2", "simple_v2"}) | |
| class MCMCStepEvent: | |
| """Read-only views of an MCMC step for diagnostics.""" | |
| step: int | |
| nodes: Sequence[torch.Tensor] | |
| edges: Sequence[torch.Tensor] | |
| energies: torch.Tensor | |
| accepted: torch.Tensor | |
| proposed_nodes: Sequence[torch.Tensor] | |
| proposed_edges: Sequence[torch.Tensor] | |
| proposed_energies: Optional[torch.Tensor] | |
| def _amp_autocast_ctx(device: torch.device, amp_dtype: Optional[str] = None): | |
| """Autocast context controlled by desired AMP dtype. | |
| If `amp_dtype` is None or empty, returns a no-op context to keep FP32. | |
| Otherwise, uses the requested dtype ("fp16"/"float16" or "bf16"/"bfloat16"). | |
| """ | |
| if device.type != "cuda": | |
| return nullcontext() | |
| if not amp_dtype: | |
| return nullcontext() | |
| amp = amp_dtype.lower() | |
| if amp in {"bf16", "bfloat16"}: | |
| return torch.amp.autocast("cuda", dtype=torch.bfloat16) | |
| if amp in {"fp16", "float16"}: | |
| return torch.amp.autocast("cuda", dtype=torch.float16) | |
| return nullcontext() | |
| def should_vectorize_simple_warmup( | |
| proposal: str, | |
| *, | |
| vectorized: bool = True, | |
| ) -> bool: | |
| """Return whether a configured warmup can use the dense simple-v2 runner.""" | |
| return bool(vectorized) and str(proposal or "").strip().lower() in ( | |
| _VECTORIZED_SIMPLE_WARMUP_PROPOSALS | |
| ) | |
| def make_proposal(method: str, **kwargs) -> Proposal: | |
| """Factory for proposal mechanisms.""" | |
| m = (method or "").lower() | |
| if m == "random": | |
| return RandomProposal() | |
| if m == "gwd": | |
| beta = float(kwargs.get("gwd_beta", 1.0)) | |
| return GWDProposal(beta=beta) | |
| if m in {"gwg", "gwg_block", "gibbs_grad"}: | |
| beta = float(kwargs.get("gwd_beta", 1.0)) | |
| return GWGBlockProposal(beta=beta) | |
| if m in {"dlangevin", "dlang", "dl"}: | |
| beta = float(kwargs.get("dl_beta", 1.0)) | |
| lambda_X = float(kwargs.get("dl_lambda_X", 1.0)) | |
| lambda_E = float(kwargs.get("dl_lambda_E", 1.0)) | |
| return DLangevinProposal( | |
| beta=beta, | |
| lambda_X=lambda_X, | |
| lambda_E=lambda_E, | |
| ) | |
| if m in {"dlangevin_vec", "dlangevin_vectorized", "dlang_vec", "dl_vec"}: | |
| beta = float(kwargs.get("dl_beta", 1.0)) | |
| lambda_X = float(kwargs.get("dl_lambda_X", 1.0)) | |
| lambda_E = float(kwargs.get("dl_lambda_E", 1.0)) | |
| return DLangevinVectorizedProposal( | |
| beta=beta, | |
| lambda_X=lambda_X, | |
| lambda_E=lambda_E, | |
| ) | |
| if m in {"dlangevin_mt", "dlangevinmt", "dlang_mt", "dl_mt", "dlangevin_multi"}: | |
| beta = float(kwargs.get("dl_beta", 1.0)) | |
| lambda_X = float(kwargs.get("dl_lambda_X", 1.0)) | |
| lambda_E = float(kwargs.get("dl_lambda_E", 1.0)) | |
| num_tries_raw = ( | |
| kwargs.get("dl_num_tries", None) | |
| or kwargs.get("dl_mt_k", None) | |
| or kwargs.get("dl_k", None) | |
| ) | |
| energy_batch_size_raw = ( | |
| kwargs.get("dl_mt_energy_batch", None) | |
| or kwargs.get("dl_energy_chunk", None) | |
| ) | |
| try: | |
| num_tries = int(num_tries_raw) if num_tries_raw is not None else 1 | |
| except (TypeError, ValueError): | |
| num_tries = 1 | |
| try: | |
| energy_batch_size = int(energy_batch_size_raw) if energy_batch_size_raw is not None else 256 | |
| except (TypeError, ValueError): | |
| energy_batch_size = 256 | |
| return DLangevinMTProposal( | |
| beta=beta, | |
| lambda_X=lambda_X, | |
| lambda_E=lambda_E, | |
| num_tries=num_tries, | |
| energy_batch_size=energy_batch_size, | |
| ) | |
| if m in TWO_BETA_SCALAR_PROPOSALS: | |
| beta_prop = kwargs.get("dl_beta_prop") | |
| beta_mh = kwargs.get("dl_beta_mh") | |
| if beta_prop is None or beta_mh is None: | |
| raise ValueError("dlangevintwobetas requires dl_beta_prop and dl_beta_mh to be set.") | |
| lambda_X = float(kwargs.get("dl_lambda_X", 1.0)) | |
| lambda_E = float(kwargs.get("dl_lambda_E", 1.0)) | |
| return DLangevinTwoBetasProposal( | |
| beta_prop=float(beta_prop), | |
| beta_mh=float(beta_mh), | |
| lambda_X=lambda_X, | |
| lambda_E=lambda_E, | |
| ) | |
| if m in TWO_BETA_VECTOR_PROPOSALS: | |
| beta_prop = kwargs.get("dl_beta_prop") | |
| beta_mh = kwargs.get("dl_beta_mh") | |
| if beta_prop is None or beta_mh is None: | |
| raise ValueError( | |
| "dlangevin_two_betas_vec requires dl_beta_prop and dl_beta_mh to be set." | |
| ) | |
| lambda_X = float(kwargs.get("dl_lambda_X", 1.0)) | |
| lambda_E = float(kwargs.get("dl_lambda_E", 1.0)) | |
| return DLangevinTwoBetasVectorizedProposal( | |
| beta_prop=float(beta_prop), | |
| beta_mh=float(beta_mh), | |
| lambda_X=lambda_X, | |
| lambda_E=lambda_E, | |
| ) | |
| if m in {"dlangevin_annealing", "dlang_annealing", "dl_annealing"}: | |
| beta_init = float(kwargs.get("dl_beta_init", kwargs.get("dl_beta", 1.0))) | |
| beta_final = float(kwargs.get("dl_beta_final", beta_init)) | |
| anneal_steps_raw = kwargs.get("dl_beta_anneal_steps", kwargs.get("dl_anneal_steps", 0)) | |
| try: | |
| anneal_steps = max(int(anneal_steps_raw), 0) | |
| except (TypeError, ValueError): | |
| anneal_steps = 0 | |
| lambda_X = float(kwargs.get("dl_lambda_X", 1.0)) | |
| lambda_E = float(kwargs.get("dl_lambda_E", 1.0)) | |
| return DLangevinAnnealingProposal( | |
| beta_init=beta_init, | |
| beta_final=beta_final, | |
| anneal_steps=anneal_steps, | |
| lambda_X=lambda_X, | |
| lambda_E=lambda_E, | |
| ) | |
| if m in TWO_BETA_ANNEALING_PROPOSALS: | |
| beta_prop = kwargs.get("dl_beta_prop") | |
| beta_mh_init = kwargs.get("dl_beta_mh_init") | |
| beta_mh_final = kwargs.get("dl_beta_mh_final") | |
| beta_mh_anneal_steps = kwargs.get("dl_beta_mh_anneal_steps") | |
| missing = [ | |
| k | |
| for k, v in [ | |
| ("dl_beta_prop", beta_prop), | |
| ("dl_beta_mh_init", beta_mh_init), | |
| ("dl_beta_mh_final", beta_mh_final), | |
| ("dl_beta_mh_anneal_steps", beta_mh_anneal_steps), | |
| ] | |
| if v is None | |
| ] | |
| if missing: | |
| raise ValueError(f"dlangevintwobetas_annealing requires {', '.join(missing)}.") | |
| lambda_X = float(kwargs.get("dl_lambda_X", 1.0)) | |
| lambda_E = float(kwargs.get("dl_lambda_E", 1.0)) | |
| if m == "dlangevin_two_betas_annealing_vec_no_origin": | |
| proposal_cls = DLangevinTwoBetasAnnealingVectorizedNoOriginProposal | |
| elif m in TWO_BETA_ANNEALING_VECTOR_PROPOSALS: | |
| proposal_cls = DLangevinTwoBetasAnnealingVectorizedProposal | |
| else: | |
| proposal_cls = DLangevinTwoBetasAnnealingProposal | |
| return proposal_cls( | |
| beta_prop=float(beta_prop), | |
| beta_mh_init=float(beta_mh_init), | |
| beta_mh_final=float(beta_mh_final), | |
| beta_mh_anneal_steps=int(beta_mh_anneal_steps), | |
| lambda_X=lambda_X, | |
| lambda_E=lambda_E, | |
| ) | |
| if m in {"dlangevin_nomh", "dlang_nomh", "dl_nomh"}: | |
| beta = float(kwargs.get("dl_beta", 1.0)) | |
| lambda_X = float(kwargs.get("dl_lambda_X", 1.0)) | |
| lambda_E = float(kwargs.get("dl_lambda_E", 1.0)) | |
| return DLangevinNoMHProposal( | |
| beta=beta, | |
| lambda_X=lambda_X, | |
| lambda_E=lambda_E, | |
| ) | |
| if m == "simple": | |
| edits = kwargs.get("simple_n_edits", 5) | |
| edits_int = 5 if edits is None else int(edits) | |
| return SimpleProposal(edits_per_step=edits_int, use_conditioner=True) | |
| if m in {"simple_ver2", "simple_v2"}: | |
| edits = kwargs.get("simple_n_edits", 5) | |
| edits_int = 5 if edits is None else int(edits) | |
| return SimpleProposalV2(edits_per_step=edits_int, use_conditioner=False) | |
| if m in {"simple_ver2_guided", "simple_v2_guided"}: | |
| edits = kwargs.get("simple_n_edits", 5) | |
| edits_int = 5 if edits is None else int(edits) | |
| return SimpleProposalV2(edits_per_step=edits_int, use_conditioner=True) | |
| if m in {"simple_ver2_guided_strong", "simple_v2_guided_strong"}: | |
| edits = kwargs.get("simple_n_edits", 5) | |
| edits_int = 5 if edits is None else int(edits) | |
| return SimpleProposalV2GuidedStrong(edits_per_step=edits_int) | |
| raise ValueError( | |
| f"Unknown proposal '{method}'. Supported: ['random', 'gwd', 'gwg_block', 'dlangevin', 'dlangevin_vec', 'dlangevin_mt', 'dlangevin_annealing', 'dlangevintwobetas', 'dlangevin_two_betas_vec', 'dlangevintwobetas_annealing', 'dlangevin_two_betas_annealing_vec', 'dlangevin_two_betas_annealing_vec_no_origin', 'dlangevin_noMH', 'simple', 'simple_ver2', 'simple_ver2_guided', 'simple_ver2_guided_strong']." | |
| ) | |
| def initialize_random_graphs( | |
| batch_size: int, | |
| dataset_info, | |
| device: torch.device = torch.device("cpu"), | |
| transition: str = "marginal", | |
| ) -> List[Tuple[torch.Tensor, torch.Tensor]]: | |
| """Sample a batch of random graphs used as MCMC initial states.""" | |
| # Sample number of nodes from empirical distribution | |
| n_nodes = dataset_info.nodes_dist.sample_n(batch_size, device) | |
| n_max = torch.max(n_nodes).item() | |
| arange = torch.arange(n_max, device=device).unsqueeze(0).expand(batch_size, -1) | |
| node_mask = arange < n_nodes.unsqueeze(1) | |
| # Sample node and edge types from reference noise distribution | |
| noise_dist = NoiseDistribution(transition, dataset_info) | |
| limit_dist = noise_dist.get_limit_dist() | |
| z_T = flow_matching_utils.sample_discrete_feature_noise( | |
| limit_dist=limit_dist, node_mask=node_mask | |
| ) | |
| graphs: List[Tuple[torch.Tensor, torch.Tensor]] = [] | |
| for i in range(batch_size): | |
| n = int(n_nodes[i].item()) | |
| node_types = torch.argmax(z_T.X[i, :n], dim=-1) | |
| edge_types = torch.argmax(z_T.E[i, :n, :n], dim=-1) | |
| graphs.append((node_types.cpu(), edge_types.cpu())) | |
| return graphs | |
| def _model_eval_ctx(model: torch.nn.Module): | |
| """ | |
| Temporarily set the model to eval() and restore the previous training mode. | |
| Used to disable dropout for the entire MCMC section. | |
| """ | |
| prev = model.training | |
| try: | |
| if prev: | |
| model.eval() | |
| yield | |
| finally: | |
| if prev: | |
| model.train() | |
| def mcmc_sample_batch( | |
| model, | |
| dataset_info, | |
| node_types_list: Sequence[torch.Tensor], | |
| edge_types_list: Sequence[torch.Tensor], | |
| extra_features, | |
| domain_features, | |
| steps: int = 10, | |
| device: torch.device = torch.device("cpu"), | |
| proposal: str = "random", | |
| gwd_beta: float = 1.0, | |
| dl_beta: float = 1.0, | |
| dl_beta_init: Optional[float] = None, | |
| dl_beta_final: Optional[float] = None, | |
| dl_beta_anneal_steps: Optional[int] = None, | |
| dl_beta_prop: Optional[float] = None, | |
| dl_beta_mh: Optional[float] = None, | |
| dl_beta_mh_init: Optional[float] = None, | |
| dl_beta_mh_final: Optional[float] = None, | |
| dl_beta_mh_anneal_steps: Optional[int] = None, | |
| dl_lambda_X: float = 1.0, | |
| dl_lambda_E: float = 1.0, | |
| dl_num_tries: Optional[int] = None, | |
| dl_mt_energy_batch: Optional[int] = None, | |
| simple_n_edits: Optional[int] = None, | |
| energy_split_threshold: Optional[float] = None, | |
| dl_beta_near: Optional[float] = None, | |
| dl_lambda_X_near: Optional[float] = None, | |
| dl_lambda_E_near: Optional[float] = None, | |
| dl_beta_far: Optional[float] = None, | |
| dl_lambda_X_far: Optional[float] = None, | |
| dl_lambda_E_far: Optional[float] = None, | |
| amp_dtype: Optional[str] = None, | |
| collect_stats: bool = False, | |
| step_offset: int = 0, | |
| property_lambda_prop: Optional[float] = None, | |
| property_t_override: Optional[float] = None, | |
| property_t_schedule: Optional[Dict[str, Any]] = None, | |
| step_callback: Optional[Callable[[MCMCStepEvent], None]] = None, | |
| ): | |
| """Run parallel MCMC chains (one per graph) starting from the provided batch. | |
| Returns | |
| ------- | |
| tuple | |
| (node_types_list, edge_types_list, n_accept_total, n_steps_total) | |
| Notes on performance: | |
| - Chain states are moved to `device` once here and kept device-resident | |
| throughout the loop. We only move them back to CPU on return. | |
| - Model is put into eval() for the entire MCMC to disable dropout. | |
| - If provided, `step_callback` is called after each state update with | |
| device-resident accepted and proposed states. Callers must clone data | |
| they retain. | |
| """ | |
| assert len(node_types_list) == len(edge_types_list) | |
| B = len(node_types_list) | |
| if B == 0: | |
| return [], [], 0, 0, {} | |
| try: | |
| step_offset_int = int(step_offset) | |
| except (TypeError, ValueError): | |
| step_offset_int = 0 | |
| # --- Move chain states to device ONCE, keep them there during the loop --- | |
| node_types_list = [t.to(device, non_blocking=True) for t in node_types_list] | |
| edge_types_list = [t.to(device, non_blocking=True) for t in edge_types_list] | |
| # Keep a copy of chain origins for distance-to-origin stats | |
| if collect_stats: | |
| origin_nodes = [t.clone() for t in node_types_list] | |
| origin_edges = [t.clone() for t in edge_types_list] | |
| else: | |
| origin_nodes = [] | |
| origin_edges = [] | |
| dual_params = ( | |
| energy_split_threshold is not None | |
| and dl_beta_near is not None | |
| and dl_lambda_X_near is not None | |
| and dl_lambda_E_near is not None | |
| and dl_beta_far is not None | |
| and dl_lambda_X_far is not None | |
| and dl_lambda_E_far is not None | |
| ) | |
| if dual_params and proposal.lower() not in {"dlangevin", "dlang", "dl"}: | |
| raise ValueError( | |
| "Dual DLangevin parameters require proposal='dlangevin'." | |
| ) | |
| if dual_params and step_callback is not None: | |
| raise ValueError( | |
| "step_callback is not supported with dual DLangevin parameters." | |
| ) | |
| if proposal.lower() in TWO_BETA_PROPOSALS: | |
| if dl_beta_prop is None or dl_beta_mh is None: | |
| raise ValueError( | |
| f"proposal='{proposal}' requires dl_beta_prop and dl_beta_mh." | |
| ) | |
| if proposal.lower() in TWO_BETA_ANNEALING_PROPOSALS: | |
| missing = [ | |
| k | |
| for k, v in [ | |
| ("dl_beta_prop", dl_beta_prop), | |
| ("dl_beta_mh_init", dl_beta_mh_init), | |
| ("dl_beta_mh_final", dl_beta_mh_final), | |
| ("dl_beta_mh_anneal_steps", dl_beta_mh_anneal_steps), | |
| ] | |
| if v is None | |
| ] | |
| if missing: | |
| raise ValueError(f"dlangevintwobetas_annealing requires {', '.join(missing)}.") | |
| total_accepts = 0 | |
| # Stats accumulators for single-parameter mode | |
| stats: Dict[str, Any] = {} | |
| if collect_stats and not dual_params: | |
| stats.update( | |
| dict( | |
| total_proposals=0, | |
| total_accepted=0, | |
| nontriv_any=0, | |
| nontriv_node=0, | |
| nontriv_edge=0, | |
| acc_nontriv_any=0, | |
| acc_nontriv_node=0, | |
| acc_nontriv_edge=0, | |
| # Distances to origin accumulators (proposed and accepted) | |
| prop_dist_nodes_sum=0.0, | |
| prop_dist_edges_sum=0.0, | |
| acc_dist_nodes_sum=0.0, | |
| acc_dist_edges_sum=0.0, | |
| # Step size accumulators (current -> proposed) | |
| step_prop_nodes_sum=0.0, | |
| step_prop_edges_sum=0.0, | |
| step_acc_nodes_sum=0.0, | |
| step_acc_edges_sum=0.0, | |
| step_acc_total_sq_sum=0.0, | |
| ) | |
| ) | |
| track_distance = collect_stats | |
| total_distance_nodes = 0.0 | |
| total_distance_edges = 0.0 | |
| B_initial = B | |
| prop_impl: Optional[Proposal] = None | |
| if not dual_params: | |
| beta_init = dl_beta if dl_beta_init is None else float(dl_beta_init) | |
| beta_final = dl_beta if dl_beta_final is None else float(dl_beta_final) | |
| try: | |
| anneal_steps_val = None if dl_beta_anneal_steps is None else int(dl_beta_anneal_steps) | |
| except (TypeError, ValueError): | |
| anneal_steps_val = None | |
| prop_impl = make_proposal( | |
| proposal, | |
| gwd_beta=gwd_beta, | |
| dl_beta=dl_beta, | |
| dl_beta_init=beta_init, | |
| dl_beta_final=beta_final, | |
| dl_beta_anneal_steps=anneal_steps_val, | |
| dl_beta_prop=dl_beta_prop, | |
| dl_beta_mh=dl_beta_mh, | |
| dl_beta_mh_init=dl_beta_mh_init, | |
| dl_beta_mh_final=dl_beta_mh_final, | |
| dl_beta_mh_anneal_steps=dl_beta_mh_anneal_steps, | |
| dl_lambda_X=dl_lambda_X, | |
| dl_lambda_E=dl_lambda_E, | |
| dl_num_tries=dl_num_tries, | |
| dl_mt_energy_batch=dl_mt_energy_batch, | |
| simple_n_edits=simple_n_edits, | |
| ) | |
| if ( | |
| property_lambda_prop is not None | |
| and prop_impl is not None | |
| and bool(getattr(prop_impl, "supports_property_lambda_override", False)) | |
| ): | |
| prop_impl.set_property_lambda_override(property_lambda_prop) | |
| def _resolve_property_t(step_idx: int) -> Optional[float]: | |
| if property_t_override is not None: | |
| try: | |
| return float(property_t_override) | |
| except (TypeError, ValueError): | |
| return None | |
| if not property_t_schedule: | |
| return None | |
| mode = str(property_t_schedule.get("mode", "linear")).strip().lower() | |
| if mode not in {"linear", "lin"}: | |
| return None | |
| try: | |
| steps_total = int(property_t_schedule.get("steps", 0) or 0) | |
| t_start = float(property_t_schedule.get("t_start", 0.0)) | |
| t_end = float(property_t_schedule.get("t_end", 1.0)) | |
| except (TypeError, ValueError): | |
| return None | |
| if steps_total <= 1: | |
| return t_end | |
| max_idx = max(steps_total - 1, 1) | |
| s = min(max(int(step_idx), 0), max_idx) | |
| ratio = float(s) / float(max_idx) | |
| return float(t_start + (t_end - t_start) * ratio) | |
| def _set_property_t(step_idx: int) -> None: | |
| register_property_time_override(_resolve_property_t(step_idx)) | |
| # Disable dropout for the entire MCMC run (restored on exit) | |
| with _model_eval_ctx(model): | |
| try: | |
| _set_property_t(0) | |
| # Current energies for all graphs (detached for acceptance decisions) | |
| # Use autocast only if requested by caller | |
| with _amp_autocast_ctx(device, amp_dtype): | |
| current_E = energy_batch( | |
| model=model, | |
| node_types_list=node_types_list, | |
| edge_types_list=edge_types_list, | |
| dataset_info=dataset_info, | |
| device=device, | |
| extra_features=extra_features, | |
| domain_features=domain_features, | |
| detach=True, | |
| ) # (B,) | |
| if not dual_params: | |
| assert prop_impl is not None | |
| for step_idx in range(steps): | |
| _set_property_t(step_idx) | |
| prop_impl.on_step_start(step_offset_int + step_idx) | |
| prev_nodes = [t.clone() for t in node_types_list] if track_distance else None | |
| prev_edges = [t.clone() for t in edge_types_list] if track_distance else None | |
| # === Propose === | |
| prop_result = prop_impl.propose( | |
| model=model, | |
| dataset_info=dataset_info, | |
| node_types_list=node_types_list, | |
| edge_types_list=edge_types_list, | |
| extra_features=extra_features, | |
| domain_features=domain_features, | |
| device=device, | |
| amp_dtype=amp_dtype, | |
| ) | |
| # === Score proposals if needed by the proposal === | |
| prop_E: Optional[torch.Tensor] = None | |
| if prop_impl.needs_proposed_energy(): | |
| with _amp_autocast_ctx(device, amp_dtype): | |
| prop_E = energy_batch( | |
| model=model, | |
| node_types_list=prop_result.prop_nodes, | |
| edge_types_list=prop_result.prop_edges, | |
| dataset_info=dataset_info, | |
| device=device, | |
| extra_features=extra_features, | |
| domain_features=domain_features, | |
| detach=True, | |
| ) # (B,) | |
| # === Accept/Reject (early-reject + caching inside) === | |
| accept_mask, prop_E_from_accept = prop_impl.accept( | |
| model=model, | |
| dataset_info=dataset_info, | |
| current_nodes=node_types_list, | |
| current_edges=edge_types_list, | |
| prop_result=prop_result, | |
| current_E=current_E, | |
| prop_E=prop_E, | |
| extra_features=extra_features, | |
| domain_features=domain_features, | |
| device=device, | |
| amp_dtype=amp_dtype, | |
| ) | |
| # ---- Stats accumulation (use pre-update current state) ---- | |
| if collect_stats: | |
| B_local = len(node_types_list) | |
| stats["total_proposals"] += B_local | |
| # Loop per-chain due to variable sizes | |
| for b in range(B_local): | |
| nt_cur = node_types_list[b] | |
| et_cur = edge_types_list[b] | |
| nt_prop = prop_result.prop_nodes[b] | |
| et_prop = prop_result.prop_edges[b] | |
| # Step change (current -> proposed) | |
| # Nodes | |
| h_node_step = int((nt_prop != nt_cur).sum().item()) | |
| # Edges: count only i<j to avoid double count | |
| if et_prop.numel() == 0: | |
| h_edge_step = 0 | |
| else: | |
| diff = et_prop != et_cur | |
| h_edge_step = int(torch.triu(diff, diagonal=1).sum().item()) | |
| nontriv_node = (h_node_step > 0) | |
| nontriv_edge = (h_edge_step > 0) | |
| nontriv_any = (nontriv_node or nontriv_edge) | |
| stats["nontriv_node"] += int(nontriv_node) | |
| stats["nontriv_edge"] += int(nontriv_edge) | |
| stats["nontriv_any"] += int(nontriv_any) | |
| acc_b = bool(accept_mask[b].item()) | |
| if acc_b: | |
| stats["total_accepted"] += 1 | |
| if acc_b and nontriv_any: | |
| stats["acc_nontriv_any"] += 1 | |
| if acc_b and nontriv_node: | |
| stats["acc_nontriv_node"] += 1 | |
| if acc_b and nontriv_edge: | |
| stats["acc_nontriv_edge"] += 1 | |
| # Accumulate step sizes (current -> proposed) | |
| stats["step_prop_nodes_sum"] += float(h_node_step) | |
| stats["step_prop_edges_sum"] += float(h_edge_step) | |
| if acc_b: | |
| stats["step_acc_nodes_sum"] += float(h_node_step) | |
| stats["step_acc_edges_sum"] += float(h_edge_step) | |
| stats["step_acc_total_sq_sum"] += float( | |
| (h_node_step + h_edge_step) ** 2 | |
| ) | |
| # Distances to origin | |
| nt0 = origin_nodes[b] | |
| et0 = origin_edges[b] | |
| prop_dist_nodes = int((nt_prop != nt0).sum().item()) | |
| if et_prop.numel() == 0: | |
| prop_dist_edges = 0 | |
| else: | |
| diff0 = et_prop != et0 | |
| prop_dist_edges = int(torch.triu(diff0, diagonal=1).sum().item()) | |
| stats["prop_dist_nodes_sum"] += float(prop_dist_nodes) | |
| stats["prop_dist_edges_sum"] += float(prop_dist_edges) | |
| if acc_b: | |
| stats["acc_dist_nodes_sum"] += float(prop_dist_nodes) | |
| stats["acc_dist_edges_sum"] += float(prop_dist_edges) | |
| # Update states and energies where accepted (keep everything on device) | |
| # Ensure mask device matches energies for indexing | |
| accept_mask_dev = accept_mask.to(current_E.device) | |
| n_acc = int(accept_mask.sum().item()) | |
| total_accepts += n_acc | |
| if n_acc > 0: | |
| acc_idx = torch.nonzero(accept_mask, as_tuple=False).flatten().tolist() | |
| for i in acc_idx: | |
| node_types_list[i] = prop_result.prop_nodes[i] | |
| edge_types_list[i] = prop_result.prop_edges[i] | |
| eff_prop_E = prop_E if prop_E is not None else prop_E_from_accept | |
| assert eff_prop_E is not None, "Proposal did not provide proposed energies." | |
| current_E[accept_mask_dev] = eff_prop_E[accept_mask_dev] | |
| if track_distance and prev_nodes is not None and prev_edges is not None: | |
| for prev_nt, prev_et, cur_nt, cur_et in zip(prev_nodes, prev_edges, node_types_list, edge_types_list): | |
| dist_nodes = int((cur_nt != prev_nt).sum().item()) | |
| if cur_et.numel() == 0: | |
| dist_edges = 0 | |
| else: | |
| diff_step = cur_et != prev_et | |
| dist_edges = int(torch.triu(diff_step, diagonal=1).sum().item()) | |
| total_distance_nodes += float(dist_nodes) | |
| total_distance_edges += float(dist_edges) | |
| if step_callback is not None: | |
| step_callback( | |
| MCMCStepEvent( | |
| step=step_offset_int + step_idx, | |
| nodes=node_types_list, | |
| edges=edge_types_list, | |
| energies=current_E, | |
| accepted=accept_mask, | |
| proposed_nodes=prop_result.prop_nodes, | |
| proposed_edges=prop_result.prop_edges, | |
| proposed_energies=( | |
| prop_E | |
| if prop_E is not None | |
| else prop_E_from_accept | |
| ), | |
| ) | |
| ) | |
| else: | |
| threshold_val = float(energy_split_threshold) | |
| def _run_subset( | |
| mask: torch.Tensor, | |
| beta_val: float, | |
| lambda_X_val: float, | |
| lambda_E_val: float, | |
| ) -> int: | |
| nonlocal current_E, node_types_list, edge_types_list | |
| if mask is None: | |
| return 0 | |
| idx = torch.nonzero(mask, as_tuple=False).flatten() | |
| if idx.numel() == 0: | |
| return 0 | |
| idx_list = idx.tolist() | |
| sub_nodes = [node_types_list[i] for i in idx_list] | |
| sub_edges = [edge_types_list[i] for i in idx_list] | |
| prop_local = DLangevinProposal( | |
| beta=beta_val, | |
| lambda_X=lambda_X_val, | |
| lambda_E=lambda_E_val, | |
| ) | |
| prop_res = prop_local.propose( | |
| model=model, | |
| dataset_info=dataset_info, | |
| node_types_list=sub_nodes, | |
| edge_types_list=sub_edges, | |
| extra_features=extra_features, | |
| domain_features=domain_features, | |
| device=device, | |
| amp_dtype=amp_dtype, | |
| ) | |
| prop_E_local: Optional[torch.Tensor] = None | |
| if prop_local.needs_proposed_energy(): | |
| with _amp_autocast_ctx(device, amp_dtype): | |
| prop_E_local = energy_batch( | |
| model=model, | |
| node_types_list=prop_res.prop_nodes, | |
| edge_types_list=prop_res.prop_edges, | |
| dataset_info=dataset_info, | |
| device=device, | |
| extra_features=extra_features, | |
| domain_features=domain_features, | |
| detach=True, | |
| ) | |
| sub_idx = idx.to(current_E.device) | |
| current_E_subset = current_E.index_select(0, sub_idx) | |
| accept_mask_local, prop_E_updated = prop_local.accept( | |
| model=model, | |
| dataset_info=dataset_info, | |
| current_nodes=sub_nodes, | |
| current_edges=sub_edges, | |
| prop_result=prop_res, | |
| current_E=current_E_subset, | |
| prop_E=prop_E_local, | |
| extra_features=extra_features, | |
| domain_features=domain_features, | |
| device=device, | |
| amp_dtype=amp_dtype, | |
| ) | |
| eff_prop_E_local = prop_E_local if prop_E_local is not None else prop_E_updated | |
| assert eff_prop_E_local is not None, "Proposal did not provide updated energies." | |
| acc_idx_local = torch.nonzero(accept_mask_local, as_tuple=False).flatten().tolist() | |
| for rel_idx in acc_idx_local: | |
| global_idx = idx_list[rel_idx] | |
| node_types_list[global_idx] = prop_res.prop_nodes[rel_idx] | |
| edge_types_list[global_idx] = prop_res.prop_edges[rel_idx] | |
| current_E.index_copy_(0, sub_idx, eff_prop_E_local) | |
| return int(accept_mask_local.sum().item()) | |
| for step_idx in range(steps): | |
| _set_property_t(step_idx) | |
| prev_nodes = [t.clone() for t in node_types_list] if track_distance else None | |
| prev_edges = [t.clone() for t in edge_types_list] if track_distance else None | |
| mask_near = current_E < threshold_val | |
| mask_far = ~mask_near | |
| total_accepts += _run_subset( | |
| mask_near, | |
| float(dl_beta_near), | |
| float(dl_lambda_X_near), | |
| float(dl_lambda_E_near), | |
| ) | |
| total_accepts += _run_subset( | |
| mask_far, | |
| float(dl_beta_far), | |
| float(dl_lambda_X_far), | |
| float(dl_lambda_E_far), | |
| ) | |
| if track_distance and prev_nodes is not None and prev_edges is not None: | |
| for prev_nt, prev_et, cur_nt, cur_et in zip(prev_nodes, prev_edges, node_types_list, edge_types_list): | |
| dist_nodes = int((cur_nt != prev_nt).sum().item()) | |
| if cur_et.numel() == 0: | |
| dist_edges = 0 | |
| else: | |
| diff_step = cur_et != prev_et | |
| dist_edges = int(torch.triu(diff_step, diagonal=1).sum().item()) | |
| total_distance_nodes += float(dist_nodes) | |
| total_distance_edges += float(dist_edges) | |
| finally: | |
| register_property_time_override(None) | |
| # Return CPU tensors to keep external API stable | |
| # Finalize stats means | |
| if collect_stats and stats.get("total_proposals", 0) > 0: | |
| total_props = max(int(stats["total_proposals"]), 1) | |
| total_acc = max(int(stats["total_accepted"]), 0) | |
| n_any = max(int(stats["nontriv_any"]), 0) | |
| n_node = max(int(stats["nontriv_node"]), 0) | |
| n_edge = max(int(stats["nontriv_edge"]), 0) | |
| mean_step_acc_total = ( | |
| (stats["step_acc_nodes_sum"] + stats["step_acc_edges_sum"]) | |
| / max(total_acc, 1) | |
| if total_acc > 0 | |
| else 0.0 | |
| ) | |
| variance_step_acc_total = ( | |
| max( | |
| stats["step_acc_total_sq_sum"] / total_acc | |
| - mean_step_acc_total**2, | |
| 0.0, | |
| ) | |
| if total_acc > 0 | |
| else 0.0 | |
| ) | |
| stats.update( | |
| dict( | |
| overall_accept=(stats["total_accepted"] / total_props), | |
| accept_nontrivial_any=(stats["acc_nontriv_any"] / n_any) if n_any > 0 else 0.0, | |
| accept_nontrivial_node=(stats["acc_nontriv_node"] / n_node) if n_node > 0 else 0.0, | |
| accept_nontrivial_edge=(stats["acc_nontriv_edge"] / n_edge) if n_edge > 0 else 0.0, | |
| mean_prop_distance_nodes=(stats["prop_dist_nodes_sum"] / total_props), | |
| mean_prop_distance_edges=(stats["prop_dist_edges_sum"] / total_props), | |
| mean_prop_distance_total=((stats["prop_dist_nodes_sum"] + stats["prop_dist_edges_sum"]) / total_props), | |
| mean_acc_distance_nodes=(stats["acc_dist_nodes_sum"] / max(total_acc, 1)) if total_acc > 0 else 0.0, | |
| mean_acc_distance_edges=(stats["acc_dist_edges_sum"] / max(total_acc, 1)) if total_acc > 0 else 0.0, | |
| mean_acc_distance_total=((stats["acc_dist_nodes_sum"] + stats["acc_dist_edges_sum"]) / max(total_acc, 1)) if total_acc > 0 else 0.0, | |
| # Mean step sizes per proposal and per accepted | |
| mean_step_distance_nodes=(stats["step_prop_nodes_sum"] / total_props), | |
| mean_step_distance_edges=(stats["step_prop_edges_sum"] / total_props), | |
| mean_step_distance_total=((stats["step_prop_nodes_sum"] + stats["step_prop_edges_sum"]) / total_props), | |
| mean_step_acc_distance_nodes=(stats["step_acc_nodes_sum"] / max(total_acc, 1)) if total_acc > 0 else 0.0, | |
| mean_step_acc_distance_edges=(stats["step_acc_edges_sum"] / max(total_acc, 1)) if total_acc > 0 else 0.0, | |
| mean_step_acc_distance_total=mean_step_acc_total, | |
| std_step_acc_distance_total=variance_step_acc_total**0.5, | |
| ) | |
| ) | |
| distance_total_nodes = float(total_distance_nodes) | |
| distance_total_edges = float(total_distance_edges) | |
| distance_total = distance_total_nodes + distance_total_edges | |
| steps_effective = max(int(steps), 1) | |
| chains_effective = max(int(B_initial), 1) | |
| distance_per_step = distance_total / steps_effective if steps_effective > 0 else 0.0 | |
| denom = steps_effective * chains_effective | |
| distance_per_chain_step = distance_total / denom if denom > 0 else 0.0 | |
| distance_summary = dict( | |
| distance_total=distance_total, | |
| distance_total_nodes=distance_total_nodes, | |
| distance_total_edges=distance_total_edges, | |
| distance_per_step=distance_per_step, | |
| distance_per_chain_step=distance_per_chain_step, | |
| ) | |
| stats.update(distance_summary) | |
| node_types_cpu = [t.detach().cpu() for t in node_types_list] | |
| edge_types_cpu = [t.detach().cpu() for t in edge_types_list] | |
| return node_types_cpu, edge_types_cpu, total_accepts, steps * B, (stats if collect_stats else {}) | |