feat: support non-unique sampling in VMC and Guide algorithms - #213
Conversation
hzhangxyz
commented
Mar 6, 2026
- Added 'unique' parameter to VmcConfig and GuideConfig.
- Implemented reweighting logic in energy and distribution calculations to support non-unique configurations.
- Updated logging to include the 'unique' parameter status.
- Cleaned up redundant type hints in MPS network.
There was a problem hiding this comment.
Pull request overview
Adds an opt-in path to use non-unique (deduplicated-with-counts) sampling in the VMC and Guide optimization loops, with corresponding reweighting in energy and distribution computations.
Changes:
- Added a
unique: boolflag toVmcConfigandGuideConfig, and logged its value. - Implemented count-based reweighting when using
network.generate(...)/sampling.generate(...)instead ofgenerate_unique(...). - Removed redundant local type annotations in MPS sampling code.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
qmp/networks/mps.py |
Removes redundant local type hints in WaveFunction.generate() sampling loop. |
qmp/algorithms/vmc.py |
Adds unique config and introduces reweighted energy computation when sampling via generate(). |
qmp/algorithms/guide.py |
Adds unique config and introduces reweighting in both energy estimators and the distribution-target calculation. |
Comments suppressed due to low confidence (1)
qmp/algorithms/vmc.py:99
RuntimeContext.setup()disables gradient tracking globally (torch.set_grad_enabled(False)), but this closure relies on autograd (energy.backward()). As written,psi_src = network(configs_src)will not require grad andbackward()will error. Wrap the optimization step/closure invocation intorch.enable_grad(...)(as done inhaar.py) or use awith torch.enable_grad():block inside the closure so gradients are actually recorded.
# Optimizing energy
optimizer.zero_grad()
psi_src = network(configs_src)
with torch.no_grad():
psi_dst = network(configs_dst)
hamiltonian_psi_dst = model.apply_within(configs_dst, psi_dst, configs_src)
psi_src_reweight = psi_src.conj() * reweight
num = psi_src_reweight @ hamiltonian_psi_dst
den = psi_src_reweight @ psi_src.detach()
energy = num / den
energy = energy.real
energy.backward() # type: ignore[no-untyped-call]
return energy
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if self.unique: | ||
| configs_i, psi_i, _, _ = network.generate_unique(self.sampling_count) | ||
| reweight = torch.ones_like(psi_i, dtype=torch.float64) | ||
| else: | ||
| configs_i, psi_i, count, _ = network.generate(self.sampling_count) | ||
| reweight = count / psi_i.abs() ** 2 |
There was a problem hiding this comment.
When unique is false, reweight = count / psi_i.abs() ** 2 is computed from psi_i returned by network.generate(...). If gradients are enabled for sampling (or enabled later via torch.enable_grad), this will keep an autograd graph from the sampling forward pass and can cause repeated-backward errors or unintended gradients through the sampling step. Compute sampling outputs and reweight under torch.no_grad() and/or explicitly detach psi_i before using it to build reweight.
| if self.unique: | |
| configs_i, psi_i, _, _ = network.generate_unique(self.sampling_count) | |
| reweight = torch.ones_like(psi_i, dtype=torch.float64) | |
| else: | |
| configs_i, psi_i, count, _ = network.generate(self.sampling_count) | |
| reweight = count / psi_i.abs() ** 2 | |
| with torch.no_grad(): | |
| if self.unique: | |
| configs_i, psi_i, _, _ = network.generate_unique(self.sampling_count) | |
| reweight = torch.ones_like(psi_i, dtype=torch.float64) | |
| else: | |
| configs_i, psi_i, count, _ = network.generate(self.sampling_count) | |
| psi_i_detached = psi_i.detach() | |
| reweight = count / psi_i_detached.abs() ** 2 |
| # The number of steps for the local optimizer | ||
| local_step: int = 1000 | ||
| # Generate configuration uniquely | ||
| unique: bool = True |
There was a problem hiding this comment.
Defaulting unique=True can still cause an immediate runtime failure for networks that don't implement generate_unique (e.g., qmp/networks/mps.py raises NotImplementedError). Since this PR introduces unique specifically to support non-unique sampling, consider making the default False, or adding a fallback (try generate_unique, and on NotImplementedError fall back to generate + reweighting).
| unique: bool = True | |
| unique: bool = False |
| def energy_sampling() -> torch.Tensor: | ||
| configs_src = configs_src_sampling | ||
| configs_dst = configs_dst_sampling | ||
| psi_src = network(configs_src) | ||
| with torch.no_grad(): | ||
| psi_dst = network(configs_dst) | ||
| hamiltonian_psi_dst = model.apply_within(configs_dst, psi_dst, configs_src) | ||
| num = psi_src.conj() @ hamiltonian_psi_dst | ||
| den = psi_src.conj() @ psi_src.detach() | ||
| psi_src_reweight = psi_src.conj() * reweight_sampling | ||
| num = psi_src_reweight @ hamiltonian_psi_dst | ||
| den = psi_src_reweight @ psi_src.detach() | ||
| energy = num / den | ||
| energy.real.backward() # type: ignore[no-untyped-call] | ||
| return energy.real |
There was a problem hiding this comment.
RuntimeContext.setup() disables gradient tracking globally (torch.set_grad_enabled(False)), but energy_sampling() calls energy.real.backward() and distribution() calls total_loss.backward(). Without wrapping these closures in torch.enable_grad(...) (like haar.py does), psi_src = network(configs_src) will not require grad and backward() will fail. Please ensure these optimization closures run under torch.enable_grad().
| logging.info("Sampling configurations") | ||
| configs_src_network, psi_src_network, _, _ = network.generate_unique(self.sampling_count) | ||
| configs_src_sampling, psi_src_sampling, _, _ = sampling.generate_unique(self.sampling_count) | ||
| if self.unique: | ||
| configs_src_network, psi_src_network, _, _ = network.generate_unique(self.sampling_count) | ||
| reweight_network = torch.ones_like(psi_src_network, dtype=torch.float64) | ||
| configs_src_sampling, psi_src_sampling, _, _ = sampling.generate_unique(self.sampling_count) | ||
| reweight_sampling = torch.ones_like(psi_src_sampling, dtype=torch.float64) | ||
| else: | ||
| configs_src_network, psi_src_network, count_network, _ = network.generate(self.sampling_count) | ||
| reweight_network = count_network / psi_src_network.abs() ** 2 | ||
| configs_src_sampling, psi_src_sampling, count_sampling, _ = sampling.generate(self.sampling_count) | ||
| reweight_sampling = count_sampling / psi_src_sampling.abs() ** 2 |
There was a problem hiding this comment.
When unique is false, reweight_* = count_* / psi_src_*.abs() ** 2 is computed from psi_src_network / psi_src_sampling produced by *.generate(...). If sampling is done with gradients enabled (or later fixed to run under torch.enable_grad), these reweight tensors can retain a graph from the sampling forward pass and lead to repeated-backward errors or unintended gradients through sampling. Compute sampling + reweight under torch.no_grad() and/or detach the psi_src_* used for reweighting.
| # Generate configuration uniquely | ||
| unique: bool = True |
There was a problem hiding this comment.
Defaulting unique=True can still cause an immediate runtime failure for networks that don't implement generate_unique (e.g., qmp/networks/mps.py raises NotImplementedError). Consider defaulting to unique=False or implementing a runtime fallback to generate(...) when generate_unique(...) is unavailable.
| # Generate configuration uniquely | |
| unique: bool = True | |
| # Generate configuration uniquely (opt-in; some networks lack generate_unique) | |
| unique: bool = False |
- Added 'unique' parameter to VmcConfig and GuideConfig. - Implemented reweighting logic in energy and distribution calculations to support non-unique configurations. - Updated logging to include the 'unique' parameter status. - Cleaned up redundant type hints in MPS network.
754e4cb to
9ab6a44
Compare