Instructions to use baya1116/hypernet-sp-distill with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use baya1116/hypernet-sp-distill with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] huggingface-cli download --local-dir hypernet-sp-distill baya1116/hypernet-sp-distill
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
APP_HANDOFF β on-device recall (SP compression + gate + BGE-on-demand)
Read this and you can implement the recall system in the app. It is the on-device target: across turns you persist token IDs only; a per-position GATE detects when the model is about to need a fact that scrolled out of the exposure window, and a BGE-on-demand RETRIEVER pulls the right evicted 128-token block back in (verbatim) so the value survives SP compression.
HF repo: baya1116/hypernet-sp-distill. Resolve URL pattern:
https://huggingface.co/baya1116/hypernet-sp-distill/resolve/main/<path>
1. Artifacts and exact locations (γγγ)
| what | path in repo | format |
|---|---|---|
| GATE (use this) | trigger_experiment/ondevice_recall/gate.npz |
npz, see Β§4 |
| BGE bridge head (use this) | trigger_experiment/ondevice_recall/bge_head.npz |
npz, see Β§5 |
| SP pooler | trigger_experiment/ondevice_recall/pooler.pt (=fft_out/pooler.pt) |
torch state, AttnPoolSP |
| base LLM (HF format) | recall_runtime/fft_hf/ (model.safetensors, tokenizer, config) |
DeepSeek-R1-Distill-Qwen-1.5B, FFT student |
| base LLM (raw build) | fft_out/student.pt + build_fft_hf.py |
builds fft_hf/ |
| pooler loader | hypernet_sp/attn_export3_torch.py β load_pooler() |
defines AttnPoolSP |
| reference runtime/retriever | recall_runtime/recall_kit/{runtime,retriever,gate}.py |
Python reference impl |
| reference block archive (IDsβkeys) | hypernet_sp/block_recall.py (BlockArchive) |
raw-QK variant (server) |
| end-to-end eval (canonical loop) | git cotscan_live/recall_gen.py (reply(), retrieve_bge()) |
the exact loop to port |
| external dep | BAAI/bge-base-en-v1.5 (sentence-transformers / convertible to CoreML/MLX) |
110M encoder |
Direct links:
- gate: https://huggingface.co/baya1116/hypernet-sp-distill/blob/main/trigger_experiment/ondevice_recall/gate.npz
- bge_head: https://huggingface.co/baya1116/hypernet-sp-distill/blob/main/trigger_experiment/ondevice_recall/bge_head.npz
- pooler: https://huggingface.co/baya1116/hypernet-sp-distill/blob/main/trigger_experiment/ondevice_recall/pooler.pt
- base model dir: https://huggingface.co/baya1116/hypernet-sp-distill/tree/main/recall_runtime/fft_hf
- recall_kit (reference): https://huggingface.co/baya1116/hypernet-sp-distill/tree/main/recall_runtime/recall_kit
- full status / results: git branch
claude/hypernet-sp-spec-review-qrafn9,cotscan_live/STATUS_2026-06-14_recall_improvements.md
2. Constants (1.5B model)
hidden = 1536, Hq = 12 (attn heads), Hkv = 2 (KV heads), D = 128 (head dim)
LAYERS = (8, 14, 20) # the 3 layers the gate + retriever read
RW = 512 # exposure window: last RW tokens stay verbatim
BLOCK = 128 # evicted tokens are sealed into 128-token blocks
C = 64 # generation chunk between rebuilds
RECALL_K = 2 # inject top-2 blocks on a fire
FEATURE = len(LAYERS) * Hq * D = 3*12*128 = 4608 # gate / bridge query dim
BOS / EOS / <ο½Userο½> / <ο½Assistantο½> : DeepSeek-R1 chat special tokens
3. The runtime loop (what to implement)
State you persist across turns = token IDs of the conversation only. Everything else is
transient. Generation runs in chunks of C tokens; before each chunk you "rebuild" the prefix.
gen = [all tokens so far: prior turns + this turn's forced user tokens]
kept = [] # evicted tokens already folded into the SP gist
absorbed = 0
archive = [] # list of {ids:[128]} sealed blocks (IDs ONLY β no vectors)
rec_emb = None # currently-injected retrieved block embeddings (None until a fire)
loop until end-of-turn:
R = min(len(gen), RW) # window length (verbatim tail)
nd = len(gen) - R # everything before the window is "evicted"
if nd > absorbed: # newly evicted tokens
archive.extend(gen[absorbed:nd]) # seal into 128-tok blocks (store IDs)
kept += gen[absorbed:nd]; absorbed = nd
# ---- GATE (per rebuild) ----
if archive not empty:
gq = pre-RoPE q_proj output of the LAST token at layers 8/14/20 # captured in prev fwd
score = ((concat(gq) - mean) / scale) Β· coef + intercept # Β§4
if score > THRESH:
rid = bge_retrieve(archive, gq, k=RECALL_K) # Β§5
if rid: rec_emb = embed(rid)
# ---- build prefix and run the model ----
prefix = [BOS]
+ [ SP = pooler(embed(kept)) ] # SP gist of evicted history (if kept)
+ ([ rec_emb ] if rec_emb is not None) # injected evicted block(s), verbatim emb
+ [ embed(gen[len(gen)-R : ]) ] # the verbatim window
logits = model(prefix) # also capture gq (last-token q_proj at 8/14/20) for next gate check
generate C tokens greedily, append to gen (stop on EOS or cap)
Notes:
- The gate score uses the q of the current last token under the SP-compressed prefix β
that's the exact feature it was trained on. Capture
q_projoutput (pre-RoPE) of the last position at layers 8/14/20; concat in layer order β 4608-vector. - Once
rec_embis set it stays injected for the rest of the turn (re-evaluate per turn). pooler= AttnPoolSP (load_pooler("pooler.pt")); it maps the embeddedkepttokens to a short SP gist. Port fromhypernet_sp/attn_export3_torch.py.
4. GATE β gate.npz (logistic, per-position recall detector)
Keys (all the model needs):
coef float32 [4608]
intercept float32 scalar (= -6.2547)
mean float32 [4608]
scale float32 [4608]
thresh float32 scalar (= -2.8222, the 90%-held-recall point)
Scoring (q = the 4608 gate feature from Β§3):
score = ((q - mean) / scale) Β· coef + intercept
fire = score > THRESH
Operating threshold: fp32 generation β THRESH β β1.5 to β2.0 (β2.0 gave 88% chat recall).
4-bit (on-device) β THRESH β β3.5 (see Β§9 β heads unchanged, only the threshold shifts). The
baked-in thresh (β2.82) is the fp32 held-out 90%-recall point. Lower = more recall + more false
fires; higher = fewer fires + less recall. Calibrate per build (see Β§9).
5. RETRIEVER β BGE-on-demand (bge_head.npz + BAAI/bge-base-en-v1.5)
State-free: blocks are stored as token IDs only. At a fire you BGE-encode the candidate blocks' decoded text on the fly. The query is the gate's hidden q (free β already computed).
bge_head.npz (a 2-tower bridge; PyTorch Sequential(Linear, GELU, Linear) each):
qdim=4608, kdim=768, d=128
Wq.0.weight [128,4608] Wq.0.bias [128] Wq.2.weight [128,128] Wq.2.bias [128]
Wk.0.weight [128,768] Wk.0.bias [128] Wk.2.weight [128,128] Wk.2.bias [128]
Wq(x) = Linear2(GELU(Linear0(x))) # 4608 -> 128
Wk(x) = Linear2(GELU(Linear0(x))) # 768 -> 128
Retrieval:
def bge_retrieve(archive, gq, k=2):
cands = archive.blocks (+ pending buffer if >=16 toks) # each is token IDs
q = concat(gq) # RAW 4608 vector (NOT standardized β bge_head wants raw)
qz = Wq(q) # [128]
for each block:
text = tokenizer.decode(block.ids)
kemb = BGE.encode(text, normalize=True) # [768] (BAAI/bge-base-en-v1.5)
kz = Wk(kemb) # [128]
score = qz Β· kz
top = top-k blocks by score, in chronological order
return concat(top.ids) # inject these token IDs (embedded) in Β§3
Important: raw BGE cosine is useless (top-2 0.29) β the bridge head is mandatory (0.9997 held-out, and == raw-QK in generation). Query side is the LLM hidden q, NOT a BGE of the query.
6. Measured (so you know the target)
- Multi-fact chat needle (N=8, 24 questions, item-disjoint): recall 88% with recall on, 0% without (SP-only). BGE-on-demand == raw-QK, bit-identical (both 21/24).
- Gate detection AUC: cot β 0.90, chat β 0.93 (maintained vs Dolphin-only chat 0.888).
- Retrieval (held-out): BGE+bridge top-2 0.9997; learned QK indexer 0.998 (but worse in generation, 75%); raw BGE cosine 0.29 (don't use).
- 4-bit (on-device): same heads, threshold β3.5 β recall 100% / retrieval 100% (vs 44% at the fp32 threshold β2.0). Heads need NO retraining; only the threshold shifts. See Β§7b.
7b. 4-bit quantization (on-device) β VERIFIED, important
The 1.5B runs in 4-bit (nf4) on device. We tested whether the fp32-trained heads survive.
Result: the heads are quantization-robust. DO NOT retrain them for 4-bit.
- Held-out, scoring the fp32 heads on 4-bit q features (labels from fp32 attention, features from the 4-bit model): gate AUC cot 0.975 / chat 0.975, BGE bridge top-2 1.0. Refitting on 4-bit features was slightly worse (gate ~0.92, BGE 0.95) β so keep the fp32 heads.
- The ONLY thing that shifts under 4-bit is the gate's score scale (the logistic
decision_functionoutput drops), so the fp32 threshold fires too rarely. The separation (AUC) is intact β it's purely an operating-point shift.
The single required change for 4-bit: lower the gate threshold β β2.0 β β3.5. Verified in generation (N=3, fp32 gate, 4-bit model, only threshold varied):
| gate threshold | recall | retrieved | casual-fire |
|---|---|---|---|
| β2.0 (fp32 value) | 44% | 67% | 67% |
| β3.5 (use this at 4-bit) | 100% | 100% | 67% |
| β5.0 | 100% | 100% | 100% (over-fires) |
So: same gate.npz and bge_head.npz, threshold β β3.5 at 4-bit. Best practice: don't
hardcode β at startup, run ~10 short needle dialogues through the 4-bit model, take the gate score
distribution at known recall positions, and set the threshold to the ~10th percentile (target ~90%
recall). raw BGE cosine stays 0.29 (bridge still required).
Residual caveat: in 4-bit, recall < retrieved (the right block is injected but the value is
not always copied). That gap is the 4-bit base model's copy ability, not the recall mechanism
(retrieval hits 100% at β3.5). If it matters, mitigate on the base model (e.g. keep a few layers /
the lm_head at higher precision), not on the heads.
(The recall_q4/ folder on HF holds the 4-bit refit experiment + these numbers; the recommended
heads remain the fp32 ones in ondevice_recall/.)
7. On-device notes
- Base 1.5B is the only heavy part β 4-bit quant (MLX / llama.cpp-Metal / CoreML). MLX
scaffolding exists:
cotscan_live/and*_mlx.py(pooler_mlx, sp_mlx). - GATE = one 4608-dim dot product per rebuild. Negligible.
- BGE = BAAI/bge-base-en-v1.5 (~110M), convert to CoreML/MLX; 4-bit β 30β60 MB. Runs only on a fire, encoding a few short blocks β tens of ms. ~15β25Γ cheaper than recomputing the 1.5B's own keys, which is why BGE-on-demand is the on-device choice.
- Persisted state = token IDs only. No KV / key vectors kept across turns.
- Open item (don't be surprised): per-turn casual false-fire in generation is ~62% at 88% recall (held-out per-position is 3β5%). It is a recall/precision trade, best fixed at runtime, not with more gate data: fire-once-per-turn, hysteresis (require N consecutive fires before injecting), or a relevance gate on the top retrieval score before injecting. Or pick THRESH β β1.2 for 75% recall / 38% false-fire if precision matters more than recall.
8. Minimal port checklist
- Load
fft_hf(4-bit) + tokenizer; expose hidden states /q_projoutputs at layers 8/14/20. - Port
AttnPoolSP(pooler.pt) β SP gist ofkept. - Implement the Β§3 chunked SP loop (window RW=512, blocks of 128, inject between SP and window).
- Gate: load
gate.npz, score per rebuild, fire at THRESHββ1.5β¦β2. - BGE-on-demand: bundle
BAAI/bge-base-en-v1.5(CoreML/MLX) +bge_head.npz; retrieve top-2 on fire; inject block IDs. - Keep only conversation token IDs across turns.
Reference implementation to mirror exactly: git cotscan_live/recall_gen.py β reply() (the
loop) and retrieve_bge() (the retriever), plus recall_runtime/recall_kit/runtime.py.