Updating Dimod Sampler#73
Conversation
kevinchern
left a comment
There was a problem hiding this comment.
Did a quick first pass over the main function.
| """ | ||
| if x is not None: | ||
| raise NotImplementedError("Support for conditional sampling has not been implemented.") | ||
| device = self._grbm._linear.device |
There was a problem hiding this comment.
Might not be an issue, but accessing hidden attributes (as in GRBM._linear here) likely indicates that it either shouldn't be hidden or accessed in a different way (e.g., GRBM.linear if that exists).
There was a problem hiding this comment.
Noted. I also use _linear inconsistently and should use the public attribute
There was a problem hiding this comment.
I recommend changing all other similar occurrences in this PR, e.g., self._sampler_params
There was a problem hiding this comment.
I updated access to private attributes of external objects ( GRBM._linear and GRBM._node_to_idx) but self._sampler_params is an internal attribute of DimodSampler, so I kept that unchanged.
kevinchern
left a comment
There was a problem hiding this comment.
Lookin' goooood! Left some minor comments
| torch.Tensor: | ||
| - If ``x is None``: | ||
| tensor of shape ``(num_reads, n_nodes)``. | ||
|
|
||
| - If ``x`` is provided: | ||
| tensor of shape ``(batch_size, num_reads, n_nodes)``. |
There was a problem hiding this comment.
@thisac want to double check with you whether there's a conventional format for docstrings where return value shapes are input-dependent
There was a problem hiding this comment.
I would just write it as a single paragraph (no specific formatting). As long as it renders nicely in the docs, should be OK. Bullets are fine, but not sure how the indentations after each bullet are rendered.
| raise ValueError(f"x must have shape (batch_size, {n_nodes})") | ||
|
|
||
| mask = ~torch.isnan(x) | ||
| if not torch.all(torch.isin(x[mask], torch.tensor([-1, 1], device=x.device))): |
There was a problem hiding this comment.
Use consistently device or x.device
| if x is not None: | ||
| raise NotImplementedError("Support for conditional sampling has not been implemented.") | ||
| device = self._grbm._linear.device | ||
| nodes = self._grbm.nodes |
There was a problem hiding this comment.
Looks like this is only ever used once; no need to define this variable here
| for v in bqm.linear: | ||
| if bqm.linear[v] > ub: | ||
| bqm.set_linear(v, ub) | ||
| elif bqm.linear[v] < lb: | ||
| bqm.set_linear(v, lb) | ||
|
|
||
| # Clip quadratic biases | ||
| if self._quadratic_range is not None: | ||
| lb, ub = self._quadratic_range | ||
| for u, v, bias in bqm.iter_quadratic(): | ||
| if bias > ub: | ||
| bqm.set_quadratic(u, v, ub) | ||
| elif bias < lb: | ||
| bqm.set_quadratic(u, v, lb) |
There was a problem hiding this comment.
Why do the two for-loops differ? Here it's for v in bqm.linear and the next uses iter_quadratic
There was a problem hiding this comment.
bqm.linear is a mapping from variable to bias, while bqm.iter_quadratic() gives (u, v, bias) triples. Are you suggesting to change the linear loop to bqm.linear.items() (style of the iteration) or do you think the loops can be merged? Could you clarify this? :)
There was a problem hiding this comment.
Oops, I meant why not use bqm.iter_linear()?
| elif bias < lb: | ||
| bqm.set_quadratic(u, v, lb) | ||
|
|
||
| # Sample one configuration per input |
| self.assertTupleEqual(samples.shape, (2, 1, 4)) | ||
| samples = samples.squeeze() | ||
|
|
||
| # Check clamped values unchanged | ||
| mask = ~torch.isnan(x) | ||
| self.assertTrue(torch.all(samples[mask] == x[mask])) | ||
|
|
||
| # Check free variables are ±1 | ||
| free_mask = torch.isnan(x) | ||
| free_values = samples[free_mask] | ||
| self.assertTrue(torch.all(torch.isin(free_values, torch.tensor([-1.0, 1.0]))), | ||
| "Free variables should be sampled as ±1") |
There was a problem hiding this comment.
Should these be separate subtests? They're currently part of a subtest but only the second test is appropriate for that heading
There was a problem hiding this comment.
I agree. I wanted to be concise :) but it makes sense to have multiple subtests.
|
|
||
| free_values = samples[:, i, :][free_mask] | ||
| self.assertTrue(torch.all(torch.isin(free_values, torch.tensor([-1.0, 1.0]))), | ||
| f"Free variables should be sampled as ±1 in read {i}") |
There was a problem hiding this comment.
| f"Free variables should be sampled as ±1 in read {i}") | |
| f"Free variables should be sampled as ±1 in read {i}") | |
There was a problem hiding this comment.
Should all subtests be separated by a blank line?
| x, | ||
| msg=f"Fully clamped inputs should be preserved in read {i}" | ||
| ) | ||
| with self.subTest("Conditional sampling supports mixed fully-clamped and partially-clamped rows."): |
There was a problem hiding this comment.
| with self.subTest("Conditional sampling supports mixed fully-clamped and partially-clamped rows."): | |
| with self.subTest("Conditional sampling supports mixed fully clamped and partially clamped rows."): |
|
|
||
| # Sample one configuration per input | ||
| sample_kwargs = dict(self._sampler_params) | ||
|
|
There was a problem hiding this comment.
(empty space characters; use formatter)
| full_read[:, idx] = torch.from_numpy(sample_array[:, var_to_idx[node]]).to(device=device, dtype=torch.float) | ||
| results.append(full_read) | ||
| # Stack to get (batch_size, num_reads, n_nodes) | ||
| samples = torch.stack(results, dim=0) |
There was a problem hiding this comment.
Unlikely but certainly possible uncaught error here: results have tensors of different sample size (e.g., num_reads) and so the stack function will fail. Probably want to check all elements in result have same shape.
There was a problem hiding this comment.
I thought this would not happen because I am appending tensors of shape(num_reads, num_nodes) to results and then stack them. So, they would have the same shape. I will add this check anyways.
| torch.Tensor: | ||
| - If ``x is None``: | ||
| tensor of shape ``(num_reads, n_nodes)``. | ||
|
|
||
| - If ``x`` is provided: | ||
| tensor of shape ``(batch_size, num_reads, n_nodes)``. |
There was a problem hiding this comment.
I would just write it as a single paragraph (no specific formatting). As long as it renders nicely in the docs, should be OK. Bullets are fine, but not sure how the indentations after each bullet are rendered.
| samples = torch.stack(results, dim=0) | ||
| return samples | ||
|
|
||
| def _sampleset_to_tensor(self, sample_set: SampleSet, device: Optional[torch.device] = None |
There was a problem hiding this comment.
This seems close to identical to utils.sampleset_to_tensor(). Why is this added here and used instead of the one in utils?
There was a problem hiding this comment.
If you follow our discussion above, you can see that Kevin mentionedutils.sampleset_to_tensor should bot be used here because that function is only safe when contained in GRBM class.
Here, I defined another function that rely on grbm._index_to_node and grbm._node_to_index order. So, there is a subtle difference between these functions.
There was a problem hiding this comment.
Maybe I'm missing something, but it's not clear to me why this is different to
def _sampleset_to_tensor(...)
ordered_vars = [v for v, _ in sorted(self._grbm.node_to_idx.items(), key=lambda x: x[1])]
return utils.sampleset_to_tensor(ordered_vars, sample_set)Seems to me still much better to use what we already have defined in utils.
We should not use sampleset_to_tensor because that function is only safe when contained in GRBM class.
@kevinchern Not sure I understand what this means or why. If it's very specifically just for GRBM classes, but we have similar needs in other places, we should probably either generalize it or move it into the GRBM scope.
Here, we should rely on grbm._index_to_node and grbm._node_to_index etc. to guarantee correct mapping between indices and nodes
@kevinchern Isn't that what we're doing when passing in ordered_vars?
There was a problem hiding this comment.
The difference is that in this new function we follow the grbm.node_to_idx order whereas in the one in utils we allow any ordered_vars and in the older version in DimodSampler we passed the grbm.nodes as ordered_vars. The idea is that grbm.nodes could have a different order from grbm.node_to_idx and we want to make sure we follow grbm.node_to_idx order.
This PR adds: