-
Notifications
You must be signed in to change notification settings - Fork 263
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
Shorten critical section in findEviction #217
Conversation
if (!toRecycle) | ||
continue; |
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.
if (!toRecycle) | |
continue; | |
if (!toRecycle) { | |
continue; | |
} |
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.
This happens iff we exhausted the entire linked list and hasn't found a candidate while not exhausting searchTries right?
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.
Yes, that's correct.
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.
Please fix the format suggestion.
// remove the child from the mmContainer as we will not be evicting | ||
// it. We could abort right here, but we need to cleanup in case | ||
// unmarkForEviction() returns 0 - so just go through normal path. | ||
if (!toRecycle_->isChainedItem() || |
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.
Can you elaborate more?
In the old way of eviction, if we are trying to recycle a chained item (toRecycle.isChainedItem()) but the parent has changed, we don't do any specific treatment until in releaseBackToAllocator, we'll go through the chain with parent being candidate
, and found out that toRecycle
was never reached by walking the chain. So releaseBackToAllocator fails and we continue iterating the eviction queue.
How does the new way work?
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.
The new way is pretty much the same. This comment states that we don't do any specific treatment as well. We'll just try to evict the item as usual and leverage the existing logic in releaseBackToAllocator.
It might be possible to optimize this case but for this PR I felt like having similar behavior is better.
The only reason for this if
here is to optimize the happy case: if the item is not a chained item or the parent hasn't changed, we can safely remove the item from MMContainer (we already hold the MMContainer lock so doing it here has low overhead). If the parent has changed we cannot call the mmContainer.remove()
here as it could lead to deadlocks. Instead, we'll just call mmContainer.remove()
inside unlinkItemForEviction
which is slower (need to acquire the lock again) but safe.
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.
I understand the optimization now.
I think as long as the chained item can't change parent after this point then this is safe. And since we marked the parent as forEviction, it can't be moving nor accessed, so we don't have to do the chainedItemLock_ here.
How much does this optimization give us?
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.
Using default hit_ratio/graph_cache_leader_fbobj/config.json
this optimization improves throughput by ~40% on my machine. This is mostly due to almost 100% eviction Success rate (without the optimization I get ~50% Success rate).
|
||
token = createPutToken(*candidate_); | ||
|
||
if (shouldWriteToNvmCache(*candidate_) && !token.isValid()) { |
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.
shouldWriteToNvmCache
is called once in createPutToken
and another time in here, can we make it called only once? I think we can just do it as the old way?
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.
Since I wrapped token creation in createPutToken
I no longer know if token is invalid due to FailConcurrentFill
or due because shouldWriteToNvmCache
returned false.
Perhaps I should just put stats_.evictFailConcurrentFill.inc();
inside createPutToken
?
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.
Is there a reason you have to create a function for createPutToken?
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.
I wanted to use it in other places as well. But since for now it's only used in findEviction I can remove it. Done.
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.
Also, I just realized I bumped evictFailParentAC
and evictFailAC
even if the failure was due to invalid token - I fixed this as well.
and and enable combinedLocking support. Shorten MMContainer critical section by removing from Access Container after MMContainer lock is droped. Only markForEviction is executed under the MMContainer critical section now. If markForEviction succeeds, the item is guaranteed to be evicted.
|
||
token = createPutToken(*candidate_); | ||
|
||
if (shouldWriteToNvmCache(*candidate_) && !token.isValid()) { |
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.
Is there a reason you have to create a function for createPutToken?
// remove the child from the mmContainer as we will not be evicting | ||
// it. We could abort right here, but we need to cleanup in case | ||
// unmarkForEviction() returns 0 - so just go through normal path. | ||
if (!toRecycle_->isChainedItem() || |
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.
I understand the optimization now.
I think as long as the chained item can't change parent after this point then this is safe. And since we marked the parent as forEviction, it can't be moving nor accessed, so we don't have to do the chainedItemLock_ here.
How much does this optimization give us?
efba6bc
to
0253683
Compare
@haowu14 has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
if (evictToNvmCache) | ||
token = nvmCache_->createPutToken(candidate_->getKey()); | ||
|
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.
if (evictToNvmCache) | |
token = nvmCache_->createPutToken(candidate_->getKey()); | |
if (evictToNvmCache) { | |
token = nvmCache_->createPutToken(candidate_->getKey()); | |
} | |
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.
And this might have been lost in one of the patches you add after you ran test but here is a bug.
The token can be created on candidate A, then candidate A failed to markForEviction and we move to the next item.
For candidate B, evictToNvmCache is false but we successfully markForEviction.
We'll exit this loop with candidate B but the token created from candidate A.
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.
I saw a crash in production test with exception "Item is not nvm evicted and nvm clean". And I suspect this is the reason. I'm putting together a hacky fix on my end to resume the test.
if (!toRecycle) | ||
continue; |
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.
Please fix the format suggestion.
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.
@igchor I continued the production test with a hacky change where I do auto tokoen_ = createPutToken(...)
and only do token == std::move(token)
iff the item successfully mark for eviction. However, I noticed a regression in allocation latency mostly caused by creating and destroying PutTokens.
I think this is because in this PR, we try to create put token before mark for eviction while before this PR we do refcount==0 and markMoving before trying to create PutToken.
Can we do it the same way as before?
@igchor has updated the pull request. You must reimport the pull request before landing. |
We need to create token before marking for eviction because Could you share what is the Also, have you tried running with "useCombinedLockForIterators": true ? |
@igchor
Not yet. I'd need to make sure with this being false there's no regression (I hope)? |
@haowu14 has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
Got it. Right, regarding useCombinedLockForIterators, I would expect there is no regression when it's set to false. When it's set to 'true' I'd expect lower allocate latency and higher throughput at the cost of slightly higher find latency. |
In the most recent version there's no significant regression however I don't see improvement with combined locking on either. Our test is a shadow set tup, meaning that the binary running base (trunk) and test(this PR) will receive exact same get/set at the same time regardless of how quickly that binary can process the request. So we can't look at QPS directly for comparison. I'll run the experiment again on a new experiment since this existing pair of servers ran my buggy code and may not have exactly the same cache content (but I don't think it matters much). My questions for you:
|
Also, one more thing that we've noticed when running benchmarks. With this patch, we can get similar (or even better) performance (QPS and latency) while using lower AC |
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.
Sorry for the late review. This LGTM overall. A few comments inline.
template <typename CacheTrait> | ||
void CacheAllocator<CacheTrait>::unlinkItemForEviction(Item& it) { | ||
XDCHECK(it.isMarkedForEviction()); | ||
XDCHECK(it.getRefCount() == 0); |
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.
nitpick: XDCHECK_EQ(0, it.getRefcount());
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.
Done.
XDCHECK(it.isMarkedForEviction()); | ||
XDCHECK(it.getRefCount() == 0); | ||
accessContainer_->remove(it); | ||
removeFromMMContainer(it); |
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.
didn't we already remove this item from mm-container in findEviction at L1313? (if it were a parent or just a regular item?)
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.
Yes, we might have removed the item but removeFromMMContainer
checks if the item is still in the MMContainer and only then calls mmcontainer.remove(). We need to keep this call here because If the parent of the item changed, the item will still be in the mmcontainer.
// Since we managed to mark the item for eviction we must be the only | ||
// owner of the item. | ||
const auto ref = it.unmarkForEviction(); | ||
XDCHECK(ref == 0u); |
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.
same nitpick: XDCHECK_EQ
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.
Done
if (evictToNvmCache && !token_.isValid()) { | ||
stats_.evictFailConcurrentFill.inc(); | ||
} else if (candidate_->markForEviction()) { | ||
XDCHECK(candidate_->isMarkedForEviction()); |
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.
Can we refactor this a bit to have fewer nested if-stmt?
e.g.
if (evictToNvmCache && !token_.isValid()) {
stats_.evictFailConcurrentFill.inc();
++itr;
continue;
}
auto markedForEviction = candidate_->markForEviction();
if (!markedForEviction) {
++itr;
continue;
}
// ...
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.
Sure, done.
if (candidate->getRefCount() != 0 || !candidate->markMoving()) { | ||
++itr; | ||
while (config_.evictionSearchTries == 0 || | ||
config_.evictionSearchTries > searchTries) { |
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.
Do we still need this when we're doing a while loop already inside withEvictionIterator
?
Can we just reset the iterator internally and restart the search under lock?
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.
Potentially yes, but it might have an impact on performance. The current implementation behaves pretty much the same as the original implementation. If we got rid of this loop, the MMContainer lock could be held for a longer duration.
Resetting the iterator under the lock might actually be a better solution but I just didn't want to change the behavior too much in this PR - perhaps it would be better to do it in a separate PR and evaluate it?
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.
Also, I think the outer loop is also need for cases when releaseBackToAllocator ends up not releasing the item we intend to - this is already done outside the lock.
7a033e6
to
5d5c3f0
Compare
@igchor has updated the pull request. You must reimport the pull request before landing. |
Performance tests have passed with autoFDO off. So we are good performance wise. |
@haowu14 has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
This pull request has been reverted by ed9e088. |
Crashed in staging test for a service. Validating a fix. We'll land it again once it's fixed. |
We upstreamed this again ae7442a The problem was the lifetime of putToken. It could live after the |
@haowu14 thanks for letting us now! We'll try to apply the fix on our end and validate it as well. We've actually seen a few |
This is the next part of the 'critical section' patch after #183.
Original PR (some of the changes already upstreamed): #172
This PR contains changes to findEviction only. The remaining part (changes in SlabRelease code) will be provided in the next (final) PR.