GR-004: bind galleries to the embedder that built them
A gallery is only valid for the embedder that produced its vectors. Cosine
similarities across models are meaningless but *look* plausible, so the mistake
is silent and every measurement taken afterwards is suspect. Stamp the embedder
identity into the gallery at build; verify it at every load.
The stamp is the model file's basename plus the SHA-256 of its bytes (plus
embed_dim). The hash decides, the name explains. A name alone is a promise
rather than a fact — models get re-exported and overwritten in place under an
unchanged filename, which is exactly the case where the weights differ and
nothing else does. A hash alone is correct but unactionable in an error message.
SHA-256 is derived from the artefact, needs no registry kept current, and costs
~0.1s for a 250MB ONNX, memoised per process.
Mismatch is a hard error in every mode, with no bypass, naming both sides.
Unstamped legacy galleries warn loudly and proceed: unknown is not known-bad,
and hard-failing every pre-existing gallery would turn the check into something
people disable rather than trust. --require-gallery-stamp (or
SAE_REQUIRE_GALLERY_STAMP=1, which propagates to subprocesses) promotes that to
a hard error — the mode measurement work should run in. scripts/stamp_gallery.py
re-binds an existing gallery with no re-embedding, so "warn" is a cheap state to
leave rather than a permanent one.
Embedding dumps carry the same stamp: a replay has no live embedder, so the dump
is the embedder as far as the gallery is concerned. Derived galleries inherit
their source's stamp; --merge and the JSON gallery merge check before writing,
since one file holding two embedding spaces cannot be untangled afterwards.
Verified in: scene_analyze, scene_preview, the sae_kpn matcher binding,
replay.py, optimize.py (once per film at startup, before the first evaluation),
movienet_eval.py and both merge paths.
Stamp logic lives in src/gallery/embedder_stamp.{hpp,cpp} and its Python twin
scripts/sae_stamp.py, kept dependency-light so replay subprocesses do not pay
sae_gallery's requests/Pillow import to ask whether two models match.
Tests: 12 new cases in test_gallery_store.cpp covering the comparison logic,
both round trips, and the SHA-256 vectors that guarantee the C++ and hashlib
stamps agree. No ONNX or GPU required.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "types.hpp"
|
||||
#include "config.hpp"
|
||||
#include "gallery/embedder_stamp.hpp"
|
||||
|
||||
#include <H5Cpp.h>
|
||||
|
||||
@@ -25,7 +26,13 @@ struct EmbeddingDumpFunc {
|
||||
: path_(cfg.dump_embeddings_path), movie_(cfg.movie_path),
|
||||
sample_fps_(cfg.sample_fps), done_(done)
|
||||
{
|
||||
std::cerr << "[embedding_dump] writing " << path_ << "\n";
|
||||
/// TRACES: GR-004 | SR-001
|
||||
// A dump is a bag of embeddings with no model attached, replayed against a
|
||||
// gallery hours or weeks later — the same silent cross-model hazard as the
|
||||
// gallery itself, so it carries the same stamp.
|
||||
stamp_ = make_embedder_stamp(cfg.arcface_model);
|
||||
std::cerr << "[embedding_dump] writing " << path_
|
||||
<< " embedder: " << stamp_.describe() << "\n";
|
||||
}
|
||||
|
||||
void operator()(EmbeddedSceneFrame ef) {
|
||||
@@ -91,6 +98,9 @@ private:
|
||||
H5::StrType str(H5::PredType::C_S1, H5T_VARIABLE);
|
||||
auto mv = file.createAttribute("movie", str, scalar);
|
||||
mv.write(str, movie_);
|
||||
/// TRACES: GR-004 | SR-001
|
||||
file.createAttribute("embedder_model", str, scalar).write(str, stamp_.model_name);
|
||||
file.createAttribute("embedder_sha256", str, scalar).write(str, stamp_.model_sha256);
|
||||
|
||||
H5::Group frames = file.createGroup("frames");
|
||||
write_vec(frames, "timestamp_sec", ts_, H5::PredType::NATIVE_DOUBLE);
|
||||
@@ -110,7 +120,8 @@ private:
|
||||
<< conf_.size() << " faces → " << path_ << "\n";
|
||||
}
|
||||
|
||||
std::string path_, movie_;
|
||||
std::string path_, movie_;
|
||||
EmbedderStamp stamp_;
|
||||
float sample_fps_;
|
||||
std::atomic<bool>& done_;
|
||||
std::atomic<bool> written_{false};
|
||||
|
||||
Reference in New Issue
Block a user