mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 07:49:59 +00:00
ifcviewer: move the live budget only on sustained device readings
A 66-model session oscillated with a ~4 s period — 298 releases in one log: the pool grew to its ceiling, the next report read ~83 MB free, the budget dropped and the pool shrank, the reading rebounded, the budget rose and the pool re-grew, reloading the same chunks each time. Objects flickered on and off continuously. The report includes transients the viewer itself creates: the upload staging behind a burst of chunk loads (~170 MB in that session) and a released sub-buffer the driver has not yet reclaimed. A budget that followed every reading fed those straight back into growth decisions. GpuBudget::update now bounds the cache outright on the first device report and afterwards moves only on sustained readings: lower when free memory is below half the margin on two consecutive scheduled reports, raise when it is above 1.5× the margin on two, and nothing in between. Transients drain well within a poll interval, so a momentary low never reaches the pool, while a process that really took memory still does a second later. A refused allocation (onPressure) is never deferred. Verified in the saturated regime (working set ~990 MB against a 683 MB budget, continuous streaming): zero releases over 75 s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,24 @@ void GpuBudget::update(std::uint64_t device_free_bytes,
|
||||
if (device_free_bytes == 0) return;
|
||||
const std::uint64_t available = cache_capacity_bytes + device_free_bytes;
|
||||
const std::uint64_t margin = margin_bytes();
|
||||
bound(available > margin ? available - margin : 0);
|
||||
const std::uint64_t reading = available > margin ? available - margin : 0;
|
||||
if (!had_device_report_) {
|
||||
had_device_report_ = true;
|
||||
bound(reading);
|
||||
return;
|
||||
}
|
||||
|
||||
const bool tight = device_free_bytes < margin / 2;
|
||||
const bool roomy = device_free_bytes > margin + margin / 2 && reading > budget_;
|
||||
low_reports_ = tight ? low_reports_ + 1 : 0;
|
||||
high_reports_ = roomy ? high_reports_ + 1 : 0;
|
||||
if (low_reports_ >= kConfirmReports) {
|
||||
bound(std::min(budget_, reading));
|
||||
low_reports_ = 0;
|
||||
} else if (high_reports_ >= kConfirmReports) {
|
||||
bound(reading);
|
||||
high_reports_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool GpuBudget::onPressure(std::uint64_t cache_capacity_bytes,
|
||||
@@ -64,5 +81,6 @@ bool GpuBudget::onPressure(std::uint64_t cache_capacity_bytes,
|
||||
const std::uint64_t lowered = std::max(target, kMinCacheBudgetBytes);
|
||||
if (bounded_ && lowered >= budget_) return false;
|
||||
bound(lowered);
|
||||
low_reports_ = high_reports_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -88,8 +88,21 @@ public:
|
||||
|
||||
// Desktop: a fresh driver report. `device_free_bytes` 0 = the query
|
||||
// could not answer -- ignored, the budget keeps its last value.
|
||||
//
|
||||
// The first report bounds the cache outright. After that the budget
|
||||
// moves only on *sustained* readings, because the report includes
|
||||
// transients the viewer itself creates -- the upload staging behind a
|
||||
// burst of chunk loads, a released sub-buffer the driver has not yet
|
||||
// reclaimed -- and a budget that followed every reading oscillated:
|
||||
// grow, read a momentary low, shrink, read the rebound, grow again,
|
||||
// reloading the same chunks every few seconds. So: lower when free
|
||||
// memory is below half the margin on kConfirmReports consecutive
|
||||
// reports; raise when it is above 1.5× the margin on as many; between
|
||||
// those nothing changes. A refused allocation (onPressure) is never
|
||||
// deferred.
|
||||
void update(std::uint64_t device_free_bytes,
|
||||
std::uint64_t cache_capacity_bytes);
|
||||
static constexpr int kConfirmReports = 2;
|
||||
|
||||
// A required allocation of `bytes_needed` failed while the cache held
|
||||
// `cache_capacity_bytes` and the driver reported `device_free_bytes`
|
||||
@@ -124,6 +137,9 @@ private:
|
||||
|
||||
bool bounded_ = false;
|
||||
std::uint64_t budget_ = 0;
|
||||
bool had_device_report_ = false;
|
||||
int low_reports_ = 0; // consecutive reports below the lower band
|
||||
int high_reports_ = 0; // consecutive reports above the upper band
|
||||
std::uint64_t hard_cap_ = 0;
|
||||
// Reported-free memory that a refusal proved unusable, plus slack.
|
||||
std::uint64_t learned_margin_ = 0;
|
||||
|
||||
@@ -40,23 +40,68 @@ TEST_CASE("nothing known leaves the cache unbounded", "[gpu_budget]") {
|
||||
REQUIRE_FALSE(b.bounded());
|
||||
}
|
||||
|
||||
TEST_CASE("a device report bounds the cache at held + free - margin", "[gpu_budget]") {
|
||||
TEST_CASE("the first device report bounds the cache at held + free - margin", "[gpu_budget]") {
|
||||
GpuBudget b;
|
||||
b.update(2800 * MB, 0);
|
||||
REQUIRE(b.bounded());
|
||||
REQUIRE(b.cache_budget_bytes() == 2800 * MB - GpuBudget::kFixedMarginBytes);
|
||||
|
||||
// The pool now holds 1000 MB and the driver reports 1800 MB free: the
|
||||
// cache's own bytes count as available to it.
|
||||
// cache's own bytes count as available to it, so nothing moves.
|
||||
b.update(1800 * MB, 1000 * MB);
|
||||
b.update(1800 * MB, 1000 * MB);
|
||||
REQUIRE(b.cache_budget_bytes() == 2800 * MB - GpuBudget::kFixedMarginBytes);
|
||||
}
|
||||
|
||||
// Another process took 1000 MB: the budget follows the device down.
|
||||
b.update(800 * MB, 1000 * MB);
|
||||
REQUIRE(b.cache_budget_bytes() == 1800 * MB - GpuBudget::kFixedMarginBytes);
|
||||
// ...and back up when it is released.
|
||||
b.update(1800 * MB, 1000 * MB);
|
||||
REQUIRE(b.cache_budget_bytes() == 2800 * MB - GpuBudget::kFixedMarginBytes);
|
||||
TEST_CASE("a momentary reading never moves the budget; a sustained one does", "[gpu_budget]") {
|
||||
const std::uint64_t margin = GpuBudget::kFixedMarginBytes;
|
||||
GpuBudget b;
|
||||
b.update(2800 * MB, 0);
|
||||
const std::uint64_t initial = b.cache_budget_bytes();
|
||||
|
||||
// Pool at budget; one poll reads 80 MB free (upload staging in flight).
|
||||
b.update(80 * MB, initial);
|
||||
REQUIRE(b.cache_budget_bytes() == initial);
|
||||
// The staging drained: back in the dead band, streak reset.
|
||||
b.update(margin, initial);
|
||||
b.update(80 * MB, initial);
|
||||
REQUIRE(b.cache_budget_bytes() == initial);
|
||||
|
||||
// Tight on two consecutive reports: another process really took it.
|
||||
b.update(80 * MB, initial);
|
||||
REQUIRE(b.cache_budget_bytes() == initial + 80 * MB - margin);
|
||||
const std::uint64_t lowered = b.cache_budget_bytes();
|
||||
|
||||
// One roomy report is not enough to raise it...
|
||||
b.update(1500 * MB, lowered);
|
||||
REQUIRE(b.cache_budget_bytes() == lowered);
|
||||
// ...two are.
|
||||
b.update(1500 * MB, lowered);
|
||||
REQUIRE(b.cache_budget_bytes() == lowered + 1500 * MB - margin);
|
||||
}
|
||||
|
||||
TEST_CASE("free memory inside the dead band changes nothing however long it lasts", "[gpu_budget]") {
|
||||
const std::uint64_t margin = GpuBudget::kFixedMarginBytes;
|
||||
GpuBudget b;
|
||||
b.update(2800 * MB, 0);
|
||||
const std::uint64_t initial = b.cache_budget_bytes();
|
||||
for (int i = 0; i < 10; ++i) b.update(margin, initial); // exactly the margin
|
||||
for (int i = 0; i < 10; ++i) b.update(margin + margin / 2, initial); // top of the band
|
||||
for (int i = 0; i < 10; ++i) b.update(margin / 2, initial); // bottom of the band
|
||||
REQUIRE(b.cache_budget_bytes() == initial);
|
||||
}
|
||||
|
||||
TEST_CASE("a refusal lowers the budget immediately and resets the streaks", "[gpu_budget]") {
|
||||
GpuBudget b;
|
||||
b.update(2800 * MB, 0);
|
||||
const std::uint64_t initial = b.cache_budget_bytes();
|
||||
b.update(80 * MB, initial); // one tight report
|
||||
REQUIRE(b.onPressure(initial, 100 * MB, 80 * MB));
|
||||
REQUIRE(b.cache_budget_bytes() < initial);
|
||||
const std::uint64_t after = b.cache_budget_bytes();
|
||||
// The streak did not carry over: one more tight report is not two.
|
||||
b.update(80 * MB, after);
|
||||
REQUIRE(b.cache_budget_bytes() == after);
|
||||
}
|
||||
|
||||
TEST_CASE("less than the margin available floors the budget, not zero", "[gpu_budget]") {
|
||||
@@ -135,9 +180,10 @@ TEST_CASE("a refusal with memory still reported free teaches the margin", "[gpu_
|
||||
REQUIRE(b.margin_bytes()
|
||||
== GpuBudget::kFixedMarginBytes + 162 * MB + GpuBudget::kPressureSlackBytes);
|
||||
|
||||
// The next live report stops short by the learned amount, so the pool
|
||||
// The next live reports stop short by the learned amount, so the pool
|
||||
// does not grow straight back into the same refusal.
|
||||
b.update(221 * MB, 2048 * MB);
|
||||
b.update(221 * MB, 2048 * MB);
|
||||
REQUIRE(b.cache_budget_bytes() == 2048 * MB + 221 * MB - b.margin_bytes());
|
||||
|
||||
// Learning only ever grows; a later refusal with less phantom free
|
||||
|
||||
Reference in New Issue
Block a user