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

[BEAM-5987] Cache and share materialized side inputs between Spark tasks #7091

Closed

Conversation

dmvk
Copy link
Member

@dmvk dmvk commented Nov 20, 2018

We should try to reuse deserialized side inputs among spark tasks.


Follow this checklist to help us incorporate your contribution quickly and easily:

  • Format the pull request title like [BEAM-XXX] Fixes bug in ApproximateQuantiles, where you replace BEAM-XXX with the appropriate JIRA issue, if applicable. This will automatically link the pull request to the issue.
  • If this contribution is large, please file an Apache Individual Contributor License Agreement.

It will help us expedite review of your Pull Request if you tag someone (e.g. @username) to look at it.

Post-Commit Tests Status (on master branch)

Lang SDK Apex Dataflow Flink Gearpump Samza Spark
Go Build Status --- --- --- --- --- ---
Java Build Status Build Status Build Status Build Status Build Status Build Status Build Status Build Status
Python Build Status --- Build Status
Build Status
Build Status --- --- ---

@dmvk dmvk force-pushed the dejv/spark_shared_cached_side_inputs branch from b997857 to 976678f Compare November 20, 2018 17:41
@dmvk
Copy link
Member Author

dmvk commented Nov 20, 2018

Run Spark ValidatesRunner

Collections.synchronizedMap(new WeakHashMap<>());

/**
* Id that is consistent among executors. We can not use stepName because of possible collisions.
Copy link
Contributor

Choose a reason for hiding this comment

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

I do not get meaning of 'consistent' here. Do you mean random (most likely distinct) even within one JVM ?

Copy link
Member Author

Choose a reason for hiding this comment

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

After deserialization on the executor side

Copy link
Contributor

Choose a reason for hiding this comment

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

ok, I see.

Copy link
Contributor

@VaclavPlajt VaclavPlajt left a comment

Choose a reason for hiding this comment

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

LGTM

@dmvk
Copy link
Member Author

dmvk commented Nov 21, 2018

This still needs some effort as it does not handle the case when sideinput is used in different DoFns

@mareksimunek mareksimunek force-pushed the dejv/spark_shared_cached_side_inputs branch 3 times, most recently from b804a31 to 6a22322 Compare November 27, 2018 14:46
@mareksimunek
Copy link
Contributor

Run Spark ValidatesRunner

@mareksimunek mareksimunek force-pushed the dejv/spark_shared_cached_side_inputs branch 2 times, most recently from ab6adc1 to a84bbc1 Compare November 29, 2018 16:01
@mareksimunek
Copy link
Contributor

Run Spark ValidatesRunner

@mareksimunek mareksimunek force-pushed the dejv/spark_shared_cached_side_inputs branch from a84bbc1 to 80e147c Compare December 3, 2018 13:19
* Side inputs are stored in {@link Cache} with weakValues so if there is no reference to a value,
* sideInput is garbage collected.
*/
public class SideInputStorage {
Copy link
Member

@iemejia iemejia Dec 10, 2018

Choose a reason for hiding this comment

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

package private and same for constructor, make access as tight as needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

done

* Keep references for the whole lifecycle of CachedSideInputReader otherwise sideInput needs to
* be de-serialized again.
*/
private Set<?> sideInputReferences = new HashSet<>();
Copy link
Member

Choose a reason for hiding this comment

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

Aren't values ever removed from here? Or I am misreading this one, seems like it can overflow and even prevent SideInputStorage from being GCed.

Copy link
Contributor

Choose a reason for hiding this comment

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

references removed because different solution was used. more

@iemejia
Copy link
Member

iemejia commented Dec 10, 2018

It would be nice to test that this behaves as expected and does not leak (not being GCed) and does not rematerialize.

Copy link
Member

@iemejia iemejia left a comment

Choose a reason for hiding this comment

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

Let some comments, given the potential issue of 'growing memory use' as a side effect it would be really nice to add some test(s).

@mareksimunek mareksimunek force-pushed the dejv/spark_shared_cached_side_inputs branch from 0b542a9 to cc404ac Compare January 7, 2019 14:30
@mareksimunek
Copy link
Contributor

mareksimunek commented Jan 7, 2019

After trying several approaches we decided to keep it simple and go with expireAfterAccess to drop values from cache.

Solution with weak values didn't bring desired behavior because when MultiDoFnFunction finished for one executor, it immediately garbage collected de-serialized side input (because it lost references due to end of life for CachedSideInputReader). It kept side input in cache only if there was overlapped running of multiple MultiDoFnFunction . In our case for one JVM it a de-serialized side input up to 10x.

With expireAfterAccess side input is de-serialized only once. I chose 5 min eviction duration as best compromise but I am open to discussion if it should be configurable.

Disadvantage for expireAfterAccess solution could be potential higher memory consumption if SideInputStorage isn't access long time so nothing can be evicted. I don't know how to recognize when MultiDoFnFunction is finished so I can call cache.cleanup() to trigger eviction for expired items. Also not sure if this is even a problem.

@mareksimunek
Copy link
Contributor

Run Spark ValidatesRunner

1 similar comment
@mareksimunek
Copy link
Contributor

Run Spark ValidatesRunner

@mareksimunek mareksimunek force-pushed the dejv/spark_shared_cached_side_inputs branch from 8c5c5b0 to 72aee8e Compare January 11, 2019 15:18
@mareksimunek
Copy link
Contributor

Run Java PreCommit

@VaclavPlajt VaclavPlajt force-pushed the dejv/spark_shared_cached_side_inputs branch from 72aee8e to 0e32d96 Compare January 14, 2019 11:15
@iemejia
Copy link
Member

iemejia commented Jan 16, 2019

Run Spark ValidatesRunner

Copy link
Member

@iemejia iemejia left a comment

Choose a reason for hiding this comment

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

I let two comments on possible issues that I am not 100% sure if they are correct with this PR.
Pinging also @amitsela to see if he has something to say in particular in the static state. Thanks!

class SideInputStorage {

/** JVM deserialized side input cache. */
private static final Cache<Key<?>, Optional<?>> materializedSideInputs =
Copy link
Member

Choose a reason for hiding this comment

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

I am a bit worried on the possible consequence of a collision of the Key<view, window> tuple in particular if a bad implementation of equals is around. This is not relative to this PR but since the state is now static this makes the likelihood of this happening bigger.

Copy link
Member

Choose a reason for hiding this comment

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

how do other runners cache side inputs? by which key? this sounds like something the SDK could provide guidance on (@kennknowles)

Copy link
Member

Choose a reason for hiding this comment

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

The window should use windowCoder.structuralValue(window) which is required to behave identically to a full serialization for shuffle purposes. The view itself is just a tag so it should have good enough equals as-is. There is already caching in the now-donated Dataflow Java worker, if you look through uses and subclasses of SideInputReader.

Copy link
Contributor

Choose a reason for hiding this comment

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

I also think you have bigger troubles if you have collision in view or window(on more places sparkRunner relies on that) so I will leave it as it is. Is that ok?

@@ -86,9 +55,27 @@ private CachedSideInputReader(SideInputReader delegate) {
@Override
public <T> T get(PCollectionView<T> view, BoundedWindow window) {
Copy link
Member

@iemejia iemejia Jan 16, 2019

Choose a reason for hiding this comment

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

I am worried on the possible semantics consequences of CachedSideInputReader.get() returning a null value when it is not in the Cache. Wouldn't it imply that a window could get an empty side input assigned?
The documentation on this is not really clear (pinging @kennknowles to see if I am misreading it).
Wonder if there is a test to validate that this cannot happen or if we can create one somehow?

Copy link
Member

Choose a reason for hiding this comment

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

Ah, the meaning of that comment is that null is a value. You can have a PCollection<@Nullable Foo> that contains just one copy of null and use View.asSingleton() and the side input returns the null.

In other words, get must return a value of type T. But the type T may itself be @Nullable Something. The annotation on SideInputReader should be removed. It is is incorrect if we use a static analysis that understands this. Findbugs does not understand this but we should aspire for our annotations to be correct so the documentation is clear.

@@ -85,6 +88,13 @@ private SideInputBroadcast createBroadcastHelper(
PCollectionView<?> view, JavaSparkContext context) {
Tuple2<byte[], Coder<Iterable<WindowedValue<?>>>> tuple2 = pviews.get(view);
SideInputBroadcast helper = SideInputBroadcast.create(tuple2._1, tuple2._2);
String pCollectionName =
view.getPCollection() != null ? view.getPCollection().getName() : "UNKNOWN";
LOG.info(
Copy link
Member

Choose a reason for hiding this comment

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

Maybe LOG.debug?

Copy link
Contributor

Choose a reason for hiding this comment

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

changed to debug

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import com.google.common.cache.Cache;
Copy link
Member

Choose a reason for hiding this comment

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

Vendored Guava?

Copy link
Contributor

Choose a reason for hiding this comment

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

fixed to vendored

@@ -86,9 +55,27 @@ private CachedSideInputReader(SideInputReader delegate) {
@Override
public <T> T get(PCollectionView<T> view, BoundedWindow window) {
Copy link
Member

Choose a reason for hiding this comment

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

Ah, the meaning of that comment is that null is a value. You can have a PCollection<@Nullable Foo> that contains just one copy of null and use View.asSingleton() and the side input returns the null.

In other words, get must return a value of type T. But the type T may itself be @Nullable Something. The annotation on SideInputReader should be removed. It is is incorrect if we use a static analysis that understands this. Findbugs does not understand this but we should aspire for our annotations to be correct so the documentation is clear.

SizeEstimator.estimate(result));
return Optional.ofNullable(result);
});
return optionalResult.orElse(null);
Copy link
Member

Choose a reason for hiding this comment

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

Ismaël is right. The delegate.get is not @Nullable at this place in the abstraction. You don't need to check for it (unless there's some other bug somewhere) and you shouldn't convert Optional.absent() to null.

Copy link
Contributor

Choose a reason for hiding this comment

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

I chose this solution because guava Cache doesn't allow null values and I didn't realize I will break semantic meaning. I will try to find out different solution.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, thank you for clarifying. That is a good attempt. The problem is that these will incorrectly be turned into the same thing:

Optional.ofNullable(null).orElse(null) == null

Optional.ofNullable(Optional.absent()).orElse(null) == null

The fact that Optional.of(null) throws NPE is a mistake in the design (both Java and Guava). Maybe the point of the design is to convince people to not use null, which is a billion dollar good idea. But it makes Optional<T> not correctly parametric in T.

I think that if you actually convert null into Optional.of(Optional.absent()) and other values v into Optional.of(Optional.of(v)) you can simulate the behavior it should have had in the first place. Or you could make your own little replacement of Optional.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for suggestions, Optional combo would be not very readable.
I made my own wrapper where I simply wrap the value so I can put null into the cache.
https://github.com/apache/beam/pull/7091/files#diff-b123f0f1ca9646966a641a458b74cfbcR92

@mareksimunek mareksimunek force-pushed the dejv/spark_shared_cached_side_inputs branch from 0e32d96 to ddfe7dd Compare January 22, 2019 16:17
@apache apache deleted a comment from mareksimunek Feb 1, 2019
@apache apache deleted a comment from mareksimunek Feb 1, 2019
@apache apache deleted a comment from mareksimunek Feb 1, 2019
@apache apache deleted a comment from mareksimunek Feb 1, 2019
Copy link
Member

@iemejia iemejia left a comment

Choose a reason for hiding this comment

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

LGTM, will do some minor touches and rebase manually to merge. Thanks a lot @mareksimunek and @dmvk !

@iemejia iemejia changed the title [BEAM-5987] Spark: Share cached side inputs between tasks. [BEAM-5987] Cache and share materialized side inputs between Spark tasks Feb 8, 2019
iemejia added a commit that referenced this pull request Feb 8, 2019
@iemejia
Copy link
Member

iemejia commented Feb 8, 2019

Merged!

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.

None yet

6 participants