Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a84057e
Ship every ggml CPU variant in the CPU bundles
danielhanchen Oct 11, 2026
baadd8d
Repack quantized matmul weights for the CPU backend
danielhanchen Oct 11, 2026
7b31854
Pick at_runtime LoRA on CPU when the weights end up quantized
danielhanchen Oct 11, 2026
861bcf5
Offer ggml's AVX512 CPU variants only on CPUs with AVX-VNNI
danielhanchen Oct 11, 2026
89efda1
Number the AVX512 gate patch 0008 so it applies after the 0007 GPU patch
danielhanchen Oct 11, 2026
d8008ba
CPU repack: judge an allocated weight by its buffer, not the process-…
danielhanchen Oct 11, 2026
fa66fe6
CPU repack: keep the LoRA-merge opt-out per context; runtime LoRA for…
danielhanchen Oct 11, 2026
cf1483f
LoRA auto mode: count TE and VAE routed to the CPU, not only the DiT
danielhanchen Oct 11, 2026
a61ca94
CPU repack: say when --mmap keeps quantized weights out of the repack…
danielhanchen Oct 11, 2026
2ec6fe2
LoRA auto mode: only tensor-type rules that quantize force runtime LoRA
danielhanchen Oct 11, 2026
ee03001
CPU repack: give repacked matmuls contiguous 2D activation rows
danielhanchen Oct 11, 2026
b180550
Model loading: never copy a duplicate destination from a CPU repack b…
danielhanchen Oct 11, 2026
98c4daa
ggml: do not search the current directory for dynamic backends
danielhanchen Oct 11, 2026
5897deb
cpu_variants_gate: clean up temp dirs on early failure; cover shared …
danielhanchen Oct 11, 2026
45e0134
Trim comments
danielhanchen Oct 11, 2026
8e47549
CPU: hand large matmuls to ggml's BLAS backend when it is built in
danielhanchen Oct 11, 2026
a38052c
macOS prebuilts: Accelerate BLAS on the arm64 leg, off on the Intel leg
danielhanchen Oct 12, 2026
8a40827
Trim comments for CPU BLAS scheduling
danielhanchen Oct 12, 2026
d6736ba
CPU BLAS: run eval callbacks on the scheduled CPU path
danielhanchen Oct 12, 2026
0fab806
CPU BLAS: release the BLAS conversion scratch at runner end
danielhanchen Oct 12, 2026
ef6f8c6
Trim comments
danielhanchen Oct 12, 2026
ad62867
Merge remote-tracking branch 'origin/master' into pr29-head
danielhanchen Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/unsloth-sd-prebuilt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -266,12 +266,14 @@ jobs:
- label: Darwin-macOS-arm64
runner: macos-14
arch: arm64
defines: "-DSD_METAL=ON -DGGML_METAL_EMBED_LIBRARY=ON -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0"
# Static: apple_m1 variant drops FP16_VA (~40% slower). BLAS explicit: sd.cpp routes big matmuls to it.
defines: "-DSD_METAL=ON -DGGML_METAL_EMBED_LIBRARY=ON -DGGML_BLAS=ON -DGGML_BLAS_VENDOR=Apple -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0"
gate: "true"
- label: Darwin-macOS-x86_64
runner: macos-15-intel
arch: x86_64
defines: "-DSD_METAL=OFF -DCMAKE_OSX_DEPLOYMENT_TARGET=13.3"
# BLAS off: its matmul path was only measured on Apple Silicon.
defines: "-DSD_METAL=OFF -DGGML_BLAS=OFF -DCMAKE_OSX_DEPLOYMENT_TARGET=13.3"
gate: "true"
cpu_floor: x64
- label: Linux-Ubuntu-22.04-x86_64
Expand Down
59 changes: 54 additions & 5 deletions src/core/compute_workspace.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "core/compute_workspace.h"

#include <algorithm>
#include <atomic>
#include <cstring>
#include <exception>
#include <map>
Expand All @@ -14,10 +15,24 @@
#include "ggml-impl.h"

namespace sd {
ComputeWorkspace::ComputeWorkspace(ggml_backend_t backend)
: backend_(backend) {
if (sd_backend_is_cpu(backend_)) {
blas_backend_ = sd_backend_blas_init();
}
}

ComputeWorkspace::~ComputeWorkspace() {
segment_end();
release();
ggml_backend_free(cpu_backend_);
ggml_backend_free(blas_backend_);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reclaim BLAS scratch at runner end

When BLAS processes an F16 or unrepacked quantized weight, ggml_backend_blas_mul_mat retains an F32 conversion buffer as large as the biggest weight seen. Because this backend is freed only by the workspace destructor, GGMLRunner::runner_end() releases the scheduler and model weights but leaves that scratch resident; running the conditioner, diffusion model, and VAE sequentially can therefore accumulate one large, untracked buffer per live runner and exhaust RAM on memory-constrained Macs. Reclaim or recreate the BLAS backend at runner-session boundaries rather than retaining it until model destruction.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 0fab806. ggml-blas only frees work_data when the backend is freed, so runner_end() now frees and recreates the idle BLAS backend right after releasing the workspace (thread count is set again on every segment). Within a run the buffer is kept: the diffusion runner does not end between sampling steps, so a txt2img run releases it three times (after the conditioner, after sampling, after the VAE). Peak RSS on SD1.5 at 256 px, 2 steps, CPU with OpenBLAS, 5 runs each: 2525 to 2564 MB before, 2475 to 2500 MB after. Rendered images are byte-identical with BLAS on and with SD_CPU_BLAS=0.

}

void ComputeWorkspace::set_accelerator_threads(int n_threads) const {
if (blas_backend_ != nullptr) {
sd_backend_set_n_threads_any(blas_backend_, n_threads);
}
}

void ComputeWorkspace::set_extra_backends(const std::vector<ggml_backend_t>& backends) {
Expand All @@ -33,7 +48,11 @@ namespace sd {
return true;
}
for (int i = 0; i < ggml_graph_n_nodes(graph); ++i) {
if (!ggml_backend_supports_op(backend_, ggml_graph_node(graph, i))) {
ggml_tensor* node = ggml_graph_node(graph, i);
if (!ggml_backend_supports_op(backend_, node)) {
return true;
}
if (uses_blas() && node->op == GGML_OP_MUL_MAT && ggml_backend_supports_op(blas_backend_, node)) {
return true;
Comment on lines +55 to 56

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve eval callbacks when enabling the BLAS scheduler

When BLAS is available and a CPU graph contains a supported MUL_MAT, this branch forces execution through the scheduler, whose path in GGMLRunner::execute_segment explicitly ignores sd_get_backend_eval_callback(). Consequently, the documented --imat-out/enable_imatrix_collection() flow on the new macOS arm64 prebuilt does not invoke collect_imatrix for these graphs and can silently save an empty or incomplete importance matrix; disable BLAS scheduling while an eval callback is installed or add callback support to scheduled execution.

AGENTS.md reference: AGENTS.md:L69-L76

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in d6736ba. When the workspace is CPU plus BLAS every tensor stays in host memory, so execute_segment now passes the eval callback to the scheduler through ggml_backend_sched_set_eval_callback; ggml's scheduler uses the same ask/collect protocol as the plain CPU path. Other scheduler setups keep the existing warning. Checked on SD1.5 at 256 px, 2 steps, with OpenBLAS: --imat-out with BLAS on used to save an empty imatrix (0 entries, the warning printed 3 times) and now saves 256 entries over 880 calls, the same tensor names and call counts as SD_CPU_BLAS=0. The rendered image is byte-identical to before.

}
}
Expand All @@ -43,6 +62,10 @@ namespace sd {
ggml_backend_sched_t ComputeWorkspace::make_scheduler(size_t graph_size) {
std::vector<ggml_backend_t> backends{backend_};
backends.insert(backends.end(), extra_backends_.begin(), extra_backends_.end());
if (uses_blas()) {
// Repacked weights are not host buffers, so their matmuls stay on the repack kernels.
backends.insert(backends.begin(), blas_backend_);
}
if (!sd_backend_is_cpu(backend_)) {
if (cpu_backend_ == nullptr) {
cpu_backend_ = sd_backend_cpu_init();
Expand Down Expand Up @@ -109,6 +132,17 @@ namespace sd {
release();
return false;
}
if (uses_blas()) {
static std::atomic<int> logged{0};
if (logged.fetch_add(1) < 4) {
int on_blas = 0;
for (int i = 0; i < ggml_graph_n_nodes(graph); ++i) {
on_blas += ggml_backend_sched_get_tensor_backend(scheduler_, ggml_graph_node(graph, i)) == blas_backend_;
}
LOG_INFO("cpu blas: %d of %d graph nodes run on %s", on_blas, ggml_graph_n_nodes(graph),
ggml_backend_name(blas_backend_));
}
}
} else {
if (scheduler_ != nullptr) {
release();
Expand Down Expand Up @@ -207,11 +241,13 @@ namespace sd {
auto scheduler = make_scheduler(graph_size);
if (scheduler != nullptr) {
assign_nodes(scheduler, copy_graph);
std::vector<size_t> sizes(extra_backends_.size() + 2);
std::vector<size_t> sizes(extra_backends_.size() + 3);
ggml_backend_sched_reserve_size(scheduler, copy_graph, sizes.data());
result.buffers.push_back({backend_, sizes[0]});
// BLAS shares host memory: count it as the CPU backend's for residency.
const size_t first = uses_blas() ? 1 : 0;
result.buffers.push_back({backend_, sizes[first] + (first ? sizes[0] : 0)});
for (size_t i = 0; i < extra_backends_.size(); ++i) {
result.buffers.push_back({extra_backends_[i], sizes[i + 1]});
result.buffers.push_back({extra_backends_[i], sizes[i + 1 + first]});
}
ggml_backend_sched_free(scheduler);
}
Expand Down Expand Up @@ -262,9 +298,22 @@ namespace sd {
return true;
}

void ComputeWorkspace::release_blas_scratch() {
if (blas_backend_ == nullptr || active_ || scheduler_ != nullptr) {
return;
}
// ggml-blas keeps its F32 dequantization buffer until the backend is freed.
ggml_backend_free(blas_backend_);
blas_backend_ = sd_backend_blas_init();
}

size_t ComputeWorkspace::bytes(ggml_backend_t backend) const {
if (scheduler_ != nullptr) {
return ggml_backend_sched_get_buffer_size(scheduler_, backend);
size_t total = ggml_backend_sched_get_buffer_size(scheduler_, backend);
if (backend == backend_ && uses_blas()) {
total += ggml_backend_sched_get_buffer_size(scheduler_, blas_backend_);
}
return total;
}
return allocator_ != nullptr && backend == backend_ ? ggml_gallocr_get_buffer_size(allocator_, 0) : 0;
}
Expand Down
7 changes: 5 additions & 2 deletions src/core/compute_workspace.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ namespace sd {
ggml_backend_t backend_;
std::vector<ggml_backend_t> extra_backends_;
ggml_backend_t cpu_backend_ = nullptr;
ggml_backend_t blas_backend_ = nullptr;
ggml_gallocr_t allocator_ = nullptr;
ggml_backend_sched_t scheduler_ = nullptr;
size_t scheduler_capacity_ = 0;
Expand All @@ -34,8 +35,7 @@ namespace sd {
};
using AssignNodes = std::function<void(ggml_backend_sched_t, ggml_cgraph*)>;

explicit ComputeWorkspace(ggml_backend_t backend)
: backend_(backend) {}
explicit ComputeWorkspace(ggml_backend_t backend);
~ComputeWorkspace();
ComputeWorkspace(const ComputeWorkspace&) = delete;
ComputeWorkspace& operator=(const ComputeWorkspace&) = delete;
Expand All @@ -53,9 +53,12 @@ namespace sd {
void synchronize() const;
bool segment_end() noexcept;
bool release();
void release_blas_scratch();
bool uses_blas() const { return blas_backend_ != nullptr && extra_backends_.empty(); }
bool active() const { return active_; }
ggml_backend_sched_t scheduler() const { return scheduler_; }
ggml_backend_t cpu_backend() const { return cpu_backend_; }
void set_accelerator_threads(int n_threads) const;
size_t bytes(ggml_backend_t backend) const;
size_t reservation_count() const { return reservations_; }
};
Expand Down
27 changes: 27 additions & 0 deletions src/core/ggml_extend_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,33 @@ ggml_backend_t sd_backend_cpu_init() {
return ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_CPU, nullptr);
}

ggml_backend_t sd_backend_blas_init() {
static const bool enabled = [] {
const char* v = std::getenv("SD_CPU_BLAS");
return v == nullptr || !(v[0] == '0' && v[1] == '\0');
}();
if (!enabled) {
return nullptr;
}
ggml_backend_load_all_once();
ggml_backend_reg_t reg = ggml_backend_reg_by_name("BLAS");
if (reg == nullptr || ggml_backend_reg_dev_count(reg) == 0) {
return nullptr;
}
return ggml_backend_dev_init(ggml_backend_reg_dev_get(reg, 0), nullptr);
}

bool sd_backend_set_n_threads_any(ggml_backend_t backend, int n_threads) {
auto dev = backend == nullptr ? nullptr : ggml_backend_get_device(backend);
auto reg = dev == nullptr ? nullptr : ggml_backend_dev_backend_reg(dev);
auto fn = reg == nullptr ? nullptr : (ggml_backend_set_n_threads_t)ggml_backend_reg_get_proc_address(reg, "ggml_backend_set_n_threads");
if (fn == nullptr) {
return false;
}
fn(backend, n_threads);
return true;
}

bool sd_backend_cpu_set_n_threads(ggml_backend_t backend, int n_threads) {
if (backend == nullptr) {
return false;
Expand Down
2 changes: 2 additions & 0 deletions src/core/ggml_extend_backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ ggml_backend_buffer_t sd_backend_dev_buffer_from_host_ptr(ggml_backend_dev_t dev
size_t size,
size_t max_tensor_size);
ggml_backend_t sd_backend_cpu_init();
ggml_backend_t sd_backend_blas_init();
bool sd_backend_set_n_threads_any(ggml_backend_t backend, int n_threads);
bool sd_backend_cpu_set_n_threads(ggml_backend_t backend_cpu, int n_threads);
ggml_status sd_backend_graph_compute_with_eval_callback(ggml_backend_t backend,
ggml_cgraph* gf,
Expand Down
7 changes: 6 additions & 1 deletion src/core/ggml_runner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ void GGMLRunner::runner_end() {
return;
}
workspace_.release();
workspace_.release_blas_scratch();
cache_.clear();
logged_compute_bytes_.clear();
logged_segment_count_ = 0;
Expand Down Expand Up @@ -766,13 +767,17 @@ bool GGMLRunner::execute_segment(ggml_cgraph* graph, int n_threads) {
if (workspace_.cpu_backend() != nullptr) {
sd_backend_cpu_set_n_threads(workspace_.cpu_backend(), n_threads);
}
workspace_.set_accelerator_threads(n_threads);
auto scheduler = workspace_.scheduler();
ggml_status status;
if (scheduler != nullptr) {
if (sd_get_backend_eval_callback() != nullptr && !multi_device_eval_callback_warned) {
// CPU + BLAS keeps every tensor in host memory, so callbacks see the same data as on the plain CPU path.
auto eval_callback = workspace_.uses_blas() ? sd_get_backend_eval_callback() : nullptr;
if (eval_callback == nullptr && sd_get_backend_eval_callback() != nullptr && !multi_device_eval_callback_warned) {
LOG_WARN("%s: eval callback is not supported with the backend scheduler; ignoring", get_desc().c_str());
multi_device_eval_callback_warned = true;
}
ggml_backend_sched_set_eval_callback(scheduler, eval_callback, sd_get_backend_eval_callback_data());
status = ggml_backend_sched_graph_compute(scheduler, graph);
} else {
status = sd_backend_graph_compute_with_eval_callback(runtime_backend, graph,
Expand Down