File size: 6,079 Bytes
a484e22 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | """Phase 4.3 — shared-user-task personalization overlap sweep.
Stress-tests the personalization claim (per-user memory vs. a live global/shared
store) as users' task sets *overlap*. Workload: every user has ``tasks_per_user``
signature tasks with a FIXED per-user argument realization (user A always NYC,
user B always Boston), consistent across the user's sessions. A fraction
``overlap_frac`` of those tasks come from ONE shared template pool that every
user reuses (same templates, different per-user args); the rest are private and
disjoint. As overlap rises, a global store sees many users' different calls for
the same template — does per-user memory still win, or does a shared store catch
up?
Two arms, both live + evicting (only partitioning differs), so the contrast is
personalization alone:
- global_evict : one live global store (no per-user view)
- personal_memory (ours): per-user live evicting store
Targets are the served model's greedy calls (real, generated once per unique
query on the migrated node and cached per overlap level). Run from ``code/``:
python -m harness.phase4_overlap --url http://localhost:30000/v1
"""
from __future__ import annotations
import argparse
import json
import os
from collections import defaultdict
from pathlib import Path
from . import metrics
from .client import ToolClient
from .data import load_bfcl
from .memory import Embedder, GlobalEvict, PersonalMemory
from .run_accept import _parse_target, generate_targets
from .simulate import build_users
ROOT = Path(__file__).resolve().parent.parent
RESULTS = ROOT / "results"
# Tokenizer for the token-LCP accept metric: HF hub id by default;
# override with a local snapshot path if running offline.
MODEL_PATH = os.environ.get("SPECMEM_TOKENIZER", "openai/gpt-oss-120b")
class Slot:
"""generate_targets expects objects with .query and .functions; the
functions registry is identical across BFCL tasks' first-tool schema, so we
attach it lazily below."""
__slots__ = ("query", "functions")
def __init__(self, query, functions=None):
self.query = query
self.functions = functions
def _replay(instances, targets, embedder, capacity):
arms = [GlobalEvict(capacity=40 * capacity),
PersonalMemory(capacity=capacity, eviction="lru")]
agg = {a.name: defaultdict(list) for a in arms}
for ins in instances:
tgt = targets.get(ins.query)
if tgt is None:
continue
for a in arms:
agg[a.name][ins.session].append(metrics.score(
a.draft(ins.query, ins.functions, ins.user_id, embedder), tgt))
cn, ca = _parse_target(tgt)
for a in arms:
a.observe(ins.query, ins.functions, ins.user_id, cn, ca, embedder)
out = {}
for n, v in agg.items():
post = [x for s, xs in v.items() if s > 0 for x in xs]
out[n] = round(sum(x["accept_length"] for x in post) /
max(1, len(post)), 3)
return out
def main():
p = argparse.ArgumentParser()
p.add_argument("--url", default="http://localhost:30000/v1")
p.add_argument("--model", default="gpt-oss-120b")
p.add_argument("--workers", type=int, default=16)
p.add_argument("--overlaps", nargs="+", type=float,
default=[0.0, 0.25, 0.5, 0.75, 1.0])
p.add_argument("--seeds", nargs="+", type=int, default=[0, 1, 2])
p.add_argument("--capacity", type=int, default=48)
args = p.parse_args()
metrics.get_tokenizer(MODEL_PATH)
tasks = load_bfcl()
embedder = Embedder()
client = ToolClient(url=args.url, model=args.model)
if not client.ping():
raise SystemExit(f"served model not reachable at {args.url}")
sweep = {}
for ov in args.overlaps:
per_seed = {"global_evict": [], "personal_memory": []}
for sd in args.seeds:
instances = build_users(
tasks, n_users=40, tasks_per_user=15, n_sessions=12,
queries_per_session=6, seed=sd, overlap_frac=ov,
user_consistent=True)
# attach the (shared) functions registry to Slots for target gen
fmap = {ins.query: ins.functions for ins in instances}
cache_f = RESULTS / f"phase4_overlap_targets_ov{ov}_seed{sd}.json"
cache = json.loads(cache_f.read_text()) if cache_f.exists() else {}
miss = [Slot(q, fmap[q]) for q in {i.query for i in instances}
if q not in cache]
if miss:
cache.update(generate_targets(client, miss, workers=args.workers))
cache_f.write_text(json.dumps(cache))
res = _replay(instances, cache, embedder, args.capacity)
per_seed["global_evict"].append(res["global_evict"])
per_seed["personal_memory"].append(res["personal_memory"])
print(f"ov={ov} seed={sd}: {res}", flush=True)
import statistics as st
g = st.mean(per_seed["global_evict"])
pm = st.mean(per_seed["personal_memory"])
sweep[ov] = {
"global_evict": round(g, 3),
"personal_memory": round(pm, 3),
"personal_advantage_pct": round(100 * (pm - g) / g, 1),
"per_seed": per_seed}
print(f"== ov={ov}: global {g:.3f} | personal {pm:.3f} | "
f"adv {sweep[ov]['personal_advantage_pct']}% ==", flush=True)
out = {"config": {"users": 40, "tasks_per_user": 15, "sessions": 12,
"capacity": args.capacity, "seeds": args.seeds,
"user_consistent_args": True,
"arms": "global_evict (live shared) vs personal_memory (ours)"},
"sweep": sweep}
(RESULTS / "phase4_personalization_overlap.json").write_text(
json.dumps(out, indent=2))
print(json.dumps({ov: {"global": s["global_evict"],
"personal": s["personal_memory"],
"adv%": s["personal_advantage_pct"]}
for ov, s in sweep.items()}, indent=1))
if __name__ == "__main__":
main()
|