TenStrip commited on
Commit
0419e7e
·
verified ·
1 Parent(s): a24bd42

Upload scripts/graft_krea_to_zimage.py

Browse files
Files changed (1) hide show
  1. 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()