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

Improve allocation success when evicting from cache #6844

Closed
wants to merge 1 commit into from

Conversation

oerling
Copy link
Contributor

@oerling oerling commented Oct 1, 2023

An allocation will evict cache to make space for data. If the the evicted data is freed and then an allocation is attempted, it can be that another thread has snatched the freed data. With high transient memory allocation rate and many threads, it can happen that a large allocation runs out of retries even if it could be satisfied.

Therefore, when evicting in order to make space for an allocation, we keep hold of the non-contiguous Allocations we remove from cache. Many allocations can in this way be concatenated into an allocation that covers the needed number of pages. This can then atomically be converted into a new non-contiguous or contiguous allocation. The allocate*WithoutRetry methods accept a non-contiguous allocation to be freed to provide pages for the new allocation. Memory mapping magic can convert non-contiguous freed pages to contiguous ones.

Note that the allocate*WithoutRetry methods mishandle the atomicity of the exchange: The allocated count is decremented by the size of the freed collateral and then incremented by the size of the new allocation. This should be a single atomic increment of allocatedSize

  • collateralSize. This always succeeds if the collateral is >= the allocated amount. Even so, the window of vulnerability to another thread snatching the freed capacity before it is reacquired is probably negligible, only a few instructions.

The AsyncDataCache::makeSpace loop first tries to allocate. If it fails, it evicts and keeps up to the required amount in a a grab bag allocation. It loops until this allocation is large enough to convert into the desired allocation. If nothing is evicted and the allocation repeatedly fails, it gives up.

@netlify
Copy link

netlify bot commented Oct 1, 2023

Deploy Preview for meta-velox canceled.

Name Link
🔨 Latest commit 3cb81e1
🔍 Latest deploy log https://app.netlify.com/sites/meta-velox/deploys/65272ea607884d000899e45a

@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 1, 2023
@oerling oerling requested a review from xiaoxmeng October 1, 2023 23:07
@facebook-github-bot
Copy link
Contributor

@oerling has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

Copy link
Contributor

@xiaoxmeng xiaoxmeng left a comment

Choose a reason for hiding this comment

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

@oerling overall looks good to me. Thanks!

@@ -380,7 +384,14 @@ void CacheShard::evict(uint64_t bytesToFree, bool evictAllUnpinned) {
continue;
}
largeFreed += candidate->data_.byteSize();
toFree.push_back(std::move(candidate->data()));
if (acquirePages) {
Copy link
Contributor

Choose a reason for hiding this comment

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

if (acquiredPages > 0) {

if (canTryAllocate(numPages - collateral.numPages(), evicted)) {
try {
if (allocate(evicted)) {
if (isCounted) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Shall we just put isCounted is safe guard processing? Thanks!

if (isCounted) {
--numThreadsInAllocate_;
}
return true;
throw;
Copy link
Contributor

Choose a reason for hiding this comment

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

Shall we use rethrow here?

}
++shardCounter_;
int32_t toAcquire =
Copy link
Contributor

Choose a reason for hiding this comment

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

const int32_t numPagesToAcquire =

numPages * sizeMultiplier * memory::AllocationTraits::kPageSize,
nthAttempt >= kNumShards);
std::max<int32_t>(kMinEvictPages, numPages) * sizeMultiplier *
memory::AllocationTraits::kPageSize,
Copy link
Contributor

Choose a reason for hiding this comment

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

Use memory::AllocationTraits::pageBytes()

auto candidatePages = candidate->data().numPages();
acquirePages =
candidatePages > acquirePages ? 0 : acquirePages - candidatePages;
allocation->appendMove(candidate->data());
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a check candidate is empty?

@@ -37,6 +37,14 @@ void Allocation::append(uint8_t* address, int32_t numPages) {
"Appending a duplicate address into a PageRun");
runs_.emplace_back(address, numPages);
}
void Allocation::appendMove(Allocation& other) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a unit test for this? Thanks!

@@ -185,9 +186,14 @@ bool MemoryAllocator::allocateContiguous(
return allocateContiguousWithoutRetry(
numPages, collateral, allocation, reservationCB, maxPages);
}
return cache()->makeSpace(numPages, [&]() {
Allocation toFree;
if (collateral) {
Copy link
Contributor

Choose a reason for hiding this comment

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

if (collateral != nullptr) {

@@ -198,7 +204,9 @@ bool MemoryAllocator::growContiguous(
if (cache() == nullptr) {
return growContiguousWithoutRetry(increment, allocation, reservationCB);
}
return cache()->makeSpace(increment, [&]() {
Allocation empty;
return cache()->makeSpace(increment, empty, [&](Allocation& evicted) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we change collateral passed to makeSpace to a pointer? Thanks!

void evict(
uint64_t bytesToFree,
bool evictAllUnpinned,
int32_t acquirePages = 0,
Copy link
Contributor

Choose a reason for hiding this comment

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

s/acquirePages/pagesToAcquire/

@facebook-github-bot
Copy link
Contributor

@oerling has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

@@ -706,7 +744,8 @@ CacheStats AsyncDataCache::refreshStats() const {

void AsyncDataCache::clear() {
for (auto& shard : shards_) {
shard->evict(std::numeric_limits<int32_t>::max(), true);
memory::Allocation acquired;
shard->evict(std::numeric_limits<int32_t>::max(), true, 0, acquired);
Copy link
Contributor

Choose a reason for hiding this comment

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

Put an assert acquired is empty?

@@ -548,8 +548,14 @@ class CacheShard {
// not pinned. This favors first removing older and less frequently
// used entries. If 'evictAllUnpinned' is true, anything that is
// not pinned is evicted at first sight. This is for out of memory
// emergencies.
void evict(uint64_t bytesToFree, bool evictAllUnpinned);
// emergencies. If 'acquireBytes' is set, up to this amount is added to
Copy link
Contributor

Choose a reason for hiding this comment

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

s/acquireBytes/pagesToAcquire/

virtual bool makeSpace(
memory::MachinePageCount numPages,
std::function<bool()> allocate) = 0;
std::function<bool(Allocation& evicted)> allocate) = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

std::function<bool(Allocation&)>

@facebook-github-bot
Copy link
Contributor

@oerling has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

oerling pushed a commit to oerling/velox-1 that referenced this pull request Oct 5, 2023
…r#6844)

Summary:
An allocation will evict cache to make space for data. If the the evicted data is freed and then an allocation is attempted, it can be that another thread has snatched the freed data. With high transient memory allocation rate and many threads, it can happen that a large allocation runs out of retries even if it could be satisfied.

Therefore, when evicting in order to make space for an allocation, we keep hold of the non-contiguous Allocations we remove from cache. Many allocations can in this way be concatenated into an allocation that covers the needed number of pages. This can then atomically be converted into a new non-contiguous or contiguous allocation. The allocate*WithoutRetry methods accept a non-contiguous allocation to be freed to provide pages for the new allocation. Memory mapping magic can convert non-contiguous freed pages to contiguous ones.

Note that the allocate*WithoutRetry methods mishandle the atomicity of the exchange: The allocated count is decremented by the size of the freed collateral and then incremented by the size of the new allocation. This should be a single atomic increment of allocatedSize
- collateralSize. This always succeeds if the collateral is >= the allocated amount. Even so, the window of vulnerability to another thread snatching the freed capacity before it is reacquired is probably negligible, only a few instructions.

The AsyncDataCache::makeSpace loop first tries to allocate. If it fails, it evicts and keeps up to the required amount in a a grab bag allocation. It loops until this allocation is large enough to convert into the desired allocation. If nothing is evicted and the allocation repeatedly fails, it gives up.


Reviewed By: xiaoxmeng

Differential Revision: D49829731

Pulled By: oerling
@facebook-github-bot
Copy link
Contributor

This pull request was exported from Phabricator. Differential Revision: D49829731

@oerling oerling force-pushed the evict-pr branch 4 times, most recently from c828bc8 to 9e437f0 Compare October 9, 2023 14:35
@facebook-github-bot
Copy link
Contributor

@oerling has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

@facebook-github-bot
Copy link
Contributor

@oerling has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

@oerling oerling force-pushed the evict-pr branch 2 times, most recently from 6ae77bb to f49b59d Compare October 11, 2023 21:57
@facebook-github-bot
Copy link
Contributor

@oerling has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

…r#6844)

Summary:
An allocation will evict cache to make space for data. If the the evicted data is freed and then an allocation is attempted, it can be that another thread has snatched the freed data. With high transient memory allocation rate and many threads, it can happen that a large allocation runs out of retries even if it could be satisfied.

Therefore, when evicting in order to make space for an allocation, we keep hold of the non-contiguous Allocations we remove from cache. Many allocations can in this way be concatenated into an allocation that covers the needed number of pages. This can then atomically be converted into a new non-contiguous or contiguous allocation. The allocate*WithoutRetry methods accept a non-contiguous allocation to be freed to provide pages for the new allocation. Memory mapping magic can convert non-contiguous freed pages to contiguous ones.

Note that the allocate*WithoutRetry methods mishandle the atomicity of the exchange: The allocated count is decremented by the size of the freed collateral and then incremented by the size of the new allocation. This should be a single atomic increment of allocatedSize
- collateralSize. This always succeeds if the collateral is >= the allocated amount. Even so, the window of vulnerability to another thread snatching the freed capacity before it is reacquired is probably negligible, only a few instructions.

The AsyncDataCache::makeSpace loop first tries to allocate. If it fails, it evicts and keeps up to the required amount in a a grab bag allocation. It loops until this allocation is large enough to convert into the desired allocation. If nothing is evicted and the allocation repeatedly fails, it gives up.


Reviewed By: xiaoxmeng

Differential Revision: D49829731

Pulled By: oerling
@facebook-github-bot
Copy link
Contributor

This pull request was exported from Phabricator. Differential Revision: D49829731

@facebook-github-bot
Copy link
Contributor

@oerling merged this pull request in 68e7698.

@conbench-facebook
Copy link

Conbench analyzed the 1 benchmark run on commit 68e76988.

There were no benchmark performance regressions. 🎉

The full Conbench report has more details.

ericyuliu pushed a commit to ericyuliu/velox that referenced this pull request Oct 12, 2023
…r#6844)

Summary:
An allocation will evict cache to make space for data. If the the evicted data is freed and then an allocation is attempted, it can be that another thread has snatched the freed data. With high transient memory allocation rate and many threads, it can happen that a large allocation runs out of retries even if it could be satisfied.

Therefore, when evicting in order to make space for an allocation, we keep hold of the non-contiguous Allocations we remove from cache. Many allocations can in this way be concatenated into an allocation that covers the needed number of pages. This can then atomically be converted into a new non-contiguous or contiguous allocation. The allocate*WithoutRetry methods accept a non-contiguous allocation to be freed to provide pages for the new allocation. Memory mapping magic can convert non-contiguous freed pages to contiguous ones.

Note that the allocate*WithoutRetry methods mishandle the atomicity of the exchange: The allocated count is decremented by the size of the freed collateral and then incremented by the size of the new allocation. This should be a single atomic increment of allocatedSize
- collateralSize. This always succeeds if the collateral is >= the allocated amount. Even so, the window of vulnerability to another thread snatching the freed capacity before it is reacquired is probably negligible, only a few instructions.

The AsyncDataCache::makeSpace loop first tries to allocate. If it fails, it evicts and keeps up to the required amount in a a grab bag allocation. It loops until this allocation is large enough to convert into the desired allocation. If nothing is evicted and the allocation repeatedly fails, it gives up.

Pull Request resolved: facebookincubator#6844

Reviewed By: xiaoxmeng

Differential Revision: D49829731

Pulled By: oerling

fbshipit-source-id: ef5dae3fd6a35ea5eb2c81f41f41f2c41fc1fea2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. fb-exported Merged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants