Skip to content
Merged
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
6 changes: 6 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ target_compile_features(test-server-prefill PRIVATE cxx_std_17)
add_test(NAME test-server-prefill COMMAND test-server-prefill)
set_tests_properties(test-server-prefill PROPERTIES LABELS "main")

# Model-free validator: whether a saved page's KV belongs to the loaded adapters.
add_executable(test-server-page-adapters test-server-page-adapters.cpp)
target_compile_features(test-server-page-adapters PRIVATE cxx_std_17)
add_test(NAME test-server-page-adapters COMMAND test-server-page-adapters)
set_tests_properties(test-server-page-adapters PROPERTIES LABELS "main")

function(llama_build source)
set(TEST_SOURCES ${source} ${ARGN})

Expand Down
64 changes: 64 additions & 0 deletions tests/test-server-page-adapters.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#include "../tools/server/server-page-adapters.h"

#include <cstdlib>
#include <iostream>
#include <limits>

static void require(bool condition, const char * message) {
if (!condition) {
std::cerr << message << '\n';
std::exit(1);
}
}

using page = std::vector<server_page_adapter>;
using loaded = std::vector<server_loaded_adapter>;

static std::optional<std::vector<float>> decide(const page & p, const loaded & l) {
std::string why;
auto scales = server_page_adapter_scales(p, l, why);
require(scales.has_value() == why.empty(), "a refusal always names its reason, a match never does");
return scales;
}

int main() {
const server_loaded_adapter gene { "/genome/delia/adapter.gguf", 4365792, 100 };
const server_loaded_adapter dream { "/engine-train/gene-b758.gguf", 4365792, 100 };

// what this catches: the IntelMac 11-03 refusal. The page carried the gene and the dream
// stage's runtime adapter, both at 0.0; the relaunched engine loads only the gene.
auto s = decide({ { gene.path, 0.0f, gene.size, gene.mtime }, { dream.path, 0.0f, dream.size, dream.mtime } }, { gene });
require(s && s->size() == 1 && (*s)[0] == 0.0f, "a page whose adapters were all at 0.0 restores under any loaded set");

// the same lists in both: the active adapter keeps the page's scale
s = decide({ { gene.path, 0.5f, gene.size, gene.mtime } }, { gene });
require(s && (*s)[0] == 0.5f, "an active adapter runs at the scale the KV was computed under");

// an adapter loaded now but absent from the page runs at 0.0, never at its default scale
s = decide({ { gene.path, 0.5f, gene.size, gene.mtime } }, { dream, gene });
require(s && (*s)[0] == 0.0f && (*s)[1] == 0.5f, "an extra loaded adapter restores at 0.0, in load order");

// a page computed with no adapters restores on an engine that has some loaded
s = decide({}, { gene });
require(s && (*s)[0] == 0.0f, "an adapter-free page is base-model KV");

// an active adapter that is not loaded: its KV is not this engine's
require(!decide({ { dream.path, 1.0f, dream.size, dream.mtime } }, { gene }), "an active adapter missing from the engine refuses");

// the same path with new weights (a gene adopted in place)
require(!decide({ { gene.path, 1.0f, gene.size, gene.mtime - 1 } }, { gene }), "an active adapter whose file changed refuses");
// an unreadable file proves nothing about its weights
require(!decide({ { gene.path, 1.0f, gene.size, gene.mtime } }, { { gene.path, 0, 0 } }), "an active adapter whose file cannot be read refuses");
// ... even when the page was saved while it was unreadable too: two unknowns never match
require(!decide({ { gene.path, 1.0f, 0, 0 } }, { { gene.path, 0, 0 } }), "an unreadable revision never equals another");
// an inactive adapter's revision is irrelevant: it never touched the KV
require(decide({ { gene.path, 0.0f, 1, 1 } }, { gene }).has_value(), "an inactive adapter's revision never refuses");

// one loaded copy cannot stand for two active entries
require(!decide({ { gene.path, 0.5f, gene.size, gene.mtime }, { gene.path, 0.5f, gene.size, gene.mtime } }, { gene }),
"each active entry claims its own loaded adapter");

require(!decide({ { gene.path, std::numeric_limits<float>::quiet_NaN(), gene.size, gene.mtime } }, { gene }),
"a non-finite recorded scale refuses");
return 0;
}
47 changes: 30 additions & 17 deletions tools/server/server-context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "server-queue.h"
#include "server-schema.h"
#include "server-stream.h"
#include "server-page-adapters.h"
#include "server-prefill.h"

#include "build-info.h"
Expand Down Expand Up @@ -2892,9 +2893,10 @@ struct server_context_impl {
size_t nread = 0;
try {
// Continuum fork: the adapters the state was computed under (see SLOT_SAVE), read
// FIRST so a page whose identity is unknown or foreign never touches the KV. A
// page without them cannot be reused safely (its KV may belong to other adapters),
// so it is refused: the caller's cold prefill is correct; a mislabelled reuse is not.
// FIRST so a page whose identity is unknown or foreign never touches the KV. A page
// without them cannot be reused safely, so it is refused: the caller's cold prefill
// is correct; a mislabelled reuse is not. Its identity is its ACTIVE adapters
// (server-page-adapters.h): an adapter it carried at 0.0 never refuses it.
std::vector<common_adapter_lora_info> page_lora = params_base.lora_adapters; // copy: ptr, task_name, prompt_prefix
{
const std::string lora_path = filepath + ".lora";
Expand All @@ -2903,27 +2905,38 @@ struct server_context_impl {
auto get = [&](void * ptr, size_t n) { in.read(reinterpret_cast<char *>(ptr), n); ok = ok && (bool) in; };
uint32_t magic = 0, version = 0, n_lora = 0;
get(&magic, sizeof(magic)); get(&version, sizeof(version)); get(&n_lora, sizeof(n_lora));
ok = ok && magic == 0x4C4F5241u && version == 2 && n_lora == page_lora.size();
ok = ok && magic == 0x4C4F5241u && version == 2 && n_lora <= 4096;
std::vector<server_page_adapter> recorded;
for (uint32_t i = 0; ok && i < n_lora; ++i) {
uint32_t n_path = 0;
get(&n_path, sizeof(n_path));
ok = ok && n_path <= 4096;
if (!ok) break;
std::string path(n_path, '\0');
get(path.data(), n_path);
float scale = 0.0f;
get(&scale, sizeof(scale));
uint64_t size = 0; int64_t mtime = 0;
get(&size, sizeof(size)); get(&mtime, sizeof(mtime));
// the same file AS LOADED: a gene adopted in place (same path, new weights)
// is another adapter, and KV computed under the old weights is not its own;
// an unreadable file proves nothing, so it refuses (BigMama on #54)
const auto rev = lora_file_revision(page_lora[i].path);
ok = ok && path == page_lora[i].path && std::isfinite(scale) && rev.first != 0 && size == rev.first && mtime == rev.second;
if (ok) page_lora[i].scale = scale;
server_page_adapter a { std::string(n_path, '\0'), 0.0f, 0, 0 };
get(a.path.data(), n_path);
get(&a.scale, sizeof(a.scale));
get(&a.size, sizeof(a.size)); get(&a.mtime, sizeof(a.mtime));
recorded.push_back(std::move(a));
}
if (!ok) {
throw std::runtime_error("the page's adapter identity is missing or does not match the loaded adapters: " + lora_path);
throw std::runtime_error("the page's adapter identity is missing or unreadable: " + lora_path);
}
// the same file AS LOADED: a gene adopted in place (same path, new weights) is
// another adapter, and KV computed under the old weights is not its own; an
// unreadable file proves nothing, so it refuses (BigMama on #54)
std::vector<server_loaded_adapter> loaded;
loaded.reserve(page_lora.size());
for (const auto & l : page_lora) {
const auto rev = lora_file_revision(l.path);
loaded.push_back({ l.path, rev.first, rev.second });
}
std::string why;
const auto scales = server_page_adapter_scales(recorded, loaded, why);
if (!scales) {
throw std::runtime_error("the page's adapter identity does not match the loaded adapters (" + why + "): " + lora_path);
}
for (size_t i = 0; i < page_lora.size(); ++i) {
page_lora[i].scale = (*scales)[i];
}
}

Expand Down
72 changes: 72 additions & 0 deletions tools/server/server-page-adapters.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#pragma once

#include <cmath>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>

// Continuum fork: whether a saved page's KV belongs to this engine's adapters.
//
// A page's `.lora` sidecar records every adapter its slot carried, with its scale and the
// file revision (size, mtime) it was computed under. An adapter at scale 0 adds nothing to
// the KV, so the page's identity is the base model plus its ACTIVE adapters (scale != 0).
// Refusing on the full loaded list made every relaunch discard valid pages: an engine that
// had a training adapter attached at 0.0 saved pages a relaunched engine, without it, could
// never restore, and each turn re-prefilled its whole prompt (IntelMac, 2026-11-03).

// One adapter as the page recorded it.
struct server_page_adapter {
std::string path;
float scale;
uint64_t size;
int64_t mtime;
};

// One adapter as this engine has it loaded: its path and its file's revision now
// (size 0 when the file cannot be read, which proves nothing about its weights).
struct server_loaded_adapter {
std::string path;
uint64_t size;
int64_t mtime;
};

// The scale the restored slot runs each LOADED adapter at, in load order, or nullopt with
// `why` when the page's KV is not this engine's. Each active page adapter claims one loaded
// adapter at the same path and revision and keeps the page's scale, so the slot computes
// exactly what the KV holds; every other loaded adapter runs at 0.0, never at its default.
inline std::optional<std::vector<float>> server_page_adapter_scales(
const std::vector<server_page_adapter> & page,
const std::vector<server_loaded_adapter> & loaded,
std::string & why) {
std::vector<float> scales(loaded.size(), 0.0f);
std::vector<bool> claimed(loaded.size(), false);
for (const auto & p : page) {
if (!std::isfinite(p.scale)) {
why = "a recorded scale is not finite: " + p.path;
return std::nullopt;
}
if (p.scale == 0.0f) {
continue;
}
bool matched = false;
for (size_t i = 0; i < loaded.size() && !matched; ++i) {
const auto & l = loaded[i];
if (claimed[i] || l.path != p.path) {
continue;
}
if (l.size == 0 || l.size != p.size || l.mtime != p.mtime) {
why = "an active adapter's file changed since the page was saved: " + p.path;
return std::nullopt;
}
claimed[i] = true;
scales[i] = p.scale;
matched = true;
}
if (!matched) {
why = "an active adapter is not loaded: " + p.path;
return std::nullopt;
}
}
return scales;
}
Loading