opencoti-llamafile / docs /features /rys_layer_duplication.md
ManniX-ITA's picture
Upload docs/features/rys_layer_duplication.md with huggingface_hub
f296d5b verified
|
Raw
History Blame Contribute Delete
19.7 kB
# RYS runtime layer duplication (weight-shared "Repeat-Your-Self")
> Status: **DESIGN + PROTOTYPE** (2026-07-12). Parser/expander prototyped and
> validated; edit surface mapped; MVP scoped. No source landed yet.
>
> Child of [MASTER_PLAN.md](../MASTER_PLAN.md). Additive soft-fork feature β€”
> new load-time flag, per-arch surgical hooks in the graph loop + KV alloc.
## 1. What this is
David Noel Ng's **RYS** ("Repeat Your Self", [rys-ii][rys]) shows that
re-running a contiguous block of *middle* transformer layers β€” **no weight
changes, no training** β€” measurably improves a model (the layers duplicated are
the ones operating in the format-agnostic "reasoning" band; encoding/decoding
boundary layers must not be touched). For Qwen3.5-27B the Pareto-optimal blocks
are tiny: `(33,34)` (+1 layer) already captures most of the EQ gain, up through
`(26,34)` (+8) for the best absolute score.
The naive way to ship a RYS variant is a **physically merged GGUF** (mergekit
passthrough) β€” but that *doubles the duplicated layers' weight bytes* on disk
and in VRAM. dnhkng is instead working with TurboDerp on **pointer-based**
duplication in ExLlamaV3: the repeated layers **share the weight tensors** with
their originals, so *"no additional VRAM is consumed for the parameters
themselves β€” you only pay extra for the compute time and KV cache of the
additional forward passes."*
**This feature ports that pointer-shared idea to opencoti llamafile, driven
purely by a load-time flag.** No new GGUF, no re-quant, quant-agnostic:
```
llamafile --server -m Qwen3.5-27B-*.gguf --repeat-layers 26,34 # RYS-XL, +8 layers
llamafile --server -m Qwen3.5-27B-*.gguf --repeat-layers 33,34 # RYS-S, +1 layer
```
Stock llama.cpp has **no** runtime layer-repeat; this is a genuine opencoti
differentiator and fits our "additive, load-time" ethos.
## 2. Mechanism: a layer execution plan (`eff β†’ src`)
llama.cpp's per-architecture graph builder runs a residual loop
`for (il = 0; il < n_layer; ++il)` that (a) reads weights from
`model.layers[il]` and (b) uses `il` to index the KV cache, RoPE, and per-layer
hparams. We replace the identity iteration with a **plan**: a vector of *source*
layer indices to execute in order.
- **`layer_plan[eff] = src`** β€” for each *effective* position `eff`, which
source layer's weights to run. Default (no flag) = identity `[0,1,…,n_layer-1]`.
- A RYS block `(i,j)` (half-open, matching the article) inserts source indices
`i…j-1` a second time **right after** their first pass.
- **`n_layer_eff = layer_plan.size()`** β€” the *effective* layer count. This is
what the KV cache, RoPE loop, and residual loop use. The GGUF's `n_layer`
(the *source* weight count) is unchanged β€” weight loading is untouched.
Forward loop, conceptually:
```cpp
for (int eff = 0; eff < n_layer_eff; ++eff) {
const int src = layer_plan[eff];
const auto & layer = model.layers[src]; // WEIGHTS: shared by pointer, zero extra VRAM
cur = build_norm(inpL, layer.attn_norm, …, eff);
cur = build_layer_attn(inp, cur, inp_pos, …, /*kv_il=*/eff); // KV/cache index: eff (distinct slot)
…
inpL = cur;
}
```
The duplicated pass reads the *same* weights but writes its *own* K/V at the
same token positions β€” exactly the RYS semantics.
### 2a. Prototype (validated)
The plan expander is prototyped and unit-tested against every config in the
article's Qwen3.5-27B Pareto table (reference:
`scratchpad/rys_plan.py`; the C++ will mirror it):
| spec | blocks | `n_eff` (from 64) | extra | article |
|------------|--------------|-------------------|-------|---------|
| `33,34` | (33,34) | 65 | +1 | +1 βœ“ |
| `31,34` | (31,34) | 67 | +3 | +3 βœ“ |
| `30,35` | (30,35) | 69 | +5 | +5 βœ“ |
| `26,34` | (26,34) | 72 | +8 | +8 βœ“ |
| `24,35` | (24,35) | 75 | +11 | +11 βœ“ |
| `29,34` | (29,34) | 69 | +5 | +5 βœ“ |
`(26,34)` expands to `…,24,25,[26,27,28,29,30,31,32,33],[26,27,28,29,30,31,32,33],34,35,…`
β€” the seam is the duplicated `26..33`. Overlapping blocks are rejected in the
MVP (disjoint-ascending only; the Pareto winners are all single blocks).
## 3. Parameter grammar
```
--repeat-layers <spec>
```
- `<spec>` = one or more blocks separated by `;`.
- Each block is `i,j` **or** `i-j` = the half-open range `[i,j)` of **source**
transformer-layer indices to duplicate in place. `i,j` matches the article's
`(i,j)` notation for copy-paste from the HF configs.
- Examples: `33,34` Β· `26,34` Β· `43,45;28,34` (compose two disjoint blocks).
- Validation (fail-fast at load): `0 ≀ i < j ≀ n_transformer_layers`; blocks
disjoint; the MTP/`nextn_predict_layers` head is **never** duplicable.
Also plumbed through the opencoti TS adapter (`buildServerArgs`) as
`repeatLayers?: string` so opencode configs can set it.
## 4. Memory / cost model
- **Params: zero extra VRAM** β€” duplicated layers point at the same
`model.layers[src]` tensors.
- **KV cache: grows by the extra passes** β€” `n_layer_eff/n_layer`. E.g. `(26,34)`
on a 64-layer model = +8 β†’ **+12.5%** KV. Modest; the article calls it out
explicitly. (This is why RYS-S `(33,34)`, +1.5% KV, is the low-overhead pick.)
- **Compute: +one forward pass per duplicated layer** per token β€” same ratio as
KV. The win is quality-per-token, paid in tokens/s.
## 5. What is provably unaffected
- **Weight loading** β€” untouched; `n_layer` (source) drives `create_tensor`.
- **RoPE / positions** β€” positions come from the batch, not the layer. A source
layer run at two `eff` positions sees the same token positions; only its KV
slot differs. No self-extend-style position hacking.
- **Off (no flag)** β€” `layer_plan` = identity β‡’ byte-identical to stock. This is
the primary regression gate.
## 6. Composition with opencoti per-layer features
Everything opencoti indexes by layer must consume the **effective** index (or map
`eff→src` for source-keyed attributes). Scoped incrementally:
| feature | keyed by | MVP posture |
|---|---|---|
| plain f16 / scalar-quant KV | `eff` slot | **MVP** β€” works directly |
| RoPE / positions | batch pos | unaffected |
| iSWA (Gemma sliding/global per layer) | **src** (`is_swa(il)`) | Gemma phase — map `eff→src` |
| DCA (all-KV) | per-layer | later β€” gate off under `--repeat-layers` first, then make plan-aware |
| rolling-KV (`window_cells[il]`) | `eff` budget | later β€” size by `n_layer_eff` |
| PolyKV / sparse-attn | per-layer | later |
| MTP / NextN head | excluded | head never duplicated; spec path unchanged |
**Survey nuance (important):** DCA, rolling-KV (`window_cells[il]`), PolyKV, and
sparse-attn are all keyed through `map_layer_ids` / per-**physical**-layer
structs β€” so they **auto-inherit eff-correctness** the moment the `:927` alloc
loop (Β§8b#4) is eff-length. The residual risk is *not* the accessors; it's the
**sizing loops that still say `hparams.n_layer`** and would under-allocate for
eff slots: `n_layer_kv()`, `opencoti_compute_resident_window_cells`
(`kv-cache:249/:772`), and the sparse block-sel reserve `8*n_layer_kv`
(`kv-cache:879`). Plus DCA's `is_swa(il)` in `gemma4:217` needs `src`.
**MVP rule:** ship `--repeat-layers` composing only with plain/quant KV on the
dense arch first; **hard-error at load** if combined with DCA / rolling-KV /
sparse / MTP until each sizing loop is made eff-aware (explicit follow-ups). This
prevents silent under-allocation, which would corrupt rather than error.
> **STATUS UPDATE (RYS-12 / #667, 2026-07-13 β€” SUPERSEDES the MVP rule above):**
> the DCA / rolling-KV-residency / sparse-attn guards are **LIFTED**. The three
> residency sizing loops (`opencoti_compute_auto_gpu_heads_frac` /
> `_resident_window_cells` / `_auto_select_kv_tier`) iterate the EFFECTIVE plan
> (eff→src), the DCA build path maps `eff→src` for `is_swa`, and sparse-attn
> alloc lives inside the eff-iteration loop (authoritative comment:
> `src/llama-context.cpp` ~line 268). RYS now composes with DCA, rolling-KV
> window/spill, sparse-attn, quant-KV, and MTP (draft context runs the plain
> base stack β€” bug-2171 β€” target keeps RYS; lossless). RYS Γ— rolling-KV window
> spill was live-validated 2026-07-14 on the qwen35moe hybrid (bug-2172 gate:
> 44 eff layers + 256/40704 window+tail, needle recovered, no assert). The
> remaining hard-errors are: unsupported archs (RYS-4) and malformed/boundary
> plans (parse validation); boundary-band plans get an advisory WARN.
## 7. Per-architecture scope
### Phase 1 β€” Qwen3.5 / Qwen3.6 **dense** (MVP target)
Uniform layers (no iSWA, uniform head counts/rope) ⇒ `eff→src` is trivial for
everything except KV-slot allocation. `nextn_predict_layers` (MTP head) excluded
from the plan. Target models: Qwen3.5-27B dense, Qwen3.6 dense variants.
Forward loop: `models/qwen35.cpp` (also covers the dense path), `models/qwen3.cpp`.
### Phase 2 β€” Gemma-4 **dense** 12B and 31B
Adds the iSWA wrinkle: alternating sliding-window / global layers, per-layer
query scaling / attn logit-softcap. A duplicated layer must inherit its **source**
layer's SWA type and scaling ⇒ per-layer hparam lookups map `eff→src`. The KV
cache's SWA/global split (`kv_local_layer`, the iSWA cache) must size by
`n_layer_eff` with the src-derived SWA pattern. Forward loop: `models/gemma4.cpp`.
Targets: `gemma-4-12B-it` dense, `gemma-4-31B-it` dense.
(Gemma-4 A4B is MoE β€” out of scope for the dense MVP; RYS on MoE is a separate
question about whether per-expert routing survives block re-traversal.)
## 8. Edit surface
> Line-anchored from the source survey (2026-07-12). All paths under
> `vendors/sources/llamafile/llama.cpp/`.
**Governing finding:** in this fork the `il` handed to `build_attn(...)` is used
for BOTH weight lookup (`model.layers[il]` in the model `.cpp`) AND KV-slot
lookup (`map_layer_ids[il]`). RYS **decouples** them: **weights + architecture
props (`is_swa`, rope-freq, `n_head`, `n_rot`) index by `src`; the KV-slot index
handed to `build_attn` is `eff`.** For Qwen **dense** this is a no-op distinction
outside KV (uniform layers, `is_swa` always false), so `build_attn(eff)` is
clean. For Gemma the iSWA sub-cache choice must be `src`-driven (see Β§8b).
### 8a. C-ABI plumbing caveat (load-bearing)
`struct llama_context_params` (`include/llama.h:336`) crosses the **C ABI** β€” it
cannot carry a `std::vector`. So:
- `include/llama.h:336` β€” add a **compact int spec** (`uint32_t repeat_start,
repeat_end, repeat_count`, or a small CSV string).
- `src/llama-cparams.h:9` β€” add `std::vector<int> layer_plan;` to `llama_cparams`
(plain C++ struct, vector OK). Insert near the opencoti extension fields (69-80).
- `src/llama-context.cpp:~133` β€” **expand** the compact spec into
`cparams.layer_plan` here (where `hparams.n_layer` is known); default unset =
identity `0..n_layer-1`; set `n_layer_eff = layer_plan.size()`.
### 8b. Minimal MVP edit set β€” Qwen dense (plain KV)
| # | site | change | invasiveness |
|---|------|--------|--------------|
| 1 | `llama-cparams.h:9` | add `layer_plan` vector | trivial |
| 2 | `include/llama.h:336` | compact int spec (C-ABI) | trivial |
| 3 | `llama-context.cpp:~133` | expand spec β†’ `layer_plan`; `n_layer_eff` | trivial |
| 4 | `llama-kv-cache.cpp:927` (+ sizing `:165/:278/:451/:478`) | iterate `n_layer_eff`; size each eff slot from `src=plan[eff]` (`n_embd_k_gqa(src)`, `has_kv(src)`); `map_layer_ids[eff]=layers.size()` | moderate |
| 5 | `models/qwen3.cpp:76` (Β·qwen2.cpp:76Β·qwen3moe.cpp:86) | loop over `eff`; `src=plan[eff]`; `model.layers[il]β†’[src]`; `build_attn(…,eff)`; out-ids guard `eff==n_layer_eff-1` | moderate |
| 6 | `common/arg.cpp:~2780` | `add_opt("--repeat-layers")` (mirror `--override-kv` CSV) | trivial |
| 7 | `common/common.cpp:~1660` | map `common_params` range β†’ `llama_context_params` spec | trivial |
`qwen35.cpp:169` β€” the plan is built over `[0, n_transformer_layers)` where
`n_transformer_layers = n_layer - nextn_predict_layers` (`:167`), so the **MTP
head is already excluded** by construction; never duplicate a `nextn` block.
**KV precedent:** `llama-kv-cache.cpp` already tolerates a non-identity
`map_layer_ids` β€” the `il_share` path (`:946-963`, bug-858 dual-ctx MTP) and
`il_reuse` (`:1265-1283`, SWA reuse). Both *share* physical slots; RYS is the
inverse (allocate **more, distinct** slots), so no new indirection type is
needed β€” just an **eff-length domain** on the `:927` alloc loop. The 30+ read
accessors (`get_k`/`cpy_k` via `mli_at_checked(map_layer_ids, il, …)`) are
unchanged as long as the graph passes `eff` and every `eff` has a map entry.
### 8c. Additional edits β€” Gemma-4 dense (iSWA), the invasive part
| site | change |
|------|--------|
| `models/gemma4.cpp:194` | loop rewrite splitting `src`/`eff`: `is_swa(src)`, `get_rope_freq_base/scale(cparams,src)` (`:201-202`), `n_rot(src)`, `layers[src].rope_freqs`, `f_attention_scale` by `src`; feed `eff` only to `build_attn` (`:269-283`) + KV routing |
| `gemma4.cpp:164` iSWA dual-cache | `build_attn_inp_kv_iswa()` + kv-cache filter/reuse must assign each **eff** slot to base-vs-swa sub-cache by `is_swa(plan[eff])` β€” pass a `plan`-aware filter into the iSWA cache ctor (the invasive step: it currently reads `swa_layers[il]`) |
| `llama-model.cpp:1929/1933/1937` | `get_rope_freq_base/scale/factors` only ever called with `src` (they branch on `is_swa`) |
### 8d. hparams helpers (index by `src`)
`llama-hparams.cpp`: `is_swa(il)` `:208`, `n_head(il)` `:30`, `n_head_kv(il)`
`:38`, `n_rot(il)` `:65`, `n_embd_k_gqa/v_gqa(il)` `:103/:109`, `has_kv(il)`
`:231`, `n_layer_kv()` `:250`, `swa_layers[]` (`hparams.h:141`). No signature
changes β€” the rule is "call these with `src` in the loop." RoPE positions come
from the batch (`llama-graph.cpp:161`), not the layer, so a `src` layer run at
two `eff` positions is correct with no position hacking.
## 9. Validation plan
Correctness is *mechanism* correctness (the plan re-traverses exactly the
intended layers) β€” RYS quality is the user's block choice, not ours to prove.
1. **Off = identity** β€” `--repeat-layers` absent β‡’ logits byte-identical to
stock (teacher-forced, real_frac=0). Primary regression gate.
2. **Plan structural** — boot log dumps `n_layer_eff` + the `eff→src` plan;
assert it matches the spec (e.g. `(26,34)` β†’ 72 layers, seam at 34/42).
3. **Deterministic divergence** β€” `(33,34)` vs base produces a *deterministic,
non-degenerate* logit change (greedy, same GGUF) β€” proves the duplicated pass
actually runs and feeds forward, not a no-op.
4. **KV integrity** β€” niah retrieval stays 100 at a short ctx with a duplicated
block (the extra KV slots are wired correctly; no cross-slot aliasing).
5. **Known-win spot check** β€” reproduce the article's *direction*: a small EQ/math
probe should move in the reported direction for `(33,34)` / `(26,34)` on
Qwen3.5-27B (sanity that we duplicated the reasoning band, not garbage).
6. **Gemma iSWA** β€” the src-derived SWA pattern is honored (duplicated global
layer stays global); niah 100 + coherent decode.
Correctness always via logit-equiv / niah β€” never greedy-needle-as-proof.
## 10. Risks / caveats
- **KV growth is real** (Β§4) β€” surface it in the boot log; the TS adapter should
account for it in VRAM budgeting.
- **iSWA mapping** β€” feared to be the main Gemma risk, but **verified correct**
(see §10a): a middle SWA layer duplicates coherently, so the dual-cache eff→src
wiring is sound. What actually breaks is *boundary* layers, independent of SWA.
- **MoE out of scope** for dense MVP β€” but the iSWA path was validated on **both**
dense (31B, 60 layers) and MoE (A4B, 26B-A4B-128e) with identical behaviour.
- **Upstream syncability** β€” new flag + per-arch loop hooks tagged
`opencoti-hook: rys-layer-dup` and registered in `UPSTREAM_SYNC.md`; the
`layer_plan` indirection is small and localized to keep future bumps cheap.
## 10a. Boundary-layer fragility (bug-2164) β€” a MODEL property, not an engine bug
Duplicating the **first or last** transformer layers reliably produces incoherent
output. This is the well-documented franken-merge / passthrough self-merge
fragility (mergekit `passthrough`, SOLAR depth-upscaling all DROP the first/last
*m* layers), NOT an engine defect. The engine duplicates *any* layer faithfully.
**Decisive 2Γ—2** (2026-07-13, native `google_gemma-4-26B-A4B-it-Q4_K_M`, 3090,
CHAT endpoint β€” the raw `/completion` greedy path is INVALID on this thinking
model: its answer lands in `reasoning_content`, so even RYS-off looks like garbage
there). Garbage is determined **entirely by boundary-ness, independent of
SWA/global**:
| | SWA layer | GLOBAL layer |
|---|---|---|
| **middle** (L4, L10 / L5, L17) | coherent βœ“ | coherent βœ“ |
| **boundary** (L0, L28 / L29) | garbage βœ— | garbage βœ— |
- Gemma-4 A4B coherent band β‰ˆ **L4..L24**; L0–L3 (early) and L25–L29 (late) break.
- Qwen3-8B has a wider tolerant band β€” only its **last** layer (L35) breaks.
- Every attention output + final logit stays **finite** (no NaN) even under the
garbage config β†’ finite-but-wrong = model-level, not a numeric/cache corruption.
**Guidance: duplicate MIDDLE layers.** The engine emits a boot-time **advisory
WARNING** (`llama-context.cpp`, RYS engage block) whenever a plan duplicates a
layer within the first/last `max(3, n_transformer/8)`; it warns but does not block
(the exact usable band is model-dependent). Correctness gate = coherence smoke via
the CHAT endpoint (RYS intentionally changes the model, so logit-equivalence vs
baseline is the wrong bar). Historical note: an earlier pass mis-labelled this an
"SWA sub-cache engine bug" by testing only L0 (which is *both* SWA *and* first) on
the noisy raw-greedy path β€” see [[project_rys_swa_dup_garbage]].
## 10b. RYS Γ— MTP self-spec β€” the draft context must NOT inherit the plan (bug-2171, 0124)
RYS is a **target-only** capability. With RYS active *and* self-speculation on
(`--spec-type draft-mtp`, Qwen NextN or the gemma4 assistant), the MTP draft
`llama_context` is built from `common_context_params_to_llama(params_base)`, so it
inherited the target's `params.repeat_layers` β€” and its `cparams.layer_plan` got
RYS-duplicated too. But the NextN head lives at the canonical `il = n_transformer`
(`n_layer βˆ’ nextn_predict_layers`), which the RYS effβ†’src map never contains, so
draft-context init aborts with `cpy_k: map_layer_ids MISS il=n_transformer` β†’
`failed to create MTP context`. Fix (`0124`, `src/llama-context.cpp`): the RYS
plan-expansion block gates the spec on context type β€”
`rys_spec = (cparams.ctx_type == LLAMA_CONTEXT_TYPE_MTP) ? nullptr : params.repeat_layers`
(mirrored in `sched_reserve()`), so the **draft runs the plain base stack** while the
**target keeps RYS**. This is **lossless**: greedy self-spec verifies every drafted
token against the target, so the drafter's stack depth only moves acceptance/tps.
In self-spec the NextN head consumes the target's *already-RYS* hidden state via
shared memory β€” running the drafter plain is the intended composition, not a
limitation.
## 11. References
- [dnhkng, *LLM Neuroanatomy II*][rys] β€” the RYS-II study, Pareto table, pointer-based note ("The Models").
- [dnhkng/RYS on GitHub](https://github.com/dnhkng/RYS) β€” scanner, probes, model-builder config grammar.
- mergekit `passthrough` β€” the physical-merge convention `(i,j)` mirrors.
[rys]: https://dnhkng.github.io/posts/rys-ii/