-
Notifications
You must be signed in to change notification settings - Fork 756
Add output_writer_iterator #1575
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In addition to the inline comments, I have some other suggestions for this PR:
Rather than having an output-only iterator, let's update this to support both input and output by replacing BinaryFunction
with InputBinaryFunction
and OutputBinaryFunction
, where the two functions are inverse operations. This will avoid the current mess we have with the transform iterators (we currently provide transform_iterator
, transform_output_iterator
and transform_input_output_iterator
for legacy reasons).
The name writer_iterator
isn't very clear IMO, and won't make much sense once this supports input and output. Some alternative ideas:
proxy_iterator
functor_iterator
invoke_iterator
arg_iterator
I'm not sure I like these any better, though. Let's think about this a bit more, and maybe see if anyone else has some ideas.
Edit: decorate_iterator
or something similar (decorating_
, decorated
_, etc) might be a good fit, since this effectively implements the "decorator" design pattern over an existing iterator. I've started a discussion on this with the rest of the team to see if we can find something better.
This will need tests and examples covering the input/output/both usecases.
So to summarize:
- Change to input/output iterator.
- Let's work out a more meaningful name for this.
- Add tests and examples of using this iterator for input, output, and both.
Updating with some internal conversations: @harrism suggested
Input: struct adj_diff_functor
{
const int *input;
__device__
int operator()(std::size_t i) const
{
return input[i + 1] - input[i];
}
};
auto indices = thrust::make_counting_iterator(0);
thrust::device_vector<int> data{0, 1, 4, 7, 9, 12};
auto iter = thrust::make_decorated_input_iterator(indices, adj_diff_functor{data.data()});
auto iter_end = iter + data.size() - 1;
// [iter, iter_end) -> {1, 3, 3, 2, 3} @benbarsdell correctly points out that this could be implemented using existing counting + transform iterators. Input/output is a more interesting case that cannot be implemented using other existing fancy iterators: struct input_functor
{
// Produces ones[i] + 10*tens[i]
const int *ones; // Always 0-9
const int *tens;
__device__
int operator()(std::size_t i) const
{
return ones[i] + 10 * tens[i];
}
};
struct output_functor
{
// Decomposes input values to perform the inverse of input_functor
int *ones; // Always 0-9
int *tens;
__device__
void operator()(std::size_t i, int val) const
{
ones[i] = val % 10;
tens[i] = val / 10;
}
};
thrust::device_vector<int> ones = {1, 5, 3, 2, 6};
thrust::device_vector<int> tens = {3, 2, 6, 7, 3};
auto indices = thrust::make_counting_iterator(0);
auto iter = thrust::make_decorated_iterator(indicies,
input_functor{ones.data(), tens.data()},
output_functor{ones.data(), tens.data()});
auto iter_end = iter + ones.size();
// [iter, iter_end) -> {31, 25, 63, 72, 36}
iter[0] = 19;
iter[1] = 42;
iter[2] = 98;
iter[3] = 132;
iter[4] = 15;
// ones -> {9, 2, 8, 2, 5}
// tens -> {1, 4, 9, 13, 1} |
Looking to |
I like |
I've had to start at this iterator for quite a while to understand it's story. There's nothing here that requires the adapted iterator to be an "index_iterator". Generally, it looks like a cross between a zip and transform iterator. It binds iterator Thinking about this in terms of When viewed through that lens, I'm thinking of a name more like Thinking about @allisonvacanti's case of having this be both an input and output iterator (I only thought about the output case above), the input case wouldn't be a partial binding but a full binding of a callable with all of its arguments. Dereferencing the iterator will invoke the callable with the associated arguments provided by the bound iterators. (This is basically just a normal |
My omission of the "index" iterator from my later examples definitely clouded things. With this new perspective, I'm back to liking
This is an iterator that wraps all accesses to an existing iterator with decorator functions. |
What gives me pause about this is that this pattern/definition applies just as well to In fact, the input iterator case for the I'm not sure the name Now I'm thinking that maybe this new iterator can just be a new overload of |
It is a pretty generic name, and also very similar to the existing
Hmm, perhaps. Let's compare their user interfaces directly: { // Transform:
T = value_t<Iter>;
U InFunc(T); // Used for reads
T OutFunc(U); // Used for writes
auto thrust::make_transform_iterator(Iter, InFunc, OutFunc);
}
{ // New Iterator
T = value_t<Iter>;
U InFunc(T); // Used for reads
void OutFunc(T, U); // Used for writes
auto thrust::make_XXX_iterator(Iter, InFunc, OutFunc);
} The key difference is in |
Which implies that this iterator is only useful with some external state, which could potentially be abstracted out: { // New Iterator with abstracted state:
using T = value_t<Iter>;
U InFunc(State, T); // Used for reads
void OutFunc(State, T, U); // Used for writes
auto thrust::make_XXX_iterator(Iter, State, InFunc, OutFunc);
} This could be used to simplify the functors so they don't have to capture the same state in their members: { // current
struct input_functor
{
const State *state;
U operator()(T) const;
};
struct output_functor
{
State *state;
void operator()(T, U);
};
State *state = ...;
auto iter = thrust::make_XXX_iterator(
indicies,
input_functor{state},
output_functor{state});
}
{ // with state abstraction
struct input_functor
{
U operator()(const State&, T) const;
};
struct output_functor
{
void operator()(State&, T, U);
};
State *state = ...;
auto iter = thrust::make_XXX_iterator(
indicies,
state,
input_functor{},
output_functor{});
} I'm unsure whether this abstraction would be worthwhile, but I think a key point here is that this iterator does require extra state to do anything interesting. |
Yeah, I noticed that too. I'm not a fan of the fact that the binary callable doesn't return anything, which means it has to have side-effects to do anything useful. |
I don't think that'll be a problem. For instance, Do you see a different way to implement this functionality? |
Co-authored-by: Allison Vacanti <[email protected]>
The main reason for creating this iterator is to provide a generalized output iterator.
Also, mixing input and output together would make it more than what it's designed for. My vote would be to limit this iterator's functionality to behave like output iterator only, and add new combined input and output iterator seperately. |
…ut_writer_iterator
@karthikeyann's description reminded me that I explored an alternative for this approach awhile back in NVIDIA/cccl#792. However, coming back to this after some time, it occurs to me that we are effectively just trying to indirectly recreate projections via a complicated iterator. That tells me there is probably a far more natural way to express this. Curious for @ericniebler 's thoughts. |
@jrhemstad In NVIDIA/cccl#792 godbolt example, it still uses |
This iterator serves as a generalized version of output iterator.
Typical usage is
output_writer_iterator(index_iterator, [result_begin](auto i, auto v) { result_begin[i]=v;});
Sometimes output iterator has to be complex. This output writer iterator can help achieve writing to complex data structures using binary function object or lambda which accepts an index and a value to write.
Example given in documentation is achieving functionality similar to
std::bitset
using athrust::device_vector
.