moncefem Claude Opus 5 commited on
Commit
ef8744f
Β·
1 Parent(s): 75b487a

Rewrite the model card

Browse files

The old README was a 581-line internal onboarding document; it now lives at
docs/DEEP_DIVE.md. The card that replaces it opens with what the model does and
how to use it, adds HF frontmatter (license, base_model, library_name, tags),
and documents the parts a user cannot guess:

- the head expects the specific 6-view 12288-d embedding and standardises its
input internally, so callers must pass RAW embeddings
- the emitted (A,B) follow PEFT's LoRA convention exactly
- which checkpoint to use and why the removed ones were removed

Results are reported as deltas against the frozen base on identical data,
because absolute losses across differing eval sets are not comparable -- the
confusion that hid a checkpoint scoring worse than no adapter at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Files changed (2) hide show
  1. README.md +165 -507
  2. docs/DEEP_DIVE.md +581 -0
README.md CHANGED
@@ -1,581 +1,239 @@
1
- # Memory-LoRA: A Hypernetwork that Writes Repo-Specific Adapters for Gemma-4-E2B
2
-
3
- > **One line:** we train a small neural network (a *hypernetwork*) that reads an
4
- > embedding of a codebase and **emits a LoRA adapter** for a frozen
5
- > `google/gemma-4-E2B`. The adapter injects repo-specific knowledge into the
6
- > model with **zero extra tokens at inference time** β€” no RAG, no context stuffing.
7
- > Everything runs **locally on an Apple-Silicon Mac** (M4 Pro, 64 GB).
8
-
9
- This document is the onboarding bible for the project. It covers *what* we built,
10
- *why* every non-obvious choice was made, *what data* we used, *every experiment we
11
- ran and its result*, and the **hard-won lessons** (the MPS memory leak alone cost
12
- us hours). Read it top to bottom once; after that use it as a reference.
13
-
14
  ---
15
-
16
- ## Table of Contents
17
-
18
- 1. [The idea in 60 seconds](#1-the-idea-in-60-seconds)
19
- 2. [Origin: the Code2LoRA paper](#2-origin-the-code2lora-paper)
20
- 3. [Architecture](#3-architecture)
21
- 4. [The target model: Gemma-4-E2B specifics](#4-the-target-model-gemma-4-e2b-specifics)
22
- 5. [Data pipeline: the 6 views](#5-data-pipeline-the-6-views)
23
- 6. [Data pipeline: QA generation](#6-data-pipeline-qa-generation)
24
- 7. [What is learnable β€” Tier A / B / C](#7-what-is-learnable--tier-a--b--c)
25
- 8. [Datasets inventory](#8-datasets-inventory)
26
- 9. [Experiments & results](#9-experiments--results)
27
- 10. [Key decisions & lessons learned](#10-key-decisions--lessons-learned)
28
- 11. [Repository map](#11-repository-map)
29
- 12. [How to run it](#12-how-to-run-it)
30
- 13. [Evaluation methodology](#13-evaluation-methodology)
31
- 14. [Costs & budget discipline](#14-costs--budget-discipline)
32
- 15. [Current status & roadmap](#15-current-status--roadmap)
33
- 16. [Glossary](#16-glossary)
34
-
35
  ---
36
 
37
- ## 1. The idea in 60 seconds
38
-
39
- A coding agent (Claude Code, Codex, etc.) is great at reasoning but knows nothing
40
- about *your* repo until you paste files into its context β€” which is slow, expensive,
41
- and capped by the context window. The usual fix is RAG (retrieve chunks at query
42
- time). We do something different and complementary:
43
 
44
- **We bake the repo's "personality" directly into the model's weights, once, as a
45
- LoRA adapter β€” and we generate that adapter with a neural network instead of
46
- training it.**
47
 
48
  ```
49
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
50
- repo on disk ──────► β”‚ 6-view extractor + Qwen3 β”‚ ──► 12288-d
51
- β”‚ frozen embedding encoder β”‚ repo embedding
52
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
53
- β–Ό
54
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
55
- β”‚ HYPERNETWORK (our head) β”‚
56
- β”‚ MLP β†’ per-module A,B β”‚
57
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
58
- β”‚ LoRA weights
59
- β–Ό
60
- Q: "what layer owns auth in this repo?" ──► β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
61
- β”‚ FROZEN Gemma-4-E2B + injected β”‚ ──► "the middleware
62
- β”‚ LoRA (zero extra tokens) β”‚ layer, via ..."
63
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
64
  ```
65
 
66
- The magic: **the hypernetwork is trained across hundreds of repos**, so it learns
67
- the *mapping* `repo embedding β†’ good adapter`. At inference on a brand-new repo it
68
- has never seen, it embeds the repo once and produces an adapter in a single forward
69
- pass. This is the same reason the source paper needed 400+ repos, not 1: breadth is
70
- what makes the mapping generalize.
71
 
72
- ---
73
-
74
- ## 2. Origin: the Code2LoRA paper
75
-
76
- We reverse-engineered **Code2LoRA** (arXiv 2606.06492v1) and found its released
77
- code (`anonymous.4open.science/r/code2lora-6857`, MIT). The paper's contribution:
78
- a *static hypernetwork* that maps a **repository** embedding β†’ a LoRA adapter for a
79
- frozen code LLM, evaluated on **RepoPeftBench** with IR (in-repo) / CR (cross-repo)
80
- splits. On a full H100 setup they report **63.8 % cross-repo Exact Match**.
81
 
82
- Our project is the **Doc2LoRA variant** the paper itself cites β€” hypernetwork maps a
83
- *document/repo view* β†’ LoRA β€” reimplemented against **Gemma-4-E2B**, trained fully
84
- **locally on MPS** (no CUDA/H100), and extended in two directions the paper does not
85
- cover:
86
-
87
- - **Memory / recall**: the adapter should let the model *recall facts* about the
88
- repo, not just complete code.
89
- - **Tech-Lead judgment**: architecture, data-flow, conventions, contracts, ops β€”
90
- the things a 20-year senior engineer "just knows" about a codebase.
91
-
92
- We keep the paper's proven autograd trick almost verbatim (see Β§3) and change only
93
- what Gemma-4 and Apple Silicon force us to change.
94
 
95
  ---
96
 
97
- ## 3. Architecture
98
-
99
- Three frozen/learned pieces. Only the middle one (the head) is trained.
100
 
101
- ### 3.1 Frozen encoder β€” `memory_lora/encoder.py`
 
 
 
 
 
 
 
102
 
103
- - **Qwen3-Embedding-0.6B**, frozen, no gradient flows through it.
104
- - Each repo view is chunked into token windows (2048 tokens, 128 overlap), each
105
- chunk mean-pooled, then chunks combined with **mean + max pooling** β†’ a **2048-d**
106
- vector *per view*.
107
- - Embeddings are **precomputed once and cached** to parquet β€” the encoder never runs
108
- during training.
109
 
110
- ### 3.2 The hypernetwork head β€” `memory_lora/core.py :: MemoryLoRAHead`
111
 
112
- The only trained component. Design (kept close to the paper):
113
 
114
- - **2-layer GELU MLP trunk** (`input_dim β†’ hidden_dim β†’ hidden_dim`), followed by
115
- **L2-normalize + √hidden_dim rescale** (stabilizes the magnitude of generated
116
- weights).
117
- - **Per-module-type output heads**: for each target module *type* it emits an
118
- `A ∈ [rank, in_features]` and `B ∈ [out_features, rank]`. **One (A,B) pair per
119
- type, shared across all layers of that type** β€” this is what keeps the head
120
- tractable (188.6 M params) instead of exploding per-layer.
121
- - **Squashing**: `tanh(raw) * exp(log_scale)` with a learned per-type `log_scale`
122
- (init **-3.5**). This starts the generated adapter near-zero (so training begins
123
- close to the base model) and lets each type learn its own output scale.
124
- - **Defaults**: `hidden_dim=128`, `rank=16`, `dropout=0.1`.
125
- - *Why hidden_dim=128 and not the paper's 512/1024?* A 745 M-param head
126
- (hidden_dim=512) barely moved eval loss (~1.9 β†’ ~2.7) but was far heavier to
127
- train on MPS. 128 cuts head size dramatically with negligible quality loss
128
- locally. Bump it later on real GPUs.
129
 
130
- ### 3.3 The LoRA injection β€” `memory_lora/core.py :: LoRA`
 
 
 
 
131
 
132
- ```
133
- base nn.Linear (FROZEN) hypernetwork output
134
- β”‚ β”‚
135
- x ──►│ Wx ────────────────┐ β”‚
136
- β”‚ (input detached +──► y = Wx + scaling Β· B (A x)
137
- β”‚ into base) β”‚ β–² β–²
138
- x β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
139
- A,B are NON-detached tensors so
140
- autograd flows LM-loss β†’ head
141
  ```
142
 
143
- Critical detail (straight from the paper's code): **A and B are plain, non-buffer
144
- tensor attributes, not `nn.Parameter` and not detached**, so the gradient of the LM
145
- loss flows *through* the injected weights *into the hypernetwork*. The base
146
- `nn.Linear` is frozen and its input is detached. Get this wrong and the head never
147
- learns.
148
 
149
- ### 3.4 Shape-qualified module types β€” the heterogeneity fix
 
 
 
150
 
151
- Gemma-4-E2B is **not** a uniform stack (see Β§4). Two `q_proj`s can have different
152
- shapes. If you key the head by bare type name (`q_proj`) you get
153
- *"type q_proj inconsistent dims"* crashes. Fix: key by **shape-qualified type**,
154
- e.g. `q_proj_1536x2048` vs `q_proj_1536x4096`. The v2 run discovered **14 shape
155
- types** across **205 target modules**:
156
-
157
- ```
158
- down_proj_12288x1536 down_proj_6144x1536 gate_proj_1536x12288 gate_proj_1536x6144
159
- k_proj_1536x256 k_proj_1536x512 o_proj_2048x1536 o_proj_4096x1536
160
- q_proj_1536x2048 q_proj_1536x4096 up_proj_1536x12288 up_proj_1536x6144
161
- v_proj_1536x256 v_proj_1536x512
162
  ```
163
 
164
- `get_module_specs(root_prefix="model.language_model.")` restricts wrapping to the
165
- text decoder β€” the **vision and audio towers are never touched** (not even
166
- inspected), so the multimodal forward path stays intact and they cost only idle RAM.
167
-
168
- ---
169
-
170
- ## 4. The target model: Gemma-4-E2B specifics
171
-
172
- Verified by reading the actual safetensors header, not guessed:
173
-
174
- - **Real model.** Google shipped Gemma 4 in March 2026. Apache-2.0, ungated.
175
- Class `Gemma4ForConditionalGeneration`, `model_type: "gemma4"`. Loaded via
176
- `AutoModelForImageTextToText`.
177
- - **Requires `transformers >= 5.5.0.dev0`** β€” install from the `main` branch, not a
178
- pinned PyPI release (this is the single biggest environment risk; smoke-test first).
179
- - **Decoder is nested**: layers live at `model.language_model.layers.{i}.*`, *not*
180
- `model.layers.*`. The layer-index regex had to change accordingly.
181
- - **35 text layers, heterogeneous:**
182
- - Aggressive KV sharing β€” **20 of 35 layers lack their own `k_proj`/`v_proj`**
183
- (`num_kv_shared_layers=20`).
184
- - **Every 5th layer is wider** (the `*_4096`, `*_12288` shape variants above).
185
- - **Device `mps`, precision bf16** (fall back to fp16 if unstable). No
186
- `flash_attention_2` on MPS β€” use `sdpa`, fall back to `eager`.
187
-
188
- ---
189
-
190
- ## 5. Data pipeline: the 6 views
191
-
192
- `scripts/build_repo_multiview.py` clones a repo and extracts **6 complementary
193
- views**, embeds each with Qwen3 β†’ 2048-d, and **concatenates to a 12288-d** repo
194
- vector. The views encode the different "lenses" a senior engineer uses:
195
-
196
- | View | What it captures | Source signals |
197
- |-----------------|----------------------------------------------------|----------------|
198
- | `v_graph` | call / import / dependency structure | AST for Python (`memory_lora/codegraph.py`); `IMPORT_RE`/`DEF_RE` regex fallback for other languages |
199
- | `v_arch` | architecture & layout | README, folder tree |
200
- | `v_history` | how the code evolved | `git log`, recent diffs |
201
- | `v_contracts` | behavioral contracts | test files |
202
- | `v_conventions` | idioms & style | representative source files |
203
- | `v_ops` | build / deploy / runtime | CI config, Dockerfile, build files |
204
-
205
- **Multi-language from the start.** `CODE_EXTS` + regex fallbacks mean the graph view
206
- works for 9 languages, not just Python (this was a deliberate correction β€” see Β§10).
207
- Repos with < 3 code files are skipped. The build is **resume-safe** (skips repos
208
- already in `multiview_sources.jsonl`) and **flushes the embeddings parquet every 10
209
- repos**, so a crash never loses more than 10 repos of work.
210
-
211
- ---
212
-
213
- ## 6. Data pipeline: QA generation
214
-
215
- The repo embedding is the *input*; the *target* is repo-scoped Q&A. Two generators,
216
- both parallelized (`ThreadPoolExecutor`, `--workers 10`) with a **per-prompt disk
217
- cache** (idempotent reruns) and a `--model` flag:
218
-
219
- - **`scripts/generate_repo_scoped_qa.py`** β€” reads the same 6 views and asks the LLM
220
- for **8–12 repo-level judgment questions** ("what layer owns X", "what convention
221
- does this repo use for Y", "how does data flow through Z", "why is it structured
222
- this way"). Target scope = input scope (repo-level embedding οΏ½οΏ½ repo-level QA).
223
- - **`scripts/generate_commitpack_qa.py`** β€” **breadth** generator: one commit per
224
- *distinct* repo across CommitPackFT (25k+ distinct repos), 3–4 commit-scoped
225
- judgment questions (why / conventions / contracts / impact). For a hypernetwork,
226
- **distinct-repo count is the currency of generalization**, so we favor 1 commit Γ—
227
- many repos over many commits Γ— one repo.
228
-
229
- **Discipline (both):** answers are **short judgment**, never file-path/line-number
230
- lists. This is deliberate β€” see Tier A/B/C next.
231
-
232
- **Models used (OpenRouter, OpenAI-compatible API):**
233
 
234
- | Model | Role | Notes | Cost |
235
- |-------|------|-------|------|
236
- | `google/gemini-3.6-flash` | high-quality QA | **reasoning is mandatory** β†’ needs generous `max_tokens` (3000–4000) or it returns empty | ~$0.0021 / QA |
237
- | `google/gemma-4-31b-it` | bulk / cheap QA | non-reasoning, clean JSON | ~$0.001 / repo (~$1 per 1000 repos) |
238
-
239
- The OpenRouter key lives in a **git-ignored `.env` (mode 600)** and is never pasted
240
- into a command line.
241
-
242
- ---
243
-
244
- ## 7. What is learnable β€” Tier A / B / C
245
-
246
- A LoRA adapter has finite capacity. We classify repo knowledge by whether a LoRA can
247
- hold it β€” this drives the entire QA design:
248
-
249
- - **Tier A β€” Judgment & conventions** (LEARNABLE). "This repo puts business logic in
250
- services, not views." Compressible, generalizes. β†’ **This is what we train on.**
251
- - **Tier B β€” Structural gist** (LEARNABLE). "Auth flows through middleware." The kind
252
- of thing, not the exact file.
253
- - **Tier C β€” Exact recall & multi-hop** (NOT reliably learnable). "Line 412 of
254
- `foo.py` calls `bar()`." This needs **retrieval (RAG)**, not weights.
255
-
256
- So Memory-LoRA and RAG are **complementary**: the adapter carries Tier A/B judgment
257
- for free (zero tokens); RAG handles Tier C exact lookups. The QA prompts forbid exact
258
- file/line answers precisely so we never ask the LoRA to do a job it structurally
259
- can't.
260
-
261
- ---
262
-
263
- ## 8. Datasets inventory
264
-
265
- Everything lives under `data/` (git-ignored blobs). Sizes are approximate.
266
-
267
- | Path | What | Scale |
268
- |------|------|-------|
269
- | `data/real_code2lora/` | **RepoPeftBench** from the `code2lora` HF org β€” 500 Python repos, repo-commit embeddings + diffs | 73,849 repo-commit rows; ~1.2 GB |
270
- | `data/commitpack/multilang_commits.jsonl` | **CommitPackFT** shards, 9 languages | 25k+ distinct repos |
271
- | `data/docs/multiview_sources.jsonl` | 6-view `view_text` per repo (input to QA gen) | growing (1000s of repos) |
272
- | `data/embeddings/multiview_embeddings.parquet` | 12288-d multi-view repo embeddings | 1000+ repos |
273
- | `data/embeddings/aligned6_embeddings.parquet` | **assembled training inputs** (repos with β‰₯1 QA) | 1058 repos (current) |
274
- | `data/qna/repo_scoped_qa.jsonl` | repo-level judgment QA | 11,232 QA |
275
- | `data/qna/techlead_qa_commitpack.jsonl` | commit-scoped breadth QA | 9,245 QA |
276
- | `data/qna/techlead_qa.jsonl` | SWE-bench tech-lead QA | 2,786 QA |
277
- | `data/qna/aligned6_qna.jsonl` | **assembled training targets** | 8,540 QA (current) |
278
- | `data/openrouter_cache/` | per-prompt response cache | ~19 MB |
279
-
280
- **Language balancing.** SWE-bench is ~79 % Django. Left alone, the dataset was 46 %
281
- Django. `scripts/consolidate_qa.py` applies a **per-repo cap** (default 12–15 QA/repo)
282
- which collapses Django to **~2.0 %** while preserving the 2400+ distinct repos'
283
- diversity. `assemble_6view_dataset.py` applies the same cap when building the final
284
- aligned set.
285
-
286
- ---
287
-
288
- ## 9. Experiments & results
289
-
290
- Chronological, with the actual numbers we measured. Two families of runs.
291
-
292
- ### 9.1 Reproducing the paper (single-view, real RepoPeftBench)
293
-
294
- | Run | What | Result |
295
- |-----|------|--------|
296
- | `full1` (early) | first end-to-end hypernetwork on converted real data | CR **EM 0.056–0.083**, EditSim ~0.27 β€” pipeline works, undertrained |
297
- | `sixview`/converted-real (best single-view ckpt) | after more training | **CR EM 0.524, EditSim 0.635** |
298
- | Paper (reference, H100) | their full run | CR EM **0.638** |
299
-
300
- **Headline:** on real code, after only ~2.4 % of one epoch of local MPS training, we
301
- reached **52.4 % cross-repo Exact Match** vs the paper's 63.8 % on a full H100 setup.
302
- The mechanism demonstrably works β€” the generated adapter recovers repo-specific
303
- identifiers the base model does not know.
304
-
305
- ### 9.2 The 6-view Tech-Lead model (the current line of work)
306
-
307
- Loss is causal-LM cross-entropy on QA targets; lower is better. Three eval suites:
308
- `cr_val` / `cr_test` (held-out *repos*) and `ir_test` (held-out *QA* of train repos).
309
-
310
- | Run | Dataset | Best held-out `cr_test` loss | Notes |
311
- |-----|---------|------------------------------|-------|
312
- | `sixview_v1` | 515 repos / 3,988 QA (415 train repos) | **2.848** (step ~1060) | Overfit afterward: train loss fell to 1.75 while `cr_test` drifted to 3.35. Classic small-dataset ceiling. |
313
- | `sixview_v2` | **1,058 repos / 8,540 QA (858 train repos)** | *in progress* | Resumed from `sixview_v1/head.best.pt`; 2Γ— the data specifically to break v1's ceiling. |
314
-
315
- `sixview_v1` metrics trajectory (from `runs/sixview_v1/metrics.jsonl`):
316
-
317
- ```
318
- step 1245 cr_test 2.962 ir_test 2.535 (end of epoch 2 β€” near best)
319
- step 1400 cr_test 3.245 ir_test 2.593 (overfitting begins)
320
- step 1600 cr_test 3.352 ir_test 2.655 (train loss still falling β†’ ceiling hit)
321
  ```
322
 
323
- The v1β†’v2 story is the core empirical lesson: **the small aligned set was the
324
- bottleneck, not the architecture** β€” hence the push to build 1000+ more repos.
325
 
326
- ---
327
 
328
- ## 10. Key decisions & lessons learned
 
 
329
 
330
- The expensive knowledge. Read this section twice.
 
 
331
 
332
- ### 10.1 ⚠️ The MPS gradient-checkpointing memory leak (the big one)
 
 
 
333
 
334
- **Symptom:** training with `gradient_checkpointing_enable()` (`use_reentrant=False`)
335
- **leaked ~12 GB per step** and OOM'd the whole machine within a few steps.
336
 
337
- **Diagnosis** (`scripts/diag_mps_leak.py`): forward-only was stable; train + checkpoint
338
- leaked 39 GB β†’ 18 GB free in 2 steps. Isolated the checkpointing path as the cause.
339
 
340
- **Fix:** **`--no-gradient-checkpointing`.** We have enough unified memory to hold
341
- activations without it once the multimodal towers sit idle. This is documented as a
342
- standing memory (`mps-gradient-checkpointing-leak.md`).
 
343
 
344
- ### 10.2 ⚠️ `psutil` RSS is blind to MPS memory
 
 
 
 
 
345
 
346
- Our first memory safety-net used `psutil` RSS / `ps -o rss` β€” it reported **< 1 GB**
347
- while `top` showed **55–83 GB** actually in use. MPS (GPU) allocations don't show up
348
- in process RSS.
349
 
350
- **Fix:** the safety check uses **`psutil.virtual_memory().available`** (system-wide)
351
- with a `--min-available-gb` floor (default 5). To *observe* MPS memory, use
352
- `top -l 1 -pid <PID> -stats mem`, not `ps`.
 
353
 
354
- ### 10.3 Memory competition between concurrent jobs
355
 
356
- Three concurrent jobs once pushed available memory under the 10 GB floor and training
357
- self-stopped. **Lesson:** during MPS training, run data builds/embedding on **CPU**
358
- (`--device cpu`) so they don't contend for the GPU/unified memory. We now routinely
359
- run training (MPS) + QA gen (network) + multiview build (CPU) together without
360
- contention.
361
 
362
- ### 10.4 Don't lose hours of training
363
 
364
- Every long run writes **checkpoints every 50 steps** (overwriting `head.latest.pt`),
365
- **every 30 minutes** (timestamped `head.tNNNNm.pt`), **per-epoch** (`head.epN.pt`),
366
- and a **`head.best.pt`** on eval improvement. Runs are launched with `nohup … &
367
- disown` so they survive terminal/session death. `sixview_v1` in fact survived a full
368
- session interruption and kept training. Resume with `--resume-from <ckpt>` (loads head
369
- weights; optimizer restarts fresh).
370
 
371
- ### 10.5 Data-quality corrections (user-driven)
 
372
 
373
- - **"I still see lots of Django."** SWE-bench is Django-dominated. β†’ per-repo cap +
374
- multi-language sourcing dropped Django 46 % β†’ 2.0 %.
375
- - **"It must be good for any programming language."** β†’ 9-language diversity via
376
- CommitPackFT and language-agnostic view extraction.
377
- - **"Where's the code context in the QA?"** β†’ clarified the two-channel design: the
378
- **repo embedding is the context channel**, the QA is only the target. They are
379
- joined by `doc_id` at assembly time.
380
 
381
- ### 10.6 OpenRouter gotchas
 
 
 
 
382
 
383
- - `gemini-3.6-flash` **returned empty** until we raised `max_tokens` β€” it's a
384
- mandatory-reasoning model that spends tokens on hidden reasoning before content.
385
- Reasoning **cannot be disabled** (400 error).
386
- - CommitPackFT's HF loader is deprecated β†’ fetch raw `data.jsonl` directly.
387
- - `global MODEL` after use is a `SyntaxError` β†’ set via `globals()["MODEL"] = ...`.
388
 
389
- ### 10.7 Performance fix worth knowing
390
 
391
- Loading embeddings was 5+ min because `_list_to_f32_array` used a Python loop.
392
- Vectorized via `col.combine_chunks().flatten().to_numpy()` β†’ **~220Γ— faster**.
 
 
 
 
 
393
 
394
  ---
395
 
396
- ## 11. Repository map
397
-
398
- ```
399
- memory_lora/ # the library (importable package)
400
- core.py # LoRA wrapper, MemoryLoRAHead hypernetwork,
401
- # get_module_specs / replace_with_lora / inject_lora_weights,
402
- # load_doc_rows / load_qna_rows
403
- encoder.py # Qwen3 chunk + embed + mean/max pool (frozen)
404
- codegraph.py # Python AST extractor (imports, sigs, call graph)
405
- data_paths.py # local parquet/jsonl path resolver
406
-
407
- scripts/
408
- build_repo_multiview.py # clone β†’ 6 views β†’ 12288-d embeddings (multi-language, resume-safe)
409
- generate_repo_scoped_qa.py # repo-level judgment QA (aligned to the 6 views)
410
- generate_commitpack_qa.py # commit-scoped breadth QA across 1000s of distinct repos
411
- generate_techlead_qa.py # SWE-bench tech-lead QA
412
- generate_synthetic_dataset.py # original synthetic doc + QA generator
413
- consolidate_qa.py # per-repo cap β†’ language/domain balancing
414
- assemble_6view_dataset.py # join embeddings ↔ all QA by repo β†’ aligned6_{embeddings,qna}
415
- augment_paraphrases.py # QA paraphrase augmentation
416
- convert_real_code2lora.py # RepoPeftBench β†’ our schema
417
- build_doc_embeddings.py # encoder pass over documents
418
- merge_corpora.py # combine multiple corpora
419
- train_memory_lora.py # THE trainer (MPS, one-repo-per-step, checkpoints, TensorBoard)
420
- train_direct_lora.py # baseline: plain per-repo LoRA (no hypernetwork)
421
- eval_memory_lora.py # EM / EditSim recall eval on cr/ir splits
422
- show_eval_examples.py # dump concrete base-vs-adapted examples
423
- test_embed_this_repo.py # embed the current repo (pipeline demo)
424
- test_recall_this_repo.py # query the adapted model about this repo
425
- diag_mps_leak.py # the memory-leak isolation harness
426
-
427
- data/ # git-ignored: embeddings, qna, sources, caches
428
- runs/ # git-ignored: checkpoints, logs, metrics.jsonl, tb/
429
- requirements.txt # torch 2.13 (MPS), transformers@main, openai, pyarrow, tensorboard…
430
- .env # git-ignored, mode 600: OPENROUTER_API_KEY
431
- ```
432
 
433
- ---
 
 
434
 
435
- ## 12. How to run it
 
 
 
436
 
437
- ### Setup
 
 
 
 
 
438
 
439
- ```bash
440
- python3 -m venv venv && source venv/bin/activate
441
- pip install -r requirements.txt # installs transformers from git main
442
- echo "OPENROUTER_API_KEY=sk-or-..." > .env && chmod 600 .env
443
- ```
444
 
445
- Smoke-test the environment first (gates everything): confirm `transformers` main
446
- loads `google/gemma-4-E2B` on `mps` and runs a text-only forward pass.
 
447
 
448
- ### Build data
449
 
450
  ```bash
451
- # 1) multi-view embeddings for a repo list (CPU to stay off the GPU during training)
452
- python scripts/build_repo_multiview.py \
453
- --repos-file data/multilang_repo_list.txt --max-repos 1000 --device cpu
454
-
455
- # 2) repo-scoped QA (cheap model) β€” appends, resume-safe, cached
456
- ./venv/bin/python scripts/generate_repo_scoped_qa.py \
457
- --model google/gemma-4-31b-it --workers 10
458
-
459
- # 3) balance + assemble the aligned training set
460
- python scripts/consolidate_qa.py --per-repo-cap 12
461
- python scripts/assemble_6view_dataset.py
462
- # -> data/embeddings/aligned6_embeddings.parquet + data/qna/aligned6_qna.jsonl
463
  ```
464
 
465
- ### Train (the exact `sixview_v2` command)
 
 
466
 
467
- ```bash
468
- nohup ./venv/bin/python scripts/train_memory_lora.py --output-dir sixview_v2 \
469
- --resume-from runs/sixview_v1/head.best.pt \
470
- --embeddings-path data/embeddings/aligned6_embeddings.parquet \
471
- --qna-path data/qna/aligned6_qna.jsonl --epochs 100 --max-hours 8 \
472
- --checkpoint-every-steps 50 --checkpoint-every-minutes 30 --epoch-ckpt-every 5 \
473
- --eval-every-steps 300 --limit-eval-docs 40 --max-seq-len 512 --fixed-seq-len \
474
- --max-qna-per-doc 12 --lm-micro-batch 2 --device mps --no-gradient-checkpointing \
475
- --rank 16 --head-hidden-dim 128 --head-dropout 0.1 --weight-decay 0.05 \
476
- --early-stop-patience 25 --lr 8e-5 --lr-total-steps 9000 --min-available-gb 5 \
477
- > runs/sixview_v2_train.log 2>&1 &
478
- disown
479
- ```
480
-
481
- **Flags you must not forget:** `--no-gradient-checkpointing` (the leak),
482
- `--device cpu` for builds during training (contention), `--min-available-gb`
483
- (system-wide memory floor).
484
-
485
- ### Watch it
486
 
487
- ```bash
488
- tensorboard --logdir runs/sixview_v2/tb # train/loss, train/lr, eval/{suite}_loss
489
- tail -f runs/sixview_v2_train.log
490
- ```
491
 
492
- ### Evaluate & inspect
493
 
494
  ```bash
495
- python scripts/eval_memory_lora.py --ckpt runs/sixview_v2/head.best.pt # EM / EditSim
496
- python scripts/show_eval_examples.py # base vs adapted
 
497
  ```
498
 
499
- ---
500
-
501
- ## 13. Evaluation methodology
502
-
503
- - **Splits (deterministic, by `md5(repo) % 100`):** 80 % train / 10 % `cr_val` /
504
- 10 % `cr_test` **by repo**, so cross-repo suites are **entirely held-out
505
- repositories** the hypernetwork never trained on. Within train repos, ~15 % of QA
506
- is held out β†’ `ir_test` (in-repo generalization to unseen questions of seen repos).
507
- - **Metrics:** causal-LM **eval loss** during training (fast, every N steps on
508
- `--limit-eval-docs` docs to stay cheap on CPU), plus generation-time **Exact Match
509
- (EM)** and **EditSim** for the recall eval.
510
- - **The proof spot-check:** query the *adapted* model with repo-specific questions and
511
- confirm the *base* (un-adapted) model gets them wrong/vague β€” proving the
512
- **adapter**, not the base model's pretraining, does the work.
513
-
514
- CPU eval of a float32 5B model is slow (~20 min for a full pass) β†’ we cap eval docs
515
- (e.g. 10–40) for in-loop evals and run full EM eval separately.
516
 
517
  ---
518
 
519
- ## 14. Costs & budget discipline
520
-
521
- - **Spend baseline:** $31.00 (`runs/spend_baseline.txt`); ~**$33.90 total** to date;
522
- ~**$11 remaining**. The project is run under explicit budget caps ("spend at most
523
- $4 more") with spend-guards.
524
- - **Unit economics:** `gemini-3.6-flash` β‰ˆ **$0.0021/QA**; `gemma-4-31b-it` β‰ˆ
525
- **$0.001/repo (~$1 per 1000 repos)** β€” which is exactly why the 1000-repo expansion
526
- uses the gemma model.
527
- - **Free levers:** the per-prompt cache makes reruns free; embedding and training are
528
- local (electricity only).
529
-
530
- ---
531
 
532
- ## 15. Current status & roadmap
533
-
534
- **Live right now (three jobs in parallel, no contention):**
535
-
536
- - **`sixview_v2` training** β€” resumed from `head.best.pt` on the doubled
537
- **1,058-repo / 8,540-QA** dataset (858 steps/epoch), MPS. First held-out eval at
538
- step 300 tells us whether doubling the data broke v1's 2.848 ceiling.
539
- - **QA generation** β€” `gemma-4-31b-it` filling in all ~1,032 new repos for the
540
- **complete** dataset (next training run).
541
- - **Multiview build** β€” cloning/embedding toward the full +1,000-new target (CPU).
542
-
543
- **Roadmap:**
544
-
545
- 1. Finish the complete 1000-new-repo dataset (embeddings + QA).
546
- 2. Assemble the full aligned set (~1,650 repos) and train **`sixview_v3`** on it.
547
- 3. Run generation-time **EM/EditSim** on the 6-view model (base vs adapted).
548
- 4. Push `head_hidden_dim` back up once on real GPUs; the 128 default was an
549
- MPS-locality compromise.
550
- 5. Broaden Tier-A/B QA toward agent-harness use cases (Jira/ticket tracking,
551
- diff/impact reasoning) already scaffolded in `generate_techlead_qa.py`.
552
-
553
- **Open questions:**
554
-
555
- - Does the 12288-d 6-view embedding actually beat the single 2048-d view on
556
- generation EM, or only on loss? (loss says yes; EM eval pending)
557
- - What's the real Tier-B ceiling β€” how much structural gist fits in rank-16?
558
- - Optimal per-repo QA cap for the breadth/depth trade-off.
559
-
560
- ---
561
-
562
- ## 16. Glossary
563
-
564
- - **Hypernetwork** β€” a network that outputs the weights of another network. Here:
565
- repo embedding β†’ LoRA (A,B) matrices.
566
- - **LoRA** β€” Low-Rank Adaptation: `y = Wx + scaling Β· B(Ax)`, with `A,B` low-rank
567
- (rank 16). We *generate* A,B instead of training them per-repo.
568
- - **6 views** β€” graph / arch / history / contracts / conventions / ops; each 2048-d,
569
- concatenated to 12288-d.
570
- - **CR / IR** β€” cross-repo (held-out repos) / in-repo (held-out QA of seen repos).
571
- - **EM / EditSim** β€” Exact Match / edit-distance similarity of generated vs gold.
572
- - **Tier A/B/C** β€” judgment (learnable) / structural gist (learnable) / exact recall
573
- (needs RAG).
574
- - **MPS** β€” Apple's Metal Performance Shaders GPU backend for PyTorch.
575
- - **RepoPeftBench** β€” the paper's benchmark; 500 Python repos, repo-commit
576
- embeddings + diffs, IR/CR splits.
577
-
578
- ---
579
 
580
- *Maintained as living documentation. If you change a default, a path, or a flag,
581
- update the matching section here β€” onboarding depends on it.*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ license: apache-2.0
3
+ base_model: google/gemma-4-E2B
4
+ library_name: peft
5
+ tags:
6
+ - hypernetwork
7
+ - lora
8
+ - code
9
+ - repository-adaptation
10
+ - gemma
11
+ pipeline_tag: text-generation
 
 
 
 
 
 
 
 
 
 
12
  ---
13
 
14
+ # Memory-LoRA β€” a hypernetwork that writes repo-specific LoRA adapters for Gemma-4-E2B
 
 
 
 
 
15
 
16
+ Give it a code repository; it returns a **LoRA adapter for that repository in one
17
+ forward pass**. No fine-tuning, no retrieval, and **zero repository tokens at
18
+ inference time** β€” the knowledge lives in the weights.
19
 
20
  ```
21
+ repo ──► 6-view embedding (12288-d) ──► hypernetwork ──► LoRA adapter ──► frozen Gemma-4-E2B
22
+ frozen Qwen3-Embedding-0.6B 750M params rank 16, Ξ± 32
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  ```
24
 
25
+ On repositories **absent from the training corpus**, the generated adapter makes
26
+ correct answers roughly **200Γ— more likely** than the frozen model (βˆ’5.31 nats)
27
+ and wins **9 of 9** benchmark family/repo combinations.
 
 
28
 
29
+ | question (`pallets/click`, never trained on) | frozen Gemma-4-E2B | + generated adapter |
30
+ |---|---|---|
31
+ | What testing framework does this use? | "Jest" βœ— | **pytest** βœ“ |
32
+ | How is this project built and packaged? | "a Dockerfile" βœ— | **setuptools, setup.py** βœ“ |
33
+ | What documentation tool? | "a Gantt chart" βœ— | **Sphinx** βœ“ |
 
 
 
 
34
 
35
+ Code, demo app, and full engineering write-up:
36
+ **https://github.com/moncifem/memory-lora-gemma4** Β· deep dive in
37
+ [`docs/DEEP_DIVE.md`](docs/DEEP_DIVE.md).
 
 
 
 
 
 
 
 
 
38
 
39
  ---
40
 
41
+ ## What's in this repository
 
 
42
 
43
+ | path | what it is |
44
+ |---|---|
45
+ | `runs/all_lora/all_lora_best_cpt.pth` | **the model.** Merged corpus (2,146 repos / 55,700 QA). `cr_val` 2.6811, `cr_test` 2.6266 |
46
+ | `runs/h200_run/head.best.pt` | prose-QA-only variant, kept as a fallback |
47
+ | `data/embeddings/*.parquet` | precomputed 12288-d repo embeddings |
48
+ | `data/qna/*.jsonl` | training QA corpora |
49
+ | `data/real_code2lora/` | RepoPeftBench (Code2LoRA's benchmark) |
50
+ | `memory_lora/` `scripts/` `app/` `deploy/` | code, mirrored from GitHub |
51
 
52
+ Earlier `sixview_v1` / `sixview_v2` weights were **removed**: they predate the
53
+ input-standardisation fix, and `sixview_v2` measurably *degrades* the base model
54
+ (see below). Their `metrics.jsonl` remain for provenance.
 
 
 
55
 
56
+ ---
57
 
58
+ ## Using the hypernetwork
59
 
60
+ ### 1. Get the pieces
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
+ ```bash
63
+ git lfs install
64
+ git clone https://huggingface.co/moncefem/memory-lora-gemma4
65
+ cd memory-lora-gemma4
66
+ git lfs pull --include="runs/all_lora/all_lora_best_cpt.pth"
67
 
68
+ pip install torch transformers peft safetensors pyarrow numpy
69
+ python app/engine/fetch_base_model.py # google/gemma-4-E2B, 10.25 GB
 
 
 
 
 
 
 
70
  ```
71
 
72
+ ### 2. Embed a repository (6 views β†’ 12288-d)
 
 
 
 
73
 
74
+ The head is conditioned on a specific representation: six views of the repo β€”
75
+ call graph, architecture, git history, contracts/tests, conventions, ops β€” each
76
+ embedded by a frozen `Qwen3-Embedding-0.6B` and concatenated. **A different
77
+ embedding will not work.**
78
 
79
+ ```bash
80
+ git clone --depth 80 https://github.com/pallets/click /tmp/click
81
+ python app/engine/build_embedding.py --repo /tmp/click --out /tmp/click.npy
 
 
 
 
 
 
 
 
82
  ```
83
 
84
+ ### 3. Generate the adapter
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
+ ```bash
87
+ python app/engine/generate_and_merge.py \
88
+ --embedding /tmp/click.npy \
89
+ --checkpoint runs/all_lora/all_lora_best_cpt.pth \
90
+ --adapter-out /tmp/click-adapter \
91
+ --no-merge
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  ```
93
 
94
+ That writes a **standard PEFT adapter**. Use `--merged-out DIR` instead of
95
+ `--no-merge` for a self-contained merged model (for vLLM).
96
 
97
+ ### 4. Use it
98
 
99
+ ```python
100
+ from peft import PeftModel
101
+ from transformers import AutoModelForImageTextToText, AutoTokenizer
102
 
103
+ tok = AutoTokenizer.from_pretrained("models/gemma-4-E2B")
104
+ model = AutoModelForImageTextToText.from_pretrained("models/gemma-4-E2B")
105
+ model = PeftModel.from_pretrained(model, "/tmp/click-adapter")
106
 
107
+ prompt = "Q: What testing framework does this repository use?\nA:"
108
+ out = model.generate(**tok(prompt, return_tensors="pt"), max_new_tokens=32)
109
+ print(tok.decode(out[0], skip_special_tokens=True))
110
+ ```
111
 
112
+ `with model.disable_adapter():` gives the frozen baseline for an A/B β€” the
113
+ comparison that actually matters.
114
 
115
+ ### Calling the head directly
 
116
 
117
+ ```python
118
+ import numpy as np, torch, sys
119
+ sys.path.insert(0, "app/engine")
120
+ from generate_and_merge import load_head
121
 
122
+ head, cfg, alpha = load_head("runs/all_lora/all_lora_best_cpt.pth")
123
+ emb = np.load("/tmp/click.npy").astype("float32") # 12288-d, RAW
124
+ out = head(torch.from_numpy(emb).unsqueeze(0))
125
+ # out["A"][type] -> [1, r, in_features]
126
+ # out["B"][type] -> [1, out_features, r]
127
+ ```
128
 
129
+ The head standardises its input internally using statistics stored in the
130
+ checkpoint β€” **pass raw embeddings**, do not normalise them yourself.
 
131
 
132
+ Its update is `Ξ” = (Ξ±/r)Β·(xΒ·Aα΅€)Β·Bα΅€` with `A:[r,in]`, `B:[out,r]` β€” identically
133
+ PEFT's LoRA convention, so the output drops straight into a standard adapter.
134
+ One `(A, B)` pair per shape-qualified module type, shared across the transformer
135
+ layers of that shape (205 target modules, 14 types).
136
 
137
+ ---
138
 
139
+ ## Results
 
 
 
 
140
 
141
+ Cross-repo held-out loss versus the **frozen base on identical data**:
142
 
143
+ | checkpoint | `cr_val` | `cr_test` | vs base | verdict |
144
+ |---|---|---|---|---|
145
+ | sixview_v2 *(removed)* | 2.606 | β€” | **+0.198** | worse than no adapter |
146
+ | `h200_run/head.best.pt` | 2.7168 | 2.7155 | βˆ’5.191 | good |
147
+ | **`all_lora_best_cpt.pth`** | **2.6811** | **2.6266** | **βˆ’5.246 / βˆ’5.310** | **best** |
 
148
 
149
+ Absolute losses are *not* comparable across rows β€” the eval sets differ. The
150
+ delta against the frozen model is.
151
 
152
+ Benchmark on three unseen repositories, three task families each (FACT = the
153
+ trained Q&A format, CODE = real source-line completion, TEXT = repo prose):
 
 
 
 
 
154
 
155
+ | repo | FACT | CODE | TEXT | keyword accuracy |
156
+ |---|---|---|---|---|
157
+ | `psf/requests` | βˆ’10.31 | βˆ’5.38 | βˆ’2.83 | 0% β†’ 83% |
158
+ | `pallets/click` | βˆ’11.74 | βˆ’7.66 | βˆ’3.26 | 25% β†’ 75% |
159
+ | `OpenLLM-France/AudioBench` | βˆ’5.08 | βˆ’3.81 | βˆ’1.73 | 0% β†’ 0% |
160
 
161
+ **100% win rate on all nine.**
 
 
 
 
162
 
163
+ ### Limits, stated plainly
164
 
165
+ - It learns a repo's **stack and conventions**, not what the project *does*. On
166
+ `requests` it answers "XML library" instead of HTTP. That is exact factual
167
+ recall, which a rank-16 LoRA structurally cannot hold β€” retrieval covers it.
168
+ - `AudioBench` keyword accuracy stayed at **0%**: projects whose identity is not
169
+ inferable from structure transfer poorly.
170
+ - Base losses of 12–16 on short gold targets inflate the deltas. The keyword
171
+ accuracy and the generations are the trustworthy evidence.
172
 
173
  ---
174
 
175
+ ## The failure worth knowing about
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
+ An earlier checkpoint reached a healthy-looking eval loss of **2.606** while
178
+ being **worse than applying no adapter at all** β€” and worse than random noise of
179
+ matched magnitude.
180
 
181
+ **Cause:** 64% of every repo embedding is a constant vector shared by all
182
+ repositories (the frozen encoder's mean response to "source code"). It dominated
183
+ the trunk, which collapsed to emitting essentially the same adapter for every
184
+ repo.
185
 
186
+ | stage | mean pairwise cosine across repos |
187
+ |---|---|
188
+ | input embedding | 0.73 |
189
+ | **after trunk** | **0.978** ← discriminability destroyed |
190
+ | emitted adapter | 0.96 |
191
+ | input, centered | **0.00** ← the signal was there all along |
192
 
193
+ **Fix:** `MemoryLoRAHead.fit_input_stats()` standardises the conditioning input
194
+ with training-set statistics, stored in the checkpoint so training and inference
195
+ apply the same transform. Emitted-adapter cosine went **0.96 β†’ 0.21 in 40 steps**;
196
+ the shipped model sits at **0.32**.
 
197
 
198
+ **Why it went unnoticed:** training logged only the adapted loss. A number like
199
+ 2.606 says nothing without the frozen-model baseline beside it. Every eval now
200
+ reports `delta_vs_baseline` and `diag/adapter_cosine`.
201
 
202
+ Before trusting any checkpoint:
203
 
204
  ```bash
205
+ python app/scripts/diagnose_head.py --job <id> --checkpoints runs/<run>/head.best.pt
 
 
 
 
 
 
 
 
 
 
 
206
  ```
207
 
208
+ It scores against `none`, `random` noise of matched scale, and a `zero-B` control
209
+ that must reproduce the baseline *exactly*. A head that cannot beat `random` has
210
+ not learned the mapping.
211
 
212
+ ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
+ ## Training
 
 
 
215
 
216
+ Reproduce or continue on a single GPU:
217
 
218
  ```bash
219
+ python3 deploy/h200/preflight.py # validates before spending GPU time
220
+ GATE=1 bash deploy/h200/train_h200.sh # ~20-min go/no-go
221
+ bash deploy/h200/train_h200.sh # full run
222
  ```
223
 
224
+ `preflight.py` fails fast on git-LFS pointers masquerading as data,
225
+ pre-standardised embeddings, a head without `fit_input_stats`, and missing eval
226
+ splits β€” then auto-tunes the largest micro-batch that fits.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
  ---
229
 
230
+ ## Credits
 
 
 
 
 
 
 
 
 
 
 
231
 
232
+ Reimplements and extends **Code2LoRA** (arXiv 2606.06492) β€” a static hypernetwork
233
+ mapping a repository embedding to a LoRA adapter β€” retargeted to
234
+ `google/gemma-4-E2B`, extended from single-view code completion to a six-view
235
+ representation, and wrapped in a serving stack that speaks the OpenAI and
236
+ Anthropic APIs. Training data includes RepoPeftBench from that work.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
 
238
+ Base model: [`google/gemma-4-E2B`](https://huggingface.co/google/gemma-4-E2B) Β·
239
+ Encoder: [`Qwen/Qwen3-Embedding-0.6B`](https://huggingface.co/Qwen/Qwen3-Embedding-0.6B)
docs/DEEP_DIVE.md ADDED
@@ -0,0 +1,581 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Memory-LoRA: A Hypernetwork that Writes Repo-Specific Adapters for Gemma-4-E2B
2
+
3
+ > **One line:** we train a small neural network (a *hypernetwork*) that reads an
4
+ > embedding of a codebase and **emits a LoRA adapter** for a frozen
5
+ > `google/gemma-4-E2B`. The adapter injects repo-specific knowledge into the
6
+ > model with **zero extra tokens at inference time** β€” no RAG, no context stuffing.
7
+ > Everything runs **locally on an Apple-Silicon Mac** (M4 Pro, 64 GB).
8
+
9
+ This document is the onboarding bible for the project. It covers *what* we built,
10
+ *why* every non-obvious choice was made, *what data* we used, *every experiment we
11
+ ran and its result*, and the **hard-won lessons** (the MPS memory leak alone cost
12
+ us hours). Read it top to bottom once; after that use it as a reference.
13
+
14
+ ---
15
+
16
+ ## Table of Contents
17
+
18
+ 1. [The idea in 60 seconds](#1-the-idea-in-60-seconds)
19
+ 2. [Origin: the Code2LoRA paper](#2-origin-the-code2lora-paper)
20
+ 3. [Architecture](#3-architecture)
21
+ 4. [The target model: Gemma-4-E2B specifics](#4-the-target-model-gemma-4-e2b-specifics)
22
+ 5. [Data pipeline: the 6 views](#5-data-pipeline-the-6-views)
23
+ 6. [Data pipeline: QA generation](#6-data-pipeline-qa-generation)
24
+ 7. [What is learnable β€” Tier A / B / C](#7-what-is-learnable--tier-a--b--c)
25
+ 8. [Datasets inventory](#8-datasets-inventory)
26
+ 9. [Experiments & results](#9-experiments--results)
27
+ 10. [Key decisions & lessons learned](#10-key-decisions--lessons-learned)
28
+ 11. [Repository map](#11-repository-map)
29
+ 12. [How to run it](#12-how-to-run-it)
30
+ 13. [Evaluation methodology](#13-evaluation-methodology)
31
+ 14. [Costs & budget discipline](#14-costs--budget-discipline)
32
+ 15. [Current status & roadmap](#15-current-status--roadmap)
33
+ 16. [Glossary](#16-glossary)
34
+
35
+ ---
36
+
37
+ ## 1. The idea in 60 seconds
38
+
39
+ A coding agent (Claude Code, Codex, etc.) is great at reasoning but knows nothing
40
+ about *your* repo until you paste files into its context β€” which is slow, expensive,
41
+ and capped by the context window. The usual fix is RAG (retrieve chunks at query
42
+ time). We do something different and complementary:
43
+
44
+ **We bake the repo's "personality" directly into the model's weights, once, as a
45
+ LoRA adapter β€” and we generate that adapter with a neural network instead of
46
+ training it.**
47
+
48
+ ```
49
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
50
+ repo on disk ──────► β”‚ 6-view extractor + Qwen3 β”‚ ──► 12288-d
51
+ β”‚ frozen embedding encoder β”‚ repo embedding
52
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
53
+ β–Ό
54
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
55
+ β”‚ HYPERNETWORK (our head) β”‚
56
+ β”‚ MLP β†’ per-module A,B β”‚
57
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
58
+ β”‚ LoRA weights
59
+ β–Ό
60
+ Q: "what layer owns auth in this repo?" ──► β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
61
+ β”‚ FROZEN Gemma-4-E2B + injected β”‚ ──► "the middleware
62
+ β”‚ LoRA (zero extra tokens) β”‚ layer, via ..."
63
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
64
+ ```
65
+
66
+ The magic: **the hypernetwork is trained across hundreds of repos**, so it learns
67
+ the *mapping* `repo embedding β†’ good adapter`. At inference on a brand-new repo it
68
+ has never seen, it embeds the repo once and produces an adapter in a single forward
69
+ pass. This is the same reason the source paper needed 400+ repos, not 1: breadth is
70
+ what makes the mapping generalize.
71
+
72
+ ---
73
+
74
+ ## 2. Origin: the Code2LoRA paper
75
+
76
+ We reverse-engineered **Code2LoRA** (arXiv 2606.06492v1) and found its released
77
+ code (`anonymous.4open.science/r/code2lora-6857`, MIT). The paper's contribution:
78
+ a *static hypernetwork* that maps a **repository** embedding β†’ a LoRA adapter for a
79
+ frozen code LLM, evaluated on **RepoPeftBench** with IR (in-repo) / CR (cross-repo)
80
+ splits. On a full H100 setup they report **63.8 % cross-repo Exact Match**.
81
+
82
+ Our project is the **Doc2LoRA variant** the paper itself cites β€” hypernetwork maps a
83
+ *document/repo view* β†’ LoRA β€” reimplemented against **Gemma-4-E2B**, trained fully
84
+ **locally on MPS** (no CUDA/H100), and extended in two directions the paper does not
85
+ cover:
86
+
87
+ - **Memory / recall**: the adapter should let the model *recall facts* about the
88
+ repo, not just complete code.
89
+ - **Tech-Lead judgment**: architecture, data-flow, conventions, contracts, ops β€”
90
+ the things a 20-year senior engineer "just knows" about a codebase.
91
+
92
+ We keep the paper's proven autograd trick almost verbatim (see Β§3) and change only
93
+ what Gemma-4 and Apple Silicon force us to change.
94
+
95
+ ---
96
+
97
+ ## 3. Architecture
98
+
99
+ Three frozen/learned pieces. Only the middle one (the head) is trained.
100
+
101
+ ### 3.1 Frozen encoder β€” `memory_lora/encoder.py`
102
+
103
+ - **Qwen3-Embedding-0.6B**, frozen, no gradient flows through it.
104
+ - Each repo view is chunked into token windows (2048 tokens, 128 overlap), each
105
+ chunk mean-pooled, then chunks combined with **mean + max pooling** β†’ a **2048-d**
106
+ vector *per view*.
107
+ - Embeddings are **precomputed once and cached** to parquet β€” the encoder never runs
108
+ during training.
109
+
110
+ ### 3.2 The hypernetwork head β€” `memory_lora/core.py :: MemoryLoRAHead`
111
+
112
+ The only trained component. Design (kept close to the paper):
113
+
114
+ - **2-layer GELU MLP trunk** (`input_dim β†’ hidden_dim β†’ hidden_dim`), followed by
115
+ **L2-normalize + √hidden_dim rescale** (stabilizes the magnitude of generated
116
+ weights).
117
+ - **Per-module-type output heads**: for each target module *type* it emits an
118
+ `A ∈ [rank, in_features]` and `B ∈ [out_features, rank]`. **One (A,B) pair per
119
+ type, shared across all layers of that type** β€” this is what keeps the head
120
+ tractable (188.6 M params) instead of exploding per-layer.
121
+ - **Squashing**: `tanh(raw) * exp(log_scale)` with a learned per-type `log_scale`
122
+ (init **-3.5**). This starts the generated adapter near-zero (so training begins
123
+ close to the base model) and lets each type learn its own output scale.
124
+ - **Defaults**: `hidden_dim=128`, `rank=16`, `dropout=0.1`.
125
+ - *Why hidden_dim=128 and not the paper's 512/1024?* A 745 M-param head
126
+ (hidden_dim=512) barely moved eval loss (~1.9 β†’ ~2.7) but was far heavier to
127
+ train on MPS. 128 cuts head size dramatically with negligible quality loss
128
+ locally. Bump it later on real GPUs.
129
+
130
+ ### 3.3 The LoRA injection β€” `memory_lora/core.py :: LoRA`
131
+
132
+ ```
133
+ base nn.Linear (FROZEN) hypernetwork output
134
+ β”‚ β”‚
135
+ x ──►│ Wx ────────────────┐ β”‚
136
+ β”‚ (input detached +──► y = Wx + scaling Β· B (A x)
137
+ β”‚ into base) β”‚ β–² β–²
138
+ x β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
139
+ A,B are NON-detached tensors so
140
+ autograd flows LM-loss β†’ head
141
+ ```
142
+
143
+ Critical detail (straight from the paper's code): **A and B are plain, non-buffer
144
+ tensor attributes, not `nn.Parameter` and not detached**, so the gradient of the LM
145
+ loss flows *through* the injected weights *into the hypernetwork*. The base
146
+ `nn.Linear` is frozen and its input is detached. Get this wrong and the head never
147
+ learns.
148
+
149
+ ### 3.4 Shape-qualified module types β€” the heterogeneity fix
150
+
151
+ Gemma-4-E2B is **not** a uniform stack (see Β§4). Two `q_proj`s can have different
152
+ shapes. If you key the head by bare type name (`q_proj`) you get
153
+ *"type q_proj inconsistent dims"* crashes. Fix: key by **shape-qualified type**,
154
+ e.g. `q_proj_1536x2048` vs `q_proj_1536x4096`. The v2 run discovered **14 shape
155
+ types** across **205 target modules**:
156
+
157
+ ```
158
+ down_proj_12288x1536 down_proj_6144x1536 gate_proj_1536x12288 gate_proj_1536x6144
159
+ k_proj_1536x256 k_proj_1536x512 o_proj_2048x1536 o_proj_4096x1536
160
+ q_proj_1536x2048 q_proj_1536x4096 up_proj_1536x12288 up_proj_1536x6144
161
+ v_proj_1536x256 v_proj_1536x512
162
+ ```
163
+
164
+ `get_module_specs(root_prefix="model.language_model.")` restricts wrapping to the
165
+ text decoder β€” the **vision and audio towers are never touched** (not even
166
+ inspected), so the multimodal forward path stays intact and they cost only idle RAM.
167
+
168
+ ---
169
+
170
+ ## 4. The target model: Gemma-4-E2B specifics
171
+
172
+ Verified by reading the actual safetensors header, not guessed:
173
+
174
+ - **Real model.** Google shipped Gemma 4 in March 2026. Apache-2.0, ungated.
175
+ Class `Gemma4ForConditionalGeneration`, `model_type: "gemma4"`. Loaded via
176
+ `AutoModelForImageTextToText`.
177
+ - **Requires `transformers >= 5.5.0.dev0`** β€” install from the `main` branch, not a
178
+ pinned PyPI release (this is the single biggest environment risk; smoke-test first).
179
+ - **Decoder is nested**: layers live at `model.language_model.layers.{i}.*`, *not*
180
+ `model.layers.*`. The layer-index regex had to change accordingly.
181
+ - **35 text layers, heterogeneous:**
182
+ - Aggressive KV sharing β€” **20 of 35 layers lack their own `k_proj`/`v_proj`**
183
+ (`num_kv_shared_layers=20`).
184
+ - **Every 5th layer is wider** (the `*_4096`, `*_12288` shape variants above).
185
+ - **Device `mps`, precision bf16** (fall back to fp16 if unstable). No
186
+ `flash_attention_2` on MPS β€” use `sdpa`, fall back to `eager`.
187
+
188
+ ---
189
+
190
+ ## 5. Data pipeline: the 6 views
191
+
192
+ `scripts/build_repo_multiview.py` clones a repo and extracts **6 complementary
193
+ views**, embeds each with Qwen3 β†’ 2048-d, and **concatenates to a 12288-d** repo
194
+ vector. The views encode the different "lenses" a senior engineer uses:
195
+
196
+ | View | What it captures | Source signals |
197
+ |-----------------|----------------------------------------------------|----------------|
198
+ | `v_graph` | call / import / dependency structure | AST for Python (`memory_lora/codegraph.py`); `IMPORT_RE`/`DEF_RE` regex fallback for other languages |
199
+ | `v_arch` | architecture & layout | README, folder tree |
200
+ | `v_history` | how the code evolved | `git log`, recent diffs |
201
+ | `v_contracts` | behavioral contracts | test files |
202
+ | `v_conventions` | idioms & style | representative source files |
203
+ | `v_ops` | build / deploy / runtime | CI config, Dockerfile, build files |
204
+
205
+ **Multi-language from the start.** `CODE_EXTS` + regex fallbacks mean the graph view
206
+ works for 9 languages, not just Python (this was a deliberate correction β€” see Β§10).
207
+ Repos with < 3 code files are skipped. The build is **resume-safe** (skips repos
208
+ already in `multiview_sources.jsonl`) and **flushes the embeddings parquet every 10
209
+ repos**, so a crash never loses more than 10 repos of work.
210
+
211
+ ---
212
+
213
+ ## 6. Data pipeline: QA generation
214
+
215
+ The repo embedding is the *input*; the *target* is repo-scoped Q&A. Two generators,
216
+ both parallelized (`ThreadPoolExecutor`, `--workers 10`) with a **per-prompt disk
217
+ cache** (idempotent reruns) and a `--model` flag:
218
+
219
+ - **`scripts/generate_repo_scoped_qa.py`** β€” reads the same 6 views and asks the LLM
220
+ for **8–12 repo-level judgment questions** ("what layer owns X", "what convention
221
+ does this repo use for Y", "how does data flow through Z", "why is it structured
222
+ this way"). Target scope = input scope (repo-level embedding ↔ repo-level QA).
223
+ - **`scripts/generate_commitpack_qa.py`** β€” **breadth** generator: one commit per
224
+ *distinct* repo across CommitPackFT (25k+ distinct repos), 3–4 commit-scoped
225
+ judgment questions (why / conventions / contracts / impact). For a hypernetwork,
226
+ **distinct-repo count is the currency of generalization**, so we favor 1 commit Γ—
227
+ many repos over many commits Γ— one repo.
228
+
229
+ **Discipline (both):** answers are **short judgment**, never file-path/line-number
230
+ lists. This is deliberate β€” see Tier A/B/C next.
231
+
232
+ **Models used (OpenRouter, OpenAI-compatible API):**
233
+
234
+ | Model | Role | Notes | Cost |
235
+ |-------|------|-------|------|
236
+ | `google/gemini-3.6-flash` | high-quality QA | **reasoning is mandatory** β†’ needs generous `max_tokens` (3000–4000) or it returns empty | ~$0.0021 / QA |
237
+ | `google/gemma-4-31b-it` | bulk / cheap QA | non-reasoning, clean JSON | ~$0.001 / repo (~$1 per 1000 repos) |
238
+
239
+ The OpenRouter key lives in a **git-ignored `.env` (mode 600)** and is never pasted
240
+ into a command line.
241
+
242
+ ---
243
+
244
+ ## 7. What is learnable β€” Tier A / B / C
245
+
246
+ A LoRA adapter has finite capacity. We classify repo knowledge by whether a LoRA can
247
+ hold it β€” this drives the entire QA design:
248
+
249
+ - **Tier A β€” Judgment & conventions** (LEARNABLE). "This repo puts business logic in
250
+ services, not views." Compressible, generalizes. β†’ **This is what we train on.**
251
+ - **Tier B β€” Structural gist** (LEARNABLE). "Auth flows through middleware." The kind
252
+ of thing, not the exact file.
253
+ - **Tier C β€” Exact recall & multi-hop** (NOT reliably learnable). "Line 412 of
254
+ `foo.py` calls `bar()`." This needs **retrieval (RAG)**, not weights.
255
+
256
+ So Memory-LoRA and RAG are **complementary**: the adapter carries Tier A/B judgment
257
+ for free (zero tokens); RAG handles Tier C exact lookups. The QA prompts forbid exact
258
+ file/line answers precisely so we never ask the LoRA to do a job it structurally
259
+ can't.
260
+
261
+ ---
262
+
263
+ ## 8. Datasets inventory
264
+
265
+ Everything lives under `data/` (git-ignored blobs). Sizes are approximate.
266
+
267
+ | Path | What | Scale |
268
+ |------|------|-------|
269
+ | `data/real_code2lora/` | **RepoPeftBench** from the `code2lora` HF org β€” 500 Python repos, repo-commit embeddings + diffs | 73,849 repo-commit rows; ~1.2 GB |
270
+ | `data/commitpack/multilang_commits.jsonl` | **CommitPackFT** shards, 9 languages | 25k+ distinct repos |
271
+ | `data/docs/multiview_sources.jsonl` | 6-view `view_text` per repo (input to QA gen) | growing (1000s of repos) |
272
+ | `data/embeddings/multiview_embeddings.parquet` | 12288-d multi-view repo embeddings | 1000+ repos |
273
+ | `data/embeddings/aligned6_embeddings.parquet` | **assembled training inputs** (repos with β‰₯1 QA) | 1058 repos (current) |
274
+ | `data/qna/repo_scoped_qa.jsonl` | repo-level judgment QA | 11,232 QA |
275
+ | `data/qna/techlead_qa_commitpack.jsonl` | commit-scoped breadth QA | 9,245 QA |
276
+ | `data/qna/techlead_qa.jsonl` | SWE-bench tech-lead QA | 2,786 QA |
277
+ | `data/qna/aligned6_qna.jsonl` | **assembled training targets** | 8,540 QA (current) |
278
+ | `data/openrouter_cache/` | per-prompt response cache | ~19 MB |
279
+
280
+ **Language balancing.** SWE-bench is ~79 % Django. Left alone, the dataset was 46 %
281
+ Django. `scripts/consolidate_qa.py` applies a **per-repo cap** (default 12–15 QA/repo)
282
+ which collapses Django to **~2.0 %** while preserving the 2400+ distinct repos'
283
+ diversity. `assemble_6view_dataset.py` applies the same cap when building the final
284
+ aligned set.
285
+
286
+ ---
287
+
288
+ ## 9. Experiments & results
289
+
290
+ Chronological, with the actual numbers we measured. Two families of runs.
291
+
292
+ ### 9.1 Reproducing the paper (single-view, real RepoPeftBench)
293
+
294
+ | Run | What | Result |
295
+ |-----|------|--------|
296
+ | `full1` (early) | first end-to-end hypernetwork on converted real data | CR **EM 0.056–0.083**, EditSim ~0.27 β€” pipeline works, undertrained |
297
+ | `sixview`/converted-real (best single-view ckpt) | after more training | **CR EM 0.524, EditSim 0.635** |
298
+ | Paper (reference, H100) | their full run | CR EM **0.638** |
299
+
300
+ **Headline:** on real code, after only ~2.4 % of one epoch of local MPS training, we
301
+ reached **52.4 % cross-repo Exact Match** vs the paper's 63.8 % on a full H100 setup.
302
+ The mechanism demonstrably works β€” the generated adapter recovers repo-specific
303
+ identifiers the base model does not know.
304
+
305
+ ### 9.2 The 6-view Tech-Lead model (the current line of work)
306
+
307
+ Loss is causal-LM cross-entropy on QA targets; lower is better. Three eval suites:
308
+ `cr_val` / `cr_test` (held-out *repos*) and `ir_test` (held-out *QA* of train repos).
309
+
310
+ | Run | Dataset | Best held-out `cr_test` loss | Notes |
311
+ |-----|---------|------------------------------|-------|
312
+ | `sixview_v1` | 515 repos / 3,988 QA (415 train repos) | **2.848** (step ~1060) | Overfit afterward: train loss fell to 1.75 while `cr_test` drifted to 3.35. Classic small-dataset ceiling. |
313
+ | `sixview_v2` | **1,058 repos / 8,540 QA (858 train repos)** | *in progress* | Resumed from `sixview_v1/head.best.pt`; 2Γ— the data specifically to break v1's ceiling. |
314
+
315
+ `sixview_v1` metrics trajectory (from `runs/sixview_v1/metrics.jsonl`):
316
+
317
+ ```
318
+ step 1245 cr_test 2.962 ir_test 2.535 (end of epoch 2 β€” near best)
319
+ step 1400 cr_test 3.245 ir_test 2.593 (overfitting begins)
320
+ step 1600 cr_test 3.352 ir_test 2.655 (train loss still falling β†’ ceiling hit)
321
+ ```
322
+
323
+ The v1β†’v2 story is the core empirical lesson: **the small aligned set was the
324
+ bottleneck, not the architecture** β€” hence the push to build 1000+ more repos.
325
+
326
+ ---
327
+
328
+ ## 10. Key decisions & lessons learned
329
+
330
+ The expensive knowledge. Read this section twice.
331
+
332
+ ### 10.1 ⚠️ The MPS gradient-checkpointing memory leak (the big one)
333
+
334
+ **Symptom:** training with `gradient_checkpointing_enable()` (`use_reentrant=False`)
335
+ **leaked ~12 GB per step** and OOM'd the whole machine within a few steps.
336
+
337
+ **Diagnosis** (`scripts/diag_mps_leak.py`): forward-only was stable; train + checkpoint
338
+ leaked 39 GB β†’ 18 GB free in 2 steps. Isolated the checkpointing path as the cause.
339
+
340
+ **Fix:** **`--no-gradient-checkpointing`.** We have enough unified memory to hold
341
+ activations without it once the multimodal towers sit idle. This is documented as a
342
+ standing memory (`mps-gradient-checkpointing-leak.md`).
343
+
344
+ ### 10.2 ⚠️ `psutil` RSS is blind to MPS memory
345
+
346
+ Our first memory safety-net used `psutil` RSS / `ps -o rss` β€” it reported **< 1 GB**
347
+ while `top` showed **55–83 GB** actually in use. MPS (GPU) allocations don't show up
348
+ in process RSS.
349
+
350
+ **Fix:** the safety check uses **`psutil.virtual_memory().available`** (system-wide)
351
+ with a `--min-available-gb` floor (default 5). To *observe* MPS memory, use
352
+ `top -l 1 -pid <PID> -stats mem`, not `ps`.
353
+
354
+ ### 10.3 Memory competition between concurrent jobs
355
+
356
+ Three concurrent jobs once pushed available memory under the 10 GB floor and training
357
+ self-stopped. **Lesson:** during MPS training, run data builds/embedding on **CPU**
358
+ (`--device cpu`) so they don't contend for the GPU/unified memory. We now routinely
359
+ run training (MPS) + QA gen (network) + multiview build (CPU) together without
360
+ contention.
361
+
362
+ ### 10.4 Don't lose hours of training
363
+
364
+ Every long run writes **checkpoints every 50 steps** (overwriting `head.latest.pt`),
365
+ **every 30 minutes** (timestamped `head.tNNNNm.pt`), **per-epoch** (`head.epN.pt`),
366
+ and a **`head.best.pt`** on eval improvement. Runs are launched with `nohup … &
367
+ disown` so they survive terminal/session death. `sixview_v1` in fact survived a full
368
+ session interruption and kept training. Resume with `--resume-from <ckpt>` (loads head
369
+ weights; optimizer restarts fresh).
370
+
371
+ ### 10.5 Data-quality corrections (user-driven)
372
+
373
+ - **"I still see lots of Django."** SWE-bench is Django-dominated. β†’ per-repo cap +
374
+ multi-language sourcing dropped Django 46 % β†’ 2.0 %.
375
+ - **"It must be good for any programming language."** β†’ 9-language diversity via
376
+ CommitPackFT and language-agnostic view extraction.
377
+ - **"Where's the code context in the QA?"** β†’ clarified the two-channel design: the
378
+ **repo embedding is the context channel**, the QA is only the target. They are
379
+ joined by `doc_id` at assembly time.
380
+
381
+ ### 10.6 OpenRouter gotchas
382
+
383
+ - `gemini-3.6-flash` **returned empty** until we raised `max_tokens` β€” it's a
384
+ mandatory-reasoning model that spends tokens on hidden reasoning before content.
385
+ Reasoning **cannot be disabled** (400 error).
386
+ - CommitPackFT's HF loader is deprecated β†’ fetch raw `data.jsonl` directly.
387
+ - `global MODEL` after use is a `SyntaxError` β†’ set via `globals()["MODEL"] = ...`.
388
+
389
+ ### 10.7 Performance fix worth knowing
390
+
391
+ Loading embeddings was 5+ min because `_list_to_f32_array` used a Python loop.
392
+ Vectorized via `col.combine_chunks().flatten().to_numpy()` β†’ **~220Γ— faster**.
393
+
394
+ ---
395
+
396
+ ## 11. Repository map
397
+
398
+ ```
399
+ memory_lora/ # the library (importable package)
400
+ core.py # LoRA wrapper, MemoryLoRAHead hypernetwork,
401
+ # get_module_specs / replace_with_lora / inject_lora_weights,
402
+ # load_doc_rows / load_qna_rows
403
+ encoder.py # Qwen3 chunk + embed + mean/max pool (frozen)
404
+ codegraph.py # Python AST extractor (imports, sigs, call graph)
405
+ data_paths.py # local parquet/jsonl path resolver
406
+
407
+ scripts/
408
+ build_repo_multiview.py # clone β†’ 6 views β†’ 12288-d embeddings (multi-language, resume-safe)
409
+ generate_repo_scoped_qa.py # repo-level judgment QA (aligned to the 6 views)
410
+ generate_commitpack_qa.py # commit-scoped breadth QA across 1000s of distinct repos
411
+ generate_techlead_qa.py # SWE-bench tech-lead QA
412
+ generate_synthetic_dataset.py # original synthetic doc + QA generator
413
+ consolidate_qa.py # per-repo cap β†’ language/domain balancing
414
+ assemble_6view_dataset.py # join embeddings ↔ all QA by repo β†’ aligned6_{embeddings,qna}
415
+ augment_paraphrases.py # QA paraphrase augmentation
416
+ convert_real_code2lora.py # RepoPeftBench β†’ our schema
417
+ build_doc_embeddings.py # encoder pass over documents
418
+ merge_corpora.py # combine multiple corpora
419
+ train_memory_lora.py # THE trainer (MPS, one-repo-per-step, checkpoints, TensorBoard)
420
+ train_direct_lora.py # baseline: plain per-repo LoRA (no hypernetwork)
421
+ eval_memory_lora.py # EM / EditSim recall eval on cr/ir splits
422
+ show_eval_examples.py # dump concrete base-vs-adapted examples
423
+ test_embed_this_repo.py # embed the current repo (pipeline demo)
424
+ test_recall_this_repo.py # query the adapted model about this repo
425
+ diag_mps_leak.py # the memory-leak isolation harness
426
+
427
+ data/ # git-ignored: embeddings, qna, sources, caches
428
+ runs/ # git-ignored: checkpoints, logs, metrics.jsonl, tb/
429
+ requirements.txt # torch 2.13 (MPS), transformers@main, openai, pyarrow, tensorboard…
430
+ .env # git-ignored, mode 600: OPENROUTER_API_KEY
431
+ ```
432
+
433
+ ---
434
+
435
+ ## 12. How to run it
436
+
437
+ ### Setup
438
+
439
+ ```bash
440
+ python3 -m venv venv && source venv/bin/activate
441
+ pip install -r requirements.txt # installs transformers from git main
442
+ echo "OPENROUTER_API_KEY=sk-or-..." > .env && chmod 600 .env
443
+ ```
444
+
445
+ Smoke-test the environment first (gates everything): confirm `transformers` main
446
+ loads `google/gemma-4-E2B` on `mps` and runs a text-only forward pass.
447
+
448
+ ### Build data
449
+
450
+ ```bash
451
+ # 1) multi-view embeddings for a repo list (CPU to stay off the GPU during training)
452
+ python scripts/build_repo_multiview.py \
453
+ --repos-file data/multilang_repo_list.txt --max-repos 1000 --device cpu
454
+
455
+ # 2) repo-scoped QA (cheap model) β€” appends, resume-safe, cached
456
+ ./venv/bin/python scripts/generate_repo_scoped_qa.py \
457
+ --model google/gemma-4-31b-it --workers 10
458
+
459
+ # 3) balance + assemble the aligned training set
460
+ python scripts/consolidate_qa.py --per-repo-cap 12
461
+ python scripts/assemble_6view_dataset.py
462
+ # -> data/embeddings/aligned6_embeddings.parquet + data/qna/aligned6_qna.jsonl
463
+ ```
464
+
465
+ ### Train (the exact `sixview_v2` command)
466
+
467
+ ```bash
468
+ nohup ./venv/bin/python scripts/train_memory_lora.py --output-dir sixview_v2 \
469
+ --resume-from runs/sixview_v1/head.best.pt \
470
+ --embeddings-path data/embeddings/aligned6_embeddings.parquet \
471
+ --qna-path data/qna/aligned6_qna.jsonl --epochs 100 --max-hours 8 \
472
+ --checkpoint-every-steps 50 --checkpoint-every-minutes 30 --epoch-ckpt-every 5 \
473
+ --eval-every-steps 300 --limit-eval-docs 40 --max-seq-len 512 --fixed-seq-len \
474
+ --max-qna-per-doc 12 --lm-micro-batch 2 --device mps --no-gradient-checkpointing \
475
+ --rank 16 --head-hidden-dim 128 --head-dropout 0.1 --weight-decay 0.05 \
476
+ --early-stop-patience 25 --lr 8e-5 --lr-total-steps 9000 --min-available-gb 5 \
477
+ > runs/sixview_v2_train.log 2>&1 &
478
+ disown
479
+ ```
480
+
481
+ **Flags you must not forget:** `--no-gradient-checkpointing` (the leak),
482
+ `--device cpu` for builds during training (contention), `--min-available-gb`
483
+ (system-wide memory floor).
484
+
485
+ ### Watch it
486
+
487
+ ```bash
488
+ tensorboard --logdir runs/sixview_v2/tb # train/loss, train/lr, eval/{suite}_loss
489
+ tail -f runs/sixview_v2_train.log
490
+ ```
491
+
492
+ ### Evaluate & inspect
493
+
494
+ ```bash
495
+ python scripts/eval_memory_lora.py --ckpt runs/sixview_v2/head.best.pt # EM / EditSim
496
+ python scripts/show_eval_examples.py # base vs adapted
497
+ ```
498
+
499
+ ---
500
+
501
+ ## 13. Evaluation methodology
502
+
503
+ - **Splits (deterministic, by `md5(repo) % 100`):** 80 % train / 10 % `cr_val` /
504
+ 10 % `cr_test` **by repo**, so cross-repo suites are **entirely held-out
505
+ repositories** the hypernetwork never trained on. Within train repos, ~15 % of QA
506
+ is held out β†’ `ir_test` (in-repo generalization to unseen questions of seen repos).
507
+ - **Metrics:** causal-LM **eval loss** during training (fast, every N steps on
508
+ `--limit-eval-docs` docs to stay cheap on CPU), plus generation-time **Exact Match
509
+ (EM)** and **EditSim** for the recall eval.
510
+ - **The proof spot-check:** query the *adapted* model with repo-specific questions and
511
+ confirm the *base* (un-adapted) model gets them wrong/vague β€” proving the
512
+ **adapter**, not the base model's pretraining, does the work.
513
+
514
+ CPU eval of a float32 5B model is slow (~20 min for a full pass) β†’ we cap eval docs
515
+ (e.g. 10–40) for in-loop evals and run full EM eval separately.
516
+
517
+ ---
518
+
519
+ ## 14. Costs & budget discipline
520
+
521
+ - **Spend baseline:** $31.00 (`runs/spend_baseline.txt`); ~**$33.90 total** to date;
522
+ ~**$11 remaining**. The project is run under explicit budget caps ("spend at most
523
+ $4 more") with spend-guards.
524
+ - **Unit economics:** `gemini-3.6-flash` β‰ˆ **$0.0021/QA**; `gemma-4-31b-it` β‰ˆ
525
+ **$0.001/repo (~$1 per 1000 repos)** β€” which is exactly why the 1000-repo expansion
526
+ uses the gemma model.
527
+ - **Free levers:** the per-prompt cache makes reruns free; embedding and training are
528
+ local (electricity only).
529
+
530
+ ---
531
+
532
+ ## 15. Current status & roadmap
533
+
534
+ **Live right now (three jobs in parallel, no contention):**
535
+
536
+ - **`sixview_v2` training** β€” resumed from `head.best.pt` on the doubled
537
+ **1,058-repo / 8,540-QA** dataset (858 steps/epoch), MPS. First held-out eval at
538
+ step 300 tells us whether doubling the data broke v1's 2.848 ceiling.
539
+ - **QA generation** β€” `gemma-4-31b-it` filling in all ~1,032 new repos for the
540
+ **complete** dataset (next training run).
541
+ - **Multiview build** β€” cloning/embedding toward the full +1,000-new target (CPU).
542
+
543
+ **Roadmap:**
544
+
545
+ 1. Finish the complete 1000-new-repo dataset (embeddings + QA).
546
+ 2. Assemble the full aligned set (~1,650 repos) and train **`sixview_v3`** on it.
547
+ 3. Run generation-time **EM/EditSim** on the 6-view model (base vs adapted).
548
+ 4. Push `head_hidden_dim` back up once on real GPUs; the 128 default was an
549
+ MPS-locality compromise.
550
+ 5. Broaden Tier-A/B QA toward agent-harness use cases (Jira/ticket tracking,
551
+ diff/impact reasoning) already scaffolded in `generate_techlead_qa.py`.
552
+
553
+ **Open questions:**
554
+
555
+ - Does the 12288-d 6-view embedding actually beat the single 2048-d view on
556
+ generation EM, or only on loss? (loss says yes; EM eval pending)
557
+ - What's the real Tier-B ceiling β€” how much structural gist fits in rank-16?
558
+ - Optimal per-repo QA cap for the breadth/depth trade-off.
559
+
560
+ ---
561
+
562
+ ## 16. Glossary
563
+
564
+ - **Hypernetwork** β€” a network that outputs the weights of another network. Here:
565
+ repo embedding β†’ LoRA (A,B) matrices.
566
+ - **LoRA** β€” Low-Rank Adaptation: `y = Wx + scaling Β· B(Ax)`, with `A,B` low-rank
567
+ (rank 16). We *generate* A,B instead of training them per-repo.
568
+ - **6 views** β€” graph / arch / history / contracts / conventions / ops; each 2048-d,
569
+ concatenated to 12288-d.
570
+ - **CR / IR** β€” cross-repo (held-out repos) / in-repo (held-out QA of seen repos).
571
+ - **EM / EditSim** β€” Exact Match / edit-distance similarity of generated vs gold.
572
+ - **Tier A/B/C** β€” judgment (learnable) / structural gist (learnable) / exact recall
573
+ (needs RAG).
574
+ - **MPS** β€” Apple's Metal Performance Shaders GPU backend for PyTorch.
575
+ - **RepoPeftBench** β€” the paper's benchmark; 500 Python repos, repo-commit
576
+ embeddings + diffs, IR/CR splits.
577
+
578
+ ---
579
+
580
+ *Maintained as living documentation. If you change a default, a path, or a flag,
581
+ update the matching section here β€” onboarding depends on it.*