Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 25 additions & 0 deletions src/adapter-merge.h
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,18 @@ static bool adapter_merge_on_backend(WeightCtx *
pc->src = merged_buf;
pc->nbytes = merged_bytes;
}
// The PendingCopy keeps the file range it was pushed with (file_src/file_nbytes/
// file_base/file_len, untouched by the swap above), so wctx_alloc releases the
// base weight's staged pages exactly as it does for an unmerged tensor -- an
// adapter load gets the same residency reduction as a plain one, and the release
// shows up in its "unmapped N MB" line. Nothing is released here, mid-merge: the
// pages are only dead once every pending entry has been copied.
//
// A second merge targeting the same base tensor is last-wins and safe as-is: it
// re-reads the pristine base from the mapping (the merge never writes back to
// base_ptr), recomputes, and replaces pc->src. Within one pass that cannot happen
// anyway -- both merge paths visit each base tensor once -- and merges always run
// before wctx_alloc, so the released pages are never read here.
wctx->staging.push_back(std::move(staging_buf));

ggml_backend_buffer_free(buf);
Expand Down Expand Up @@ -962,6 +974,19 @@ static bool adapter_merge(WeightCtx * wctx,
const char * adapter_path,
float scale,
ggml_backend_t backend) {
// Merges read the base weights straight out of gf.mapping, which wctx_alloc
// punches holes in as it copies -- so the ordering (merge first, alloc second)
// is a correctness rule, not a preference. Enforce it -- but gracefully: the one
// caller already turns a false return into a clean failed load, which beats
// exiting the process (the default build) or unwinding mid-request (the app).
// Once the buffer exists some source pages may be released and the upload could
// fault, so refuse to run rather than guess.
if (wctx->buffer != nullptr) {
fprintf(stderr,
"[Adapter] FATAL: adapter merge after wctx_alloc; the base weight pages may already be "
"released\n");
return false;
}
std::string sf_path;
std::string cfg_dir;

Expand Down
5 changes: 4 additions & 1 deletion src/dit.h
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,10 @@ static bool dit_ggml_load(DiTGGML * m,
if (adapter_path) {
Timer adapter_timer;
if (!adapter_merge(&m->wctx, gf, adapter_path, adapter_scale, m->backend)) {
fprintf(stderr, "[Adapter] FATAL: no tensors merged (model mismatch)\n");
// adapter_merge already printed the specific reason (bad path, layout,
// no matching tensors, or a merge-after-alloc ordering bug) -- a generic
// "model mismatch" here would send an operator re-exporting a good LoRA.
fprintf(stderr, "[Adapter] FATAL: merge failed; see the [Adapter] messages above\n");
gf_close(&gf);
return false;
}
Expand Down
62 changes: 48 additions & 14 deletions src/gguf-weights.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
#include "gguf.h"
#include "weight-ctx.h"

#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>

#ifdef _WIN32
Expand All @@ -30,11 +32,11 @@
#endif

struct GGUFModel {
struct gguf_context * gguf; // parsed header (KV + tensor metadata)
struct ggml_context * meta; // tensor descriptors (no data)
uint8_t * mapping; // mmapped file
size_t file_size;
size_t data_offset; // gguf_get_data_offset(gguf)
struct gguf_context * gguf = nullptr; // parsed header (KV + tensor metadata)
struct ggml_context * meta = nullptr; // tensor descriptors (no data)
uint8_t * mapping = nullptr; // mmapped file
size_t file_size = 0;
size_t data_offset = 0; // gguf_get_data_offset(gguf)
#ifdef _WIN32
HANDLE fh;
HANDLE mh;
Expand Down Expand Up @@ -62,7 +64,16 @@ static void gf_close(GGUFModel * gf) {
}
#else
if (gf->mapping) {
munmap(gf->mapping, gf->file_size);
// After a 2.7 load this range carries interior holes where wctx_alloc already
// released staged pages. Unmapping over holes works on macOS and Linux
// (verified), but hole tolerance is implementation behaviour rather than a
// POSIX guarantee -- so a failure here means the whole mapping stays resident
// until process exit. Plain fprintf, not warn-once: this runs once per model,
// and every failing close is a distinct mapping worth its own line.
if (munmap(gf->mapping, gf->file_size) != 0) {
fprintf(stderr, "[GGUF] WARNING: munmap of the model mapping failed (%s); its pages stay resident\n",
strerror(errno));
}
}
if (gf->fd >= 0) {
close(gf->fd);
Expand Down Expand Up @@ -153,6 +164,20 @@ static bool gf_load(GGUFModel * gf, const char * path) {
return true;
}

// Queue a copy straight from the file mapping, recording where the bytes came from so
// wctx_alloc can release those staged pages once copied (Apple: munmap, Linux:
// madvise). The recorded range survives an adapter merge repointing src at a heap
// staging buffer, so the original pages are still released. Copies from heap staging
// or the stack push PendingCopy directly -- they have nothing to release.
static void wctx_push_file_copy(WeightCtx * wctx,
struct ggml_tensor * tensor,
const void * file_src,
size_t nbytes,
size_t offset,
const GGUFModel & gf) {
wctx->pending.push_back(wctx_make_file_copy(tensor, file_src, nbytes, offset, gf.mapping, gf.file_size));
}

// Load a tensor from GGUF into the weight context.
// Returns ggml_tensor (not yet backed by memory; call wctx_alloc after all loads).
// Tensor shapes are already in ggml order (ne[0]=innermost).
Expand Down Expand Up @@ -196,7 +221,7 @@ static struct ggml_tensor * gf_load_tensor(WeightCtx * wctx,
const void * data = gf.mapping + gf.data_offset + offset;
size_t nbytes = ggml_nbytes(src);

wctx->pending.push_back({ tensor, data, nbytes, 0 });
wctx_push_file_copy(wctx, tensor, data, nbytes, 0, gf);
return tensor;
}

Expand Down Expand Up @@ -266,6 +291,17 @@ static struct ggml_tensor * gf_load_tensor_f32(WeightCtx * wctx, const GGUFModel
// Get raw pointer to tensor data in the mmapped file.
// Useful for CPU-side operations (e.g. bf16 embed lookup for lyrics).
// Returns NULL if not found.
//
// Lifetime: the pointer is valid only while this GGUFModel is open AND before any
// wctx_alloc has run on a WeightCtx loading from it -- on Apple wctx_alloc unmaps
// each copied tensor's pages as it goes (weight-ctx.h), so a late read of a copied
// tensor now faults instead of harmlessly refaulting the page. Current users are safe
// for different reasons, and both matter: model-store metadata and store_silence open
// their own short-lived mapping that no WeightCtx touches, and vae.h reads its mapping
// across phases but never calls wctx_alloc at all -- nothing releases its pages. Note
// the second half: a module may read before OR after its own buffer allocation; the
// rule is only about WeightCtx's release, so a module converted TO the WeightCtx
// idiom must move every post-alloc mapping read into the push/copy flow.
static const void * gf_get_data(const GGUFModel & gf, const char * name) {
int64_t idx = gguf_find_tensor(gf.gguf, name);
if (idx < 0) {
Expand Down Expand Up @@ -312,10 +348,9 @@ static struct ggml_tensor * gf_load_qkv_fused(WeightCtx * wctx,
size_t off = gguf_get_tensor_offset(gf.gguf, idx);
return gf.mapping + gf.data_offset + off;
};

wctx->pending.push_back({ fused, get_data(q_name), q_bytes, 0 });
wctx->pending.push_back({ fused, get_data(k_name), k_bytes, q_bytes });
wctx->pending.push_back({ fused, get_data(v_name), v_bytes, q_bytes + k_bytes });
wctx_push_file_copy(wctx, fused, get_data(q_name), q_bytes, 0, gf);
wctx_push_file_copy(wctx, fused, get_data(k_name), k_bytes, q_bytes, gf);
wctx_push_file_copy(wctx, fused, get_data(v_name), v_bytes, q_bytes + k_bytes, gf);
return fused;
}

Expand Down Expand Up @@ -347,9 +382,8 @@ static struct ggml_tensor * gf_load_pair_fused(WeightCtx * wctx,
size_t off = gguf_get_tensor_offset(gf.gguf, idx);
return gf.mapping + gf.data_offset + off;
};

wctx->pending.push_back({ fused, get_data(a_name), a_bytes, 0 });
wctx->pending.push_back({ fused, get_data(b_name), b_bytes, a_bytes });
wctx_push_file_copy(wctx, fused, get_data(a_name), a_bytes, 0, gf);
wctx_push_file_copy(wctx, fused, get_data(b_name), b_bytes, a_bytes, gf);
return fused;
}

Expand Down
10 changes: 9 additions & 1 deletion src/qwen3-lm.h
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,15 @@ static bool qw3lm_load(Qwen3LM * m, const char * gguf_path, int max_seq_len, int
qwen3_load_layer(&m->wctx, gf, &m->layers[i], prefix, i);
}

wctx_alloc(&m->wctx, m->backend);
// The only wctx_alloc caller that may not discard the bool: a failed backend
// allocation leaves buffer == NULL, and a module that reports success anyway
// dereferences it on the first prefill. (Every other loader checks.) No
// self-teardown on this failure: store_require_lm's LoadGuard owns the
// module, same as the other five loaders, and gf_close below is still
// reached on the way out.
if (!wctx_alloc(&m->wctx, m->backend)) {
return false;
}
gf_close(&gf);

// KV cache
Expand Down
12 changes: 11 additions & 1 deletion src/safetensors.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
// Only handles the flat safetensors JSON structure.
// Not a general purpose JSON parser.

#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstring>
Expand Down Expand Up @@ -305,7 +306,16 @@ static void st_close(STFile * st) {
}
#else
if (st->mapping) {
munmap(st->mapping, st->file_size);
// Adapter payloads come from user-supplied files, so this is the likeliest
// unmap to actually fail; a silent failure would leave the mapping resident
// until process exit with nothing on stderr. Runs once per file, so a plain
// line per failure is right.
if (munmap(st->mapping, st->file_size) != 0) {
fprintf(stderr,
"[Safetensors] WARNING: munmap of the adapter mapping failed (%s); its pages stay "
"resident\n",
strerror(errno));
}
}
if (st->fd >= 0) {
close(st->fd);
Expand Down
Loading