Full reference for the deployed carrier (idea097/d001, 8/light backbone) shown on
imagehide-demo:
payload + ECC layout, architecture, training, eval pipeline, API, diagnostics, constraints.
For a one-page overview see spec; for the design rationale (why patch-tile, why α=1, what was rejected) see design.
Paper name MWIP, internal A-tolerant.
| field | value |
|---|---|
| internal name | A-tolerant (do not use "β-minimal" — obsolete) |
| paper name | MWIP — Minimal Whole-Image Provenance |
| version | INN v3 — locked 2026-04-25 after two codex review rounds |
| payload size | exactly 896 b in pixels |
| payload format | [ H(128) | sig(512) | pk(256) ] |
| scope | whole-image, one signature per image. No tile grid, no localization. Crop = tamper. |
| trust model | offline content-binding (signature over H from pixels alone) + online registry lookup for device validity |
| delivery | canonical-resolution / canonical-trim. Out-of-scope: crop, rotate, large platform resize that breaks the 32×32 patch grid. |
| code | src/inn_model.py · src/exp0_inn_train.py · src/exp0_inn_eval.py · src/robust_hash.py |
| full rationale | docs/inn_arch_notes.md (paged-out into design.html eventually) |
This section is the precise spec for the deployed carrier — autohide/.../1024bit_noamp_keepckpt/idea097_design001,
the same checkpoint that powers the public imagehide-demo.
Variable / module names in this section refer to autohide/.../idea097_design001/code/inn_model.py
(verified against train_args.txt: --num_blocks 8 --subnet_type light --secret_len 1024 --no-amp).
The simpler "uniform mean-pool" patch-tile description in design § 3 is the
v3 reference design; the deployed carrier is the v3 base plus three small extensions (idea031 MoE decode
router, idea034 DWT cover predictor, and the 12-permutation balanced-scatter adapter) carried forward
from the autohide design line.
| symbol | shape | meaning |
|---|---|---|
x = host RGB | (B, 3, H, W) | cover image, x ∈ [-1, 1], with H, W ≡ 0 (mod 64) |
m = bits | (B, N) | ECC codeword fed into the carrier, m ∈ {0, 1}; deployed N = 1024 (BCH(127,99) + RS(128,100) over the 896-b data payload) |
xw, w | (B, 12, h, w) | host DWT and secret DWT respectively, with h = H/2, w = W/2 |
y = (y1, y2) | (B, 24, h, w) | INN output split halves; y1 becomes container DWT, y2 is the discarded info channel |
x̂ = container RGB | (B, 3, H, W) | watermarked image, x̂ ∈ [-1, 1] |
l = bit logits | (B, N) | recovered logits; bits = sign(l) > 0 |
C = 12 | — | DWT channel count (3 RGB × 4 subbands: LL, LH, HL, HH) |
S = 1024 | — | grid slot count = 32 × 32 (fixed, not a function of resolution) |
# inputs: x ∈ ℝ^{B×3×H×W}, [-1,1]; m ∈ {0,1}^{B×N}, N=1024. # preconditions: H % 64 == 0, W % 64 == 0. x_w = haar_dwt2(x) # (B, 12, H/2, W/2) — fixed, 0 params w = PatchBitAdapter.encode(m, H/2, W/2) # (B, 12, H/2, W/2) — see §2.4 y = INN_forward(concat[x_w, w], keys_1..8) # (B, 24, H/2, W/2) — see §2.3 c_dwt = y[:, :12] # first half kept x̂ = haar_idwt2(c_dwt) # (B, 3, H, W) — fixed, 0 params # discard: y[:, 12:] is z; not stored, not transmitted.
# inputs: x̂ ∈ ℝ^{B×3×H×W}, [-1,1] (possibly attacked). # outputs: l ∈ ℝ^{B×N} bit logits. c_dwt = haar_dwt2(x̂) # (B, 12, H/2, W/2) host_dwt_est = CoverPredictor(x̂) # (B, 12, H/2, W/2) — idea034 c_dwt_aug = c_dwt + 0.5 · (c_dwt − host_dwt_est) # residual injection z_zero = zeros_like(c_dwt_aug) y_rev = INN_reverse(concat[c_dwt_aug, z_zero], keys_1..8) # (B, 24, H/2, W/2) secret_rec = y_rev[:, 12:] # the recovered w-slot # idea031 MoE decode combine: router_w = DecodeRouter(x̂) # (B, K=2) softmax channel_w = router_w @ expert_bank # (B, 12) per-batch decode weights l = PatchBitAdapter.decode_with_weights(secret_rec, channel_w) # (B, 1024) — see §2.5
Twelve independent balanced permutations πc : {0, …, S−1} × pc → {0, …, h·w − 1}
(one per DWT channel c ∈ {0, …, 11}) scatter each slot value to a disjoint set of pixel positions.
Permutations are deterministic from seed 0xD1FF5E ⊕ c ⊕ (h · 1000003 + w), so the verifier can reconstruct
them from the resolution alone — no extra state shipped with the image.
# Encode (per autohide PatchBitAdapter.encode): # 1) ±1 bipolar: b = 2·m − 1 # (B, N) # 2) place first N of S=1024 slots; pad rest with 0: slot_full = zeros((B, S)); slot_full[:, :N] = b # (B, 1024) # 3) fixed ±1 sign-mask scramble (seed 0xB175CA10): scrambled = slot_full · sign_mask # (B, 1024) # 4) per-channel balanced scatter to the (h·w) DWT pixels: for c in range(12): perm_c = balanced_permutation(h·w, S, seed = 0xD1FF5E ^ c ^ ...) # (S, h·w / S) secret[:, c] = scatter(scrambled, perm_c) # each slot's bipolar value # replicated to (h·w / S) distinct pixels secret = secret.reshape(B, 12, h, w) # magnitude = 1 by construction
Two things to flag against the simpler "32×32 grid → nearest-neighbor tile → broadcast across 12 channels"
description in design § 3: (a) the deployed carrier uses
different permutations per channel — bit redundancy is spread across both space and DWT channel,
not just across space, and (b) at sizes ≠ 256² the permutations are recomputed for each (h, w) and cached;
there is no nearest-neighbor up-sample step. The earlier "broadcast across 12 channels" wording is a
v3-base simplification, kept in design.html for design rationale clarity; the deployed adapter
generalises it.
# Decode (per autohide PatchBitAdapter.decode_with_weights): # 1) gather each slot's pixels per channel and slot-mean: for c in range(12): gathered_c = gather(secret_rec[:, c].flatten(), perm_c) # (B, S, p) per_chan[:, c] = gathered_c.mean(dim=-1) # (B, S) # 2) per-batch MoE-weighted channel combine (idea031): per_slot = sum_c ( channel_w[:, c] · per_chan[:, c] ) # (B, 1024) # 3) un-scramble + slice back to N bits: unscrambled = per_slot · sign_mask # (B, 1024) l = unscrambled[:, :N] # (B, N)
The MoE bank is a (K=2, 12) learnable matrix initialized to a fixed subband-prior of
[4, 4, 4, 2, 2, 2, 2, 2, 2, 1, 1, 1] / sum (LL × 4, LH/HL × 2, HH × 1) — biasing the combine
toward Haar bands that survive JPEG/WeChat low-pass. The DecodeRouter reads the container RGB and
produces a length-2 softmax that picks between the two expert vectors.
Each of the 8 blocks Bi defines an exactly invertible map. Let αi = 0.6i, (k1,i, k2,i) be the per-block fixed sign-mask (channel-wise ±1) and 4×4 channel-tile permutation (seed 0xDB15). Then:
# forward step B_i : (x1, x2) → (y1, y2) y1 = x1 + α_i · f_i(x2) x2 = x2 · k_{1,i} x2 = shuffle(x2, k_{2,i}) y2 = exp( g_i(y1) ) · x2 + h_i(y1) # reverse step B_i⁻¹ : (y1, y2) → (x1, x2) — exact arithmetic, no learning x2 = ( y2 − h_i(y1) ) / exp( g_i(y1) ) x2 = unshuffle(x2, k_{2,i}) x2 = x2 · k_{1,i} # k_{1,i} ∈ {-1,+1}, so · k_{1,i} is its own inverse x1 = y1 − α_i · f_i(x2)
In the deployed model the three subnets fi, gi, hi are
LightConvBlock = Conv2d(in→32, 3×3) · LeakyReLU · Conv2d(32→out, 3×3),
the second conv re-initialized as kaiming_normal × 0.1 at construction. This breaks
DKiS-upstream's exact-identity init that otherwise leaves f = g = h ≡ 0 at step 0 (see
design § 2).
| module | params | note |
|---|---|---|
| Haar DWT / iDWT | 0 | fixed orthogonal basis |
DKiS INN — 8 × LightConvBlock (f, g, h triples) | ~ 0.167 M | 8 coupling blocks · ~20 k each · 6 conv layers per block (2 per subnet × 3 subnets) |
| PatchBitAdapter — sign-mask + 12 balanced permutations | 0 (buffers only) | seed-derived, stored as register_buffer |
PatchBitAdapter — expert_bank (idea031) | 2 × 12 = 24 | two K = 2 per-channel decode-weight rows; learnable |
DecodeRouter (idea031) | ~ 12 k | 4-conv stack reading container RGB → length-2 softmax; extract-only |
CoverPredictor (idea034) | ~ 35 k | 3-block residual DWT-domain head; extract-only |
| Total trained | ~ 0.21 M | 0.167 M (INN) + ~0.05 M (idea031 + idea034 heads); fp32 disk < 1 MB |
The "0.167 M / 0.7 MB" figure quoted in index, spec and earlier
drafts of this page counted the INN coupling backbone only. The idea031 router + expert bank and the idea034 cover
predictor together add ~50 k params + ~0.2 MB; the deployed checkpoint ckpt_latest.pt is ~0.9 MB fp32 in total.
The router and predictor are extract-side only — embedding latency is governed by the 0.167 M INN backbone, so
inference-cost numbers for the camera-side embed pipeline remain valid as quoted.
All learnable parameters (INN coupling subnets, expert bank, router, cover predictor) sit between bipolar
bit input and bit-logit output. The chain is not a cryptographic primitive — it's a noisy channel.
Security comes from the Ed25519 signature carried inside the bit payload (which the INN treats as
opaque), not from any property of the network. The INN's job is exactly to deliver p > 1 − 14/128 ≈ 0.986
bit accuracy under the threat model so the outer RS(128, 100) succeeds. See §5 Payload & ECC.
| component | params | role |
|---|---|---|
| Haar DWT / iDWT | 0 | spatial → 4-subband decomposition; reversible by construction |
| PatchBitAdapter (v3) | 0 | bits ↔ tensor via 32×32 grid + fixed ±1 sign-mask scramble + nearest-neighbor tile + mean-pool decode. See §6. |
| DKiS INN backbone (8 / light) | 0.167 M | 8 coupling blocks · LightConvBlock subnets · conv5 kaiming×0.1 init breaks DKiS default identity init |
| Total | 0.167 M | 0.7 MB on disk fp32 (deployed idea097/d001, sibling autohide tree) |
One backbone — the deployed mobile target. (Other (num_blocks, subnet_type) points were ablated during development; not maintained.)
| backbone | blocks | subnet | params | note |
|---|---|---|---|---|
| 8 / light | 8 | 2-conv flat (Light) | 0.167 M | powers idea097/d001 on the public imagehide-demo; survives α-ramp; ~24× fewer FLOPs vs. the early 16/RDB prototype |
LightConvBlock: Conv2d(in→32, 3×3) → LeakyReLU → Conv2d(32→out, 3×3) with the second conv kaiming-initialized at scale 0.1 to match the small-residual init invariant that breaks the embed-nothing fixed point at step 0. Regular memory access pattern (no dense concat) pipelines well on Apple Neural Engine / Qualcomm Hexagon.
# 8/light is the default — no flags needed.
python src/exp0_inn_train.py ...
896 b headline = data. 1024 b actually embedded after channel coding. The trick: instead of transmitting all 128 H bits, send a 28-b BCH syndrome of H — the receiver computes H' from the attacked image and uses syndrome+H' to recover H. Saves 100 bits for RS parity.
| layer | code | budget | role |
|---|---|---|---|
| data | H(128) + sig(512) + pk(256) | 896 b | headline payload — the verifiable claim |
| inner (perceptual side-info) | BCH(127, 99) Slepian-Wolf on H | 28 b syndrome | transmit a 28-b syndrome instead of all 128 H bits; receiver decodes H = H' XOR BCH_decode(S ⊕ BCH_syn(H')). Corrects ≤ 4 bits of PDQ drift. |
| compressed message | syndrome + sig + pk | 796 b = 100 B | fits RS(128, 100) |
| outer (channel) | RS(128, 100) over GF(2^8) | 28 B parity | corrects up to 14 byte errors in the 1024-b codeword |
| embedded | RS codeword | 1024 b | what the INN actually carries in pixels |
Standard Meta PDQ pipeline through the 256-bit hash, then keep the 128 lowest-frequency bits in zigzag anti-diagonal order. The global 256-coef median is retained (preserves PDQ's ~50/50 bit balance); the 128 high-freq bits are discarded because they drift much more under low-pass attacks.
Rec.601 luma → Jarosz blur → 64×64 decimate → 64×64 DCT-II
→ top-left 16×16 → threshold against median of all 256 coefs
→ 256-b hash → keep 128 lowest-freq bits (zigzag order)
→ BCH canonicalization 128 → 127 (drop one canonical index)
Reproduced on 100 DIV2K_valid_HR @ 512² with the bit-exact src/pdq_js_port.py, across the baseline 9-chain catalog + 4 adversarial multi-pass JPEG chains. Max drift ≤ 3 bits / 128 over the design's threat model — comfortably inside the BCH t=4 budget. Original verification was 30 COCO val2017 × 11-attack sweep in the JS demo.
| attack class | max drift | within t=4? |
|---|---|---|
| single + chained JPEG (Q 30–95) | 2 | yes |
| resize 0.5–0.9 | 3 | yes |
| 9-chain baseline (Insta / X / WhatsApp / WeChat etc.) | 3 | yes |
| screenshot-of-screenshot (2× up-down + gamma + 2-pass JPEG) | 7 | ~4% over budget — if in scope, bump to t=5 (35-b syndrome, still saves 93 b net) |
| crop (any size) | 20–50+ | out of scope — PDQ has no crop invariance |
src/robust_hash.py PHash128 is a simpler 12×12 row-major variant — NOT bit-exact with pdq.js (different drift behavior). The PyPI pdqhash package emits bits in an undocumented order that does not preserve frequency locality (~50% agreement with pdq.js). The canonical reference is the JS demo at ~/yanghangAI.github.io/assets/imagehide/.
Zero learnable parameters. Bits ↔ tensor by construction.
| step | operation |
|---|---|
| 1. bipolar | bits ∈ {0,1}^896 → ±1 |
| 2. grid pad | place 896 of 1024 slots in a 32×32 grid (use 896, ignore 128) |
| 3. scramble | fixed sign-mask multiply (PRNG seed locked in code) |
| 4. tile | nearest-neighbor up-sample to DWT spatial dims (H/2, W/2) |
| 5. broadcast | repeat across 12 DWT channels → secret (B, 12, H/2, W/2) |
| ← decode | mean-pool over patch + sign-mask invert + sign-threshold → bit logits |
Adapter round-trip is mathematically perfect (validated on smoke test). The patch grid is fixed at 32×32 regardless of image size, requiring H % 32 == 0 and W % 32 == 0 (so DWT dims divide 32 evenly).
α = 1 fixed.
| setting | value |
|---|---|
| loss | img_weight · L1(container, cover) + bit_weight · BCE(logits, bits) |
| optimizer | AdamW · lr 2e-4 (baseline) / 4e-4 (mobile, sqrt-scaled) · no weight decay · cosine schedule · grad-clip 5.0 · AMP |
| resolution | variable. 18-entry (H, W) pool: squares + 5:3 / 7:5 / 16:9 / 12:5, all /64. Batch derived from a fixed pixel budget (default 1.0 M ≈ 256² × 16). See src/multi_res_dataset.py. |
| batch (mobile, 256² baseline) | 16 · lr 4e-4 — smaller activation footprint allows 4× the rdb-baseline batch on 11 GB 2080Ti |
| steps | 30,000 (cut from 100 k after run 2 collapsed; the architecture-viability decision is reachable by ~30 k under full α) |
| threat model | compression-only — JPEG (single + chained) + down-then-up resize. Geometric attacks (crop, rotate) and other photometric attacks out-of-scope per canonical-delivery design. |
| resume | checkpoint every 2k to ckpt_latest.pt (model + optim + sched + scaler + RNG). Preempt-safe by design. |
Replaces the 3-stage step function (clean → low → high) that collapsed runs 1+2 at the discrete A→B boundary.
| step range | α | what happens |
|---|---|---|
| 0 → 5,000 | 0 | clean — attack module skipped entirely |
| 5,000 → 15,000 | linear 0 → 1 | each kornia op fires at p_max · α; encoder sees a continuously-increasing fraction of attacked batches |
| 15,000 → 30,000 | 1 | full target severity |
AttackLayer (src/attacks.py) — severity="high", α=1| op | Q range / scale | Bernoulli p (α=1) |
|---|---|---|
resize (down-then-up) | scale ~ U(0.5, 1.0) | 0.5 |
jpeg1 | Q ~ U(40, 95) | 0.9 |
jpeg2 (chained) | Q ~ U(20, 40) | 0.3 |
Forward order: resize first, then JPEG — matches platform pipelines (downsize → encode). Output spatial dims preserved by construction.
src/exp0_inn_eval.py)| # | size | source |
|---|---|---|
| 1 | 256×256 | training-matched |
| 2 | 1024×1024 | Insta square / SDXL |
| 3 | 1088×1920 | vertical phone / Reels |
| 4 | 1920×1088 | Full HD landscape |
| 5 | 1280×768 | HD-ish |
| 6 | 2048×1536 | 3 MP photo |
| 7 | 3008×4032 | iPhone vertical |
| 8 | 4032×3008 | iPhone landscape |
| 9 | random | per-image, H, W ∈ {256, 320, …, 2048} |
| category | attacks |
|---|---|
| single JPEG | q90 / q70 / q50 / q30 |
| JPEG chain | 85→60 · 70×3 |
| resize only | 0.9 / 0.7 / 0.5 |
| platform chain | chain_insta 0.5+Q85 · chain_x 0.4+Q75 · chain_whatsapp_std 0.5+Q70 · chain_wechat 0.3+Q50 |
All spatial-dim preserving (down-then-up). Geometric crop/rotate intentionally absent. Platform chains are the load-bearing rows.
| flag | effect | verdict |
|---|---|---|
--amp |
torch.autocast(fp16) around embed/extract; PSNR/SSIM/bit decisions in fp32 |
recommended ~16% wallclock cut; bit_acc max-diff 0.0001 vs fp32 ref. Output → eval_amp.json to preserve byte-reproducible eval.json. |
--compile |
torch.compile(dynamic=True) |
not recommended 9 distinct sizes + Python-iterated _INNBackbone + generator graph-break ⇒ recompile-per-shape dwarfs any win |
PIXEL_BUDGET_BY_SUBNET = {"rdb": 1_500_000, "light": 8_000_000}. The 8/light variant tolerates a ~5× larger per-batch pixel cap (24× fewer FLOPs, flat activation profile). Override with --pixel_budget. Measured peak VRAM on 11 GB 2080Ti for 8/light v2 = ~2.0 GB, pinned by 12 MP batch=1.
idea097/d001)
100 DIV2K val · 7 sizes (256² → 2048×1536 + random) · 14 in-scope attacks (clean + 4 JPEG + 2 JPEG-chain + 3 resize + 4 platform).
Deployed checkpoint trained at 256² for 30 k steps; secret_len = 1024 (post-ECC); --no-amp; lr 4e-4; bit_weight 20.0.
| category | attack | avg (7 sizes) | @ 1024² only | note |
|---|---|---|---|---|
| clean | — | 1.000 | 1.000 | no-attack baseline |
| single JPEG | q90 / q70 / q50 / q30 | 1.000 / 0.999 / 0.999 / 0.996 | 1.000 / 1.000 / 1.000 / 1.000 | platform re-encode |
| JPEG chain | 85→60 · 70×3 | 0.999 / 0.999 | 1.000 / 1.000 | cross-platform re-uploads |
| resize only | 0.9 / 0.7 / 0.5 | 1.000 / 1.000 / 1.000 | 1.000 / 1.000 / 1.000 | down-then-up resample |
| platform | chain_insta 0.5+Q85 | 0.998 | 1.000 | Instagram feed |
| platform | chain_x 0.4+Q75 | 0.986 | 1.000 | X (Twitter) post-2022 default |
| platform | chain_whatsapp_std 0.5+Q70 | 0.996 | 1.000 | WhatsApp Std / FB feed |
| platform | chain_wechat 0.3+Q50 | 0.955 | 0.992 | WeChat — worst attack overall; 1024² holds 0.99 |
| aggregate | avg 0.995 · min 0.955 | avg 0.999 · min 0.992 | aggregates over the 13 attack rows (clean excluded) | |
| size | PSNR clean | SSIM clean |
|---|---|---|
| 256×256 (training resolution) | 35.00 dB | 0.945 |
| 1024×1024 (canonical target) | 37.35 dB | 0.940 |
| 1280×768 | 37.42 dB | 0.941 |
| 1088×1920 / 1920×1088 | 38.13 / 38.16 dB | 0.939 / 0.937 |
| 2048×1536 | 38.35 dB | 0.935 |
| random (per-image) | 37.51 dB | 0.940 |
chain_*_full) and Gaussian-noise rows from eval.json are out of the canonical-delivery threat model — see §13.
Source: autohide/experiments/1024bit_noamp_keepckpt/idea097_design001/eval.json (sibling autohide tree, 2026-05-17, step 30 k).
Numbers in this section are computed directly from that file. The historical v1 → v2 ablation that lived here (resize-in-training necessity) has moved to the design & plan pages.
| model | params | disk fp32 | 12 MP fp16 · ~17 TFLOPS NPU |
|---|---|---|---|
| TrustMark Q | 21 M | 85 MB | ~100 ms (claimed) |
| MWIP (deployed) · 8/light | 0.167 M | 0.7 MB | ~25 ms (~6–12 ms int8) |
| MetaSeal | ~300 M | ~1.2 GB | seconds |
The deployed 8/light backbone uses LightConvBlock subnets (no dense concat) plus 8 coupling blocks total — small parameter footprint and a flat memory-access profile that pipelines well on phone NPUs. Single-shot photo capture latency is comfortable; 30 fps video is not. Actual on-device measurement is Exp 4.
from src.inn_model import INNCodec model = INNCodec(n_bits=896).cuda() # embed container = model.embed(host_rgb, bits) # (B,3,H,W) ∈ [-1,1] # extract (bit logits) logits = model.extract(container) # (B, 896) # extract with diagnostic intermediate (added 2026-04-25) logits, secret_rec = model.extract_with_secret(container) # secret_rec: (B,12,H/2,W/2) — recovered before adapter.decode # convenience container, logits = model(host_rgb, bits)
Inputs in [-1, 1]; bits in {0, 1}; H, W divisible by 64.
from robust_hash import PHash128, hamming_distance hasher = PHash128().to(device) H = hasher(image_tensor) # (B, 3, H, W) → (B, 128) ∈ {0, 1} hamming_distance(H_a, H_b) # element-wise → (B,)
PHash128 here is the simpler 12×12 row-major variant, not bit-exact pdq.js. For the production payload use the pdq.js port at src/pdq_js_port.py.
Per --log_every step. Both secret_rec_rms and secret_rec_err_rms added 2026-04-25 to catch the embed-nothing collapse the moment it starts.
| metric | healthy | collapse signal |
|---|---|---|
bit_acc, bit_acc_ema | → 1.0 | flat at 0.5 |
psnr(container, cover) | 30–35 dB | rises past ~50 dB = container ≈ cover, secret discarded |
l1_container_cover | ~0.01–0.02 | drops toward 0 with bit_acc still 0.5 |
secret_rec_rms | stable through ramp (run 1 saw ~93 in clean phase) | drops toward 0 = INN killed the secret pathway |
secret_rec_err_rms | small relative to secret_rec_rms | rises toward secret_rms (~1.0) = recovered tensor uncorrelated |
alpha | matches curriculum schedule | discontinuities = curriculum bug |
img_loss, bit_loss | finite, decreasing | NaN |
| constraint | reason |
|---|---|
H, W % 64 == 0 | DWT halves spatial dims; adapter patch grid 32×32 requires DWT dims % 32 == 0; so input H,W % 64 == 0 |
Inputs in [-1, 1] | training convention; (image / 255) * 2 - 1 |
Bits are {0, 1} not ±1 | adapter converts internally; logits returned for BCE |
| Single canonical center-trim at verify | verifier rejects images that don't fit the trim convention; preserves false-accept surface (codex tightening — replaces multi-shift search) |
| Crop / rotate = tamper | by design; no graceful partial verification under crop |
| Online registry lookup required for device validity | fully-offline device trust dropped in v4 (cert chain pushes payload past Exp 0 stall threshold) |
56248029), 2 (56250090), 3 (56250331) all hit the same embed-nothing basin under the 16-block dense-residual backbone. Escalation to the embedding-energy floor pending for any future 16/RDB attempt.
56391691 NaN'd at 40k.
Held bit_acc≈1.0 / PSNR≈28 / sec_rec_rms~40 through step 40,000. PSNR/L1/loss went NaN at 40,200; decoder froze to constant output by step 60k (val PSNR pinned 11.09 dB). Different failure mode from runs 1–3 (which collapsed at the noise transition). Multi-res sampler is the new variable. Root cause pending.