Repository navigation
fix: make capacity checks account for host-pinned parameter memory (#1976) #2166
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
Changes from all commits
a8c1a77
d4a2e9a
16068f9
ed5b5f8
713e5bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,11 @@ | |
| #include <tuple> | ||
| #include <unordered_set> | ||
|
|
||
| #ifdef _WIN32 | ||
| #define NOMINMAX | ||
| #include <windows.h> | ||
| #endif | ||
|
|
||
| #include "core/ggml_extend_backend.h" | ||
| #include "core/util.h" | ||
| #include "model/adapter/lora.hpp" | ||
|
|
@@ -17,6 +22,20 @@ static size_t aligned_offset(const void* buffer, size_t offset, size_t alignment | |
| return offset + align; | ||
| } | ||
|
|
||
| // True when allocations through this buffer type consume host-visible memory: | ||
| // plain host buffer types, or a compute device's host-pinned pool (e.g. the | ||
| // Vulkan "Vulkan_Host" buffer type backed by pinned system memory). | ||
| static bool model_manager_buffer_type_is_host_pinned(ggml_backend_buffer_type_t buft) { | ||
| if (buft == nullptr) { | ||
| return false; | ||
| } | ||
| if (ggml_backend_buft_is_host(buft)) { | ||
| return true; | ||
| } | ||
| ggml_backend_dev_t device = ggml_backend_buft_get_device(buft); | ||
| return device != nullptr && buft == ggml_backend_dev_host_buffer_type(device); | ||
| } | ||
|
|
||
| static bool lora_specs_equal(const std::vector<ModelManager::LoraSpec>& lhs, | ||
| const std::vector<ModelManager::LoraSpec>& rhs) { | ||
| if (lhs.size() != rhs.size()) { | ||
|
|
@@ -1780,6 +1799,11 @@ ModelManager::CapacityCheck ModelManager::check_capacity( | |
| const size_t weights_resident = compute_backend_resident_bytes(backend); | ||
| const size_t other_runtime = other_runtime_resident_bytes(request.owner_id, backend); | ||
| const size_t resident = add(weights_resident, add(other_runtime, request.runtime_resident_bytes)); | ||
| LOG_VERBOSE("model manager memory on %s: reported free %.2f MB / total %.2f MB, tracked weights %.2f MB / other runtime %.2f MB / current runtime %.2f MB", | ||
| ggml_backend_name(backend), | ||
| free_bytes / (1024.0 * 1024.0), total_bytes / (1024.0 * 1024.0), | ||
| weights_resident / (1024.0 * 1024.0), other_runtime / (1024.0 * 1024.0), | ||
| request.runtime_resident_bytes / (1024.0 * 1024.0)); | ||
| if (log_details) { | ||
| LOG_WARN("model manager memory on %s: reported free %.2f MB / total %.2f MB, tracked weights %.2f MB / other runtime %.2f MB / current runtime %.2f MB", | ||
| ggml_backend_name(backend), | ||
|
|
@@ -1838,6 +1862,82 @@ ModelManager::CapacityCheck ModelManager::check_capacity( | |
| result.available_budget_bytes = std::min(result.available_budget_bytes, available); | ||
| } | ||
| } | ||
| // Host dimension (#1976): memory allocated through host-visible buffer | ||
| // types is invisible to the compute device's heap report. With an | ||
| // offloaded model (params backend != compute backend), params buffers | ||
| // take the compute device's host-pinned buffer type (e.g. Vulkan_Host) | ||
| // and are loaded lazily, so they accumulate on the host while every | ||
| // device-side gate stays green. Account for the actual placement: only | ||
| // bytes this request would allocate through host-visible buffer types | ||
| // count against host availability; device-local staging/prefetch stay on | ||
| // the device/budget dimensions. A host failure feeds the existing | ||
| // eviction path, which reclaims params storage blocks and staged replicas. | ||
| size_t host_missing_bytes = 0; | ||
| bool host_pool_in_use = false; | ||
| // Existing host-pinned parameter storage keeps the floor active even when | ||
| // this request pins nothing new. | ||
| for (const auto& block : params_storage_blocks_) { | ||
| if (block != nullptr && block->buffer != nullptr && | ||
| model_manager_buffer_type_is_host_pinned(ggml_backend_buffer_get_type(block->buffer))) { | ||
| host_pool_in_use = true; | ||
| break; | ||
| } | ||
| } | ||
| // Placement probing is only valid for unloaded tensors (the operator | ||
| // probe rejects loaded ones); ignored and optional-missing states never | ||
| // load, so they must not be counted as missing host capacity. | ||
| for (TensorState* state : states) { | ||
| if (state == nullptr || should_ignore(*state) || is_optional_missing_tensor(state->name)) { | ||
| continue; | ||
| } | ||
| if (!state->loaded_to_params_backend && state->tensor != nullptr && | ||
| model_manager_buffer_type_is_host_pinned(params_buffer_type_for(*state))) { | ||
| host_missing_bytes += ggml_nbytes(state->tensor); | ||
| host_pool_in_use = true; | ||
| } | ||
| } | ||
| if (missing > 0 && model_manager_buffer_type_is_host_pinned( | ||
| ggml_backend_get_default_buffer_type(request.compute_backend))) { | ||
| host_missing_bytes += missing; | ||
| host_pool_in_use = true; | ||
| } | ||
| if (host_pool_in_use) { | ||
| // The +1 byte enforces the safety floor itself — once the host-pinned | ||
| // pool is in use, a request that pins nothing new must still fail | ||
| // when availability has fallen to zero, so reclamation runs before | ||
| // the driver's allocations start failing. | ||
| result.required_host_bytes = host_missing_bytes + 1; | ||
| #ifdef _WIN32 | ||
| MEMORYSTATUSEX status{}; | ||
| status.dwLength = sizeof(status); | ||
| if (GlobalMemoryStatusEx(&status)) { | ||
| // Two thresholds, from measurements on the #1976 machine (32 GB | ||
| // RAM, both resident models loaded): the driver fails allocations | ||
| // once available physical memory drops below ~0.5-0.8 GB (512 MiB | ||
| // + request bytes = hard refusal region), while up to ~0.5 GB is | ||
| // consumed between a capacity check and the following submit | ||
| // (1 GiB = proactive reclamation watermark so eviction keeps the | ||
| // pool above the failing region instead of only reacting at it). | ||
| constexpr uint64_t host_safety_margin = 512ULL * 1024ULL * 1024ULL; | ||
| constexpr uint64_t host_reclaim_watermark = 1ULL * 1024ULL * 1024ULL * 1024ULL; | ||
| result.available_host_bytes = status.ullAvailPhys > host_safety_margin | ||
| ? static_cast<size_t>(status.ullAvailPhys - host_safety_margin) | ||
| : 0; | ||
| if (status.ullAvailPhys < host_reclaim_watermark) { | ||
| result.requires_reclamation = true; | ||
| } | ||
|
Comment on lines
+1926
to
+1928
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Do not turn the 1 GiB reclamation watermark into a hard refusal.
Consider a Windows machine where other processes or resident pinned weights hold available memory between 512 MiB and 1 GiB. After eviction, nothing evictable is left. Keep the watermark as an eviction trigger only. Use a separate flag for it. Proposed direction struct CapacityCheck {
...
bool requires_reclamation = false;
+ bool host_reclaim_advised = false;- if (status.ullAvailPhys < host_reclaim_watermark) {
- result.requires_reclamation = true;
- }
+ if (status.ullAvailPhys < host_reclaim_watermark) {
+ result.host_reclaim_advised = true;
+ }In 🤖 Prompt for AI Agents |
||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| #endif | ||
| } | ||
| LOG_VERBOSE("capacity check verdict on %s: required device %.2f MB / available %.2f MB, required budget %.2f MB / available %.2f MB, required host %.2f MB / available %.2f MB, fits %s", | ||
| ggml_backend_name(request.compute_backend), | ||
| result.required_device_bytes / (1024.0 * 1024.0), | ||
| result.available_device_bytes / (1024.0 * 1024.0), | ||
| result.required_budget_bytes / (1024.0 * 1024.0), | ||
| result.available_budget_bytes / (1024.0 * 1024.0), | ||
| result.required_host_bytes / (1024.0 * 1024.0), | ||
| result.available_host_bytes / (1024.0 * 1024.0), | ||
| result.fits() ? "yes" : "no"); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| return result; | ||
| } | ||
|
|
||
|
|
@@ -1974,17 +2074,37 @@ bool ModelManager::ensure_compute_backend_capacity( | |
| } | ||
| } | ||
| }; | ||
| // Offloaded parameter storage can also sit in host-pinned memory (the | ||
| // compute device's host buffer type); under host pressure those blocks | ||
| // must be reclaimable too, or only device staging gets evicted while the | ||
| // host pool cannot recover. | ||
| std::unordered_set<TensorState*> host_pinned_states; | ||
| for (const auto& block : params_storage_blocks_) { | ||
| if (block != nullptr && block->buffer != nullptr && | ||
| model_manager_buffer_type_is_host_pinned(ggml_backend_buffer_get_type(block->buffer))) { | ||
| host_pinned_states.insert(block->states.begin(), block->states.end()); | ||
| } | ||
| } | ||
| auto can_demote_to_disk = [&](TensorState* state) { | ||
| return state != nullptr && state->tensor != nullptr && state->tensor->view_src == nullptr && | ||
| state->residency_mode == ResidencyMode::ParamBackend && | ||
| state->params_backend != nullptr && state->params_backend == state->compute_backend && | ||
| state->compute_backend == compute_backend && !sd_backend_is_cpu(state->params_backend) && | ||
| state->pin_count == 0 && !state->staged_to_compute_backend && state->loaded_to_params_backend && | ||
| protected_states.find(state) == protected_states.end() && | ||
| required_set.find(state) == required_set.end() && | ||
| mapped_states.find(state) == mapped_states.end() && | ||
| !state->sources.empty() && state->applied_lora_epoch == UINT64_MAX && | ||
| state->split_buffer_type == nullptr; | ||
| if (state == nullptr || state->tensor == nullptr || state->tensor->view_src != nullptr || | ||
| state->residency_mode != ResidencyMode::ParamBackend || | ||
| state->pin_count != 0 || state->staged_to_compute_backend || !state->loaded_to_params_backend || | ||
| protected_states.find(state) != protected_states.end() || | ||
| required_set.find(state) != required_set.end() || | ||
| state->sources.empty() || state->applied_lora_epoch != UINT64_MAX || | ||
| state->split_buffer_type != nullptr) { | ||
| return false; | ||
| } | ||
| const bool device_resident = state->params_backend != nullptr && | ||
| state->params_backend == state->compute_backend && | ||
| state->compute_backend == compute_backend && | ||
| !sd_backend_is_cpu(state->params_backend) && | ||
| mapped_states.find(state) == mapped_states.end(); | ||
| // Host-pinned parameter storage of an offloaded model reloads from | ||
| // the model loader like Disk-resident states; mmap-backed blocks are | ||
| // still freed (the pinned copy is a separate allocation). | ||
| const bool host_pinned = host_pinned_states.find(state) != host_pinned_states.end(); | ||
| return device_resident || host_pinned; | ||
|
Comment on lines
+2103
to
+2107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: rg -n 'assign_compute_backend|release_params_storage_blocks|ResidencyMode::Disk|params_follow_compute' src/model_manager.cpp src/model_manager.h
sed -n '1990,2040p' src/model_manager.cpp
sed -n '2085,2135p' src/model_manager.cppRepository: leejet/stable-diffusion.cpp Length of output: 7074 🏁 Script executed: set -o pipefail
printf '%s\n' '--- assign_compute_backend and nearby helpers ---'
nl -ba src/model_manager.cpp | sed -n '1240,1335p;1390,1485p'
printf '%s\n' '--- all assign_compute_backend call sites ---'
rg -n -F -- 'assign_compute_backend(' src --glob '*.{cpp,h}' || test "$?" -eq 1
printf '%s\n' '--- release/reload-related call sites and definitions ---'
rg -n -F -e 'release_params_storage_blocks(' -e 'load_params' -e 'reload' -e 'residency_mode = ResidencyMode::Disk' src/model_manager.cpp src/model_manager.h || test "$?" -eq 1Repository: leejet/stable-diffusion.cpp Length of output: 10993 🏁 Script executed: set -o pipefail
printf '%s\n' '--- runtime assignment call paths ---'
nl -ba src/core/ggml_runner.cpp | sed -n '330,475p'
printf '%s\n' '--- model-manager allocation and reload paths ---'
nl -ba src/model_manager.cpp | sed -n '1540,1730p;1800,1895p'
printf '%s\n' '--- parameter backend mutations and loader entrypoints ---'
rg -n -F -e 'params_backend =' -e 'load_params' -e 'load_tensor' -e 'loaded_to_params_backend' src/model_manager.cpp src/core/ggml_runner.cpp src/model_manager.h || test "$?" -eq 1Repository: leejet/stable-diffusion.cpp Length of output: 25128 🏁 Script executed: set -o pipefail
printf '%s\n' '--- parameter reload implementation ---'
nl -ba src/model_manager.cpp | sed -n '400,565p;960,1165p'
printf '%s\n' '--- runner lifecycle calls ---'
rg -n -F -e 'assign_graph_cut_layer_split_backends(' -e 'prepare_params(' -e 'load_tensors_to_params_backend(' src/core/ggml_runner.cpp src --glob '*.cpp' --glob '*.h' || test "$?" -eq 1Repository: leejet/stable-diffusion.cpp Length of output: 19725 🏁 Script executed: set -o pipefail
printf '%s\n' '--- runner assignment and preparation order ---'
nl -ba src/core/ggml_runner.cpp | sed -n '800,855p'
printf '%s\n' '--- demotion and revert implementation ---'
nl -ba src/model_manager.cpp | sed -n '2025,2125p'Repository: leejet/stable-diffusion.cpp Length of output: 8547 Evict host-pinned states and restore their residency after release.
Insert host-pinned states directly into Suggested fix state->residency_mode = ResidencyMode::Disk;
demoted_states.push_back(state);
- add_evictable_state(state);
+ if (host_pinned_states.count(state) != 0) {
+ eviction_states.insert(state);
+ } else {
+ add_evictable_state(state);
+ }
if (release_eviction_states()) {
+ for (TensorState* demoted : demoted_states) {
+ if (host_pinned_states.count(demoted) != 0 &&
+ !demoted->loaded_to_params_backend) {
+ demoted->residency_mode = ResidencyMode::ParamBackend;
+ }
+ }
revert_demotions();
return true;
}The second change is required because 🤖 Prompt for AI Agents
Comment on lines
+2106
to
+2107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win Demote host-pinned states only when the host dimension fails.
This adds disk reloads after device-pressure eviction, and those reloads do not help the request fit. Run the initial check once before eviction. Accept host-pinned states only when host memory is short. Proposed fix+ const auto initial_capacity = check_capacity(request, required_states);
+ const bool host_short = initial_capacity.required_host_bytes > initial_capacity.available_host_bytes ||
+ (initial_capacity.requires_reclamation &&
+ initial_capacity.available_host_bytes != SIZE_MAX);
...
- const bool host_pinned = host_pinned_states.find(state) != host_pinned_states.end();
+ const bool host_pinned = host_short &&
+ host_pinned_states.find(state) != host_pinned_states.end();
return device_resident || host_pinned;🤖 Prompt for AI Agents |
||
| }; | ||
| for (TensorState* state : global_candidates) { | ||
| if (!can_demote_to_disk(state)) { | ||
|
|
@@ -1993,14 +2113,23 @@ bool ModelManager::ensure_compute_backend_capacity( | |
| state->residency_mode = ResidencyMode::Disk; | ||
| demoted_states.push_back(state); | ||
| add_evictable_state(state); | ||
| if (host_pinned_states.find(state) != host_pinned_states.end()) { | ||
| // Host-pinned offloaded params are resident through their | ||
| // parameter block (loaded_to_params_backend), not through | ||
| // compute staging, so add_evictable_state's residency test | ||
| // cannot admit them — insert directly so the block becomes | ||
| // releasable. | ||
| eviction_states.insert(state); | ||
| } | ||
| if (release_eviction_states()) { | ||
| revert_demotions(); | ||
| return true; | ||
| } | ||
| } | ||
| revert_demotions(); | ||
|
|
||
| if (check_capacity(request, required_states).fits()) { | ||
| const auto final_capacity = check_capacity(request, required_states); | ||
| if (!final_capacity.requires_reclamation && final_capacity.fits()) { | ||
| return true; | ||
| } | ||
| const auto capacity = check_capacity(request, required_states, true); | ||
|
|
@@ -2010,11 +2139,19 @@ bool ModelManager::ensure_compute_backend_capacity( | |
| const std::string available_budget = capacity.available_budget_bytes == SIZE_MAX | ||
| ? "unlimited" | ||
| : sd_format("%.2f MB", capacity.available_budget_bytes / (1024.0 * 1024.0)); | ||
| LOG_WARN("model manager cannot make enough memory available on %s: need %.2f MB device / %.2f MB budget, available %s device / %s budget", | ||
| const std::string required_host = capacity.required_host_bytes == 0 | ||
| ? "none" | ||
| : sd_format("%.2f MB", capacity.required_host_bytes / (1024.0 * 1024.0)); | ||
| const std::string available_host = capacity.available_host_bytes == SIZE_MAX | ||
| ? "unknown" | ||
| : sd_format("%.2f MB", capacity.available_host_bytes / (1024.0 * 1024.0)); | ||
| LOG_WARN("model manager cannot make enough memory available on %s: need %.2f MB device / %.2f MB budget / %s host, available %s device / %s budget / %s host", | ||
| ggml_backend_name(compute_backend), | ||
| capacity.required_device_bytes / (1024.0 * 1024.0), | ||
| capacity.required_budget_bytes / (1024.0 * 1024.0), | ||
| required_host.c_str(), | ||
| available_device.c_str(), | ||
| available_budget.c_str()); | ||
| available_budget.c_str(), | ||
| available_host.c_str()); | ||
| return false; | ||
| } | ||
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.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: leejet/stable-diffusion.cpp
Length of output: 12311
🏁 Script executed:
Repository: leejet/stable-diffusion.cpp
Length of output: 18838
🏁 Script executed:
Repository: leejet/stable-diffusion.cpp
Length of output: 21238
🏁 Script executed:
Repository: leejet/stable-diffusion.cpp
Length of output: 15235
Do not recompute parameter placement for resident states.
params_buffer_type_forprobes operator support. For a loaded tensor,device_supports_param_oprejects the tensor becauseweight->buffer != nullptr. Both probes can fail, so the function logs an error and returnsnullptr. Repeated capacity checks can therefore log errors and fail to sethost_pool_in_usefor a loaded operator tensor whose parameter buffer is host-pinned.The loop also charges ignored and optional-missing tensors. The normal parameter-loading path skips those states, so they can remain unloaded and their bytes can be counted as missing host capacity.
Skip null, ignored, and optional-missing states. For unloaded states, use
params_buffer_type_forand add their tensor size. For loaded states, inspect the actual parameter buffer without callingparams_buffer_type_for. A staged state needs special handling:state->tensor->bufferpoints to the compute staging buffer, while the matchingComputeStagingBlock::staged_tensorsentry retains the original parameter buffer. Use that retained buffer to preserve host-pinned accounting.🤖 Prompt for AI Agents