Spaces:
Sleeping
Sleeping
chore: script to fix PsyEmbedding HF repos' missing 1_Pooling config
Browse filesUploads the 1_Pooling/config.json that modules.json references but the four
Culture-and-Morality-Lab/psyembedding-* repos never shipped (the reason for
the pooling_fallback workaround). Dry-run by default; --write needs lab HF
credentials; --verify asserts auto-load == fallback assembly.
scripts/fix_psyembedding_pooling.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Fix the PsyEmbedding repos' broken sentence-transformers packaging.
|
| 3 |
+
|
| 4 |
+
All four Culture-and-Morality-Lab/psyembedding-* repos ship a modules.json
|
| 5 |
+
that references a 1_Pooling module, but the 1_Pooling/config.json it points
|
| 6 |
+
at was never uploaded - so SentenceTransformer(repo_id) cannot auto-load
|
| 7 |
+
them (the reason for the pooling_fallback workaround in models.yaml and
|
| 8 |
+
backend/app/ccr.py). This uploads the missing config, which makes the repos
|
| 9 |
+
load the standard way for everyone, platform or not.
|
| 10 |
+
|
| 11 |
+
The pooling config is derived, not assumed: pooling mode comes from the
|
| 12 |
+
registry entry (mean, per the model cards - the same thing the platform's
|
| 13 |
+
fallback has been computing all along) and the embedding dimension is read
|
| 14 |
+
from each repo's own config.json at the pinned revision. Adding the file
|
| 15 |
+
changes NOTHING numerically: same weights, same pooling math, byte-identical
|
| 16 |
+
embeddings - it only lets the packaged loader do what our fallback did.
|
| 17 |
+
|
| 18 |
+
Usage (write needs lab credentials: `hf auth login` or HF_TOKEN env var):
|
| 19 |
+
python scripts/fix_psyembedding_pooling.py # dry run: show plan
|
| 20 |
+
python scripts/fix_psyembedding_pooling.py --write # commit to the repos
|
| 21 |
+
python scripts/fix_psyembedding_pooling.py --verify REPO_ID [REVISION]
|
| 22 |
+
# download one fixed repo and check auto-load == manual assembly
|
| 23 |
+
|
| 24 |
+
After --write, finish in the platform repo (do NOT blanket-run
|
| 25 |
+
pin_revisions.py --write; it would repin every model, not just these):
|
| 26 |
+
1. models.yaml: set the four psyembedding revision lines to the new SHAs
|
| 27 |
+
printed below, and delete their pooling_fallback lines.
|
| 28 |
+
2. python packages/model_registry/validate_models.py
|
| 29 |
+
3. cd backend && python -m pytest -q
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
import json
|
| 33 |
+
import sys
|
| 34 |
+
|
| 35 |
+
from huggingface_hub import HfApi, hf_hub_download
|
| 36 |
+
|
| 37 |
+
REPOS = [
|
| 38 |
+
"Culture-and-Morality-Lab/psyembedding-bert-large-uncased",
|
| 39 |
+
"Culture-and-Morality-Lab/psyembedding-roberta-large",
|
| 40 |
+
"Culture-and-Morality-Lab/psyembedding-gte-large",
|
| 41 |
+
"Culture-and-Morality-Lab/psyembedding-e5-large-v2",
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
COMMIT_MESSAGE = (
|
| 45 |
+
"Add missing 1_Pooling/config.json referenced by modules.json\n\n"
|
| 46 |
+
"modules.json declares a Pooling module at 1_Pooling, but the config "
|
| 47 |
+
"was never uploaded, so SentenceTransformer auto-loading fails. Mean "
|
| 48 |
+
"pooling per the model card; no change to weights or outputs."
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# The classic 5-key Pooling config: loads on every sentence-transformers
|
| 52 |
+
# version (newer optional flags default to the same behavior when absent).
|
| 53 |
+
def pooling_config(dim: int) -> dict:
|
| 54 |
+
return {
|
| 55 |
+
"word_embedding_dimension": dim,
|
| 56 |
+
"pooling_mode_cls_token": False,
|
| 57 |
+
"pooling_mode_mean_tokens": True,
|
| 58 |
+
"pooling_mode_max_tokens": False,
|
| 59 |
+
"pooling_mode_mean_sqrt_len_tokens": False,
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def plan(api: HfApi, repo: str) -> dict | None:
|
| 64 |
+
"""Validate assumptions against the live repo; return the upload plan."""
|
| 65 |
+
files = {f.path for f in api.list_repo_tree(repo)}
|
| 66 |
+
if "1_Pooling/config.json" in files:
|
| 67 |
+
print(f" already fixed - skipping")
|
| 68 |
+
return None
|
| 69 |
+
|
| 70 |
+
modules = json.load(open(hf_hub_download(repo, "modules.json")))
|
| 71 |
+
pooling = [m for m in modules if m["type"].endswith("models.Pooling")]
|
| 72 |
+
assert pooling and pooling[0]["path"] == "1_Pooling", (
|
| 73 |
+
f"{repo}: modules.json does not reference 1_Pooling as expected: {modules}"
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
hf_config = json.load(open(hf_hub_download(repo, "config.json")))
|
| 77 |
+
dim = hf_config["hidden_size"]
|
| 78 |
+
assert dim == 1024, f"{repo}: hidden_size {dim} != 1024 in the registry"
|
| 79 |
+
|
| 80 |
+
content = json.dumps(pooling_config(dim), indent=2) + "\n"
|
| 81 |
+
print(f" will add 1_Pooling/config.json (mean pooling, dim {dim})")
|
| 82 |
+
return {"repo": repo, "content": content}
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def write(api: HfApi, p: dict) -> None:
|
| 86 |
+
info = api.create_commit(
|
| 87 |
+
repo_id=p["repo"],
|
| 88 |
+
operations=[
|
| 89 |
+
__import__("huggingface_hub").CommitOperationAdd(
|
| 90 |
+
path_in_repo="1_Pooling/config.json",
|
| 91 |
+
path_or_fileobj=p["content"].encode(),
|
| 92 |
+
)
|
| 93 |
+
],
|
| 94 |
+
commit_message=COMMIT_MESSAGE,
|
| 95 |
+
)
|
| 96 |
+
print(f" committed: {info.oid}")
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def verify(repo: str, revision: str | None) -> None:
|
| 100 |
+
"""Auto-load must equal the manual Transformer+Pooling assembly."""
|
| 101 |
+
import numpy as np
|
| 102 |
+
from sentence_transformers import SentenceTransformer
|
| 103 |
+
from sentence_transformers import models as st_models
|
| 104 |
+
|
| 105 |
+
texts = [
|
| 106 |
+
"I am deeply satisfied with my life.",
|
| 107 |
+
"The bus was late again this morning.",
|
| 108 |
+
"Caring for the vulnerable is the most important virtue.",
|
| 109 |
+
]
|
| 110 |
+
auto = SentenceTransformer(repo, revision=revision)
|
| 111 |
+
word = st_models.Transformer(repo, max_seq_length=512)
|
| 112 |
+
get_dim = getattr(word, "get_embedding_dimension", None) or word.get_word_embedding_dimension
|
| 113 |
+
manual = SentenceTransformer(modules=[word, st_models.Pooling(get_dim(), pooling_mode="mean")])
|
| 114 |
+
|
| 115 |
+
a = auto.encode(texts, convert_to_numpy=True, normalize_embeddings=True)
|
| 116 |
+
m = manual.encode(texts, convert_to_numpy=True, normalize_embeddings=True)
|
| 117 |
+
assert a.shape == m.shape, (a.shape, m.shape)
|
| 118 |
+
max_diff = float(np.abs(a - m).max())
|
| 119 |
+
print(f"{repo}: shapes {a.shape}, max |auto - manual| = {max_diff:.2e}")
|
| 120 |
+
assert max_diff == 0.0, "auto-load does not match the platform's fallback assembly"
|
| 121 |
+
print("verified: byte-identical to the fallback the platform has been using")
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def main() -> None:
|
| 125 |
+
if "--verify" in sys.argv:
|
| 126 |
+
i = sys.argv.index("--verify")
|
| 127 |
+
repo = sys.argv[i + 1]
|
| 128 |
+
rev = sys.argv[i + 2] if len(sys.argv) > i + 2 else None
|
| 129 |
+
verify(repo, rev)
|
| 130 |
+
return
|
| 131 |
+
|
| 132 |
+
do_write = "--write" in sys.argv
|
| 133 |
+
api = HfApi()
|
| 134 |
+
plans = []
|
| 135 |
+
for repo in REPOS:
|
| 136 |
+
print(repo)
|
| 137 |
+
p = plan(api, repo)
|
| 138 |
+
if p:
|
| 139 |
+
plans.append(p)
|
| 140 |
+
|
| 141 |
+
if not plans:
|
| 142 |
+
print("\nNothing to do.")
|
| 143 |
+
return
|
| 144 |
+
if not do_write:
|
| 145 |
+
print(f"\nDry run: {len(plans)} repo(s) need the fix. "
|
| 146 |
+
"Re-run with --write (requires lab HF credentials).")
|
| 147 |
+
print("Config that would be uploaded:\n" + plans[0]["content"])
|
| 148 |
+
return
|
| 149 |
+
|
| 150 |
+
who = api.whoami()
|
| 151 |
+
print(f"\nWriting as: {who['name']} ({who.get('email', '?')})")
|
| 152 |
+
for p in plans:
|
| 153 |
+
print(p["repo"])
|
| 154 |
+
write(api, p)
|
| 155 |
+
print("\nDone. New commit SHAs are above - now update models.yaml "
|
| 156 |
+
"(revision lines + remove pooling_fallback) per the module docstring.")
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
if __name__ == "__main__":
|
| 160 |
+
main()
|