Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Containers] Add Array::Map #12692

Merged
merged 7 commits into from
Sep 20, 2022
Merged

[Containers] Add Array::Map #12692

merged 7 commits into from
Sep 20, 2022

Conversation

Lunderberg
Copy link
Contributor

Previously, an in-place mutation could be applied to an array using Array::MutateByApply, but this couldn't be used for transformations that return a new array, or for transformations that return a new type. The PR adds Array::Map, which can map to any ObjectRef subclass. For mappings that return the same type, this is done by delegating to Array::MutateByApply, to take advantage of the same copy-on-write behavior.

For ease of review, this PR consists of two separate commits. The first implements Array::Map, while the second performs several small refactors

Previously, an in-place mutation could be applied to an array using
`Array::MutateByApply`, but this couldn't be used for transformations
that return a new array, or for transformations that return a new
type.

The commit adds `Array::Map`, which can map to any `ObjectRef`
subclass.  For mappings that return the same type, this is done by
delegating to `Array::MutateByApply`, to take advantage of the same
copy-on-write behavior.
With the new `Array::Map` functionality, many places that previously
used explicit loops or `tvm::tir::MutateArray` can be cleaned.
* \return The transformed array.
*/
template <typename F, typename U = std::invoke_result_t<F, T>>
Array<U> Map(F fmap) const {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was curious if we could unify and migrate MutateByApply into Map?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly, and that would allow for avoiding copies in a few additional cases (e.g. map from T to Optional<T>, or to a superclass of T) that aren't currently handled. I'll take a quick stab at it and see if I can unify the two.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you on the suggestion, and it ended up being much cleaner that way. Both Map and MutateByApply are now implemented in terms of the same underlying helper function. The helper function applies both the mutate-in-place and copy-on-write optimizations, with if constexpr type checks to avoid attempting the optimization if they wouldn't be possible.

}

// Normal path for incompatible types, or post-copy path for
// copy-on-write instances.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will be left over on the copy-on-write instance? Will there be some items that are incompatible? How are those guaranteed to be at the end?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will be left over on the copy-on-write instance?

If we have compatible types, and we've reached this point, we've found at least one element for which the mapped.same_as(*it) check on line 776 has failed. In that case, output will contain everything in the range [arr->begin(), it). That is, output contains all elements that are identical, and the first non-identical element. it will point to the next element that should be transformed, and so the next loop over it can continue where the first loop left off.

Will there be some items that are incompatible?

It's entirely possible, either at compile-time or at runtime. For example, I could have an Array<PrimExpr> buffer_shape and map it to allowed ranges buffer_shape.Map([](PrimExpr expr) { return Range::FromMinExtent(0, expr);});, which would be incompatible and identified as such at compile-time. In that case, the if constexpr could identify that they cannot be represented by the same underlying array, and can skip the attempts to do so altogether.

If a type is incompatible at runtime, then it will also fail the mapped.same_as(*it) check on line 776. So if I have an Array<Var> being mapped to Array<PrimExpr> with var_array.Map([&](Var var) { return var.same_as(to_replace) ? replace_with : var;});, it may or may not be compatible, depending on whether to_replace shows up in the array.

How are those guaranteed to be at the end?

Incompatible items may occur at any point in the mapped output, even at the very first iteration. In that case, the commands executed in the conditional on !mapped.same_as(*it) are the same as would be executed up through the first iteration of the mapping loop.

// Same as the else branch on `compatible_types`
output = ArrayNode::CreateRepeated(arr->size(), U());

// For the first iteration, it is `arr->begin()`, so this would be an
// empty range [begin, begin), nothing is initialized, and this
// statement has no effect.
output->InitRange(0, arr->begin(), it);

// The newly mapped item is stored to the first location of the output.
output->SetItem(it - arr->begin(), std::move(mapped));

// The loop increment that would have happened
it++;

// `it` now points to the second element of the input, and we have one
// mapped element in the output.  We're now ready to start the second
// loop, just at the second iteration instead of the first.

Essentially, we only need to check for identical return values up until we find a single non-identical element, at which point we know that we can't avoid the copy anyways. But once we reach the first non-identical value, we don't need to repeat the function calls up to that point, because we know that everything is either identical (and can therefore be copied from the input) or is non-identical (is which case it is the first such non-identical value).

Copy link
Collaborator

@janetsc janetsc Sep 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this explanation! Maybe it would be helpful to others as well to summarize this in the comment block on 796...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem, and updated!

}

return output;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General comment - Can you add a unit test to exercise the edge cases in MapHelper?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Certainly, and thank you for pointing that out! There are some existing tests in container_test.cc, along with a large amount of usage when lowering TIR, but no tests that would specifically point to these edge cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests added for each of the compatible types, to validate that copies are avoided, and to ensure correct fail-through behavior when a copy is required. A double thanks for requesting it, as it also caught a type conversion error that I had missed.

Copy link
Collaborator

@janetsc janetsc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unit test looks good - thanks!

@tmoreau89
Copy link
Contributor

@junrushao any additional requests on this PR? Thank you!

Copy link
Member

@junrushao junrushao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@tmoreau89 tmoreau89 merged commit 534378b into apache:main Sep 20, 2022
@tmoreau89
Copy link
Contributor

Awesome thank you @Lunderberg @janetsc @junrushao , the PR has been merged!

@Lunderberg Lunderberg deleted the tvm_array_map branch September 21, 2022 13:04
xinetzone pushed a commit to daobook/tvm that referenced this pull request Nov 25, 2022
* [Containers] Add Array::Map

Previously, an in-place mutation could be applied to an array using
`Array::MutateByApply`, but this couldn't be used for transformations
that return a new array, or for transformations that return a new
type.

The commit adds `Array::Map`, which can map to any `ObjectRef`
subclass.  For mappings that return the same type, this is done by
delegating to `Array::MutateByApply`, to take advantage of the same
copy-on-write behavior.

* [Refactor] Use Array::Map where possible

With the new `Array::Map` functionality, many places that previously
used explicit loops or `tvm::tir::MutateArray` can be cleaned.

* Merge the Map and MutateInPlace implementations

* Fix off-by-one error in MapHelper

* Updated with unit tests for Array::Map conversions

* Improved comments explaining the copy-on-write in MapHelper
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants