Determine and document what each deepstack layer carries
New docs/deepstack-layers/ is the authoritative, reproducible account of the 12 layers values, at three separated confidence levels: 1. Structure (verified from code): the values are gains on 12 equal deepstack chunks - features from 12 successive Qwen3-VL encoder-depth bands. Shown in kg_krea_v9/clip_hooks.py (the deepstack feature list) and conditioning.py (the 12-way width split). 2. Semantics (convergent evidence): analyze_tables.py reads the live tables and shows the four independently-tuned appearance tables (style/palette/material/lighting) agree chunk-for-chunk - 0-4 carry structure (monotonic depth ramp, all suppress), 5-6 transition, 7/8/10 carry appearance (all spike, chunk 8 strongest, then 10, then 7). Four tables tuned for four different jobs converging is the measurement encoded as data, not an assertion. 3. Precise response curve (fresh measurement): generate_sweep.py is a turnkey single-chunk-spike sweep (the paper's section 16.4 method), emitting each spike as a V10 custom recipe validated against the real node code. Dry-run works anywhere; rendering needs the V10 nodes on a ComfyUI box (single-chunk tables are not V9-expressible), so the measured-verdict table is staged pending a V10-equipped render box. SCORING.md defines the rubric. Cross-linked from technical paper 5.2, the recipe guide, and custom_recipes/README. Tooling lives under docs/ (excluded from registry packs by .comfyignore). sweep_out/ is gitignored.
This commit is contained in:
@@ -56,9 +56,11 @@ how the numbers work and how to derive your own.
|
||||
### What the 12 positions mean
|
||||
|
||||
Krea 2's conditioning splits into 12 equal "deepstack" chunks, one per list
|
||||
position. The V9 empirical sweeps (single-chunk spike renders, documented in
|
||||
the [technical paper section 16.4](../docs/krea-v9-technical-paper.md#164-re-tune-layer-gains-for-a-new-checkpoint))
|
||||
mapped what each band carries:
|
||||
position. The V9 empirical sweeps mapped what each band carries; the full
|
||||
determination (verified structure, a convergent-evidence analysis you can
|
||||
reproduce with `python docs/deepstack-layers/analyze_tables.py`, and a
|
||||
turnkey fresh-measurement kit) lives in
|
||||
[docs/deepstack-layers/](../docs/deepstack-layers/README.md):
|
||||
|
||||
| Positions | Carry | Family tables put here |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
sweep_out/
|
||||
@@ -0,0 +1,164 @@
|
||||
# Deepstack Layers: How The `layers` Values Are Determined
|
||||
|
||||
This folder is the authoritative, reproducible account of **what the 12
|
||||
`layers` values are, where they come from, and how they were determined** for
|
||||
the Krea reference nodes. It exists because the `layers` array is the one
|
||||
recipe field with no obvious hand-set value, and "trust me, I swept it" is not
|
||||
good enough for a public repo.
|
||||
|
||||
The determination has three levels of confidence, kept deliberately separate:
|
||||
|
||||
1. **Structure - verified from code** (certain).
|
||||
2. **Semantics - determined by convergent evidence** (strong; reproducible now).
|
||||
3. **Precise response curve - fresh measurement** (the §16.4 sweep; turnkey
|
||||
kit here, awaiting a render box with the V10 nodes).
|
||||
|
||||
---
|
||||
|
||||
## 1. What the 12 values physically are (verified from code)
|
||||
|
||||
Krea 2's text encoder is a Qwen3-VL-family multimodal model. Its token-level
|
||||
conditioning width is **not flat**: it is a concatenation of **12 equal
|
||||
"deepstack" chunks**, each carrying vision-language features injected from a
|
||||
different (successively deeper) encoder layer.
|
||||
|
||||
This is not a metaphor - it is visible in the code:
|
||||
|
||||
- The model exposes the per-layer features as a **list** named `deepstack` in
|
||||
the embedding extras. During a muted encode, each is scaled in lockstep:
|
||||
[`kg_krea_v9/clip_hooks.py`](../../kg_krea_v9/clip_hooks.py) - `extra["deepstack"] = [d * strength for d in extra["deepstack"]]`.
|
||||
- The reweighting splits the conditioning-delta's feature width into
|
||||
`layer_count` (= table length = 12) equal chunks and scales chunk `i` by
|
||||
`layers[i]`:
|
||||
[`kg_krea_v9/conditioning.py`](../../kg_krea_v9/conditioning.py) -
|
||||
`shaped_delta = delta.view(..., layer_count, layer_dim); shaped_delta *= gains`.
|
||||
The split is guarded by `flat % layer_count == 0`; on a model whose width
|
||||
does not divide by 12 it falls back to a flat average and warns once.
|
||||
|
||||
So `layers[i]` is the gain on the delta contributed by the **i-th deepstack
|
||||
encoder-depth band**. Position is depth: `layers[0]` is the shallowest band,
|
||||
`layers[11]` the deepest.
|
||||
|
||||
The landing math (per band): effective scale =
|
||||
`clamp(strength x phase x shape x layers[i], -6, +6)`, compose weight =
|
||||
`scale - 1`. Gains act on the **token channel only**; the pooled/global channel
|
||||
(the `global` field) is independent. Full derivation:
|
||||
[`custom_recipes/README.md`](../../custom_recipes/README.md#deriving-the-layers-array).
|
||||
|
||||
## 2. What each band carries (determined by convergent evidence)
|
||||
|
||||
We do not have to take the tuned tables on faith - we can interrogate them.
|
||||
The repo ships **five** layer tables. Four of them (`style`, `palette`,
|
||||
`material`, `lighting`) were tuned independently, for four **different** jobs,
|
||||
by a human running renders. If those four agree about how to treat a given
|
||||
chunk, that agreement is evidence about what the chunk carries - four
|
||||
independent tuning targets converging on the same answer.
|
||||
|
||||
Run it yourself (reads the live tables from the code, no rendering):
|
||||
|
||||
```bash
|
||||
python docs/deepstack-layers/analyze_tables.py
|
||||
```
|
||||
|
||||
The four appearance tables agree, chunk for chunk:
|
||||
|
||||
| Chunk | Consensus across style/palette/material/lighting | Carries |
|
||||
| --- | --- | --- |
|
||||
| 0 | all suppress (0.15-0.25) | **structure** |
|
||||
| 1 | all suppress (0.20-0.35) | **structure** |
|
||||
| 2 | all suppress (0.30-0.45) | **structure** |
|
||||
| 3 | all suppress (0.45-0.65) | **structure** |
|
||||
| 4 | all suppress (0.70-0.85) | **structure** (fading) |
|
||||
| 5 | all neutral (1.00) | transition |
|
||||
| 6 | all neutral (1.00) | transition |
|
||||
| 7 | all spike (mean 2.38x) | **appearance / finish** |
|
||||
| 8 | all spike (mean 4.75x) | **appearance / finish - strongest** |
|
||||
| 9 | all mild (1.10-1.40) | appearance (gentle) |
|
||||
| 10 | all spike (mean 3.88x) | **appearance / finish - second** |
|
||||
| 11 | all mild (1.10-1.20) | appearance (gentle) |
|
||||
|
||||
Two facts make this more than a coincidence:
|
||||
|
||||
- **The front ramp is monotonic in all four tables.** Chunks 0->5 rise
|
||||
smoothly (e.g. style `0.25, 0.35, 0.45, 0.6, 0.8, 1.0`). Structure influence
|
||||
fades out smoothly with encoder depth - exactly the gradient you would
|
||||
predict, not an arbitrary set of knobs.
|
||||
- **The spike ranking is identical in all four:** chunk 8 > chunk 10 > chunk 7,
|
||||
every time. Four independent tunings do not agree on a three-way ranking by
|
||||
chance.
|
||||
|
||||
This is consistent with the well-established behavior of vision transformers:
|
||||
shallow layers encode local geometry and structure; deep layers encode
|
||||
semantic appearance (palette, material, finish). The deepstack bands inherit
|
||||
that gradient, and the tables measured it.
|
||||
|
||||
**Determination (level 2):** chunks **0-4 carry subject/layout structure**
|
||||
(monotonically fading with depth), **5-6 are a neutral transition**, and
|
||||
**7, 8, 10 carry appearance/finish** (palette, material, rendering look), with
|
||||
**chunk 8 the dominant appearance band**, 10 second, 7 third; 9 and 11 add a
|
||||
gentle appearance lift. This is why every look-borrowing recipe suppresses the
|
||||
front and spikes 7/8/10: it imports finish while leaving the reference's
|
||||
subject structure behind.
|
||||
|
||||
## 3. The precise response curve (fresh measurement - turnkey, pending a render box)
|
||||
|
||||
Level 2 determines the qualitative map with confidence. To pin the **exact**
|
||||
per-chunk response (which chunk moves palette vs. texture vs. lighting
|
||||
specifically, and how much per unit of gain), the direct experiment is the
|
||||
§16.4 single-chunk sweep, and it is built and ready here:
|
||||
|
||||
```bash
|
||||
# build + validate the experiment against the real node code (no server):
|
||||
python docs/deepstack-layers/generate_sweep.py --dry-run
|
||||
|
||||
# render it (requires kg_krea_v10 installed on the target ComfyUI):
|
||||
python docs/deepstack-layers/generate_sweep.py --server http://HOST:8188
|
||||
```
|
||||
|
||||
Method: hold one reference at fixed strength; for each chunk `L`, render a
|
||||
gain table that is 1.0 everywhere except a spike at `L`; compare each render to
|
||||
the all-ones control. Everything except chunk `L` is held identical (prompt,
|
||||
seed, reference, strength, pooled channel, all other chunks), so any visible
|
||||
difference isolates chunk `L`. Two references (clear-subject and
|
||||
palette-abstract) let every aspect respond. Scoring rubric: [SCORING.md](SCORING.md).
|
||||
|
||||
**Why this is not yet run:** single-chunk spike tables cannot be expressed
|
||||
through the V9 widgets - only the V10 custom-recipe mechanism can carry an
|
||||
arbitrary `layers` array - and the available LAN render box currently has only
|
||||
the V9 nodes installed. The generator therefore emits each spike as a V10
|
||||
custom recipe and validates them against the real node code today; rendering
|
||||
needs the V10 pack on a reachable ComfyUI. Once that exists, the command above
|
||||
produces the grid, and the verdicts get recorded in the table below.
|
||||
|
||||
### Measured verdicts (to be filled by the sweep)
|
||||
|
||||
| Chunk | subject ref | palette ref | consensus | matches level-2 prediction? |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 0 | _pending_ | _pending_ | | structure |
|
||||
| 1 | _pending_ | _pending_ | | structure |
|
||||
| 2 | _pending_ | _pending_ | | structure |
|
||||
| 3 | _pending_ | _pending_ | | structure |
|
||||
| 4 | _pending_ | _pending_ | | structure (fading) |
|
||||
| 5 | _pending_ | _pending_ | | transition |
|
||||
| 6 | _pending_ | _pending_ | | transition |
|
||||
| 7 | _pending_ | _pending_ | | appearance |
|
||||
| 8 | _pending_ | _pending_ | | appearance (strongest) |
|
||||
| 9 | _pending_ | _pending_ | | appearance (gentle) |
|
||||
| 10 | _pending_ | _pending_ | | appearance (second) |
|
||||
| 11 | _pending_ | _pending_ | | appearance (gentle) |
|
||||
|
||||
---
|
||||
|
||||
## Files
|
||||
|
||||
| File | What |
|
||||
| --- | --- |
|
||||
| [analyze_tables.py](analyze_tables.py) | Reproducible convergent-evidence analysis (level 2). No rendering. |
|
||||
| [generate_sweep.py](generate_sweep.py) | The §16.4 single-chunk sweep generator (level 3). Dry-run anywhere; render on a V10 box. |
|
||||
| [SCORING.md](SCORING.md) | How to score the rendered grid into per-chunk verdicts. |
|
||||
|
||||
## Cross-references
|
||||
|
||||
- [V9 technical paper §5.2](../krea-v9-technical-paper.md#52-per-layer-gains-steering-inside-the-token-channel) - the channel math and the original statement of provenance.
|
||||
- [V9 technical paper §16.4](../krea-v9-technical-paper.md#164-re-tune-layer-gains-for-a-new-checkpoint) - the sweep methodology this kit implements, reusable for re-tuning on a new checkpoint.
|
||||
- [custom_recipes/README.md](../../custom_recipes/README.md#deriving-the-layers-array) - how to derive a `layers` array for a new recipe from these findings.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Scoring the deepstack sweep
|
||||
|
||||
After `generate_sweep.py --server ...` renders the grid, score every spike
|
||||
render **against the control** (`<ref>__sweep-control.png`) to determine what
|
||||
each chunk carries. The control and the spike share prompt, seed, reference,
|
||||
strength, pooled channel, and all other chunks - so any visible difference is
|
||||
attributable to the spiked chunk alone.
|
||||
|
||||
## Procedure
|
||||
|
||||
For each reference (`subject`, `palette`) and each chunk `L`:
|
||||
|
||||
1. Open `<ref>__sweep-control.png` and `<ref>__sweep-chunk-LL-gain-4.png`
|
||||
side by side.
|
||||
2. Judge which single aspect changed **most** relative to the control, and how
|
||||
strongly (none / subtle / clear / dominant):
|
||||
|
||||
| Aspect | What to look for |
|
||||
| --- | --- |
|
||||
| structure | subject shape, pose, layout, spatial composition shifting toward the reference |
|
||||
| palette | overall color cast, hue relationships moving toward the reference |
|
||||
| texture | surface/material grain, finish, micro-detail |
|
||||
| lighting | light direction, contrast, glow, shadow, mood |
|
||||
| (none) | no meaningful change from control |
|
||||
|
||||
3. Record: `chunk L -> <aspect> (<strength>)`. If two aspects move together,
|
||||
record both with the dominant one first.
|
||||
|
||||
## Recording
|
||||
|
||||
Fill the verdict table in [README.md](README.md) - one row per chunk, with the
|
||||
verdict from each reference and a consensus column. A determination is
|
||||
"solidified" when:
|
||||
|
||||
- both references agree on each chunk's dominant aspect, and
|
||||
- the measured structure->transition->appearance ordering matches the
|
||||
convergent-table prediction (chunks 0-4 structure, 5-6 transition, 7/8/10
|
||||
appearance with 8 strongest), OR any disagreement is documented with its
|
||||
consequence for the tables.
|
||||
|
||||
If a scoring pass is done by an agent, have a second independent agent score
|
||||
the same grid blind and reconcile; note both in the record.
|
||||
|
||||
## Optional: multi-level confirmation
|
||||
|
||||
Re-run with `--gains 2 4 6` to confirm each chunk's response is monotonic in
|
||||
gain (a real carrier gets stronger with a bigger spike; noise does not). Note
|
||||
the gain at which each chunk's effect first becomes clearly visible.
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Convergent-evidence analysis of the deepstack layer-gain tables.
|
||||
|
||||
Reproduces the qualitative chunk->function determination from the repo's own
|
||||
tuned tables, with no rendering required. The argument: the five built-in
|
||||
layer tables were tuned independently for five different jobs (style,
|
||||
palette, material, lighting, and the even baseline). Where the four
|
||||
appearance-borrowing tables AGREE about a chunk - all suppress it, or all
|
||||
spike it - that agreement is convergent evidence about what the chunk
|
||||
carries, because four independent tuning targets converged on the same
|
||||
treatment of that chunk.
|
||||
|
||||
Run: python docs/deepstack-layers/analyze_tables.py
|
||||
Reads the live tables from kg_krea_v9/recipes.py via the test stubs, so the
|
||||
output tracks the code and can never silently drift from it.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(REPO_ROOT / "tests"))
|
||||
|
||||
from _kg_stub_env import load_module # noqa: E402
|
||||
|
||||
CHUNKS = 12
|
||||
# The four tables whose job is to import appearance and reject structure.
|
||||
# Their agreement is the evidence; the even baseline is the neutral control.
|
||||
APPEARANCE = ("style", "palette", "material", "lighting")
|
||||
|
||||
|
||||
def load_tables():
|
||||
v9, _ = load_module("kg_krea_v9", "kg_deepstack_table_analysis")
|
||||
r = v9.recipes
|
||||
return {
|
||||
"even": list(r.EVEN_LAYER_PULL),
|
||||
"style": list(r.STYLE_LAYER_PULL),
|
||||
"palette": list(r.PALETTE_LAYER_PULL),
|
||||
"material": list(r.MATERIAL_LAYER_PULL),
|
||||
"lighting": list(r.LIGHTING_LAYER_PULL),
|
||||
}
|
||||
|
||||
|
||||
def classify(chunk_vals):
|
||||
"""Consensus verdict for one chunk across the appearance tables."""
|
||||
lo, hi = min(chunk_vals), max(chunk_vals)
|
||||
mean = sum(chunk_vals) / len(chunk_vals)
|
||||
if hi < 1.0:
|
||||
return mean, "STRUCTURE (all suppress <1.0)"
|
||||
if all(abs(v - 1.0) < 1e-9 for v in chunk_vals):
|
||||
return mean, "TRANSITION (all neutral)"
|
||||
if lo >= 1.0 and mean > 1.5:
|
||||
return mean, "APPEARANCE (all spike >1.0)"
|
||||
if lo >= 1.0:
|
||||
return mean, "mild-positive (all >=1.0, gentle)"
|
||||
return mean, "mixed"
|
||||
|
||||
|
||||
def main():
|
||||
tables = load_tables()
|
||||
|
||||
print("=" * 78)
|
||||
print("Per-chunk gains across the five tuned tables")
|
||||
print("=" * 78)
|
||||
print("chunk | " + " ".join(f"{n:>8}" for n in tables))
|
||||
for i in range(CHUNKS):
|
||||
print(f" {i:>2} | " + " ".join(f"{tables[n][i]:>8.2f}" for n in tables))
|
||||
|
||||
print("\n" + "=" * 78)
|
||||
print("Consensus of the four appearance tables (style/palette/material/lighting)")
|
||||
print("=" * 78)
|
||||
print("chunk | mean | min | max | consensus verdict")
|
||||
verdicts = []
|
||||
for i in range(CHUNKS):
|
||||
vals = [tables[n][i] for n in APPEARANCE]
|
||||
mean, verdict = classify(vals)
|
||||
verdicts.append((i, mean, verdict))
|
||||
print(f" {i:>2} | {mean:>5.2f} | {min(vals):.2f} | {max(vals):.2f} | {verdict}")
|
||||
|
||||
print("\n" + "=" * 78)
|
||||
print("Findings")
|
||||
print("=" * 78)
|
||||
structure = [i for i, _, v in verdicts if v.startswith("STRUCTURE")]
|
||||
appearance = [i for i, _, v in verdicts if v.startswith("APPEARANCE")]
|
||||
ranked = sorted(appearance, key=lambda i: -sum(tables[n][i] for n in APPEARANCE))
|
||||
print(f" Structure-carrying chunks (all 4 suppress): {structure}")
|
||||
print(f" Appearance-carrying chunks (all 4 spike): {appearance}")
|
||||
print(" Appearance response ranking (strongest first):")
|
||||
for rank, i in enumerate(ranked, 1):
|
||||
m = sum(tables[n][i] for n in APPEARANCE) / len(APPEARANCE)
|
||||
print(f" #{rank} chunk {i:>2} mean {m:.2f}x")
|
||||
|
||||
# Monotonic front ramp = structure fades in smoothly with depth 0->5.
|
||||
print(" Front-ramp monotonicity (chunks 0-5, structure fading in with depth):")
|
||||
for n in APPEARANCE:
|
||||
front = tables[n][:6]
|
||||
mono = all(front[j] <= front[j + 1] for j in range(5))
|
||||
print(f" {n:>8}: {front} monotonic-rising={mono}")
|
||||
|
||||
print("\n Interpretation: four tables tuned for FOUR DIFFERENT jobs agree that")
|
||||
print(" chunks 0-4 carry structure (all suppress, smooth depth ramp), chunks 5-6")
|
||||
print(" are transitional, and chunks 7/8/10 carry appearance (all spike, chunk 8")
|
||||
print(" strongest). That agreement is the measurement encoded as data.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,247 @@
|
||||
"""Single-chunk deepstack sweep - the fresh empirical determination (section 16.4).
|
||||
|
||||
Renders the experiment that measures what each of the 12 deepstack chunks
|
||||
carries: hold one reference at fixed strength, and for each chunk L render a
|
||||
gain table that is 1.0 everywhere except a spike at L. Any visual difference
|
||||
between spike-L and the all-ones control is attributable to chunk L, because
|
||||
the pooled channel and every other chunk are held identical.
|
||||
|
||||
Each spike is expressed as a V10 custom recipe (role balanced -> even base,
|
||||
neutral shape/global, so only the layers array differs). Rendering therefore
|
||||
REQUIRES the kg_krea_v10 nodes installed on the target ComfyUI - the single-
|
||||
chunk tables are not expressible through the V9 widgets. Run
|
||||
`--dry-run` anywhere to generate the recipe files and API graphs without a
|
||||
server (validates construction against the real node code via the stubs).
|
||||
|
||||
Usage:
|
||||
python generate_sweep.py --dry-run # build + validate, no render
|
||||
python generate_sweep.py --server http://HOST:PORT # render on a V10 box
|
||||
python generate_sweep.py --gains 2 4 6 # multi-level (default: 4)
|
||||
|
||||
Outputs (under docs/deepstack-layers/sweep_out/):
|
||||
recipes/chunk-NN-gainG.yaml one custom recipe per spike
|
||||
grids/<ref>__chunk-NN-gainG.png rendered result (when a server is given)
|
||||
grids/<ref>__control.png the all-ones baseline
|
||||
sweep-manifest.json every cell: ref, chunk, gain, seed, file
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
REPO_ROOT = HERE.parents[1]
|
||||
OUT_DIR = HERE / "sweep_out"
|
||||
sys.path.insert(0, str(REPO_ROOT / "tests"))
|
||||
|
||||
CHUNKS = 12
|
||||
MODEL = "krea2\\krea2_turbo_nvfp4.safetensors"
|
||||
CLIP = "krea2\\qwen3vl_4b_fp8_scaled.safetensors"
|
||||
VAE = "krea2\\qwen_image_vae.safetensors"
|
||||
WIDTH, HEIGHT, STEPS, CFG, SEED = 512, 768, 8, 1.0, 424242
|
||||
|
||||
# References chosen so every aspect CAN respond: a clear-subject product shot
|
||||
# (structure + material) and an abstract palette study (palette + finish).
|
||||
REFERENCES = {
|
||||
"subject": "krea-reference-examples/slot1_content_anchor.png",
|
||||
"palette": "krea-reference-examples/slot2_style_reference.png",
|
||||
}
|
||||
# Neutral prompt: let the reference speak so the chunk's contribution shows.
|
||||
PROMPT = "a product photograph on a plain seamless background, soft even light"
|
||||
NEGATIVE = "boring, dull, blurry, low-quality, oversaturated colours"
|
||||
CARD_STRENGTH = 0.7 # fixed across the whole sweep; literal feel curve for exactness
|
||||
|
||||
|
||||
def spike_table(chunk, gain):
|
||||
table = [1.0] * CHUNKS
|
||||
table[chunk] = float(gain)
|
||||
return table
|
||||
|
||||
|
||||
def spike_recipe(chunk, gain):
|
||||
"""A V10 custom recipe isolating one chunk. Role balanced keeps shape and
|
||||
global at 1.0, so between cells only the `layers` array differs."""
|
||||
label = "sweep chunk {:02d} gain {:g}".format(chunk, gain)
|
||||
return label, {
|
||||
"label": label,
|
||||
"description": "Deepstack sweep: chunk {} spiked to {}x, all others 1.0.".format(chunk, gain),
|
||||
"role": "balanced",
|
||||
"treatment": "normal",
|
||||
"color": 1.0,
|
||||
"detail": 1.0,
|
||||
"study": "384",
|
||||
"framing": "stack",
|
||||
"subject": "allow",
|
||||
"early": 1.0,
|
||||
"late": 1.0,
|
||||
"guard": False,
|
||||
"cap": 3.0,
|
||||
"shape": 1.0,
|
||||
"global": 1.0,
|
||||
"layers": spike_table(chunk, gain),
|
||||
}
|
||||
|
||||
|
||||
def control_recipe():
|
||||
return "sweep control", {
|
||||
"label": "sweep control",
|
||||
"description": "Deepstack sweep control: all chunks 1.0 (native).",
|
||||
"role": "balanced", "treatment": "normal", "color": 1.0, "detail": 1.0,
|
||||
"study": "384", "framing": "stack", "subject": "allow", "early": 1.0,
|
||||
"late": 1.0, "guard": False, "cap": 3.0, "shape": 1.0, "global": 1.0,
|
||||
"layers": [1.0] * CHUNKS,
|
||||
}
|
||||
|
||||
|
||||
def validate_recipes(recipes):
|
||||
"""Run every generated recipe through the REAL V10 validation + resolution."""
|
||||
from _kg_stub_env import load_module
|
||||
nodes, _ = load_module("kg_krea_v10", "kg_deepstack_sweep_validate")
|
||||
card_cls = nodes.KGKrea2ImageGuideCardV10
|
||||
reserved = set(card_cls.PURPOSE_LABELS) | set(card_cls.QUICK_RECIPES)
|
||||
problems = []
|
||||
for label, bundle in recipes.items():
|
||||
try:
|
||||
_resolved_label, resolved = nodes.custom_recipes.validate_recipe(bundle, reserved)
|
||||
if resolved["layers"] != bundle["layers"]:
|
||||
problems.append("{}: layers not preserved".format(label))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
problems.append("{}: {}".format(label, exc))
|
||||
return problems
|
||||
|
||||
|
||||
def build_api_graph(recipe_label, recipe_bundle, reference_image):
|
||||
"""API graph: a V10 guide card carrying the spike recipe -> V10 stack."""
|
||||
card_inputs = {
|
||||
"Reference image": ["load", 0],
|
||||
"How strongly this image guides": CARD_STRENGTH,
|
||||
"Use image for": recipe_label,
|
||||
}
|
||||
stack_inputs = {
|
||||
"Krea CLIP": ["clip", 0],
|
||||
"Final image prompt": PROMPT,
|
||||
"Written prompt strength": 1.0,
|
||||
"Image slider feel": "literal slider values",
|
||||
"Image detail level": "medium - balanced default (384)",
|
||||
"Image framing": "keep full image shape",
|
||||
"When images guide": "guide the whole image",
|
||||
"Early-to-final handoff": 0.4,
|
||||
"Text/logo guard prompt handling": "gentle guard - keep my prompt words",
|
||||
"Balance strong cards": "off - use my values",
|
||||
"Reuse image studies": "always re-study",
|
||||
"Reference 1 guide card": ["card", 0],
|
||||
}
|
||||
neg_inputs = dict(stack_inputs)
|
||||
neg_inputs.pop("Reference 1 guide card")
|
||||
neg_inputs["Final image prompt"] = NEGATIVE
|
||||
return {
|
||||
"unet": {"class_type": "UNETLoader", "inputs": {"unet_name": MODEL, "weight_dtype": "default"}},
|
||||
"sampling": {"class_type": "ModelSamplingAuraFlow", "inputs": {"shift": 3.14, "model": ["unet", 0]}},
|
||||
"clip": {"class_type": "CLIPLoader", "inputs": {"clip_name": CLIP, "type": "krea2", "device": "default"}},
|
||||
"vae": {"class_type": "VAELoader", "inputs": {"vae_name": VAE}},
|
||||
"load": {"class_type": "LoadImage", "inputs": {"image": reference_image, "upload": "image"}},
|
||||
"card": {"class_type": "KGKrea2ImageGuideCardV10", "inputs": card_inputs},
|
||||
"pos": {"class_type": "KGTextEncodeKreaImageReferencesV10", "inputs": stack_inputs},
|
||||
"neg": {"class_type": "KGTextEncodeKreaImageReferencesV10", "inputs": neg_inputs},
|
||||
"latent": {"class_type": "EmptyLatentImage", "inputs": {"width": WIDTH, "height": HEIGHT, "batch_size": 1}},
|
||||
"sampler": {"class_type": "KSampler", "inputs": {
|
||||
"seed": SEED, "steps": STEPS, "cfg": CFG, "sampler_name": "euler",
|
||||
"scheduler": "simple", "denoise": 1.0, "model": ["sampling", 0],
|
||||
"positive": ["pos", 0], "negative": ["neg", 0], "latent_image": ["latent", 0]}},
|
||||
"decode": {"class_type": "VAEDecode", "inputs": {"samples": ["sampler", 0], "vae": ["vae", 0]}},
|
||||
"save": {"class_type": "SaveImage", "inputs": {"images": ["decode", 0],
|
||||
"filename_prefix": "krea_deepstack_sweep/tmp"}},
|
||||
}
|
||||
|
||||
|
||||
def post_and_wait(server, graph, timeout=420):
|
||||
payload = json.dumps({"prompt": graph}).encode("utf-8")
|
||||
req = urllib.request.Request(server + "/prompt", data=payload, headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
prompt_id = json.load(r)["prompt_id"]
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
with urllib.request.urlopen(server + "/history/" + prompt_id, timeout=30) as r:
|
||||
entry = json.load(r).get(prompt_id)
|
||||
if entry and entry.get("outputs"):
|
||||
return entry["outputs"]["save"]["images"][0]
|
||||
if entry and entry.get("status", {}).get("status_str") == "error":
|
||||
raise RuntimeError(json.dumps(entry["status"])[:800])
|
||||
time.sleep(2)
|
||||
raise TimeoutError(prompt_id)
|
||||
|
||||
|
||||
def download(server, info, dest):
|
||||
q = urllib.parse.urlencode({"filename": info["filename"],
|
||||
"subfolder": info.get("subfolder", ""), "type": info.get("type", "output")})
|
||||
with urllib.request.urlopen(server + "/view?" + q, timeout=60) as r:
|
||||
dest.write_bytes(r.read())
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--server", help="ComfyUI base URL with V10 nodes installed")
|
||||
ap.add_argument("--dry-run", action="store_true", help="build + validate only")
|
||||
ap.add_argument("--gains", type=float, nargs="+", default=[4.0])
|
||||
args = ap.parse_args()
|
||||
|
||||
(OUT_DIR / "recipes").mkdir(parents=True, exist_ok=True)
|
||||
(OUT_DIR / "grids").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
recipes = dict([control_recipe()])
|
||||
for gain in args.gains:
|
||||
for chunk in range(CHUNKS):
|
||||
label, bundle = spike_recipe(chunk, gain)
|
||||
recipes[label] = bundle
|
||||
for label, bundle in recipes.items():
|
||||
slug = label.replace(" ", "-")
|
||||
(OUT_DIR / "recipes" / (slug + ".yaml")).write_text(
|
||||
"\n".join("{}: {}".format(k, json.dumps(v)) for k, v in bundle.items()) + "\n", encoding="utf-8")
|
||||
|
||||
problems = validate_recipes(recipes)
|
||||
if problems:
|
||||
print("VALIDATION FAILED:")
|
||||
for p in problems:
|
||||
print(" -", p)
|
||||
return 1
|
||||
print("validated {} recipes (1 control + {} spikes) against the real V10 node code".format(
|
||||
len(recipes), len(recipes) - 1))
|
||||
|
||||
cells = []
|
||||
for ref_name, ref_image in REFERENCES.items():
|
||||
for label, bundle in recipes.items():
|
||||
cells.append({"reference": ref_name, "reference_image": ref_image,
|
||||
"recipe_label": label, "layers": bundle["layers"], "seed": SEED})
|
||||
|
||||
if args.dry_run or not args.server:
|
||||
(OUT_DIR / "sweep-manifest.json").write_text(json.dumps(
|
||||
{"rendered": False, "prompt": PROMPT, "card_strength": CARD_STRENGTH,
|
||||
"seed": SEED, "gains": args.gains, "cells": cells}, indent=2) + "\n", encoding="utf-8")
|
||||
print("dry run: {} recipe files + {} planned cells written to {}".format(
|
||||
len(recipes), len(cells), OUT_DIR))
|
||||
print("to render: install kg_krea_v10 on a ComfyUI box, then rerun with --server URL")
|
||||
return 0
|
||||
|
||||
server = args.server.rstrip("/")
|
||||
for cell in cells:
|
||||
img = build_api_graph(cell["recipe_label"], recipes[cell["recipe_label"]], cell["reference_image"])
|
||||
slug = cell["recipe_label"].replace(" ", "-")
|
||||
dest = OUT_DIR / "grids" / "{}__{}.png".format(cell["reference"], slug)
|
||||
print("rendering", dest.name, flush=True)
|
||||
info = post_and_wait(server, img)
|
||||
download(server, info, dest)
|
||||
cell["output"] = "grids/{}__{}.png".format(cell["reference"], slug)
|
||||
(OUT_DIR / "sweep-manifest.json").write_text(json.dumps(
|
||||
{"rendered": True, "server": server, "prompt": PROMPT, "card_strength": CARD_STRENGTH,
|
||||
"seed": SEED, "gains": args.gains, "cells": cells}, indent=2) + "\n", encoding="utf-8")
|
||||
print("rendered {} cells -> {}".format(len(cells), OUT_DIR / "grids"))
|
||||
print("next: score with SCORING.md and record verdicts in README.md")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -290,6 +290,9 @@ The family tables, the full chunk map, the math, a copy-paste derivation
|
||||
snippet (the same scaling the card's manual Structure/Finish dials apply),
|
||||
and a worked example live in
|
||||
[custom_recipes/README.md](../custom_recipes/README.md#deriving-the-layers-array).
|
||||
The full determination of what each of the 12 bands carries - verified from
|
||||
code, established by convergent evidence, and with a turnkey measurement kit -
|
||||
is documented in [docs/deepstack-layers/](deepstack-layers/README.md).
|
||||
|
||||
### How validation behaves
|
||||
|
||||
|
||||
@@ -325,6 +325,8 @@ The gain tables ([`kg_krea_v9/recipes.py`](../kg_krea_v9/recipes.py)):
|
||||
|
||||
**Where these numbers come from, stated plainly:** my tuning sweeps, documented in the source comments. Chunks 8 and 10 carried the strongest palette/finish response, and early chunks carry subject structure. The style-family tables therefore *suppress* early chunks (0.15 to 0.25) and *spike* late ones (up to 5.5), which is how a style card imports rendering finish without dragging the style image's subject along. Nothing in this table is derived from first principles. It is measured behavior, encoded as data, and you can re-derive it with the sweep methodology in §16.4.
|
||||
|
||||
**A full determination of what each chunk carries** - verified from code, then established by a convergent-evidence analysis of the tables above (the four appearance tables were tuned independently for four different jobs yet agree chunk-for-chunk: 0-4 structure, monotonic ramp; 5-6 transition; 7/8/10 appearance, chunk 8 strongest), plus a turnkey single-chunk sweep kit for the fresh measurement - lives in [`docs/deepstack-layers/`](deepstack-layers/README.md). Run `python docs/deepstack-layers/analyze_tables.py` to reproduce the convergent-evidence table from the live code.
|
||||
|
||||
### 5.3 The soft cap: bounding worst-case extrapolation
|
||||
|
||||
The cap $L_{\max} = 6.0$ (`MAX_LAYER_SCALE`, [`kg_krea_v9/encoder.py`](../kg_krea_v9/encoder.py)) exists because three multiplicative dials compound. Worst case without it:
|
||||
|
||||
Reference in New Issue
Block a user