Memory in the Weights: Teaching a 4B Model New Facts While It Sleeps, Without Teaching It to Lie
A friend lives in Barcelona. He moves to Monaco for university. A year later he calls: he lives in Lisbon now. You do not forget Barcelona; you know it is history. You also do not invent where his sister works just because you know a lot about him. A personal assistant should behave the same way, and today most do it by putting the history back into the prompt every time (retrieval, RAG).
We wanted the other thing: experience that changes the network, not the context. Messages flow in during the day; at night the model consolidates them into its own weights; the next morning it answers from memory. Our hard constraint was honesty: the model must say "I don't know" about anything it was never told, however much it knows about the person around it.
This is a lab note, not a finished result. It covers what we built, what failed, the numbers we have, and what we have not shown yet.
The short version
- Dense memory hallucinates. Training facts into always-on new units reached perfect recall and answered everything: on attributes it was never told, hallucination rose with recall from 0.25 to 1.00.
- A gate that can stay shut keeps the model's own honesty. The frozen model already abstains on what it was never told. Putting each person's facts in their own small expert, behind a learned router that adds exactly zero when the question matches no stored entry, preserved that abstention: 0 errors on people never mentioned, in every run.
- The limit with Qwen3-VL-4B was caution, not invention. The memory held 100 episodes (two seeds; a third failed at the first gated checkpoint), and failed by saying "I don't know" about facts it had learned.
- Spark-X2.6-4B looks better, and is not proven yet. One Spark seed passes the gate at 250 episodes, where every Qwen seed failed. Our pre-registered bar is a median of at least 500 over three seeds; two seeds have not run. The Spark loop also has a stronger teacher, so the comparison is not only the base model.
- It runs on our engine. Training, decoding and the whole sleep loop run in our own Rust engine, on Apple Metal and on NVIDIA CUDA.
Related work and how this differs
Consolidating experience into weights during "sleep" is an active line. Behrouz, Hashemi, Javanmard and Mirrokni, Language Models Need Sleep (2026; on OpenReview since September 2025), consolidate memories by distilling a smaller self into a larger network and then rehearse on synthetic data the model generates ("Dreaming"). Baranov's preprints extract conversational facts while awake and write them into a local 3B model at night with LoRA (Zenodo 18778760), and later inject them with MEMIT and maintain them with null-space-constrained refreshes during sleep (Zenodo 18778768). We share the schedule. Neither measures abstention on questions the model was never told about, which is the axis we optimise.
Our gate is a form of scoped editing. SERAC (Mitchell et al., ICML 2022, 2206.06520) keeps edits outside the network and classifies scope; GRACE (Hartvigsen et al., NeurIPS 2023, 2211.11031) falls back to the frozen model when a hidden state misses a stored key; WISE (Wang et al., NeurIPS 2024, 2405.14768) routes between main and side memories; MELO (Yu et al., AAAI 2024, 2312.11795) enables a LoRA block only on an index hit. Our variant is the predicate: the person's name tokens and the attribute word must select the same stored entry, above a threshold fitted on null questions. We have not compared it head to head with those gates. The storage side is Transformer-Patcher's idea (Huang et al., ICLR 2023, 2301.09785) of adding new MLP units for new facts, here grouped into one expert per person, close to OPPU's per-user adapters (Tan et al., EMNLP 2024, 2402.04401).
That fine-tuning on unknown facts raises hallucination elsewhere is Gekhman et al.'s result (EMNLP 2024, 2405.05904), and training an explicit "I don't know" is R-Tuning (Zhang et al., NAACL 2024, 2311.09677). What we add is a narrower observation inside one person's memory: refusal rows teach only the attributes they cover, a consistency vote accepts a stable fabrication, and an exact-zero gate preserves the frozen model's abstention on attributes that were never labelled either way.
We did not run locate-then-edit methods (ROME, 2202.05262; MEMIT, 2210.07229; AlphaEdit, 2410.02355), and our early one-forward weight writer is a MEND-style hypernetwork (2110.11309) whose stacking failure is the one MALMEN (2311.04661) was built to fix. Test-time memories (Titans, 2501.00663; TTT layers, 2407.04620) and latent episodic stores (Larimar, 2403.11901; MemoryLLM, 2402.04624; M+, 2502.00592) keep one shared store; we keep private experts that can be silenced. Complementary learning systems theory (McClelland, McNaughton and O'Reilly, 1995) assumes the fast trace can be dropped after replay; our teacher still rereads the raw log, which is the main thing left to fix.
The architecture
During the day the model only reads. At night, for every person mentioned that day, a teacher (the same frozen model, thinking) reads everything it was told about that person and writes a structured timeline: current values, history, unconfirmed reports, removals. The timeline becomes question–answer rows; the rows train that person's expert. At answer time a router decides whether any expert applies.
flowchart LR
subgraph Day["Day (awake)"]
M["messages
(user, emails, rumours)"] --> L[("episode log")]
end
subgraph Night["Night (sleep), per person mentioned today"]
L --> T["teacher: frozen 4B, thinking
+ one repair turn"]
T --> J["timeline JSON
value · status · history"]
J --> R["training rows
facts · history · 'I don't know' rows"]
R --> E["that person's expert
16 new units in each of 36 MLPs"]
B[("replay bank: other people's latest rows,
each trained into its own expert while it is trainable")] --> E
end
subgraph Answer["Any time (answering)"]
Q["question"] --> K{"learned two-key router
name key AND attribute key
above a null-fitted threshold?"}
K -- "yes" --> X["frozen 4B + that expert"] --> A["answer from memory"]
K -- "no: exact zero" --> F["frozen 4B alone"] --> N["answers from pretraining,
or 'I don't know'"]
end
E -.-> X- Per-entity experts. Each person (or project, or "me") gets 16 new gated units in each of the 36 MLP blocks: 4.42M parameters per expert on both models we used (both have a hidden size of 2,560). Output weights start at zero, so a new expert changes nothing until trained. Experts touched in the last few nights are trainable; older ones are frozen and their replayed rows no longer change them.
- A learned two-key router. At layer 6, the router builds one key from the person's first- and last-name tokens and one from the attribute word. An expert opens only if both keys select the same stored entry above a threshold set at the 0.99 quantile of null questions (people never mentioned, attributes never stored). Evaluation questions are never used to fit it. Below the threshold the expert's contribution is exactly zero.
- "I don't know" rows. For attributes a person was never told about, the expert is trained to abstain, except for three held-out attributes (hobby, car, deadline) that are never trained either way. That way the honesty test cannot be passed by memorising refusals.
How we measure
A simulator generates a life: 2,000 episodes over 40 days about invented people and projects, with moves, job changes, marriages, corrections ("sorry, she works at X, not Y"), removals ("he sold the car"), and rumours that must not overwrite confirmed facts. At checkpoints we ask questions the model can and cannot answer:
| kind | a correct answer |
|---|---|
| current, corrected, removal | the current value (or "none") |
| history | the previous value |
| country | derived from the city (the simulator's cities are real, so the frozen model can also do this from pretraining) |
| rumour | hedged, never stated as fact |
| unknown: withheld | "I don't know" (attribute never mentioned and never trained either way) |
| unknown: trained | "I don't know" (attribute of a known person, never stored) |
| unknown: never seen | "I don't know" (person never mentioned) |
K is the share of known-fact questions (current, corrected, removal) answered correctly. CR is K divided by the K of a retrieval oracle: the same frozen model with every message about that person in its prompt. CR above 1.0 means the memory beat that oracle, which can also happen when the oracle struggles with long, superseded histories, so we print the oracle's K beside it.
The gate, fixed before the runs, applies from checkpoint 100:
- CR ≥ 0.80;
- CR on the oldest quarter of facts ≥ 0.60;
- hallucination ≤ 0.05 in each unknown kind, counted exactly (one error in 20 passes);
- general knowledge retained ≥ 0.95.
Because general questions never opened an expert in any run, that last bar restates the frozen model's own score (Qwen 30/30, Spark 27/30 on our 30 questions).
Capacity is the last checkpoint that passes before a failure. For the Spark runs, a failure must be confirmed at the next checkpoint. The Qwen runs used the single-failure rule.
We score four answer policies:
- greedy;
- a 3-of-5 vote: answer only if three of five answers, sampled at temperature 0.7, agree;
- router-gated greedy: answer only if the router opened that person and attribute;
- router-gated with a check that the value is one this person ever had.
For Spark we raised the unknown questions to 60 per kind where the simulated state holds that many, so the 0.05 bar is three errors wide, not one.
What failed, and what it taught us
Dense memory. Our first sleep design trained the facts into new units that were always on. Recall reached 1.00. Hallucination on withheld attributes rose in step with recall, from 0.25 to 0.55 to 1.00: the model learned that questions about these people have confident answers, and gave them for car models and hobbies it had never heard of. More "I don't know" rows taught refusals only for the attributes they covered. A 3-of-5 vote did not help, because the invented answers were stable, not noisy.
Self-recall as the teacher. In one experiment, letting the model consolidate from its own recall instead of the log produced a confabulation that consolidation then made permanent: a person "born in Braga" who never was. Our rule since: recall must never raise the epistemic status of a fact; only a new source may.
Writing weights in one forward pass. A MEND-style hypernetwork that emits a weight patch from one document recovered 0.54–0.62 of the improvement that 16 gradient steps on the same document give, without measurable damage on unrelated text. Summing several such patches into one network diverged, the failure MALMEN describes.
Router keys. Mean-pooled name keys diluted. Last-token keys let near-miss names in (a stored first name with someone else's surname). Requiring both name tokens to match the same entity closed it.
Results with Qwen3-VL-4B: honest, but over-cautious
The learned router was the first configuration to pass the full gate, at checkpoint 100. We then ran 40 simulated days on two seeds and both backends:
| run | capacity | first failure |
|---|---|---|
| Mac (Metal), seed 0 | 100 | 250: recall under the vote |
| CUDA, seed 0 | 100 | 250: recall under the vote |
| CUDA, seed 1 | 0 | 100: recall and oldest facts |
Under the vote, hallucination stayed at 0 in every unknown kind at every checkpoint, with 20 questions per kind. The failure was the opposite one: the model said "I don't know" about facts it had learned. We kept seed 1 running past its failure to see the trend. By checkpoint 1,500, greedy CR was 0.90 and vote CR was 0.76, and the vote abstained on 31 % of taught facts. The same pattern shows earlier in seed 0's Mac run: at checkpoint 500, of the facts 250–999 episodes old, 17 were answered right, 16 abstained and 1 wrong.
Consolidated memories flatten with age: still the most likely answer, no longer the same answer five times in a row. Sampling agreement is a poor confidence signal for them; the router is a better one. Re-scoring the stored answers offline with router-gated greedy kept greedy's recall (0.07–0.11 above the vote at checkpoints 250 and 500, depending on seed) and removed almost all of greedy's hallucination. Its residual error was the router opening on one or two never-stored attributes of known people per 20 questions, which is the whole width of a 0.05 bar at that sample size. That re-score was exploratory and is not a capacity result.
Switching the base: Spark-X2.6-4B
We started on Spark-X2.6-4B on 21 October; the port and every Spark run in this note took the two days since. First, a screen of the frozen model on the same tasks, run in PyTorch:
| frozen model | Qwen3-VL-4B | Spark-X2.6-4B |
|---|---|---|
| teacher: simulator facts recalled, days 1–5 (of 412) | 340 (0.825) | 351 (0.852) |
| teacher: invented facts (count) | 11 | 8 |
| teacher: history kept (of 126) | 112 (0.889) | 97 (0.770) |
| bare model invents on unknowns (withheld / trained / never seen) | 0.05 / 0.05 / 0.03 | 0 / 0 / 0 |
| in-context (RAG) K, checkpoint 100 | 0.92 | 0.96–0.98 |
A second probe fitted the router's keys on each model's hidden states without any training. Spark's keys missed a known fact 0.00 / 0.00 / 0.03 of the time at checkpoints 100 / 500 / 2000, against Qwen's 0.09 / 0.07 / 0.09.
Spark's first teacher runs failed on format, not knowledge. Without thinking it wrote unparseable timelines; with full thinking it ran out of budget. Low-effort thinking with a 6,000-token budget plus one repair turn ("Output only the JSON object now") gave 0 parse failures and the better recall above. That cost 378k generated tokens against Qwen's 18k for those five days. Sleep is offline, so we accepted it.
One more teacher rule came during the 40-day runs. On one seed, 11 of 31 thinking answers over two days exhausted the budget without closing their reasoning. A half-written JSON draft inside the reasoning would sometimes parse and be accepted, and which answers ran out depended on tiny numeric differences in batched decoding. On one run and day the teacher kept 47 of 63 facts that way; on the same simulated day another run kept 62. Now every answer whose thinking did not finish goes to the repair turn. On a different day's cached answers, that took one seed from 35 to 38 of 39 facts. All Spark results below use this rule, which Qwen never had: they compare the base model and the teacher.
Spark, 2-day test (seed 0), checkpoint 100, Mac and CUDA:
| Spark (Mac / CUDA) | Qwen3-VL, same checkpoint | |
|---|---|---|
| CR, 3-of-5 vote | 0.98 / 0.96 | 0.71–0.89 |
| vote abstains on taught facts | 4 % / 4 % | 23–26 % |
| CR, router-gated | 1.00 / 0.98 | 0.78–0.91 |
| hallucination, router-gated (withheld, trained, never seen) | 0/23, 1/44, 0/60 | 0/20, 0/20, 0/20 |
| known facts the router failed to open | 0 of 50 | about 9 % |
The CUDA run reproduces the Mac run within our parity bars (0.10 on rates); they are not bit-identical.
Spark, 40-day run, seed 1, checkpoint 250, where every Qwen seed had failed:
| greedy | 3-of-5 vote | router-gated (and value-check, identical) | |
|---|---|---|---|
| CR (K = 0.98, oracle K = 0.95) / oldest-quarter CR | 1.03 / 1.08 | 0.93 / 0.85 | 1.03 / 1.08 |
| hallucination (withheld, trained, never seen) | 0/13, 1/42, 0/60 | 0, 0, 0 | 0/13, 1/42, 0/60 |
| abstains on taught facts | — | 10 % | 1 % |
The teacher kept 75/75, 85/87, 92/93 and 117/119 facts on days 2–5. The gate passes under all four policies.
The small denominators matter. With 13 withheld questions, zero errors is still consistent with a true rate near 0.2, and one error would have failed that kind. We report per-kind results for history, rumours, corrections and countries when the three seeds finish; this note does not claim them.
The engine: our Rust engine on Metal and CUDA
The research ran in Python first, as an oracle; those runs ran out of memory at day 11. Everything above now runs in our own Rust engine, where every kernel comes from generators and every execution goes through one genome → schedule → execute pipeline, on Apple Metal and on NVIDIA CUDA.
flowchart TB W["Spark / Qwen weights
(safetensors)"] --> G["genome
semantics discovered from the checkpoint"] G --> H["HEFT schedule"] H --> EX["executor"] EX --> MT["Metal backend
(Apple M-series)"] EX --> CU["CUDA backend
(RTX 3090)"] subgraph S6["sleep loop (backend-generic)"] TR["GpuTrainer: packed rows,
per-expert units, router"] DE["batched decode,
KV cache on device"] end MT --- S6 CU --- S6
What mattered for speed was structure, not kernel tweaks:
- packing variable-length rows: 2.1×;
- grouping per-expert work into one dispatch;
- batched decoding with one prefill per prompt: 5–6× on evaluation;
- keeping the KV cache on the device. On CUDA this took a day's checkpoint evaluation from 8,554 s to 139 s, and the Qwen teacher from 548 s to 7 s.
Training runs at about PyTorch's speed, not faster. Its correctness is checked by 20-step loss comparisons, not over the long runs. Spark training on the Mac ends 0.034 nats above PyTorch after 20 steps. The engine's own F32 trainer tracks PyTorch F32 within 0.008, so that gap is BF16 rounding rather than a wiring error; the comparison is still recorded as a miss against its 0.02 bar.
Porting Spark meant sliding-window attention on 27 of 36 layers, partial rotary embeddings, head dimension 256, a per-head output gate and a GELU-gated MLP, each with its backward pass and its own check against PyTorch.
Long runs found faults that short checks did not:
- a CUDA KV cache that round-tripped through the host on every token;
- padded batches that ran out of memory where packed ones fit;
- weights read from device buffers still held by pending GPU work;
- Metal kernels that shared one parameter block across two dispatches, breaking only when the backend committed work between them;
- an activation saved during decoding that leaked memory for hours;
- a power loss on the training machine that left a saved state with the right sizes and broken contents. A resumed run trained from it (day-2 loss 0.744 instead of 0.039).
Each now has a guard:
- a realistic preflight before every run;
- teacher answers cached, so a rerun skips straight to training;
- saved state flushed to disk with a checksum checked on resume, plus rolling snapshots.
What we have not shown
- Capacity on Spark. One seed is past 250 episodes; two have not run. The training machine also crashed twice while two runs shared it, cause unknown. The runs now go one at a time.
- Which change mattered. Spark arrived together with a stronger teacher; we have not separated the two.
- Generality. Everything is keyed on named entities with a fixed set of attributes. Concepts, events, procedures and relations need content-addressed keys, not name tokens.
- A memory that lives only in the network. The teacher re-reads every earlier message about a person each night, so the raw log is a hidden external store and the cost grows with history.
- Efficiency. A 4.4M-parameter expert per person stores a few hundred bytes of facts.
- Continuous learning. Sleep is one batch per simulated day. The design we want interleaves fast writes while awake with many short consolidation steps.
What comes next
- Finish the three Spark seeds and report capacity, with per-kind results, against the pre-registered bar.
- Separate base model from teacher: Spark student with the Qwen teacher, and the reverse.
- A log-free teacher: consolidate from the previous night's distilled timeline plus today's messages, and delete the raw messages after their night.
- A compressed, content-addressed memory: a small latent per memory slot, written in one forward pass and read through the same kind of thresholded gate, so "I don't know" stays the default for anything that matches nothing.