mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 07:49:59 +00:00
ifcviewer: make the geometry cache budget live, not fixed at startup
The budget was derived once at init as device free minus a reserve sized
for attachments at 4K plus margin. On a 1440p surface that idled ~550 MB
of VRAM the user's hardware could have spent on geometry, and it never
followed the device as other processes came and went.
Now, on the same once-a-second device poll that feeds the status bar,
budget = cache capacity + device free - margin
is recomputed and applied: the pool's growth ceiling moves with it, and
the pool yields whole sub-buffers when the device has less to give than
the pool holds. The attachments are eager, so at any poll they are
already inside "used" at the actual surface size; a resize that no
longer fits is answered by the existing pressure path rather than by a
permanent reserve.
The margin is 256 MB for later required allocations plus a learned part:
drivers refuse while still reporting memory free (the original crash
refused 59 MB with 221 MB "free"), so a pressure event records how much
reported-free memory proved unusable and update() stops short of it from
then on, instead of growing straight back into the same refusal.
Web is unchanged: fixed heap cap plus pressure. On the test machine the
idle-device budget goes from 1609 MB to 2212 MB; with another process
holding 1 GB mid-session the budget follows it down and back up without
evicting geometry the device could still hold.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+27
-19
@@ -21,27 +21,36 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
void GpuBudget::configure(std::uint64_t device_free_bytes,
|
||||
std::uint64_t reserve_bytes,
|
||||
std::uint64_t hard_cap_bytes) {
|
||||
bounded_ = false;
|
||||
budget_ = 0;
|
||||
if (device_free_bytes > 0) {
|
||||
bounded_ = true;
|
||||
budget_ = device_free_bytes > reserve_bytes
|
||||
? device_free_bytes - reserve_bytes
|
||||
: 0;
|
||||
}
|
||||
if (hard_cap_bytes > 0) {
|
||||
budget_ = bounded_ ? std::min(budget_, hard_cap_bytes) : hard_cap_bytes;
|
||||
bounded_ = true;
|
||||
}
|
||||
if (bounded_) budget_ = std::max(budget_, kMinCacheBudgetBytes);
|
||||
void GpuBudget::bound(std::uint64_t budget) {
|
||||
if (hard_cap_ > 0) budget = std::min(budget, hard_cap_);
|
||||
bounded_ = true;
|
||||
budget_ = std::max(budget, kMinCacheBudgetBytes);
|
||||
}
|
||||
|
||||
void GpuBudget::setHardCap(std::uint64_t hard_cap_bytes) {
|
||||
hard_cap_ = hard_cap_bytes;
|
||||
if (hard_cap_ > 0) bound(bounded_ ? budget_ : hard_cap_);
|
||||
}
|
||||
|
||||
void GpuBudget::update(std::uint64_t device_free_bytes,
|
||||
std::uint64_t cache_capacity_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);
|
||||
}
|
||||
|
||||
bool GpuBudget::onPressure(std::uint64_t cache_capacity_bytes,
|
||||
std::uint64_t bytes_needed) {
|
||||
std::uint64_t bytes_needed,
|
||||
std::uint64_t device_free_bytes) {
|
||||
++pressure_events_;
|
||||
// The driver refused bytes_needed while reporting device_free_bytes
|
||||
// free, so at least (free - needed) of what it reports is not really
|
||||
// available. Remember that so update() stops short of it next time.
|
||||
if (device_free_bytes > bytes_needed) {
|
||||
learned_margin_ = std::max(learned_margin_,
|
||||
device_free_bytes - bytes_needed + kPressureSlackBytes);
|
||||
}
|
||||
// What the cache may keep once the failed allocation and its slack
|
||||
// have been carved out of what it holds right now. The pool's actual
|
||||
// capacity, not the previous budget, is the honest baseline: the
|
||||
@@ -54,7 +63,6 @@ bool GpuBudget::onPressure(std::uint64_t cache_capacity_bytes,
|
||||
: 0;
|
||||
const std::uint64_t lowered = std::max(target, kMinCacheBudgetBytes);
|
||||
if (bounded_ && lowered >= budget_) return false;
|
||||
bounded_ = true;
|
||||
budget_ = lowered;
|
||||
bound(lowered);
|
||||
return true;
|
||||
}
|
||||
|
||||
+59
-32
@@ -33,15 +33,29 @@
|
||||
// never takes the last byte: it grows only up to this budget, and yields
|
||||
// whenever a required allocation fails.
|
||||
//
|
||||
// The budget's *source* differs per platform, the mechanism does not:
|
||||
// - desktop: derived from the driver's free-memory report minus a reserve
|
||||
// for the attachments (GpuMemory.h);
|
||||
// - web: no memory query exists, so a fixed ceiling (the wasm heap) and
|
||||
// pressure feedback alone;
|
||||
// - either: when a required allocation still fails, onPressure() lowers
|
||||
// the budget so the pool releases enough for that allocation to succeed
|
||||
// on retry. Over a session the budget converges on what the device
|
||||
// actually allows, which is the only information WebGPU ever gives.
|
||||
// The budget is *live*, the way D3D12's QueryVideoMemoryInfo and Vulkan's
|
||||
// memory_budget are meant to be used: on desktop the driver's free-memory
|
||||
// report (GpuMemory.h) is polled and
|
||||
//
|
||||
// budget = cache capacity + device free - margin
|
||||
//
|
||||
// is recomputed each time, so the cache tracks what the device can give as
|
||||
// other processes come and go. The attachments are eager, so at any poll
|
||||
// they are already inside "used" at the *actual* surface size; nothing is
|
||||
// idled for a hypothetical bigger window -- a resize that no longer fits is
|
||||
// answered by the pressure path instead.
|
||||
//
|
||||
// The margin has a fixed part for the required-tier allocations that come
|
||||
// later (the next model's metadata, staging) and a *learned* part: drivers
|
||||
// refuse allocations while still reporting memory free (measured here: a
|
||||
// refusal with 221 MB "free"), and a budget that trusts the report would
|
||||
// grow straight back into the same refusal after every shrink. A pressure
|
||||
// event therefore records how much reported-free memory turned out to be
|
||||
// unusable, and the margin keeps that from then on.
|
||||
//
|
||||
// Web has no memory query, so it keeps a fixed ceiling (the wasm heap) and
|
||||
// pressure feedback alone. The budget's source differs per platform, the
|
||||
// mechanism does not.
|
||||
//
|
||||
// Pure policy, no wgpu: the pool applies the number via
|
||||
// BufferPool::setMaxTotalCapacity / shrinkToCapacity.
|
||||
@@ -52,40 +66,53 @@ public:
|
||||
// allocation that still fails at the floor is a genuinely exhausted
|
||||
// device, and the caller degrades instead.
|
||||
static constexpr std::uint64_t kMinCacheBudgetBytes = 64ull * 1024 * 1024;
|
||||
// Headroom added on top of the failed allocation when lowering the
|
||||
// Held back for required allocations made after the cache has grown
|
||||
// (a later model's metadata buffers, readback staging, driver
|
||||
// bookkeeping).
|
||||
static constexpr std::uint64_t kFixedMarginBytes = 256ull * 1024 * 1024;
|
||||
// Headroom added on top of a failed allocation when lowering the
|
||||
// budget, so the very next small required allocation does not fail
|
||||
// again and trigger another shrink cycle.
|
||||
static constexpr std::uint64_t kPressureSlackBytes = 32ull * 1024 * 1024;
|
||||
static constexpr std::uint64_t kPressureSlackBytes = 32ull * 1024 * 1024;
|
||||
|
||||
// `device_free_bytes` 0 = unknown (web, or an unsupported driver).
|
||||
// `reserve_bytes` is what the required tier is expected to need at its
|
||||
// largest (attachments at the biggest plausible surface, plus margin).
|
||||
// `hard_cap_bytes` 0 = none; otherwise an absolute ceiling regardless
|
||||
// of device memory (the wasm heap on web).
|
||||
void configure(std::uint64_t device_free_bytes,
|
||||
std::uint64_t reserve_bytes,
|
||||
std::uint64_t hard_cap_bytes);
|
||||
// Absolute ceiling regardless of device memory (the wasm heap on web).
|
||||
// 0 = none.
|
||||
void setHardCap(std::uint64_t hard_cap_bytes);
|
||||
|
||||
// False when nothing bounds the cache yet (no device information, no
|
||||
// cap, no pressure so far). The pool then grows until the driver
|
||||
// refuses, exactly as before; the first pressure event bounds it.
|
||||
// Desktop: a fresh driver report. `device_free_bytes` 0 = the query
|
||||
// could not answer -- ignored, the budget keeps its last value.
|
||||
void update(std::uint64_t device_free_bytes,
|
||||
std::uint64_t cache_capacity_bytes);
|
||||
|
||||
// A required allocation of `bytes_needed` failed while the cache held
|
||||
// `cache_capacity_bytes` and the driver reported `device_free_bytes`
|
||||
// free (0 = unknown). Lowers the budget so that shrinking the cache to
|
||||
// it frees bytes_needed + slack, and learns the unusable headroom for
|
||||
// future update() calls. Returns false when the budget could not be
|
||||
// lowered any further (already at the floor): the device is exhausted
|
||||
// and the caller must degrade rather than retry.
|
||||
bool onPressure(std::uint64_t cache_capacity_bytes,
|
||||
std::uint64_t bytes_needed,
|
||||
std::uint64_t device_free_bytes);
|
||||
|
||||
// False until something bounds the cache (a device report, a cap, or
|
||||
// a pressure event). The pool then grows until the driver refuses,
|
||||
// exactly as before; the first of those bounds it.
|
||||
bool bounded() const { return bounded_; }
|
||||
// Meaningful only when bounded().
|
||||
std::uint64_t cache_budget_bytes() const { return budget_; }
|
||||
|
||||
// A required allocation of `bytes_needed` failed while the cache held
|
||||
// `cache_capacity_bytes`. Lowers the budget so that shrinking the cache
|
||||
// to it frees bytes_needed + slack. Returns false when the budget could
|
||||
// not be lowered any further (already at the floor): the device is
|
||||
// exhausted and the caller must degrade rather than retry.
|
||||
bool onPressure(std::uint64_t cache_capacity_bytes,
|
||||
std::uint64_t bytes_needed);
|
||||
|
||||
std::uint32_t pressure_events() const { return pressure_events_; }
|
||||
// Fixed + learned margin applied by update().
|
||||
std::uint64_t margin_bytes() const { return kFixedMarginBytes + learned_margin_; }
|
||||
std::uint32_t pressure_events() const { return pressure_events_; }
|
||||
|
||||
private:
|
||||
void bound(std::uint64_t budget);
|
||||
|
||||
bool bounded_ = false;
|
||||
std::uint64_t budget_ = 0;
|
||||
std::uint64_t hard_cap_ = 0;
|
||||
// Reported-free memory that a refusal proved unusable, plus slack.
|
||||
std::uint64_t learned_margin_ = 0;
|
||||
std::uint32_t pressure_events_ = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
|
||||
#include "CameraMath.h"
|
||||
#include "GpuAllocScope.h"
|
||||
#include "GpuMemory.h"
|
||||
#include "InstanceCompose.h"
|
||||
#include "Log.h"
|
||||
|
||||
@@ -1606,8 +1605,6 @@ bool ViewportCore::createPool() {
|
||||
// required tier (attachments, model metadata, staging) will need,
|
||||
// because those are allocated later and the frame cannot be drawn
|
||||
// without them; see GpuBudget.h for the model.
|
||||
std::uint64_t device_free = 0;
|
||||
std::uint64_t hard_cap = 0;
|
||||
#if defined(__EMSCRIPTEN__)
|
||||
// No memory query on web. Cap total pool capacity below the wasm heap
|
||||
// ceiling: a growth that would push the heap past MAXIMUM_MEMORY is a
|
||||
@@ -1616,18 +1613,17 @@ bool ViewportCore::createPool() {
|
||||
// transient decompression buffers, and wgpu overhead. Big federations
|
||||
// then keep a bounded, highest-priority resident set instead of
|
||||
// aborting. Device exhaustion below that is learnt through pressure.
|
||||
hard_cap = 3072ull * 1024 * 1024; // 3 GB (heap ceiling 4 GB)
|
||||
budget_.setHardCap(3072ull * 1024 * 1024); // 3 GB (heap ceiling 4 GB)
|
||||
#else
|
||||
{
|
||||
WGPUAdapterInfo adapter_info = WGPU_ADAPTER_INFO_INIT;
|
||||
wgpuAdapterGetInfo(adapter_, &adapter_info);
|
||||
const ifcviewer::GpuMemoryInfo mem =
|
||||
ifcviewer::queryGpuMemory(adapter_info.vendorID, adapter_info.deviceID);
|
||||
adapter_vendor_id_ = adapter_info.vendorID;
|
||||
adapter_device_id_ = adapter_info.deviceID;
|
||||
wgpuAdapterInfoFreeMembers(adapter_info);
|
||||
if (mem.valid) device_free = mem.free_bytes();
|
||||
}
|
||||
pollDeviceMemory();
|
||||
#endif
|
||||
budget_.configure(device_free, requiredTierReserveBytes(), hard_cap);
|
||||
pool_.setMaxTotalCapacity(budget_.bounded() ? budget_.cache_budget_bytes() : 0);
|
||||
|
||||
Log::info() << "wgpu: pool per-sub-buffer capacity = "
|
||||
@@ -1637,11 +1633,13 @@ bool ViewportCore::createPool() {
|
||||
if (budget_.bounded()) {
|
||||
Log::info() << "wgpu: geometry cache budget = "
|
||||
<< (budget_.cache_budget_bytes() / (1024 * 1024)) << " MB"
|
||||
<< (device_free > 0
|
||||
? " (device free " + std::to_string(device_free / (1024 * 1024))
|
||||
+ " MB - reserve "
|
||||
+ std::to_string(requiredTierReserveBytes() / (1024 * 1024))
|
||||
+ " MB)"
|
||||
<< (device_vram_total_bytes_ > 0
|
||||
? " (device free "
|
||||
+ std::to_string((device_vram_total_bytes_ - device_vram_used_bytes_)
|
||||
/ (1024 * 1024))
|
||||
+ " MB - margin "
|
||||
+ std::to_string(budget_.margin_bytes() / (1024 * 1024))
|
||||
+ " MB; tracks the driver's report)"
|
||||
: " (fixed cap)");
|
||||
} else {
|
||||
Log::info() << "wgpu: geometry cache budget unknown (no device memory "
|
||||
@@ -1650,6 +1648,44 @@ bool ViewportCore::createPool() {
|
||||
return true;
|
||||
}
|
||||
|
||||
ifcviewer::GpuMemoryInfo ViewportCore::queryDeviceMemory() const {
|
||||
#if defined(__EMSCRIPTEN__)
|
||||
return {};
|
||||
#else
|
||||
return ifcviewer::queryGpuMemory(adapter_vendor_id_, adapter_device_id_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ViewportCore::pollDeviceMemory() {
|
||||
if (device_vram_poll_timer_.isValid()
|
||||
&& device_vram_poll_timer_.elapsed() < 1000) return;
|
||||
device_vram_poll_timer_.start();
|
||||
const ifcviewer::GpuMemoryInfo mem = queryDeviceMemory();
|
||||
if (!mem.valid) return;
|
||||
device_vram_used_bytes_ = mem.used_bytes;
|
||||
device_vram_total_bytes_ = mem.total_bytes;
|
||||
budget_.update(mem.free_bytes(), pool_.total_capacity_bytes());
|
||||
applyBudgetToPool();
|
||||
}
|
||||
|
||||
void ViewportCore::applyBudgetToPool() {
|
||||
if (!budget_.bounded()) return;
|
||||
const std::uint64_t budget = budget_.cache_budget_bytes();
|
||||
pool_.setMaxTotalCapacity(budget);
|
||||
// Whole sub-buffers are the release granularity, so only act once the
|
||||
// excess is worth one; below that the fixed margin covers it.
|
||||
const std::uint64_t capacity = pool_.total_capacity_bytes();
|
||||
if (capacity < budget + BufferPool::MIN_SUB_BUFFER_BYTES) return;
|
||||
const std::uint64_t released = pool_.shrinkToCapacity(
|
||||
budget, [this](int sub_idx) { evictChunksInSubBuffer(sub_idx); });
|
||||
if (released > 0) {
|
||||
const double mb = 1.0 / (1024.0 * 1024.0);
|
||||
Log::info() << "[wgpu] device has less to give: geometry cache budget now "
|
||||
<< double(budget) * mb << " MB, released "
|
||||
<< double(released) * mb << " MB";
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t ViewportCore::attachmentBytesPerPixel() {
|
||||
// Sizes follow the formats in ensureDepthTexture / ensureMsaaColorTexture /
|
||||
// ensureSelectionOutlineTextures / createPickAttachments.
|
||||
@@ -1662,19 +1698,11 @@ std::uint64_t ViewportCore::attachmentBytesPerPixel() {
|
||||
+ 4 + 8 + 16 + 4; // pick: R32Uint id, RGBA16F normal, RGBA32F position, depth32
|
||||
}
|
||||
|
||||
std::uint64_t ViewportCore::requiredTierReserveBytes() {
|
||||
// Attachments at a 4K surface — a user can maximise onto a larger
|
||||
// monitor after load, and the cache must already have left room —
|
||||
// plus a fixed margin for per-model metadata buffers, readback
|
||||
// staging, and the driver's own bookkeeping.
|
||||
constexpr std::uint64_t kMaxPlausiblePixels = 3840ull * 2160;
|
||||
constexpr std::uint64_t kFixedMargin = 256ull * 1024 * 1024;
|
||||
return attachmentBytesPerPixel() * kMaxPlausiblePixels + kFixedMargin;
|
||||
}
|
||||
|
||||
bool ViewportCore::onRequiredAllocationFailed(const char* what, std::uint64_t bytes) {
|
||||
const std::uint64_t capacity_before = pool_.total_capacity_bytes();
|
||||
const bool lowered = budget_.onPressure(capacity_before, bytes);
|
||||
const ifcviewer::GpuMemoryInfo mem = queryDeviceMemory();
|
||||
const bool lowered = budget_.onPressure(capacity_before, bytes,
|
||||
mem.valid ? mem.free_bytes() : 0);
|
||||
const double mb = 1.0 / (1024.0 * 1024.0);
|
||||
if (!lowered) {
|
||||
Log::warn() << "[wgpu] out of memory allocating " << what << " ("
|
||||
@@ -7951,21 +7979,7 @@ void ViewportCore::render() {
|
||||
stats.vram_used_bytes = pool_.total_used_bytes();
|
||||
stats.vram_capacity_bytes = pool_.total_capacity_bytes();
|
||||
stats.vram_budget_bytes = budget_.bounded() ? budget_.cache_budget_bytes() : 0;
|
||||
#if !defined(__EMSCRIPTEN__)
|
||||
if (!device_vram_poll_timer_.isValid()
|
||||
|| device_vram_poll_timer_.elapsed() >= 1000) {
|
||||
device_vram_poll_timer_.start();
|
||||
WGPUAdapterInfo adapter_info = WGPU_ADAPTER_INFO_INIT;
|
||||
wgpuAdapterGetInfo(adapter_, &adapter_info);
|
||||
const ifcviewer::GpuMemoryInfo mem =
|
||||
ifcviewer::queryGpuMemory(adapter_info.vendorID, adapter_info.deviceID);
|
||||
wgpuAdapterInfoFreeMembers(adapter_info);
|
||||
if (mem.valid) {
|
||||
device_vram_used_bytes_ = mem.used_bytes;
|
||||
device_vram_total_bytes_ = mem.total_bytes;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
pollDeviceMemory();
|
||||
stats.device_vram_used_bytes = device_vram_used_bytes_;
|
||||
stats.device_vram_total_bytes = device_vram_total_bytes_;
|
||||
host_->onFrameStats(stats);
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
#include "AxisIndicatorRenderer.h"
|
||||
#include "BufferPool.h"
|
||||
#include "GpuBudget.h"
|
||||
#include "GpuMemory.h"
|
||||
#include "InstanceCompose.h"
|
||||
#include "InstancedGeometry.h"
|
||||
#include "ModelGpuData.h"
|
||||
@@ -1075,14 +1076,23 @@ private:
|
||||
// observed and answered by shrinking the geometry cache, instead of
|
||||
// surfacing as an invalid resource that aborts in wgpuQueueSubmit.
|
||||
|
||||
// Reserve to hold back from the cache for the required tier: the
|
||||
// per-pixel attachments at the largest plausible surface plus margin.
|
||||
static std::uint64_t requiredTierReserveBytes();
|
||||
// Bytes every per-pixel attachment set costs (MSAA colour + depth,
|
||||
// selection mask trio, pick MRT + depth) — used to size both the
|
||||
// reserve and the pressure carve-out when an attachment set fails.
|
||||
// selection mask trio, pick MRT + depth) — sizes the pressure
|
||||
// carve-out when an attachment set fails.
|
||||
static std::uint64_t attachmentBytesPerPixel();
|
||||
|
||||
// Desktop: the driver's view of the adapter wgpu picked (GpuMemory.h);
|
||||
// `valid` false on web or an unsupported driver.
|
||||
ifcviewer::GpuMemoryInfo queryDeviceMemory() const;
|
||||
// Desktop, at most once a second from render(): refresh the device
|
||||
// figures for FrameStats and re-derive the live cache budget from
|
||||
// them, shrinking the pool when the device has less to give than the
|
||||
// pool holds (another process took memory).
|
||||
void pollDeviceMemory();
|
||||
// Push budget_ to the pool: the growth ceiling, and a shrink when the
|
||||
// pool is over it by at least a sub-buffer.
|
||||
void applyBudgetToPool();
|
||||
|
||||
// A required allocation of `bytes` (`what` names it for the log)
|
||||
// failed. Lowers the budget, evicts and releases cache sub-buffers
|
||||
// down to it, and on desktop waits for the device to actually reclaim
|
||||
@@ -1120,6 +1130,10 @@ private:
|
||||
void releaseRenderAttachments();
|
||||
|
||||
GpuBudget budget_;
|
||||
// Adapter ids, read once at init, for matching the driver's memory
|
||||
// report to the card wgpu is actually using.
|
||||
std::uint32_t adapter_vendor_id_ = 0;
|
||||
std::uint32_t adapter_device_id_ = 0;
|
||||
// Latched false by ensureRenderAttachments when the device could not
|
||||
// fit the attachments; re-evaluated on the next configureSurface.
|
||||
bool render_attachments_ok_ = true;
|
||||
@@ -1557,9 +1571,10 @@ private:
|
||||
std::uint32_t last_visible_objects_ = 0;
|
||||
std::uint32_t last_visible_triangles_ = 0;
|
||||
std::uint32_t last_sub_draws_ = 0;
|
||||
// Device-wide VRAM readout for FrameStats. The driver query is too
|
||||
// slow for per-frame use, so it is re-polled at most once a second
|
||||
// and the last answer is repeated in between.
|
||||
// Device-wide VRAM readout for FrameStats and the live cache budget
|
||||
// (pollDeviceMemory). The driver query is too slow for per-frame use,
|
||||
// so it is re-polled at most once a second and the last answer is
|
||||
// repeated in between.
|
||||
std::uint64_t device_vram_used_bytes_ = 0;
|
||||
std::uint64_t device_vram_total_bytes_ = 0;
|
||||
Stopwatch device_vram_poll_timer_;
|
||||
|
||||
@@ -18,10 +18,12 @@
|
||||
********************************************************************************/
|
||||
|
||||
// GpuBudget decides how much device memory the streamed-geometry cache may
|
||||
// hold. It is pure policy: a number derived from what the platform can
|
||||
// tell us (desktop: driver free-memory report; web: nothing but a heap
|
||||
// ceiling) and lowered by pressure events when a required allocation fails
|
||||
// anyway. These pin down the arithmetic and the floor behaviour.
|
||||
// hold. It is pure policy: a live number derived from what the platform
|
||||
// can tell us (desktop: the driver's free-memory report; web: nothing but
|
||||
// a heap ceiling), lowered by pressure events when a required allocation
|
||||
// fails anyway, and learning from those how much reported-free memory the
|
||||
// driver will not actually grant. These pin down the arithmetic, the floor
|
||||
// and the learning.
|
||||
|
||||
#include "GpuBudget.h"
|
||||
|
||||
@@ -31,48 +33,62 @@ namespace {
|
||||
constexpr std::uint64_t MB = 1024ull * 1024;
|
||||
}
|
||||
|
||||
TEST_CASE("unknown device memory and no cap leaves the cache unbounded", "[gpu_budget]") {
|
||||
TEST_CASE("nothing known leaves the cache unbounded", "[gpu_budget]") {
|
||||
GpuBudget b;
|
||||
b.configure(0, 512 * MB, 0);
|
||||
REQUIRE_FALSE(b.bounded());
|
||||
b.update(0, 512 * MB); // query could not answer: still unbounded
|
||||
REQUIRE_FALSE(b.bounded());
|
||||
}
|
||||
|
||||
TEST_CASE("desktop budget is free memory minus the required-tier reserve", "[gpu_budget]") {
|
||||
TEST_CASE("a device report bounds the cache at held + free - margin", "[gpu_budget]") {
|
||||
GpuBudget b;
|
||||
b.configure(2800 * MB, 800 * MB, 0);
|
||||
b.update(2800 * MB, 0);
|
||||
REQUIRE(b.bounded());
|
||||
REQUIRE(b.cache_budget_bytes() == 2000 * MB);
|
||||
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.
|
||||
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 reserve larger than free memory floors the budget, not zero", "[gpu_budget]") {
|
||||
TEST_CASE("less than the margin available floors the budget, not zero", "[gpu_budget]") {
|
||||
GpuBudget b;
|
||||
b.configure(300 * MB, 800 * MB, 0);
|
||||
b.update(100 * MB, 0);
|
||||
REQUIRE(b.bounded());
|
||||
REQUIRE(b.cache_budget_bytes() == GpuBudget::kMinCacheBudgetBytes);
|
||||
}
|
||||
|
||||
TEST_CASE("a hard cap bounds the cache on its own (web) and clamps a device-derived budget", "[gpu_budget]") {
|
||||
GpuBudget web;
|
||||
web.configure(0, 0, 3072 * MB);
|
||||
web.setHardCap(3072 * MB);
|
||||
REQUIRE(web.bounded());
|
||||
REQUIRE(web.cache_budget_bytes() == 3072 * MB);
|
||||
|
||||
GpuBudget both;
|
||||
both.configure(8000 * MB, 800 * MB, 3072 * MB);
|
||||
both.setHardCap(3072 * MB);
|
||||
both.update(8000 * MB, 0);
|
||||
REQUIRE(both.cache_budget_bytes() == 3072 * MB);
|
||||
|
||||
GpuBudget small_device;
|
||||
small_device.configure(2800 * MB, 800 * MB, 3072 * MB);
|
||||
REQUIRE(small_device.cache_budget_bytes() == 2000 * MB);
|
||||
small_device.setHardCap(3072 * MB);
|
||||
small_device.update(2800 * MB, 0);
|
||||
REQUIRE(small_device.cache_budget_bytes() == 2800 * MB - GpuBudget::kFixedMarginBytes);
|
||||
}
|
||||
|
||||
TEST_CASE("pressure lowers the budget below what the cache currently holds", "[gpu_budget]") {
|
||||
GpuBudget b;
|
||||
b.configure(0, 0, 0);
|
||||
REQUIRE_FALSE(b.bounded());
|
||||
|
||||
// The pool grew to 2048 MB unbounded; a 120 MB attachment set then failed.
|
||||
REQUIRE(b.onPressure(2048 * MB, 120 * MB));
|
||||
REQUIRE(b.onPressure(2048 * MB, 120 * MB, 0));
|
||||
REQUIRE(b.bounded());
|
||||
REQUIRE(b.cache_budget_bytes()
|
||||
== 2048 * MB - 120 * MB - GpuBudget::kPressureSlackBytes);
|
||||
@@ -80,33 +96,57 @@ TEST_CASE("pressure lowers the budget below what the cache currently holds", "[g
|
||||
}
|
||||
|
||||
TEST_CASE("pressure is measured against actual capacity, not the previous budget", "[gpu_budget]") {
|
||||
// Budget said 2000 MB but the driver only ever granted 1024 MB; a
|
||||
// Budget said ~2500 MB but the driver only ever granted 1024 MB; a
|
||||
// failure must carve out of the 1024, else nothing would be released.
|
||||
GpuBudget b;
|
||||
b.configure(2800 * MB, 800 * MB, 0);
|
||||
REQUIRE(b.onPressure(1024 * MB, 100 * MB));
|
||||
b.update(2800 * MB, 0);
|
||||
REQUIRE(b.onPressure(1024 * MB, 100 * MB, 0));
|
||||
REQUIRE(b.cache_budget_bytes()
|
||||
== 1024 * MB - 100 * MB - GpuBudget::kPressureSlackBytes);
|
||||
}
|
||||
|
||||
TEST_CASE("pressure never raises the budget", "[gpu_budget]") {
|
||||
GpuBudget b;
|
||||
b.configure(0, 0, 500 * MB);
|
||||
// Pool is at 256 MB (below budget) and a tiny allocation fails:
|
||||
// capacity - carve is 224 MB-ish, which IS lower, so it lowers.
|
||||
REQUIRE(b.onPressure(256 * MB, 0));
|
||||
b.setHardCap(500 * MB);
|
||||
REQUIRE(b.onPressure(256 * MB, 0, 0));
|
||||
REQUIRE(b.cache_budget_bytes() == 256 * MB - GpuBudget::kPressureSlackBytes);
|
||||
// A later event whose arithmetic lands above the current budget is a no-op.
|
||||
REQUIRE_FALSE(b.onPressure(4096 * MB, 0));
|
||||
REQUIRE_FALSE(b.onPressure(4096 * MB, 0, 0));
|
||||
REQUIRE(b.cache_budget_bytes() == 256 * MB - GpuBudget::kPressureSlackBytes);
|
||||
}
|
||||
|
||||
TEST_CASE("pressure bottoms out at the floor and then reports exhaustion", "[gpu_budget]") {
|
||||
GpuBudget b;
|
||||
b.configure(0, 0, 0);
|
||||
REQUIRE(b.onPressure(100 * MB, 90 * MB));
|
||||
REQUIRE(b.onPressure(100 * MB, 90 * MB, 0));
|
||||
REQUIRE(b.cache_budget_bytes() == GpuBudget::kMinCacheBudgetBytes);
|
||||
// Already at the floor: nothing more to give.
|
||||
REQUIRE_FALSE(b.onPressure(64 * MB, 90 * MB));
|
||||
REQUIRE_FALSE(b.onPressure(64 * MB, 90 * MB, 0));
|
||||
REQUIRE(b.pressure_events() == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("a refusal with memory still reported free teaches the margin", "[gpu_budget]") {
|
||||
GpuBudget b;
|
||||
b.update(2800 * MB, 0);
|
||||
REQUIRE(b.margin_bytes() == GpuBudget::kFixedMarginBytes);
|
||||
|
||||
// 59 MB refused with 221 MB "free" (the measured crash): at least
|
||||
// 162 MB of what the driver reports is not usable.
|
||||
REQUIRE(b.onPressure(2048 * MB, 59 * MB, 221 * MB));
|
||||
REQUIRE(b.margin_bytes()
|
||||
== GpuBudget::kFixedMarginBytes + 162 * MB + GpuBudget::kPressureSlackBytes);
|
||||
|
||||
// The next live report stops short by the learned amount, so the pool
|
||||
// does not grow straight back into the same refusal.
|
||||
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
|
||||
// memory does not shrink it.
|
||||
b.onPressure(1500 * MB, 59 * MB, 100 * MB);
|
||||
REQUIRE(b.margin_bytes()
|
||||
== GpuBudget::kFixedMarginBytes + 162 * MB + GpuBudget::kPressureSlackBytes);
|
||||
// A refusal that needed more than was reported free teaches nothing.
|
||||
b.onPressure(1500 * MB, 500 * MB, 100 * MB);
|
||||
REQUIRE(b.margin_bytes()
|
||||
== GpuBudget::kFixedMarginBytes + 162 * MB + GpuBudget::kPressureSlackBytes);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user