Conversation
kevinchern
left a comment
There was a problem hiding this comment.
Added a couple minor requests
| torch.Tensor: A tensor of shape (num_chains, n_nodes) of +/-1 values sampled from the model. | ||
| """ | ||
| if x is not None: | ||
| mask = self._validate_input_and_generate_mask(x) |
There was a problem hiding this comment.
| mask = self._validate_input_and_generate_mask(x) | |
| self._validate_input(x) | |
| mask = ~torch.isnan(x) |
| h = self._grbm.hidden_idx | ||
| self._x[:, h] = torch.where(mask[:, h], x[:, h], self._x[:, h]) | ||
|
|
||
| def _validate_input_and_generate_mask(self, x: torch.Tensor) -> torch.Tensor: |
There was a problem hiding this comment.
| def _validate_input_and_generate_mask(self, x: torch.Tensor) -> torch.Tensor: | |
| def _validate_input(self, x: torch.Tensor) -> None: |
| self._x[:, h] = torch.where(mask[:, h], x[:, h], self._x[:, h]) | ||
|
|
||
| def _validate_input_and_generate_mask(self, x: torch.Tensor) -> torch.Tensor: | ||
| """Validate conditional sampling input and construct a boolean mask. |
There was a problem hiding this comment.
| """Validate conditional sampling input and construct a boolean mask. | |
| """Validate conditional sampling input. |
|
|
||
| Returns: | ||
| torch.Tensor: Boolean mask of shape ``(num_chains, n_nodes)`` where | ||
| ``True`` indicates clamped variables (observed in ``x``) and | ||
| ``False`` indicates variables that should be sampled (``NaN`` in x). |
There was a problem hiding this comment.
| Returns: | |
| torch.Tensor: Boolean mask of shape ``(num_chains, n_nodes)`` where | |
| ``True`` indicates clamped variables (observed in ``x``) and | |
| ``False`` indicates variables that should be sampled (``NaN`` in x). |
| "The input must be unclamped for visible or hidden but not both." | ||
| ) | ||
|
|
||
| return mask |
There was a problem hiding this comment.
| return mask |
|
|
||
| Args: | ||
| x (torch.Tensor): A tensor of shape (``num_chains``, ``dim``) or (``num_chains``, ``n_nodes``) | ||
| interpreted as a batch of partially-observed spins. Entries marked with ``torch.nan`` will |
There was a problem hiding this comment.
| interpreted as a batch of partially-observed spins. Entries marked with ``torch.nan`` will | |
| interpreted as a batch of partially observed spins. Entries marked with ``torch.nan`` will |
| if mask is not None: | ||
| self._x[:, block] = torch.where(mask[:, block], x[:, block], self._x[:, block]) | ||
|
|
||
| def _validate_input_and_generate_mask(self, x: torch.Tensor) -> torch.Tensor: |
There was a problem hiding this comment.
Same suggestion here as in bipartite sampler (docstring, type hints, returns, and defining mask outside)
thisac
left a comment
There was a problem hiding this comment.
I recall we talked about this, but it seems like BipartiteGibbsSampler and BlockSampler could share a lot of methods and do with some deduplication. If they're not general enough to fit into TorchSampler, there should either be a hierarchy between them or another common class that they inherit from, or, especially if you foresee some of these methods being used in other samplers, you could create one (or several) mixin classes.
| grbm = GRBM(nodes, edges, hidden_nodes=["h1", "h2"]) | ||
|
|
||
| def crayon(n): | ||
| return 0 if n in ["v1", "v2"] else 1 |
There was a problem hiding this comment.
| return 0 if n in ["v1", "v2"] else 1 | |
| return n in ["v1", "v2"] |
| if mask is not None: | ||
| self._x[:, block] = torch.where(mask[:, block], x[:, block], self._x[:, block]) | ||
|
|
||
| def _validate_input_and_generate_mask(self, x: torch.Tensor) -> torch.Tensor: |
There was a problem hiding this comment.
Bumping this suggestion to separate validation and mask generation
| self.assertEqual(mask.shape, x_valid.shape) | ||
|
|
||
| # Chain 0: visible unclamped | ||
| self.assertTrue(mask[0, 2:].all()) # First chain: hidden spins are clamped | ||
|
|
||
| # Chain 1: hidden unclamped | ||
| self.assertTrue(mask[1, :2].all()) # Second chain: visible spins are clamped |
There was a problem hiding this comment.
IF we keep the signature of validate_and_generate..., THEN an we combine these tests into one where the mask is hard-coded like expected_mask = torch.tensor([[False, ...], [...]])?
e.g., torch.testing.assertEqual(mask, expected_mask) or self.assertListEqual(mask.tolist(), expected_mask.tolist())
| # Gibbs update for hidden block (block=1) | ||
| with self.subTest("hidden block Gibbs update"): | ||
| sampler._gibbs_update(0.0, hidden_block, ones*zero_field) | ||
| torch.testing.assert_close(torch.tensor(0.0), sampler._x.mean(), atol=1e-2, rtol=1e-2) |
There was a problem hiding this comment.
Why does this one have a looser tolerance 1e-2 than the previous 1e-3?
There was a problem hiding this comment.
Yeah, I noticed that there are fewer random variables in this test compared to the above one, so the estimate has higher variance, and I needed a looser tolerance (1e-2). I could avoid this by setting sampler._x.data[:] = 1.0 just like the earlier example. I can update the test if you think so.
There was a problem hiding this comment.
I might be missing something---don't both tests use sampler._x.mean() so the sample size should be the same(?)
| def test_sample_conditional(self): | ||
| nodes = ["v1", "v2", "h1", "h2"] | ||
| edges = [["v1", "h1"], ["v1", "h2"], ["v2", "h1"], ["v2", "h2"]] | ||
| grbm = GRBM(nodes, edges, hidden_nodes=["h1", "h2"]) |
There was a problem hiding this comment.
consider setting the linear fields to be very large so the result is ~= deterministic.
Then, in the test cases, hard-code the expected results per conditional sampling step.
e.g.,
grbm.linear.data[:] = 99999999999999
and sampler._x.data[:] = 1
in one conditional step, everything but the clamped-states should become -1.
| Add conditional sampling functionality for the ``BlockSampler``. | ||
| - | | ||
| Add ``.clone()`` to the return of ``BlockSampler.sample`` to prevent | ||
| unintended in-place modification of the sampler’s internal state due to |
There was a problem hiding this comment.
| unintended in-place modification of the sampler’s internal state due to | |
| unintended in-place modification of the sampler's internal state due to |
thisac
left a comment
There was a problem hiding this comment.
Just a couple of minor comments.
| from dwave.plugins.torch.models.boltzmann_machine import ( | ||
| GraphRestrictedBoltzmannMachine as GRBM, | ||
| ) | ||
| from torch._prims_common import DeviceLikeType |
There was a problem hiding this comment.
Better avoid importing from "private" module and either have the type alias declared here directly (DeviceLikeType: TypeAlias = str | torch.device | int) or just use either the combined type or *args, **kwargs in the signature as PyTorch does for the to() method.
There was a problem hiding this comment.
have the type alias declared here directly
@thisac what does this mean 🤔? Is this something you declare at import?
There was a problem hiding this comment.
You could just add
DeviceLikeType: TypeAlias = str | torch.device | intto the top of the file (or any file, like utils.py or base.py, and import it from there).
| """ | ||
| Computes the effective field for all vertices in ``block``. |
There was a problem hiding this comment.
| """ | |
| Computes the effective field for all vertices in ``block``. | |
| """Computes the effective field for all vertices in ``block``. |
| mask (torch.Tensor, optional): Boolean tensor of shape | ||
| ``(num_chains, n_nodes)`` indicating which variables are clamped. | ||
| Entries set to ``True`` will keep their values during sampling. |
There was a problem hiding this comment.
Could this be named something clarifying, like clamp_mask, instead of generically mask?
| ValueError: If ``x`` does not match the sampler state shape | ||
| ``(num_chains, n_nodes)``, contains values other than ``±1`` | ||
| or ``NaN``, or if both visible and hidden variables are | ||
| simultaneously unclamped within the same chain. |
There was a problem hiding this comment.
Raises should be indented similarly to Args (not Returns).
| ValueError: If ``x`` does not match the sampler state shape | |
| ``(num_chains, n_nodes)``, contains values other than ``±1`` | |
| or ``NaN``, or if both visible and hidden variables are | |
| simultaneously unclamped within the same chain. | |
| ValueError: If ``x`` does not match the sampler state shape | |
| ``(num_chains, n_nodes)``, contains values other than ``±1`` | |
| or ``NaN``, or if both visible and hidden variables are | |
| simultaneously unclamped within the same chain. |
| mask = None | ||
| for beta in self._schedule: | ||
| self._step(beta, mask=mask, x=x) | ||
| return self._x.clone() No newline at end of file |
There was a problem hiding this comment.
Final empty line missing.
| return self._x.clone() | |
| return self._x.clone() | |
There was a problem hiding this comment.
@kevinchern what autoformatter are you using?
There was a problem hiding this comment.
@anahitamansouri I'd recommend using Black with a line-length set to 100 (black -l 100). Just make sure to only touch files and lines that you've added.
There was a problem hiding this comment.
Yeah, I used that on our code in a PR and noticed it was changing some lines that weren't my changes. So, I thought people are not using Black here. So, I did not use that anymore :) I'll try it again with 100.
There was a problem hiding this comment.
It's the recommended tool, but we don't enforce it, so there's a fair bit of code that isn't formatted accordingly.
There was a problem hiding this comment.
sry misesd the notification @anahitamansouri, I use autopep8 with some customization following dwave's contributor guidelinees
| else: | ||
| mask = None | ||
| for beta in self._schedule: | ||
| self._step(beta, mask=mask, x=x) |
There was a problem hiding this comment.
IMO makes it much more legible to separate the if-else from for and return with empty lines.
| else: | |
| mask = None | |
| for beta in self._schedule: | |
| self._step(beta, mask=mask, x=x) | |
| else: | |
| mask = None | |
| for beta in self._schedule: | |
| self._step(beta, mask=mask, x=x) | |
|
|
||
| Args: | ||
| beta (torch.Tensor): Inverse temperature to sample at. | ||
| mask (torch.Tensor, optional): Boolean tensor of shape |
There was a problem hiding this comment.
Same comment here re renaming.
| mask = None | ||
| for beta in self._schedule: | ||
| self._step(beta) | ||
| return self._x | ||
| self._step(beta, mask, x) | ||
| return self._x.clone() No newline at end of file |
There was a problem hiding this comment.
Same comment here also about empty lines between if-else and for.
| - | | ||
| Add ``.clone()`` to the return of ``BlockSampler.sample`` to prevent | ||
| unintended in-place modification of the sampler's internal state due to | ||
| returning a reference to the underlying tensor. |
There was a problem hiding this comment.
Less a feature and more a fix or upgrade, no?
There was a problem hiding this comment.
Actually I thought it's a mix of both. I thought BipartiteSampler is a feature and conditional sampling is an upgrade. How would you characterize a feature? :)
There was a problem hiding this comment.
So do you suggest to move both under upgrade?
There was a problem hiding this comment.
Ah, sorry if I was unclear. I only meant moving the last bullet. The other two are fine.
There was a problem hiding this comment.
Ah sorry. This makes sense :)
| def test_prepare_initial_states(self): | ||
| nodes = ["v1", "v2", "h1", "h2"] | ||
| edges = [["v1", "h1"], ["v1", "h2"], ["v2", "h1"], ["v2", "h2"]] | ||
| grbm = GRBM(nodes, edges, hidden_nodes=["h1", "h2"]) | ||
|
|
||
| sampler = BipartiteGibbsSampler(grbm, num_chains=2, schedule=[1.0],) | ||
| # Invalid spins | ||
| with self.subTest("Non-spin initial states."): | ||
| self.assertRaisesRegex(ValueError, "contain nonspin values", sampler._prepare_initial_states, | ||
| initial_states=torch.tensor([[0, 1, -1, 1]]), num_chains=1) | ||
|
|
||
| # Incorrect shape | ||
| with self.subTest("Testing initial states with incorrect shape."): | ||
| self.assertRaisesRegex(ValueError, "Initial states should be of shape", sampler._prepare_initial_states, | ||
| num_chains=2, initial_states=torch.tensor([[-1, 1, 1, 1, -1]])) |
There was a problem hiding this comment.
Only tests exceptions raised. Perhaps rename this test_prepare_initial_states_exceptions and have another test_prepare_initial_states with a test for valid arguments.



This PR adds: