Conversation
3ce54c7 to
92f61ed
Compare
92f61ed to
a6cb68d
Compare
546100c to
4b02064
Compare
There was a problem hiding this comment.
This is looking pretty good.
Mostly I want to believe there is still room to simplify things / reuse code.
This is also a good opportunity to simplify the idx_list. There's no reason to use ScalarTypes in the dummy slices, and it's complicating our equality and hashing.
What about using simple integers to indicate what is the role of each index variable?
old_idx_list = (ps.int64, slice(ps.int64, None, None), ps.int64, slice(ps.int64, None, ps.int64))
new_idx_list = (0, slice(1, None, None), 2, slice(3, None, 4))Having the indices could probably come in handy anyway. With this we shouldn't need a custom hash / eq, we can just use the default one from __props__.
| else: | ||
| x, y, *idxs = node.inputs | ||
| x, y = node.inputs[0], node.inputs[1] | ||
| tensor_inputs = node.inputs[2:] |
There was a problem hiding this comment.
I don't like the name tensor_inputs, x, y are also tensor and inputs. Use index_variables?
There was a problem hiding this comment.
Still using tensor_inputs in the last code you pushed
| # must already be raveled in the original graph, so we don't need to do anything to it | ||
| new_out = node.op(raveled_x, y, *new_idxs) | ||
| # But we must reshape the output to math the original shape | ||
| new_out = AdvancedIncSubtensor( |
There was a problem hiding this comment.
You should use type(op) so that subclasses are respected. It may also make sense to add a method to these indexing Ops like op.with_new_indices() that clones itself with a new idx_list. Maybe that will be the one that handles creating the new idx_list, instead of having to be here in the rewrite.
There was a problem hiding this comment.
Currently functions ravel_multidimensional_bool_idx and ravel_multidimensional_bool_idx don't assume the subclass in the same way, but it'd be nice if you could check. Also, if I am giving up on some rewrites too quickly here, please let me know.
pytensor/tensor/subtensor.py
Outdated
| def __init__(self, idx_list): | ||
| """ | ||
| Initialize AdvancedSubtensor with index list. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| idx_list : tuple | ||
| List of indices where slices are stored as-is, | ||
| and numerical indices are replaced by their types. | ||
| """ | ||
| self.idx_list = tuple( | ||
| index_vars_to_types(idx, allow_advanced=True) for idx in idx_list | ||
| ) | ||
| # Store expected number of tensor inputs for validation | ||
| self.expected_inputs_len = len( | ||
| get_slice_elements(self.idx_list, lambda entry: isinstance(entry, Type)) | ||
| ) | ||
|
|
||
| def __hash__(self): | ||
| msg = [] | ||
| for entry in self.idx_list: | ||
| if isinstance(entry, slice): | ||
| msg += [(entry.start, entry.stop, entry.step)] | ||
| else: | ||
| msg += [entry] | ||
|
|
||
| idx_list = tuple(msg) | ||
| return hash((type(self), idx_list)) |
There was a problem hiding this comment.
This already exists in Subtensor? If so create a BaseSubtensor class that handles idx_list and hash/equality based on it.
Make all Subtensor operations inherit from it
There was a problem hiding this comment.
Note this advice may make no sense if we simplify the idx_list to not need custom hash / eq
There was a problem hiding this comment.
hash is a bit different based on whether it is *IncSubtensor or not in the current implementation. Wrote about the Python 3.11 slice not being hashable below.
pytensor/tensor/subtensor.py
Outdated
| ) | ||
| else: | ||
| return vectorize_node_fallback(op, node, batch_x, *batch_idxs) | ||
| # With the new interface, all inputs are tensors, so Blockwise can handle them |
There was a problem hiding this comment.
Comment should not mention a specific time period. Previous status is not relevant here
There was a problem hiding this comment.
Also we still want to avoid Blockwise eagerly if we can
There was a problem hiding this comment.
All time periods should be removed from comments that were added in this PR.
pytensor/tensor/variable.py
Outdated
| pattern.append("x") | ||
| new_args.append(slice(None)) | ||
| else: | ||
| # Check for boolean index which consumes multiple dimensions |
There was a problem hiding this comment.
This is probably right, but why does it matter that boolean indexing consumes multiple dimensions? Aren't we doing expand_dims where there was None -> replace new_axis by None slice -> index again?
There was a problem hiding this comment.
There might be a more dynamic approach, but I have had trouble with multidimensional bool arrays and ellipsis, e.g. x[multi_dim_bool_tensor,None,...] will not know where to add the new axis.
|
Just wanted to repeat, this is looking great. Thanks so far @jaanerik I'm being picky because indexing is a pretty fundamental operation, so want to make sure we get it right this time. |
@ricardoV94 I am struggling a bit with understanding your example, because old_idx_list also has ints. Could you clarify how you see constant index and variable index both working here. If you have the last slice of Absolutely no problem with being picky. I am very grateful for the feedback :) |
|
I updated it, it was some copy-paste typos. Old_idx doesn't have ints, only |
4787bc9 to
5ba887b
Compare
d821794 to
02a8d24
Compare
0003a80 to
ff2b297
Compare
ricardoV94
left a comment
There was a problem hiding this comment.
This is shaping really great. I left some comments that may simplify things further
| if isinstance(op, AdvancedSubtensor1 | AdvancedSubtensor): | ||
| x, *idxs = node.inputs | ||
| x = node.inputs[0] | ||
| if isinstance(op, AdvancedSubtensor): | ||
| index_variables = node.inputs[1:] | ||
| else: | ||
| index_variables = node.inputs[1:] | ||
| else: | ||
| x, y, *idxs = node.inputs | ||
| x, y = node.inputs[:2] | ||
| index_variables = node.inputs[2:] |
There was a problem hiding this comment.
Bot keeps trying to split this fine tuple unpacking into multiple lines, please revert
|
|
||
| if ( | ||
| len(inc_inputs) == len(sub_inputs) | ||
| and _idx_list_struct_equal(x.owner.op.idx_list, node.op.idx_list) |
There was a problem hiding this comment.
Hmmm, so we should simply start counting from the first indexing, regardless of whether it's Subtensor or AdvancedSubtensor? That would allow us to compare the idx_list directly.
Start both at zero. x[idx] and x[idx].set(y) both have idx_list == [0]
8ba1d7c to
c84885b
Compare
c84885b to
e127e4b
Compare
Refactored to use Removed many comments, removed most makeslice, slicetype usage, but refactoring |
0b51466 to
13d025a
Compare
|
Let's try not to touch XTensor if we can, to keep this PR moving along. If it's using SliceTypes let it remain for those |
| # Account for scalar indices before it that remove dimensions | ||
| for i in range(out_adv_axis_pos): | ||
| if not isinstance(reconstructed_indices[i], slice): | ||
| out_adv_axis_pos -= 1 |
There was a problem hiding this comment.
I still don't understand this. This dispatch is just for AdvancedSubtensor right? In that case scalar indices count as advanced indices so there shouldn't be any need to do the -=1. Can you find what test triggers this condition?
| # Note: For simplicity this also excludes subtensor-related expand_dims (np.newaxis). | ||
| # If we wanted to support that we could rewrite it as subtensor + dimshuffle | ||
| # and make use of the dimshuffle lift rewrite | ||
| # TODO: This rewrite is aborting with dummy indexing dimensions which aren't a problem |
There was a problem hiding this comment.
I think this last TODO is still correct. It was meant for stuff like x[[0],], that's equivalent to x[0], in the sense that it can only ever index one entry and won't lead to duplicates, but looks multidimensional.
| elif len(shape_parts) == 1: | ||
| shape = shape_parts[0] |
There was a problem hiding this comment.
Is this a bug fix? If so can we add a regression test?
| @node_rewriter(tracks=[AdvancedSubtensor, AdvancedIncSubtensor]) | ||
| def bool_idx_to_nonzero(fgraph, node): | ||
| """Convert boolean indexing into equivalent vector boolean index, supported by our dispatch | ||
| def ravel_multidimensional_bool_idx(fgraph, node): |
There was a problem hiding this comment.
This seems to be a conflict not well resolved from main? Adding feature that was perhaps removed/stripped down?
ricardoV94
left a comment
There was a problem hiding this comment.
This is looking nearly done!
I saw you did some changes to xtensor. You mentioned that was growing out of scope so I didn't review. Let me know how you want to proceed there, or if you need guidance.
I flagged some changes that seem to be a conflict from main mis-resolved?
Otherwise it's really shaping up!!!
| [ | ||
| (tensor3(), (np.newaxis, slice(None), np.newaxis), ("x", 0, "x", 1, 2)), | ||
| (cscalar(), (np.newaxis,), ("x",)), | ||
| (tensor3(), (None, slice(None), None), ("x", 0, "x", 1, 2)), |
There was a problem hiding this comment.
These tests can stay as np.newaxis to reduce diff, but it's a nit given the scope of the PR
Description
Allows vectorizing AdvancedSetSubtensor.
Gemini picks up where Copilot left off.
Related Issue
Checklist
Type of change