MLX
Joblib
Safetensors
English
reasoning
chain-of-thought
context-compression
soft-prompt
apple-silicon
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
- Atomic Chat
File size: 12,267 Bytes
b5989f0 | 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 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | # 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_proj` output (pre-RoPE) of the last
position at layers 8/14/20; concat in layer order β 4608-vector.
- Once `rec_emb` is set it stays injected for the rest of the turn (re-evaluate per turn).
- `pooler` = AttnPoolSP (`load_pooler("pooler.pt")`); it maps the embedded `kept` tokens to a
short SP gist. Port from `hypernet_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_function` output 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
1. Load `fft_hf` (4-bit) + tokenizer; expose hidden states / `q_proj` outputs at layers 8/14/20.
2. Port `AttnPoolSP` (pooler.pt) β SP gist of `kept`.
3. Implement the Β§3 chunked SP loop (window RW=512, blocks of 128, inject between SP and window).
4. Gate: load `gate.npz`, score per rebuild, fire at THRESHββ1.5β¦β2.
5. BGE-on-demand: bundle `BAAI/bge-base-en-v1.5` (CoreML/MLX) + `bge_head.npz`; retrieve top-2 on
fire; inject block IDs.
6. 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`.
|