devaanand commited on
Commit
21ee87e
·
1 Parent(s): 78fa678

chore: script to fix PsyEmbedding HF repos' missing 1_Pooling config

Browse files

Uploads 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.

Files changed (1) hide show
  1. scripts/fix_psyembedding_pooling.py +160 -0
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()