Upload scripts/graft_krea_to_zimage.py
Browse files- scripts/graft_krea_to_zimage.py +947 -0
scripts/graft_krea_to_zimage.py
ADDED
|
@@ -0,0 +1,947 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""graft_krea_to_zimage.py
|
| 3 |
+
|
| 4 |
+
Cross-model weight graft from Krea 2 into Zimage (z_image_turbo).
|
| 5 |
+
|
| 6 |
+
This is a reference implementation of the cross-model graft technique
|
| 7 |
+
demonstrated on a real pair of pretrained image diffusion transformers. It
|
| 8 |
+
transfers Krea 2's learned attention head specializations and MLP feature
|
| 9 |
+
transforms into Zimage's parameter slots, respecting per-head geometry and
|
| 10 |
+
handling the dimensional reduction from Krea's larger tensors to Zimage's
|
| 11 |
+
smaller ones. No training. No dataset. No gradient descent. Just directed
|
| 12 |
+
substitution of weight subspaces in architecturally matched slots.
|
| 13 |
+
|
| 14 |
+
==============================================================================
|
| 15 |
+
THE GRAFT MECHANIC (universal to this technique)
|
| 16 |
+
==============================================================================
|
| 17 |
+
|
| 18 |
+
Given two transformer models with:
|
| 19 |
+
- the same head_dim (128 in both here)
|
| 20 |
+
- the same MLP family (both SwiGLU here)
|
| 21 |
+
- compatible attention layouts (both fused QKV here)
|
| 22 |
+
|
| 23 |
+
...you can transfer donor character into a target by:
|
| 24 |
+
|
| 25 |
+
1. Iterate over blocks; for each target block choose a donor block to source
|
| 26 |
+
from (a "block map" — shift, peak-centered, or cycled).
|
| 27 |
+
|
| 28 |
+
2. For each attention head slot in the target, choose which donor head to
|
| 29 |
+
place there ("source-head-selection" + "target-head-selection").
|
| 30 |
+
|
| 31 |
+
3. Extract that donor head as a small tensor slice, reduce its dimensions to
|
| 32 |
+
fit the target slot (truncation or SVD-based projection).
|
| 33 |
+
|
| 34 |
+
4. Blend the reduced donor slice into the target slice using bounded rotation:
|
| 35 |
+
- `linear` : direct addition
|
| 36 |
+
- `residual` : add the delta between donor and target
|
| 37 |
+
- `linear-mag` : add + rescale to preserve target's magnitude
|
| 38 |
+
(this is the safe default at higher strengths — strength
|
| 39 |
+
becomes rotation angle rather than amplification)
|
| 40 |
+
|
| 41 |
+
5. Optionally strip the component of donor that aligns with target ("orthogonal
|
| 42 |
+
projection") so the graft only adds directions the target wasn't using.
|
| 43 |
+
|
| 44 |
+
6. Repeat for out_proj (per-head column mixing) and MLP tensors (feature-space
|
| 45 |
+
transformations).
|
| 46 |
+
|
| 47 |
+
The result: target attention heads and MLPs rotate toward donor's directions
|
| 48 |
+
while preserving target's architectural roles. Character transfers.
|
| 49 |
+
|
| 50 |
+
==============================================================================
|
| 51 |
+
KREA 2 -> ZIMAGE SPECIFICS
|
| 52 |
+
==============================================================================
|
| 53 |
+
|
| 54 |
+
Zimage architecture (target):
|
| 55 |
+
layers.N.attention.qkv.weight (11520, 3840) fused Q|K|V, 30h * 128
|
| 56 |
+
layers.N.attention.out.weight (3840, 3840)
|
| 57 |
+
layers.N.attention.q_norm.weight (128,) (not grafted)
|
| 58 |
+
layers.N.attention.k_norm.weight (128,) (not grafted)
|
| 59 |
+
layers.N.feed_forward.w1.weight (10240, 3840) SwiGLU value (up)
|
| 60 |
+
layers.N.feed_forward.w3.weight (10240, 3840) SwiGLU gate
|
| 61 |
+
layers.N.feed_forward.w2.weight (3840, 10240) down projection
|
| 62 |
+
(context_refiner.N.* uses identical structure; 2 blocks vs 30 main layers)
|
| 63 |
+
|
| 64 |
+
Krea 2 architecture (donor):
|
| 65 |
+
<prefix>.blocks.N.attn.wq.weight (6144, 6144) 48 heads * 128
|
| 66 |
+
<prefix>.blocks.N.attn.wk.weight (1536, 6144) 12 heads * 128 (GQA 4:1)
|
| 67 |
+
<prefix>.blocks.N.attn.wv.weight (1536, 6144) 12 heads * 128 (GQA 4:1)
|
| 68 |
+
<prefix>.blocks.N.attn.wo.weight (6144, 6144)
|
| 69 |
+
<prefix>.blocks.N.mlp.up.weight (16384, 6144) SwiGLU value
|
| 70 |
+
<prefix>.blocks.N.mlp.gate.weight (16384, 6144) SwiGLU gate
|
| 71 |
+
<prefix>.blocks.N.mlp.down.weight (6144, 16384)
|
| 72 |
+
|
| 73 |
+
Coverage per grafted block:
|
| 74 |
+
Q: 30 target slots draw from 30 of Krea's 48 Q heads (63% selection)
|
| 75 |
+
x 3840/6144 = 62% column coverage per head
|
| 76 |
+
K/V: 12 Krea heads -> first 12 of 30 target slots (40% slot use)
|
| 77 |
+
x 3840/6144 = 62% column coverage per head
|
| 78 |
+
MLP w1/w3: 10240/16384 rows x 3840/6144 cols = 62% x 62% coverage
|
| 79 |
+
MLP w2: 3840/6144 rows x 10240/16384 cols = 62% x 62% coverage
|
| 80 |
+
|
| 81 |
+
==============================================================================
|
| 82 |
+
WHY IT WORKS (short version)
|
| 83 |
+
==============================================================================
|
| 84 |
+
|
| 85 |
+
Multi-head attention was designed so heads specialize on distinct patterns.
|
| 86 |
+
A head's "role" is fixed by its slot position in the network; a head's
|
| 87 |
+
"specialization" lives in its weights. Substituting one model's specialized
|
| 88 |
+
head for another's — at the same slot — transfers the specialization while
|
| 89 |
+
preserving the role. Character in image models lives in these specializations
|
| 90 |
+
more than in specific parameter values.
|
| 91 |
+
|
| 92 |
+
Two heads with the same magnitude but different singular directions produce
|
| 93 |
+
completely different outputs on the same input. So `linear-mag` mode is the
|
| 94 |
+
mathematically clean way to preserve target's operational scale while
|
| 95 |
+
rotating its direction toward donor's direction: bounded rotation, magnitude
|
| 96 |
+
identity, character transfers.
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
import os
|
| 100 |
+
import math
|
| 101 |
+
import sys
|
| 102 |
+
import argparse
|
| 103 |
+
from typing import Dict, List, Optional, Tuple
|
| 104 |
+
|
| 105 |
+
import torch
|
| 106 |
+
from safetensors import safe_open
|
| 107 |
+
from safetensors.torch import save_file
|
| 108 |
+
from tqdm import tqdm
|
| 109 |
+
|
| 110 |
+
# ============================================================================
|
| 111 |
+
# Constants — architectural dimensions of Krea 2 (donor) and Zimage (target)
|
| 112 |
+
# ============================================================================
|
| 113 |
+
|
| 114 |
+
# Donor: Krea 2
|
| 115 |
+
KREA_TOTAL_BLOCKS = 28
|
| 116 |
+
KREA_HIDDEN = 6144
|
| 117 |
+
KREA_Q_HEADS = 48
|
| 118 |
+
KREA_KV_HEADS = 12 # Grouped-query attention: 4 Q heads per K/V head
|
| 119 |
+
KREA_HEAD_DIM = 128
|
| 120 |
+
KREA_Q_INNER = KREA_Q_HEADS * KREA_HEAD_DIM # 6144
|
| 121 |
+
KREA_KV_INNER = KREA_KV_HEADS * KREA_HEAD_DIM # 1536
|
| 122 |
+
KREA_MLP_INNER = 16384
|
| 123 |
+
|
| 124 |
+
KREA_PREFIX_CANDIDATES = [
|
| 125 |
+
"model.diffusion_model.blocks",
|
| 126 |
+
"blocks",
|
| 127 |
+
]
|
| 128 |
+
|
| 129 |
+
# Target: Zimage
|
| 130 |
+
ZIMAGE_MAIN_BLOCKS = 30
|
| 131 |
+
ZIMAGE_REFINER_BLOCKS = 2
|
| 132 |
+
ZIMAGE_HIDDEN = 3840
|
| 133 |
+
ZIMAGE_HEADS = 30
|
| 134 |
+
ZIMAGE_HEAD_DIM = 128
|
| 135 |
+
ZIMAGE_INNER = ZIMAGE_HEADS * ZIMAGE_HEAD_DIM # 3840
|
| 136 |
+
ZIMAGE_MLP_INNER = 10240
|
| 137 |
+
|
| 138 |
+
ZIMAGE_MAIN_PREFIX = "layers"
|
| 139 |
+
ZIMAGE_REFINER_PREFIX = "context_refiner"
|
| 140 |
+
|
| 141 |
+
EPS = 1e-8
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
# ============================================================================
|
| 145 |
+
# Dimensional reduction: truncation vs SVD projection
|
| 146 |
+
# ----------------------------------------------------------------------------
|
| 147 |
+
# Donor tensors are larger than target slots in every dimension. Two ways to
|
| 148 |
+
# reduce them:
|
| 149 |
+
#
|
| 150 |
+
# truncate : drop trailing rows or columns (simple, fast, loses everything
|
| 151 |
+
# in the dropped indices).
|
| 152 |
+
#
|
| 153 |
+
# svd : project onto the top-N singular directions (preserves the
|
| 154 |
+
# "most important" directions the donor tensor spans).
|
| 155 |
+
#
|
| 156 |
+
# SVD only helps when the target reduction dim is <= the matrix rank.
|
| 157 |
+
# For per-head slices (128 x hidden_dim), rank is capped at 128, so requesting
|
| 158 |
+
# more than 128 dims via SVD falls back to truncation automatically.
|
| 159 |
+
# ============================================================================
|
| 160 |
+
|
| 161 |
+
def reduce_dim_via_svd(t: torch.Tensor, target_dim: int, axis: int,
|
| 162 |
+
device: torch.device) -> torch.Tensor:
|
| 163 |
+
"""Reduce t along `axis` to `target_dim` via truncated SVD projection.
|
| 164 |
+
|
| 165 |
+
axis=0 reduces rows, axis=1 reduces columns.
|
| 166 |
+
|
| 167 |
+
When target_dim > rank(t) = min(rows, cols), SVD literally cannot span
|
| 168 |
+
that many independent directions — falls back to truncation.
|
| 169 |
+
"""
|
| 170 |
+
orig_dtype = t.dtype
|
| 171 |
+
tf = t.to(device=device, dtype=torch.float32)
|
| 172 |
+
rows, cols = tf.shape
|
| 173 |
+
r_max = min(rows, cols)
|
| 174 |
+
|
| 175 |
+
# Rank ceiling: SVD adds nothing beyond truncation when target > rank
|
| 176 |
+
if target_dim > r_max:
|
| 177 |
+
if axis == 1:
|
| 178 |
+
return tf[:, :target_dim].to(dtype=orig_dtype).cpu().contiguous()
|
| 179 |
+
else:
|
| 180 |
+
return tf[:target_dim, :].to(dtype=orig_dtype).cpu().contiguous()
|
| 181 |
+
|
| 182 |
+
# SVD path — target_dim fits inside the achievable rank
|
| 183 |
+
# driver='gesvd' uses the stable LAPACK routine (avoids the cuSOLVER
|
| 184 |
+
# SGESVDJ Windows bug on large tensors). CPU fallback for safety.
|
| 185 |
+
try:
|
| 186 |
+
U, S, Vh = torch.linalg.svd(tf, full_matrices=False, driver='gesvd')
|
| 187 |
+
except Exception:
|
| 188 |
+
U, S, Vh = torch.linalg.svd(tf.cpu(), full_matrices=False)
|
| 189 |
+
U, S, Vh = U.to(device), S.to(device), Vh.to(device)
|
| 190 |
+
|
| 191 |
+
if axis == 1:
|
| 192 |
+
# Rank-target_dim reconstruction in column space
|
| 193 |
+
# Result shape: (rows, target_dim), preserving top-target_dim directions
|
| 194 |
+
result = U[:, :target_dim] * S[:target_dim]
|
| 195 |
+
elif axis == 0:
|
| 196 |
+
# Rank-target_dim reconstruction in row space
|
| 197 |
+
# Result shape: (target_dim, cols)
|
| 198 |
+
result = S[:target_dim].unsqueeze(1) * Vh[:target_dim, :]
|
| 199 |
+
else:
|
| 200 |
+
raise ValueError(f"axis must be 0 or 1, got {axis}")
|
| 201 |
+
|
| 202 |
+
return result.to(dtype=orig_dtype).cpu().contiguous()
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def reduce_dim(t: torch.Tensor, target_dim: int, axis: int, mode: str,
|
| 206 |
+
device: torch.device) -> torch.Tensor:
|
| 207 |
+
"""Reduce t along `axis` to `target_dim` via mode='truncate' or 'svd'."""
|
| 208 |
+
if t.shape[axis] == target_dim:
|
| 209 |
+
return t.contiguous()
|
| 210 |
+
if t.shape[axis] < target_dim:
|
| 211 |
+
raise ValueError(f"Cannot expand dim {axis}: {t.shape[axis]} -> {target_dim}")
|
| 212 |
+
|
| 213 |
+
if mode == "truncate":
|
| 214 |
+
return (t[:target_dim, :] if axis == 0 else t[:, :target_dim]).contiguous()
|
| 215 |
+
elif mode == "svd":
|
| 216 |
+
return reduce_dim_via_svd(t, target_dim, axis, device)
|
| 217 |
+
else:
|
| 218 |
+
raise ValueError(f"Unknown reduce mode: {mode!r}")
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def reduce_2d(t: torch.Tensor, target_rows: int, target_cols: int,
|
| 222 |
+
mode: str, device: torch.device) -> torch.Tensor:
|
| 223 |
+
"""Reduce t along BOTH dims to (target_rows, target_cols).
|
| 224 |
+
|
| 225 |
+
Applied to MLP tensors where both dimensions differ between donor and
|
| 226 |
+
target. Two-pass: reduce rows first, then cols. Not mathematically
|
| 227 |
+
optimal (that would be a Tucker decomposition) but simple and effective.
|
| 228 |
+
"""
|
| 229 |
+
r_now, c_now = t.shape
|
| 230 |
+
if r_now == target_rows and c_now == target_cols:
|
| 231 |
+
return t.contiguous()
|
| 232 |
+
if r_now > target_rows:
|
| 233 |
+
t = reduce_dim(t, target_rows, 0, mode, device)
|
| 234 |
+
if t.shape[1] > target_cols:
|
| 235 |
+
t = reduce_dim(t, target_cols, 1, mode, device)
|
| 236 |
+
return t
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
# ============================================================================
|
| 240 |
+
# Head selection: which donor heads go into which target slots
|
| 241 |
+
# ----------------------------------------------------------------------------
|
| 242 |
+
# Krea has 48 Q heads, Zimage has 30. We must choose 30 of Krea's 48. Different
|
| 243 |
+
# selections produce different character (which heads "specialize" in what
|
| 244 |
+
# isn't documented for either model, so this is empirical).
|
| 245 |
+
# ============================================================================
|
| 246 |
+
|
| 247 |
+
def select_source_q_heads(mode: str) -> List[int]:
|
| 248 |
+
"""Choose which Krea Q heads (of 48) to source from."""
|
| 249 |
+
if mode == "first30":
|
| 250 |
+
return list(range(0, 30))
|
| 251 |
+
if mode == "middle30":
|
| 252 |
+
# Skip 9 heads on each side (structural early, refinement late)
|
| 253 |
+
return list(range(9, 39))
|
| 254 |
+
if mode == "last30":
|
| 255 |
+
return list(range(18, 48))
|
| 256 |
+
if mode == "spread30":
|
| 257 |
+
# Evenly sample across all 48 heads
|
| 258 |
+
return [round(i * 47 / 29) for i in range(30)]
|
| 259 |
+
if mode == "groups-first":
|
| 260 |
+
# Respect Krea's GQA grouping: 12 K/V groups * 4 Q heads per group.
|
| 261 |
+
# Take one Q head from each group first, then the next, etc.
|
| 262 |
+
picks = []
|
| 263 |
+
for offset in range(4):
|
| 264 |
+
for group in range(KREA_KV_HEADS):
|
| 265 |
+
picks.append(group * 4 + offset)
|
| 266 |
+
if len(picks) == 30: return picks
|
| 267 |
+
return picks[:30]
|
| 268 |
+
if mode == "all48":
|
| 269 |
+
# All 48 heads returned; if target has fewer slots the caller truncates
|
| 270 |
+
return list(range(48))
|
| 271 |
+
raise ValueError(f"Unknown source-head-selection mode: {mode!r}")
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def select_target_q_slots(mode: str, n_needed: int) -> List[int]:
|
| 275 |
+
"""Choose which Zimage Q slots (of 30) receive donor content."""
|
| 276 |
+
if n_needed > ZIMAGE_HEADS:
|
| 277 |
+
n_needed = ZIMAGE_HEADS
|
| 278 |
+
|
| 279 |
+
if mode == "first":
|
| 280 |
+
return list(range(0, n_needed))
|
| 281 |
+
if mode == "last":
|
| 282 |
+
return list(range(ZIMAGE_HEADS - n_needed, ZIMAGE_HEADS))
|
| 283 |
+
if mode == "middle":
|
| 284 |
+
start = (ZIMAGE_HEADS - n_needed) // 2
|
| 285 |
+
return list(range(start, start + n_needed))
|
| 286 |
+
if mode == "spread":
|
| 287 |
+
return [round(i * (ZIMAGE_HEADS - 1) / max(1, n_needed - 1)) for i in range(n_needed)]
|
| 288 |
+
if mode == "all":
|
| 289 |
+
return list(range(0, min(n_needed, ZIMAGE_HEADS)))
|
| 290 |
+
raise ValueError(f"Unknown target-head-selection mode: {mode!r}")
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
# ============================================================================
|
| 294 |
+
# Block mapping: for each target block, which donor block do we sample from?
|
| 295 |
+
# ----------------------------------------------------------------------------
|
| 296 |
+
# The relationship between donor block position and target block position
|
| 297 |
+
# matters because early/middle/late blocks in transformers tend to specialize
|
| 298 |
+
# in different aspects (structure, character, detail respectively).
|
| 299 |
+
# ============================================================================
|
| 300 |
+
|
| 301 |
+
def build_block_map(zimage_targets: List[int], mode: str,
|
| 302 |
+
donor_total: int, target_total: int,
|
| 303 |
+
donor_shift: int, donor_peak: int, target_peak: int,
|
| 304 |
+
clamp: bool = True) -> List[Tuple[int, int]]:
|
| 305 |
+
"""Return list of (target_block, donor_block) pairs.
|
| 306 |
+
|
| 307 |
+
Modes:
|
| 308 |
+
shift : linear 1:1 with a start offset (target 0 -> donor donor_shift)
|
| 309 |
+
peak : center donor_peak on target_peak, scale proportionally
|
| 310 |
+
linear : evenly distribute donor blocks across target range
|
| 311 |
+
off : simple modulo (target N -> donor N % donor_total)
|
| 312 |
+
"""
|
| 313 |
+
pairs = []
|
| 314 |
+
for tb in zimage_targets:
|
| 315 |
+
if mode == "shift":
|
| 316 |
+
db = tb - min(zimage_targets) + donor_shift
|
| 317 |
+
elif mode == "peak":
|
| 318 |
+
offset = (tb - target_peak) * donor_total / max(1, target_total)
|
| 319 |
+
db = int(round(donor_peak + offset))
|
| 320 |
+
elif mode == "linear":
|
| 321 |
+
frac = (tb - min(zimage_targets)) / max(1, (max(zimage_targets) - min(zimage_targets)))
|
| 322 |
+
db = int(round(frac * (donor_total - 1)))
|
| 323 |
+
elif mode == "off":
|
| 324 |
+
db = tb % donor_total
|
| 325 |
+
else:
|
| 326 |
+
raise ValueError(f"Unknown remap mode: {mode!r}")
|
| 327 |
+
|
| 328 |
+
if clamp:
|
| 329 |
+
db = max(0, min(donor_total - 1, db))
|
| 330 |
+
pairs.append((tb, db))
|
| 331 |
+
return pairs
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def parse_block_list(s: str) -> List[int]:
|
| 335 |
+
"""Parse 'A:B' as range(A,B) or 'A,B,C' as explicit list."""
|
| 336 |
+
if ":" in s:
|
| 337 |
+
a, b = s.split(":")
|
| 338 |
+
return list(range(int(a), int(b)))
|
| 339 |
+
return [int(x) for x in s.split(",")]
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
# ============================================================================
|
| 343 |
+
# The heart of the graft: blending one weight patch into another
|
| 344 |
+
# ----------------------------------------------------------------------------
|
| 345 |
+
# Called for every per-head slice, out_proj column region, and MLP tensor.
|
| 346 |
+
# The name "blend_patch" refers to the standard math sense of a "patch" —
|
| 347 |
+
# a rectangular slice of a tensor being modified.
|
| 348 |
+
#
|
| 349 |
+
# Three modes trade off differently:
|
| 350 |
+
#
|
| 351 |
+
# linear : new = base + strength * donor
|
| 352 |
+
# Simple additive. Magnitude of result drifts up (or down) from
|
| 353 |
+
# base. At high strengths, this cascades through the network.
|
| 354 |
+
#
|
| 355 |
+
# residual : new = base + strength * (donor - base)
|
| 356 |
+
# Adds the *delta*, so strength=1 replaces base with donor.
|
| 357 |
+
# Better for iterative composition but still has magnitude drift.
|
| 358 |
+
#
|
| 359 |
+
# linear-mag : new = base + strength * donor, then rescale to |base|
|
| 360 |
+
# Rotates base toward donor while preserving base's magnitude.
|
| 361 |
+
# Strength becomes a rotation angle (bounded to 90 deg max, at
|
| 362 |
+
# strength -> infinity). This is the mathematically cleanest
|
| 363 |
+
# option for cross-model transfer at higher strengths.
|
| 364 |
+
#
|
| 365 |
+
# The orthogonal option (recommended default) first strips donor's component
|
| 366 |
+
# that aligns with base's direction. Then only the perpendicular part gets
|
| 367 |
+
# blended in — meaning the graft ADDS directions base wasn't using, rather
|
| 368 |
+
# than overwriting directions base was using. By Pythagorean argument, this
|
| 369 |
+
# cannot reduce base's response in any direction it was previously responsive
|
| 370 |
+
# to.
|
| 371 |
+
# ============================================================================
|
| 372 |
+
|
| 373 |
+
def blend_patch(h_patch: torch.Tensor, donor: torch.Tensor,
|
| 374 |
+
strength: float, mode: str, orthogonal: bool,
|
| 375 |
+
eps: float = EPS) -> Tuple[torch.Tensor, float, float]:
|
| 376 |
+
"""Blend `donor` into `h_patch` with the given strength and mode.
|
| 377 |
+
|
| 378 |
+
Returns (blended_tensor, arc_deg, rel_residual) — the arc is the angle
|
| 379 |
+
between h_patch and blended (how much the base rotated); rel_residual
|
| 380 |
+
is the fractional magnitude of the change.
|
| 381 |
+
"""
|
| 382 |
+
# Orthogonal projection: remove donor's component along h_patch direction
|
| 383 |
+
if orthogonal:
|
| 384 |
+
flat_h = h_patch.flatten()
|
| 385 |
+
flat_d = donor.flatten()
|
| 386 |
+
nh = flat_h.norm() + eps
|
| 387 |
+
proj_coef = (flat_d * flat_h).sum() / (nh * nh)
|
| 388 |
+
donor_eff = donor - proj_coef * h_patch
|
| 389 |
+
else:
|
| 390 |
+
donor_eff = donor
|
| 391 |
+
|
| 392 |
+
# Apply the chosen blend mode
|
| 393 |
+
if mode == "linear":
|
| 394 |
+
blended = h_patch + strength * donor_eff
|
| 395 |
+
elif mode == "residual":
|
| 396 |
+
diff = donor_eff - h_patch
|
| 397 |
+
blended = h_patch + strength * diff
|
| 398 |
+
elif mode == "linear-mag":
|
| 399 |
+
# Bounded rotation: add + rescale to preserve |h_patch|.
|
| 400 |
+
# If donor_eff is perpendicular to h_patch, rotation angle =
|
| 401 |
+
# arctan(strength * |donor_eff| / |h_patch|).
|
| 402 |
+
tmp = h_patch + strength * donor_eff
|
| 403 |
+
h_norm = h_patch.norm() + eps
|
| 404 |
+
tmp_norm = tmp.norm() + eps
|
| 405 |
+
blended = tmp * (h_norm / tmp_norm)
|
| 406 |
+
else:
|
| 407 |
+
raise ValueError(f"Unknown blend mode: {mode!r}")
|
| 408 |
+
|
| 409 |
+
# Diagnostics for logging
|
| 410 |
+
vh = h_patch.flatten()
|
| 411 |
+
vb = blended.flatten()
|
| 412 |
+
dot = torch.clamp((vh * vb).sum() / ((vh.norm() + eps) * (vb.norm() + eps)),
|
| 413 |
+
-1.0, 1.0)
|
| 414 |
+
arc = math.degrees(float(torch.acos(dot)))
|
| 415 |
+
rel = float((blended - h_patch).norm() / (h_patch.norm() + eps))
|
| 416 |
+
return blended, arc, rel
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
# ============================================================================
|
| 420 |
+
# Per-band graft functions
|
| 421 |
+
# ----------------------------------------------------------------------------
|
| 422 |
+
# Each band (Q, K/V, out_proj, MLP up/gate/down) needs its own placement logic
|
| 423 |
+
# because the tensor shapes and per-head slicing differ.
|
| 424 |
+
# ============================================================================
|
| 425 |
+
|
| 426 |
+
def apply_qkv_graft(zimage_base: Dict[str, torch.Tensor],
|
| 427 |
+
zimage_prefix: str,
|
| 428 |
+
krea_wq: torch.Tensor, krea_wk: Optional[torch.Tensor],
|
| 429 |
+
krea_wv: Optional[torch.Tensor],
|
| 430 |
+
q_strength: float, kv_strength: float,
|
| 431 |
+
do_kv: bool,
|
| 432 |
+
mode: str, orthogonal: bool,
|
| 433 |
+
source_q_heads: List[int],
|
| 434 |
+
target_q_slots: List[int],
|
| 435 |
+
hidden_reduce: str,
|
| 436 |
+
dev: torch.device) -> Tuple[Dict[str, torch.Tensor], Dict[str, list]]:
|
| 437 |
+
"""Graft Krea Q (and optionally K/V) into Zimage's fused attention.qkv.
|
| 438 |
+
|
| 439 |
+
Zimage stores Q, K, V concatenated along axis 0:
|
| 440 |
+
rows [0 : ZIMAGE_INNER) = Q band (30 heads)
|
| 441 |
+
rows [ZIMAGE_INNER : 2*ZIMAGE_INNER) = K band (30 heads)
|
| 442 |
+
rows [2*ZIMAGE_INNER : 3*ZIMAGE_INNER) = V band (30 heads)
|
| 443 |
+
|
| 444 |
+
Each head is 128 rows (head_dim). To graft head H of the Q band we modify
|
| 445 |
+
rows [H*128 : (H+1)*128] within the Q band.
|
| 446 |
+
"""
|
| 447 |
+
qkv_key = zimage_prefix + "attention.qkv.weight"
|
| 448 |
+
if qkv_key not in zimage_base:
|
| 449 |
+
raise KeyError(f"Zimage qkv not found: {qkv_key}")
|
| 450 |
+
|
| 451 |
+
zimage_qkv = zimage_base[qkv_key].to(dev, dtype=torch.float32).clone()
|
| 452 |
+
stats = {"q_arc": [], "q_rel": [], "kv_arc": [], "kv_rel": []}
|
| 453 |
+
|
| 454 |
+
# === Q graft: per-head placement ===
|
| 455 |
+
# Krea wq has shape (48*128, 6144). View as 48 heads of shape (128, 6144).
|
| 456 |
+
krea_q_view = krea_wq.view(KREA_Q_HEADS, KREA_HEAD_DIM, KREA_HIDDEN)
|
| 457 |
+
|
| 458 |
+
for source_head, target_slot in zip(source_q_heads, target_q_slots):
|
| 459 |
+
# Extract one Krea head: (128, 6144)
|
| 460 |
+
krea_head = krea_q_view[source_head].to(dev, dtype=torch.float32)
|
| 461 |
+
# Reduce hidden dim: (128, 6144) -> (128, 3840)
|
| 462 |
+
# For per-head slices, rank is only 128, so this falls back to truncation
|
| 463 |
+
# regardless of --hidden-reduce mode.
|
| 464 |
+
krea_head_reduced = reduce_dim(krea_head, ZIMAGE_HIDDEN, 1,
|
| 465 |
+
hidden_reduce, dev).to(dev)
|
| 466 |
+
|
| 467 |
+
# Place into target slot's rows within the Q band
|
| 468 |
+
r0 = target_slot * ZIMAGE_HEAD_DIM
|
| 469 |
+
r1 = r0 + ZIMAGE_HEAD_DIM
|
| 470 |
+
zimage_slice = zimage_qkv[r0:r1, :].clone()
|
| 471 |
+
|
| 472 |
+
blended, arc, rel = blend_patch(zimage_slice, krea_head_reduced,
|
| 473 |
+
q_strength, mode, orthogonal)
|
| 474 |
+
zimage_qkv[r0:r1, :] = blended
|
| 475 |
+
stats["q_arc"].append(arc)
|
| 476 |
+
stats["q_rel"].append(rel)
|
| 477 |
+
|
| 478 |
+
# === K/V graft (optional): 12 GQA heads into first 12 target slots ===
|
| 479 |
+
if do_kv and krea_wk is not None and krea_wv is not None:
|
| 480 |
+
# Krea uses grouped-query attention: only 12 K/V heads exist (vs 48 Q).
|
| 481 |
+
# Zimage has 30 K/V slots and no GQA. We place Krea's 12 K/V heads
|
| 482 |
+
# into the first 12 Zimage K/V slots; slots 12-29 stay native.
|
| 483 |
+
krea_k_view = krea_wk.view(KREA_KV_HEADS, KREA_HEAD_DIM, KREA_HIDDEN)
|
| 484 |
+
krea_v_view = krea_wv.view(KREA_KV_HEADS, KREA_HEAD_DIM, KREA_HIDDEN)
|
| 485 |
+
|
| 486 |
+
n_kv = min(KREA_KV_HEADS, ZIMAGE_HEADS)
|
| 487 |
+
for kv_head in range(n_kv):
|
| 488 |
+
# K band offset: rows [ZIMAGE_INNER + kv_head*128 : ...]
|
| 489 |
+
k_r0 = ZIMAGE_INNER + kv_head * ZIMAGE_HEAD_DIM
|
| 490 |
+
k_r1 = k_r0 + ZIMAGE_HEAD_DIM
|
| 491 |
+
krea_k_head = krea_k_view[kv_head].to(dev, dtype=torch.float32)
|
| 492 |
+
krea_k_reduced = reduce_dim(krea_k_head, ZIMAGE_HIDDEN, 1,
|
| 493 |
+
hidden_reduce, dev).to(dev)
|
| 494 |
+
z_k_slice = zimage_qkv[k_r0:k_r1, :].clone()
|
| 495 |
+
blended_k, k_arc, k_rel = blend_patch(z_k_slice, krea_k_reduced,
|
| 496 |
+
kv_strength, mode, orthogonal)
|
| 497 |
+
zimage_qkv[k_r0:k_r1, :] = blended_k
|
| 498 |
+
stats["kv_arc"].append(k_arc)
|
| 499 |
+
stats["kv_rel"].append(k_rel)
|
| 500 |
+
|
| 501 |
+
# V band offset: rows [2*ZIMAGE_INNER + kv_head*128 : ...]
|
| 502 |
+
v_r0 = 2 * ZIMAGE_INNER + kv_head * ZIMAGE_HEAD_DIM
|
| 503 |
+
v_r1 = v_r0 + ZIMAGE_HEAD_DIM
|
| 504 |
+
krea_v_head = krea_v_view[kv_head].to(dev, dtype=torch.float32)
|
| 505 |
+
krea_v_reduced = reduce_dim(krea_v_head, ZIMAGE_HIDDEN, 1,
|
| 506 |
+
hidden_reduce, dev).to(dev)
|
| 507 |
+
z_v_slice = zimage_qkv[v_r0:v_r1, :].clone()
|
| 508 |
+
blended_v, v_arc, v_rel = blend_patch(z_v_slice, krea_v_reduced,
|
| 509 |
+
kv_strength, mode, orthogonal)
|
| 510 |
+
zimage_qkv[v_r0:v_r1, :] = blended_v
|
| 511 |
+
stats["kv_arc"].append(v_arc)
|
| 512 |
+
stats["kv_rel"].append(v_rel)
|
| 513 |
+
|
| 514 |
+
return {qkv_key: zimage_qkv.to(torch.bfloat16).cpu()}, stats
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
def apply_out_proj_graft(zimage_base: Dict[str, torch.Tensor],
|
| 518 |
+
zimage_prefix: str,
|
| 519 |
+
krea_wo: torch.Tensor,
|
| 520 |
+
strength: float, mode: str, orthogonal: bool,
|
| 521 |
+
source_q_heads: List[int],
|
| 522 |
+
target_q_slots: List[int],
|
| 523 |
+
hidden_reduce: str,
|
| 524 |
+
dev: torch.device) -> Tuple[Dict[str, torch.Tensor], list]:
|
| 525 |
+
"""Graft Krea attn.wo into Zimage attention.out.
|
| 526 |
+
|
| 527 |
+
out_proj mixes per-head outputs into the residual stream. Its columns
|
| 528 |
+
are the per-head contribution regions:
|
| 529 |
+
cols [h*128 : (h+1)*128] = head h's output contribution to the mix
|
| 530 |
+
|
| 531 |
+
We match this per-head structure: Krea head H contributes to the same
|
| 532 |
+
target slot chosen for the Q graft. Row dim is the hidden dim (donor
|
| 533 |
+
6144 -> target 3840, reduced once globally before per-head col placement).
|
| 534 |
+
"""
|
| 535 |
+
out_key = zimage_prefix + "attention.out.weight"
|
| 536 |
+
if out_key not in zimage_base:
|
| 537 |
+
raise KeyError(f"Zimage attention.out not found: {out_key}")
|
| 538 |
+
|
| 539 |
+
zimage_out = zimage_base[out_key].to(dev, dtype=torch.float32).clone()
|
| 540 |
+
arcs = []
|
| 541 |
+
|
| 542 |
+
# Reduce Krea wo rows 6144 -> 3840 once (shared across all head columns)
|
| 543 |
+
krea_wo_reduced_rows = reduce_dim(krea_wo, ZIMAGE_HIDDEN, 0,
|
| 544 |
+
hidden_reduce, dev).to(dev, dtype=torch.float32)
|
| 545 |
+
|
| 546 |
+
# Then place per-head column regions into matching target slots
|
| 547 |
+
for source_head, target_slot in zip(source_q_heads, target_q_slots):
|
| 548 |
+
c0k = source_head * KREA_HEAD_DIM
|
| 549 |
+
c1k = c0k + KREA_HEAD_DIM
|
| 550 |
+
krea_head_cols = krea_wo_reduced_rows[:, c0k:c1k] # (3840, 128)
|
| 551 |
+
|
| 552 |
+
c0z = target_slot * ZIMAGE_HEAD_DIM
|
| 553 |
+
c1z = c0z + ZIMAGE_HEAD_DIM
|
| 554 |
+
z_slice = zimage_out[:, c0z:c1z].clone()
|
| 555 |
+
|
| 556 |
+
blended, arc, _ = blend_patch(z_slice, krea_head_cols,
|
| 557 |
+
strength, mode, orthogonal)
|
| 558 |
+
zimage_out[:, c0z:c1z] = blended
|
| 559 |
+
arcs.append(arc)
|
| 560 |
+
|
| 561 |
+
return {out_key: zimage_out.to(torch.bfloat16).cpu()}, arcs
|
| 562 |
+
|
| 563 |
+
|
| 564 |
+
def apply_mlp_graft(zimage_base: Dict[str, torch.Tensor],
|
| 565 |
+
zimage_prefix: str,
|
| 566 |
+
krea_up: torch.Tensor, krea_gate: torch.Tensor,
|
| 567 |
+
krea_down: Optional[torch.Tensor],
|
| 568 |
+
strength: float, mode: str, orthogonal: bool,
|
| 569 |
+
include_gate: bool, include_w2: bool,
|
| 570 |
+
hidden_reduce: str,
|
| 571 |
+
dev: torch.device) -> Tuple[Dict[str, torch.Tensor], Dict[str, float]]:
|
| 572 |
+
"""Graft Krea SwiGLU MLP into Zimage feed_forward.
|
| 573 |
+
|
| 574 |
+
SwiGLU is a three-tensor MLP: out = down(silu(gate(x)) * up(x))
|
| 575 |
+
Both Krea and Zimage use this family. Direct 1:1 name mapping:
|
| 576 |
+
Krea mlp.up -> Zimage feed_forward.w1 (value)
|
| 577 |
+
Krea mlp.gate -> Zimage feed_forward.w3 (gate, opt-in default ON)
|
| 578 |
+
Krea mlp.down -> Zimage feed_forward.w2 (down, opt-in default OFF)
|
| 579 |
+
|
| 580 |
+
All three require 2D reduction since both inner (16384->10240) and
|
| 581 |
+
hidden (6144->3840) dims differ.
|
| 582 |
+
|
| 583 |
+
Down projection (w2) is left off by default because it tends to be more
|
| 584 |
+
destructive to grafts than up/gate — it's the "output-shaping" tensor
|
| 585 |
+
of the MLP and target-native w2 keeps outputs consistent with the rest
|
| 586 |
+
of the network's expectations.
|
| 587 |
+
"""
|
| 588 |
+
out = {}
|
| 589 |
+
stats = {}
|
| 590 |
+
|
| 591 |
+
# w1 (up / SwiGLU value)
|
| 592 |
+
w1_key = zimage_prefix + "feed_forward.w1.weight"
|
| 593 |
+
if w1_key in zimage_base:
|
| 594 |
+
z_w1 = zimage_base[w1_key].to(dev, dtype=torch.float32).clone()
|
| 595 |
+
krea_up_reduced = reduce_2d(krea_up, ZIMAGE_MLP_INNER, ZIMAGE_HIDDEN,
|
| 596 |
+
hidden_reduce, dev).to(dev, dtype=torch.float32)
|
| 597 |
+
blended, arc, _ = blend_patch(z_w1, krea_up_reduced,
|
| 598 |
+
strength, mode, orthogonal)
|
| 599 |
+
out[w1_key] = blended.to(torch.bfloat16).cpu()
|
| 600 |
+
stats["w1_arc"] = arc
|
| 601 |
+
|
| 602 |
+
# w3 (SwiGLU gate)
|
| 603 |
+
if include_gate:
|
| 604 |
+
w3_key = zimage_prefix + "feed_forward.w3.weight"
|
| 605 |
+
if w3_key in zimage_base:
|
| 606 |
+
z_w3 = zimage_base[w3_key].to(dev, dtype=torch.float32).clone()
|
| 607 |
+
krea_gate_reduced = reduce_2d(krea_gate, ZIMAGE_MLP_INNER, ZIMAGE_HIDDEN,
|
| 608 |
+
hidden_reduce, dev).to(dev, dtype=torch.float32)
|
| 609 |
+
blended, arc, _ = blend_patch(z_w3, krea_gate_reduced,
|
| 610 |
+
strength, mode, orthogonal)
|
| 611 |
+
out[w3_key] = blended.to(torch.bfloat16).cpu()
|
| 612 |
+
stats["w3_arc"] = arc
|
| 613 |
+
|
| 614 |
+
# w2 (down projection)
|
| 615 |
+
if include_w2 and krea_down is not None:
|
| 616 |
+
w2_key = zimage_prefix + "feed_forward.w2.weight"
|
| 617 |
+
if w2_key in zimage_base:
|
| 618 |
+
z_w2 = zimage_base[w2_key].to(dev, dtype=torch.float32).clone()
|
| 619 |
+
krea_down_reduced = reduce_2d(krea_down, ZIMAGE_HIDDEN, ZIMAGE_MLP_INNER,
|
| 620 |
+
hidden_reduce, dev).to(dev, dtype=torch.float32)
|
| 621 |
+
blended, arc, _ = blend_patch(z_w2, krea_down_reduced,
|
| 622 |
+
strength, mode, orthogonal)
|
| 623 |
+
out[w2_key] = blended.to(torch.bfloat16).cpu()
|
| 624 |
+
stats["w2_arc"] = arc
|
| 625 |
+
|
| 626 |
+
return out, stats
|
| 627 |
+
|
| 628 |
+
|
| 629 |
+
# ============================================================================
|
| 630 |
+
# Main
|
| 631 |
+
# ============================================================================
|
| 632 |
+
|
| 633 |
+
def main():
|
| 634 |
+
ap = argparse.ArgumentParser(
|
| 635 |
+
description="Graft Krea 2 weights into Zimage. Cross-model weight "
|
| 636 |
+
"transfer via per-head placement and dimensional reduction, "
|
| 637 |
+
"no training involved.")
|
| 638 |
+
|
| 639 |
+
ap.add_argument("--krea-donor", required=True,
|
| 640 |
+
help="Path to Krea 2 checkpoint (safetensors).")
|
| 641 |
+
ap.add_argument("--zimage-base", required=True,
|
| 642 |
+
help="Path to Zimage base checkpoint (safetensors).")
|
| 643 |
+
ap.add_argument("--output", required=True,
|
| 644 |
+
help="Path to write the grafted Zimage checkpoint.")
|
| 645 |
+
|
| 646 |
+
ap.add_argument("--target", choices=["main", "refiner"], default="main",
|
| 647 |
+
help="Which Zimage sub-stack to graft into. "
|
| 648 |
+
"'main' targets layers.N (30 blocks — the main "
|
| 649 |
+
"denoising stack, primary character carrier). "
|
| 650 |
+
"'refiner' targets context_refiner.N (2 blocks — "
|
| 651 |
+
"text conditioning refinement; small but influential).")
|
| 652 |
+
|
| 653 |
+
ap.add_argument("--blocks", type=str, default=None,
|
| 654 |
+
help="Target block range (e.g. '0:30' for all main layers, "
|
| 655 |
+
"'15:30' for late layers only). Defaults to all blocks "
|
| 656 |
+
"of the chosen target.")
|
| 657 |
+
|
| 658 |
+
ap.add_argument("--strength", type=float, default=0.15,
|
| 659 |
+
help="Q strength (also V strength if --kv-strength not set). "
|
| 660 |
+
"Krea grafts run un-gated in some target contexts, so "
|
| 661 |
+
"effective magnitude can be higher than the value "
|
| 662 |
+
"suggests. Start moderate (0.10-0.20) and iterate. "
|
| 663 |
+
"With --mode linear-mag, strength has a bounded "
|
| 664 |
+
"rotation-angle interpretation and is safer at higher "
|
| 665 |
+
"values.")
|
| 666 |
+
|
| 667 |
+
ap.add_argument("--mode", choices=["linear", "residual", "linear-mag"],
|
| 668 |
+
default="residual",
|
| 669 |
+
help="Blend mode. 'linear': base + s*donor (standard). "
|
| 670 |
+
"'residual' (default): base + s*(donor - base). "
|
| 671 |
+
"'linear-mag': add then rescale to preserve base "
|
| 672 |
+
"magnitude — recommended for higher strengths since "
|
| 673 |
+
"it's mathematically bounded and prevents amplitude "
|
| 674 |
+
"cascade through the network.")
|
| 675 |
+
|
| 676 |
+
ap.add_argument("--orthogonal", action="store_true", default=True,
|
| 677 |
+
help="Strip donor's base-parallel component before blending. "
|
| 678 |
+
"Default ON. Preserves base capabilities in principal "
|
| 679 |
+
"directions — the graft can only add directions the "
|
| 680 |
+
"target wasn't using, not overwrite ones it was.")
|
| 681 |
+
ap.add_argument("--no-orthogonal", dest="orthogonal", action="store_false")
|
| 682 |
+
|
| 683 |
+
ap.add_argument("--remap", choices=["shift", "peak", "linear", "off"],
|
| 684 |
+
default="shift",
|
| 685 |
+
help="How to map target blocks to donor blocks. "
|
| 686 |
+
"'shift' (default): 1:1 with an offset. "
|
| 687 |
+
"'peak': center donor peak on target peak, proportional. "
|
| 688 |
+
"'linear': evenly distribute donor blocks over target range. "
|
| 689 |
+
"'off': modulo mapping (wraps).")
|
| 690 |
+
ap.add_argument("--donor-shift", type=int, default=0,
|
| 691 |
+
help="Offset for --remap shift.")
|
| 692 |
+
ap.add_argument("--krea-peak", type=int, default=14,
|
| 693 |
+
help="Krea block index at peak position (for --remap peak).")
|
| 694 |
+
ap.add_argument("--zimage-peak", type=int, default=None,
|
| 695 |
+
help="Target block index to align with Krea peak (for --remap peak). "
|
| 696 |
+
"Defaults to midpoint of the target range.")
|
| 697 |
+
|
| 698 |
+
ap.add_argument("--source-head-selection",
|
| 699 |
+
choices=["first30", "middle30", "last30", "spread30",
|
| 700 |
+
"groups-first", "all48"],
|
| 701 |
+
default="middle30",
|
| 702 |
+
help="Which 30 of Krea's 48 Q heads to source from. "
|
| 703 |
+
"'middle30' (default) skips the first and last 9 heads. "
|
| 704 |
+
"'spread30' samples evenly across all 48. "
|
| 705 |
+
"'groups-first' respects Krea's GQA groupings. "
|
| 706 |
+
"'all48' returns 48; if target has fewer slots the "
|
| 707 |
+
"last 30 are used.")
|
| 708 |
+
ap.add_argument("--target-head-selection",
|
| 709 |
+
choices=["first", "last", "middle", "spread", "all"],
|
| 710 |
+
default="all",
|
| 711 |
+
help="Which Zimage Q slots receive donor content. "
|
| 712 |
+
"'all' (default) uses all 30 target slots.")
|
| 713 |
+
|
| 714 |
+
ap.add_argument("--kv", action="store_true", default=False,
|
| 715 |
+
help="Also graft K/V. Krea's GQA (12 K/V heads) limits "
|
| 716 |
+
"coverage to 12 of Zimage's 30 K/V slots. Off by "
|
| 717 |
+
"default — enable to include the attention routing "
|
| 718 |
+
"and value projections in the graft.")
|
| 719 |
+
ap.add_argument("--kv-strength", type=float, default=None,
|
| 720 |
+
help="K/V strength. Defaults to 0.5 * --strength.")
|
| 721 |
+
|
| 722 |
+
ap.add_argument("--out-proj", action="store_true", default=False,
|
| 723 |
+
help="Also graft attention.out (head-mixing projection).")
|
| 724 |
+
ap.add_argument("--out-proj-strength", type=float, default=None,
|
| 725 |
+
help="attention.out strength. Defaults to 0.5 * --strength.")
|
| 726 |
+
|
| 727 |
+
ap.add_argument("--mlp", action="store_true", default=False,
|
| 728 |
+
help="Also graft feed_forward w1 (SwiGLU value).")
|
| 729 |
+
ap.add_argument("--mlp-blocks", type=str, default=None,
|
| 730 |
+
help="MLP block range. Defaults to --blocks. Use to restrict "
|
| 731 |
+
"MLP graft to a subset — for example, exclude the last "
|
| 732 |
+
"few blocks that specialize in detail synthesis matching "
|
| 733 |
+
"the target's VAE decoder.")
|
| 734 |
+
ap.add_argument("--mlp-strength", type=float, default=None,
|
| 735 |
+
help="MLP strength. Defaults to --strength.")
|
| 736 |
+
ap.add_argument("--include-gate", action="store_true", default=True,
|
| 737 |
+
help="Include SwiGLU gate (Krea mlp.gate -> Zimage w3). "
|
| 738 |
+
"Default ON — both models are SwiGLU-native so gate "
|
| 739 |
+
"transfers cleanly.")
|
| 740 |
+
ap.add_argument("--no-include-gate", dest="include_gate", action="store_false")
|
| 741 |
+
ap.add_argument("--mlp-include-w2", action="store_true", default=False,
|
| 742 |
+
help="Include down projection (Krea mlp.down -> Zimage w2). "
|
| 743 |
+
"Off by default — grafting w2 is typically more "
|
| 744 |
+
"destructive than up/gate.")
|
| 745 |
+
|
| 746 |
+
ap.add_argument("--hidden-reduce", choices=["truncate", "svd"], default="svd",
|
| 747 |
+
help="How to reduce Krea's larger dimensions to Zimage sizes. "
|
| 748 |
+
"'truncate' drops trailing rows/cols (fast, discards "
|
| 749 |
+
"information beyond the truncation point). "
|
| 750 |
+
"'svd' (default) projects onto top singular directions "
|
| 751 |
+
"when the target dim fits inside matrix rank, otherwise "
|
| 752 |
+
"falls back to truncation.")
|
| 753 |
+
|
| 754 |
+
ap.add_argument("--cycles", type=int, default=1,
|
| 755 |
+
help="Number of graft passes. Each pass sees the accumulated "
|
| 756 |
+
"state from the previous one. Higher cycle counts spread "
|
| 757 |
+
"character over broader donor subspaces at the cost of "
|
| 758 |
+
"runtime.")
|
| 759 |
+
|
| 760 |
+
ap.add_argument("--device", choices=["cuda", "cpu"], default="cuda")
|
| 761 |
+
ap.add_argument("--dry-run", action="store_true",
|
| 762 |
+
help="Print configuration and exit before loading models.")
|
| 763 |
+
|
| 764 |
+
args = ap.parse_args()
|
| 765 |
+
|
| 766 |
+
# Resolve derived defaults
|
| 767 |
+
if args.kv_strength is None:
|
| 768 |
+
args.kv_strength = 0.5 * args.strength
|
| 769 |
+
if args.out_proj_strength is None:
|
| 770 |
+
args.out_proj_strength = 0.5 * args.strength
|
| 771 |
+
if args.mlp_strength is None:
|
| 772 |
+
args.mlp_strength = args.strength
|
| 773 |
+
|
| 774 |
+
# Resolve target prefix and block count
|
| 775 |
+
if args.target == "main":
|
| 776 |
+
target_prefix_base = ZIMAGE_MAIN_PREFIX
|
| 777 |
+
target_total = ZIMAGE_MAIN_BLOCKS
|
| 778 |
+
else:
|
| 779 |
+
target_prefix_base = ZIMAGE_REFINER_PREFIX
|
| 780 |
+
target_total = ZIMAGE_REFINER_BLOCKS
|
| 781 |
+
|
| 782 |
+
# Parse block ranges
|
| 783 |
+
if args.blocks is None:
|
| 784 |
+
zimage_targets = list(range(target_total))
|
| 785 |
+
else:
|
| 786 |
+
zimage_targets = parse_block_list(args.blocks)
|
| 787 |
+
for b in zimage_targets:
|
| 788 |
+
if b < 0 or b >= target_total:
|
| 789 |
+
print(f"ERROR: Zimage {args.target} block {b} out of range "
|
| 790 |
+
f"[0, {target_total})"); sys.exit(1)
|
| 791 |
+
|
| 792 |
+
if args.mlp_blocks is None:
|
| 793 |
+
mlp_targets = list(zimage_targets)
|
| 794 |
+
else:
|
| 795 |
+
mlp_targets = parse_block_list(args.mlp_blocks)
|
| 796 |
+
for b in mlp_targets:
|
| 797 |
+
if b < 0 or b >= target_total:
|
| 798 |
+
print(f"ERROR: --mlp-blocks contains {b} out of range"); sys.exit(1)
|
| 799 |
+
mlp_set = set(mlp_targets)
|
| 800 |
+
|
| 801 |
+
if args.zimage_peak is None:
|
| 802 |
+
args.zimage_peak = (min(zimage_targets) + max(zimage_targets)) // 2
|
| 803 |
+
|
| 804 |
+
# Head selection
|
| 805 |
+
source_q_heads = select_source_q_heads(args.source_head_selection)
|
| 806 |
+
n_heads_selected = len(source_q_heads)
|
| 807 |
+
target_q_slots = select_target_q_slots(args.target_head_selection, n_heads_selected)
|
| 808 |
+
n_pairs = min(len(source_q_heads), len(target_q_slots))
|
| 809 |
+
source_q_heads = source_q_heads[:n_pairs]
|
| 810 |
+
target_q_slots = target_q_slots[:n_pairs]
|
| 811 |
+
|
| 812 |
+
# Print configuration banner
|
| 813 |
+
print("=" * 60)
|
| 814 |
+
print("graft_krea_to_zimage")
|
| 815 |
+
print("=" * 60)
|
| 816 |
+
print(f"Donor : {os.path.basename(args.krea_donor)}")
|
| 817 |
+
print(f"Base : {os.path.basename(args.zimage_base)}")
|
| 818 |
+
print(f"Output : {os.path.basename(args.output)}")
|
| 819 |
+
print(f"Target : Zimage.{args.target} ({target_total} blocks)")
|
| 820 |
+
print(f"Target blocks: {zimage_targets[0]}:{zimage_targets[-1]+1} ({len(zimage_targets)} blocks)")
|
| 821 |
+
print(f"Head pairs : {n_pairs} (source={args.source_head_selection}, "
|
| 822 |
+
f"target={args.target_head_selection})")
|
| 823 |
+
print(f"Krea heads : {source_q_heads[:5]}... -> Zimage slots: {target_q_slots[:5]}...")
|
| 824 |
+
print(f"Strengths : Q={args.strength} V={args.strength} "
|
| 825 |
+
f"K/V={args.kv_strength if args.kv else 'skip'} "
|
| 826 |
+
f"out_proj={args.out_proj_strength if args.out_proj else 'skip'} "
|
| 827 |
+
f"MLP={args.mlp_strength if args.mlp else 'skip'}")
|
| 828 |
+
print(f"Mode : {args.mode} Orthogonal: {args.orthogonal}")
|
| 829 |
+
print(f"Remap : {args.remap} donor-shift={args.donor_shift} "
|
| 830 |
+
f"krea-peak={args.krea_peak} zimage-peak={args.zimage_peak}")
|
| 831 |
+
print(f"Hidden reduce: {args.hidden_reduce}")
|
| 832 |
+
print(f"Cycles : {args.cycles}")
|
| 833 |
+
print(f"Device : {args.device} Dry-run: {args.dry_run}")
|
| 834 |
+
print("=" * 60)
|
| 835 |
+
|
| 836 |
+
if args.dry_run:
|
| 837 |
+
print("Dry-run: exiting before loading models.")
|
| 838 |
+
return
|
| 839 |
+
|
| 840 |
+
dev = torch.device(args.device if torch.cuda.is_available() else "cpu")
|
| 841 |
+
|
| 842 |
+
# Load Zimage base
|
| 843 |
+
print("\nLoading Zimage base...")
|
| 844 |
+
zimage_base: Dict[str, torch.Tensor] = {}
|
| 845 |
+
zimage_metadata: Dict[str, str] = {}
|
| 846 |
+
with safe_open(args.zimage_base, framework="pt", device="cpu") as f:
|
| 847 |
+
md = f.metadata()
|
| 848 |
+
if md: zimage_metadata = dict(md)
|
| 849 |
+
for k in f.keys():
|
| 850 |
+
zimage_base[k] = f.get_tensor(k)
|
| 851 |
+
print(f" {len(zimage_base)} tensors loaded.")
|
| 852 |
+
|
| 853 |
+
# Verify Krea donor and detect its key prefix
|
| 854 |
+
print("\nDetecting Krea prefix...")
|
| 855 |
+
fkrea = safe_open(args.krea_donor, framework="pt", device="cpu")
|
| 856 |
+
krea_keys = set(fkrea.keys())
|
| 857 |
+
krea_prefix = None
|
| 858 |
+
for candidate in KREA_PREFIX_CANDIDATES:
|
| 859 |
+
if f"{candidate}.0.attn.wq.weight" in krea_keys:
|
| 860 |
+
krea_prefix = candidate
|
| 861 |
+
break
|
| 862 |
+
if krea_prefix is None:
|
| 863 |
+
print(f"ERROR: could not detect Krea prefix. Tried: "
|
| 864 |
+
f"{KREA_PREFIX_CANDIDATES}"); sys.exit(1)
|
| 865 |
+
print(f" Krea prefix: '{krea_prefix}'")
|
| 866 |
+
|
| 867 |
+
# Build block map
|
| 868 |
+
pairs = build_block_map(zimage_targets, args.remap,
|
| 869 |
+
KREA_TOTAL_BLOCKS, target_total,
|
| 870 |
+
args.donor_shift, args.krea_peak, args.zimage_peak)
|
| 871 |
+
print(f"\nBlock mapping (target -> donor):")
|
| 872 |
+
for tb, db in pairs[:10]:
|
| 873 |
+
print(f" z:{tb} -> k:{db}")
|
| 874 |
+
if len(pairs) > 10:
|
| 875 |
+
print(f" ... ({len(pairs)} total pairs)")
|
| 876 |
+
|
| 877 |
+
# === Main graft loop ===
|
| 878 |
+
n_modified = 0
|
| 879 |
+
|
| 880 |
+
for cycle in range(args.cycles):
|
| 881 |
+
cycle_shift = cycle # simple shift per cycle broadens donor coverage
|
| 882 |
+
print(f"\nCycle {cycle+1}/{args.cycles} (shift={cycle_shift})")
|
| 883 |
+
|
| 884 |
+
for target_block, donor_block in tqdm(pairs, desc=f"cycle {cycle+1}", unit="blk"):
|
| 885 |
+
db_actual = (donor_block + cycle_shift) % KREA_TOTAL_BLOCKS
|
| 886 |
+
|
| 887 |
+
# Target prefix (with trailing dot)
|
| 888 |
+
z_pref = f"{target_prefix_base}.{target_block}."
|
| 889 |
+
k_pref = f"{krea_prefix}.{db_actual}."
|
| 890 |
+
|
| 891 |
+
# Load Krea tensors for this donor block
|
| 892 |
+
try:
|
| 893 |
+
krea_wq = fkrea.get_tensor(k_pref + "attn.wq.weight")
|
| 894 |
+
krea_wk = fkrea.get_tensor(k_pref + "attn.wk.weight") if args.kv else None
|
| 895 |
+
krea_wv = fkrea.get_tensor(k_pref + "attn.wv.weight") if args.kv else None
|
| 896 |
+
krea_wo = fkrea.get_tensor(k_pref + "attn.wo.weight") if args.out_proj else None
|
| 897 |
+
krea_up = fkrea.get_tensor(k_pref + "mlp.up.weight") if args.mlp else None
|
| 898 |
+
krea_gate = fkrea.get_tensor(k_pref + "mlp.gate.weight") if (args.mlp and args.include_gate) else None
|
| 899 |
+
krea_down = fkrea.get_tensor(k_pref + "mlp.down.weight") if (args.mlp and args.mlp_include_w2) else None
|
| 900 |
+
except Exception as e:
|
| 901 |
+
print(f" WARNING: could not load Krea block {db_actual}: {e}")
|
| 902 |
+
continue
|
| 903 |
+
|
| 904 |
+
# QKV graft
|
| 905 |
+
qkv_out, _ = apply_qkv_graft(
|
| 906 |
+
zimage_base, z_pref, krea_wq, krea_wk, krea_wv,
|
| 907 |
+
args.strength, args.kv_strength, args.kv,
|
| 908 |
+
args.mode, args.orthogonal,
|
| 909 |
+
source_q_heads, target_q_slots,
|
| 910 |
+
args.hidden_reduce, dev)
|
| 911 |
+
for k, v in qkv_out.items():
|
| 912 |
+
zimage_base[k] = v
|
| 913 |
+
n_modified += 1
|
| 914 |
+
|
| 915 |
+
# out_proj graft
|
| 916 |
+
if args.out_proj and krea_wo is not None:
|
| 917 |
+
op_out, _ = apply_out_proj_graft(
|
| 918 |
+
zimage_base, z_pref, krea_wo,
|
| 919 |
+
args.out_proj_strength, args.mode, args.orthogonal,
|
| 920 |
+
source_q_heads, target_q_slots,
|
| 921 |
+
args.hidden_reduce, dev)
|
| 922 |
+
for k, v in op_out.items():
|
| 923 |
+
zimage_base[k] = v
|
| 924 |
+
n_modified += 1
|
| 925 |
+
|
| 926 |
+
# MLP graft
|
| 927 |
+
if args.mlp and krea_up is not None and target_block in mlp_set:
|
| 928 |
+
mlp_out, _ = apply_mlp_graft(
|
| 929 |
+
zimage_base, z_pref, krea_up, krea_gate, krea_down,
|
| 930 |
+
args.mlp_strength, args.mode, args.orthogonal,
|
| 931 |
+
args.include_gate, args.mlp_include_w2,
|
| 932 |
+
args.hidden_reduce, dev)
|
| 933 |
+
for k, v in mlp_out.items():
|
| 934 |
+
zimage_base[k] = v
|
| 935 |
+
n_modified += 1
|
| 936 |
+
|
| 937 |
+
print(f"\nFinished {args.cycles} cycle(s). {n_modified} tensor modifications made.")
|
| 938 |
+
|
| 939 |
+
# === Save output ===
|
| 940 |
+
os.makedirs(os.path.dirname(args.output) or ".", exist_ok=True)
|
| 941 |
+
print(f"\nWriting output: {args.output}")
|
| 942 |
+
save_file(zimage_base, args.output, metadata=zimage_metadata)
|
| 943 |
+
print(f"Done. {len(zimage_base)} tensors written.")
|
| 944 |
+
|
| 945 |
+
|
| 946 |
+
if __name__ == "__main__":
|
| 947 |
+
main()
|