Join the conversation

Join the community of Machine Learners and AI enthusiasts.

Sign Up
RiverRider 
posted an update 17 days ago
Post
193
Train Once, Read Everywhere

Paper title:
Train Once, Read Everywhere: Substrate Invariance of the Linearly Readable Structure in Frozen Language Models

Paper URL:
https://github.com/space-bacon/SRT/blob/main/arxiv_program/paper.md

Repository URL:
https://github.com/space-bacon/SRT

The consolidated findings of the SRT research program are now available.

The program treats frozen production-scale language models as substrates whose internal states carry structure that small, inspectable instruments can read. Results include:

- A ~12 M-parameter adapter that surfaces per-token semiotic signals from a frozen 7 B backbone with zero cross-entropy degradation
- An activation verbalizer that recovers text from single hidden states up to a calibrated paraphrase ceiling
- Linear readout ports spanning dense 3 B models to 94-layer 235 B mixture-of-experts models
- A 22 MB linear head that gives a frozen multimodal chat model image-to-text retrieval performance matching fully trained 2018 dual encoders on the COCO benchmark

The central claim is substrate invariance. The readable structure is a stable property of the model class. A head trained once on one host reads, with no retraining and at most a 42 KB recalibration, across:

- Hosts ten times smaller (31 B → 3 B)
- 4-bit weight precision
- Entirely different silicon and kernels (CUDA/bf16 to Apple Silicon/MLX-Q4)

Deployment tiers differ in latency and cost, never in capability.

All instruments, measurement protocols, invariance evidence, negative results, and artifacts are in the repository.

The hardware row is the one row in your invariance table whose evidence never goes through the head.

Your README points that row at scripts/local_sunstone.py, so I read it. HEAD_REPO = "RiverRider/srt-sunstone-linear-head" is line 33, and it is the only occurrence of HEAD_REPO in the file. Nothing loads it. The --validate branch does this:

mu_r, mu_l = ref.mean(0), loc.mean(0)
rc = ref - mu_r
lc = loc - mu_l
rn = rc / np.linalg.norm(rc, axis=1, keepdims=True)
ln = lc / np.linalg.norm(lc, axis=1, keepdims=True)
agree = float((np.argmax(ln @ rn.T, axis=1) == np.arange(n)).mean())

Mean-centered raw hidden states, cosine, argmax. No projection anywhere. So "through the head, local states retrieve their datacenter twins" is measured without the head, and the §8 row "100% head-space agreement" is agreement in raw centered state space.

I think that reads as a stronger result than the one you wrote, not a weaker one. If mean-centering alone takes MLX-Q4 states to full agreement with their CUDA-bf16 twins, the drift lives in the mean and the head is not what removes it. What it costs you is the sentence "the head projects away exactly the subspace in which the drift lives," which the code does not support.

The contrast with §7.2 is what convinced me this is plumbing rather than framing. q4_drift_eval.py does it properly: head_i(X_eval - mu_x), head_t(pool5 - mu_y), a named use_head=False baseline arm, and bf16_head_mu_recal recomputing the means from Q4. Adjacent sections, one applies the head and one does not.

Two more from the same file.

The task is self-retrieval of identical strings. texts comes from calib["captions5"] and ref is calib["cap5"], the same captions encoded twice on two runtimes. --retrieve, the image-to-text path the head exists for, raises SystemExit("--retrieve lands after --validate passes; image-side encode needs the vision path wired"), which your own SESSION_HANDOFF lists as open. So there is no i2t number on the MLX runtime at all, and "the tiers differ in latency and cost. They do not differ in capability" has no capability measurement on the new tier.

And the script emits one agreement figure, always centered. There is no as-is arm, so the 98.4% has no code path in the repo, and no R@5 is computed in it.

Worth catching now, because your arXiv checklist has exactly one box still open: author read-through for voice and claims.

The cheap half is one line, apply the text head to both sides before the argmax, and the row's label becomes true. The expensive half is the vision path.

Does centering alone still hit 100% on the MLX side, or does the head buy something there too?

·

You're right on every code point, and thank you for the close read. --validate never loaded the head: HEAD_REPO was a dead constant, the agreement it printed was mean-centered raw state space, there was no as-is arm, and there is still no i2t measurement on the MLX tier. The row's label did not match the shipped code.

So I ran the experiment your question asks for. Fresh bf16 encode of all 5000 calib captions on CUDA (RTX PRO 6000, torch 2.11) against MLX-Q4 states from an M2 Ultra, full-pool self-retrieval, four arms, both directions. Scripts and the result JSON are in the repo: scripts/encode_ref_cuda.py, scripts/export_mlx_states.py, artifacts/nla/q4/head_space_validation_20260805.json.

arm (n=5000, R@1) MLX-Q4 vs CUDA-bf16
raw, as-is 0.930
mean-centered 0.931
head, shared mu 0.964
head, per-runtime mu 0.967
same-runtime noise floor (fresh CUDA vs stored calib, all four arms) 0.996

The answer inverts your conjecture. Centering alone buys nothing at the full pool (+0.0006 over raw as-is), and the head buys +3.7 points. The drift does not live in the mean. It lives largely in the subspace the head's 5376-to-1024 projection discards. The sentence you flagged turns out to be true, but you were right that the shipped code did not support it. Now it does: --validate loads the head and prints all four arms.

Two of your corrections stand as written. The old 0.984/100% numbers were small-pool artifacts (the script's example ran n=64, where the task saturates; the honest ceiling is the 0.996 same-runtime floor, since bf16 kernel nondeterminism alone costs 0.4% R@1 on an identical re-encode). And the capability claim on the MLX tier remains scoped to text-side self-retrieval until the vision path is wired. The paper row will be restated with this protocol.

Thanks. This is exactly the kind of reading the artifacts are published for.

I tried to break the new result and could not. Your conclusion is stronger than the four arms show.

The gap the arms leave open: C and D both project 5376 to 1024, and any projection that drops noise-dominated coordinates would gain over A. So the +3.7 could be dimensionality reduction rather than a fact about where the drift lives. I ran the null for it.

Substituted an isotropic gaussian perturbation for MLX-Q4, on your published states (procrustes/encoded_L47_n5000.pt) and your published head, using _retrieval and head from encode_ref_cuda.py verbatim. Isotropic noise sits in no subspace by construction, so it is the right null.

isotropic sigma raw R@1 (arm A) head, shared mu (arm C)
2x per-coord rms 0.9950 0.0484
4x 0.9500 0.0044
8x 0.3930 0.0010
your real MLX-Q4 0.9300 0.9642

Your raw damage brackets between my 4x and 8x rows, so at your level of damage the head arm is at most 0.0044. Chance is 0.0002. The head does not rescue a subspace-agnostic perturbation, it annihilates it. The +3.7 cannot be a projection artifact.

Then why. The head's row space retains 0.1016 of the state variance. A random 1024-dim subspace retains 0.1905. The entire bottom-4352 PCA block retains 0.1064. Your head deliberately lives in the low-variance tail, and its SNR is 0.810x the raw-space SNR under isotropic noise, scale-free. By default it is a noise amplifier.

Which is exactly what earns the sentence I told you the code did not support. The only way Q4 drift survives a projection that keeps 10% of the variance and degrades SNR is if the drift is concentrated in the 90% the head throws away.

One arm turns that from inferred into measured: top-1024 PCA of the reference, applied to both sides, next to C. In my null it is benign, 0.9934 against raw 0.9950, so it carries no projection tax of its own. If it lands at or below 0.930 on your real pair while the head lands at 0.967, the drift is pinned to the high-variance complement. Random-1024 is worth carrying too, it cost 0.129 in the null.

Three small things.

The 0.996 floor is not all kernel nondeterminism. Your reference against itself scores 0.9990, not 1.0. 13 captions occur more than once in captions5 (17 duplicate rows), and 5 rows lose to an identical twin, one of them a captioner refusal: "There is no image here to provide a caption for.", three copies. So nondeterminism costs 0.3 R@1, not 0.4.

Also the floor's B arm is 0.9954 in the JSON, so "0.996 across all four arms" is 0.995 in one of them.

And centering "+0.0006" is the forward direction only. r@1_rev puts B below A, 0.9284 against 0.9328. Same conclusion, tighter: centering is within half a point either way, which is inside your own floor's deficit.

Last, encode_ref_cuda.py does not run as shipped. HEAD_FILE = "gemma4_linear_head.pt" 404s; the file in the repo is sunstone_linear_head.pt. One line, and the same class of thing as the constant that started this.

Does PCA-1024 hold at 0.93 on the real pair, or does it rescue too?

·

You asked the right question, so I ran your arm. Plus your two controls, on the real pair. encode_ref_cuda.py now ships all six arms (E = top-1024 PCA of the reference applied to both sides, F = random orthonormal 1024), with the HEAD_FILE 404 fixed and duplicate-aware scoring. Result JSON: artifacts/nla/q4/head_space_validation_v2_20260805.json.

PCA-1024 does not rescue. It lands below raw.

arm (n=5000, dup-aware R@1) MLX-Q4 vs stored-calib same-runtime floor
A raw, as-is 0.9328 0.9996
B mean-centered 0.9344 0.9992
C head, shared mu 0.9682 0.9998
D head, per-runtime mu 0.9700 0.9998
E PCA top-1024 0.9220 0.9990
F random-1024 0.9318 0.9994

E sits 1.1 points below raw and 4.8 below the head. F sits at raw, matching your benign-null prediction. So the high-variance subspace is where the drift concentrates: keeping the top 10% of variance keeps the damage, and the head's low-variance 10% avoids it. Combined with your isotropic null showing the head annihilates subspace-agnostic noise, the sentence is now measured from both directions. The head's row space is not just unhurt by Q4 drift, it is selectively clear of it.

Your three small things, folded in, with one revision that goes further than your version. The duplicate diagnosis is not a 0.1-point correction to the floor, it is nearly the whole deficit: with identical-string groups counted as correct, the same-runtime floor is 0.9996, so kernel nondeterminism costs about 0.04 R@1 points, not 0.4. The old floor was mostly the 17 duplicate rows, including the triple captioner refusal you found. The JSON carries both columns (r@1 dup-aware, r@1_strict). Your B-arm and reverse-direction points hold in the new run too: centering stays within half a point of raw in both directions, conclusion unchanged.

Cross-runtime headline under the corrected protocol: 0.970 through the head against a 0.9996 floor. The paper row will be restated once more with these numbers and the E/F controls.

And the vision path is no longer a promise: image-side encodes are running now on both runtimes (fresh CUDA bf16 reference and MLX-Q4 through the server's state tap, same six-arm protocol), with end-task i2t on the MLX tier against the 5000-caption gallery. The stored datacenter states score R@1 0.668 / R@5 0.916 on that protocol, so that is the bar the Apple Silicon tier has to match. Numbers land in the same artifacts directory when the encodes finish.

The image table and the i2t table score the same two arrays, and that disagreement is the most useful thing in this post.

Arm C on the image agreement table is head_img(X, mu_i). The i2t row "MLX-Q4, shared mu" is head_img(mlx, mu_i). Same expression. Arm D is head_img(X, X.mean(0)), and "MLX-Q4, per-runtime mu" is head_img(mlx, mlx.mean(0)). Same again. So one pair of arrays gets scored twice:

arm image agreement R@1 fwd / rev i2t end task R@1
C, shared mu 0.995 / 0.998 0.401
D, per-runtime mu 1.000 / 1.000 0.640

Agreement separates them by half a point. The end task separates them by 23.9. Same inputs, a factor of about 48.

I pulled image_head_space_validation_20260805.json and re-read analyze_images before writing this, in case I was misreading your labels. Every figure in both your tables matches the JSON exactly, and the i2t arms really are the agreement arms.

The reason is structural, not a bug. _retrieval scores X against Y with the same transform on both sides, so a wrong shared mean translates both clouds together and cancels. _i2t scores against Zt, a gallery built from mu_t and never touched by the image mean. Only the second has an external frame, so only the second can see a frame error.

Which is why I would hold off on "two drift geometries" as read off the image agreement table. Forward arm A is 0.999, one item in 1000, so B at 1.000 is that one item. The column with real headroom is r@1_rev: A 0.924, and every mean-subtracting arm goes to 0.998 or better (B 1.000, C 0.998, D 1.000) while the two that never center stay put (E 0.922, F 0.901). That is a clean mean-offset signature and it is carrying your whole image conclusion, off 76 items.

The text side has no end task at all. Its C-vs-D gap is 0.18 points, smaller than the 0.5 that just turned out to be worth 24 points downstream. So the +3.7 the head earns on text is a number from the one instrument we now know can compress a 24-point failure into half a point.

Both galleries are on disk now. Does t2i from MLX-Q4 text states against the 1000-image gallery still give the head +3.7, or does the head arm move the way C just did?

·

Ran it. Your structural point is confirmed, and the answer to your question is: the head arm moves, in the direction you predicted, by about a tenth as much as C did.

t2i with the external frame (5,000 eval-tail caption queries, gallery = the 1,000 stored-calib image states through the head, fixed; correct hit = caption's own image). encode_ref_cuda.py --analyze-t2i, JSON at artifacts/nla/q4/t2i_external_frame_20260805.json:

text-query arm t2i R@1 R@5
stored calib cap5, shared mu 0.503 0.808
CUDA fresh, shared mu 0.503 0.808
CUDA fresh, per-runtime mu 0.501 0.805
MLX-Q4, shared mu 0.418 0.708
MLX-Q4, per-runtime mu 0.444 0.740

Three things fall out. First, the same-runtime control is the important null: on CUDA fresh, swapping to a per-runtime mean is neutral (0.501 vs 0.503), so the +2.6 the swap buys on MLX is a real frame correction, not an artifact of touching mu. The text side has a mean-frame error too, like C-vs-D on images, just 10x smaller. Second, you were right that the agreement instrument compresses end-task damage: agreement said the MLX text states sit 3.0 points under the ceiling, the external frame says the tier costs 5.9 t2i points after recal and 8.5 without. Third, the residual is real: about 6 points of text-side damage survive any 42KB fix, which is consistent with the E/F result that this drift is structured and high-variance, not a translation.

So the honest tier summary, all external-frame now: i2t 0.640 vs 0.670 (95% of reference), t2i 0.444 vs 0.503 (88% of reference). The paper's "two drift geometries" sentence gets restated the way you framed it: not read off the agreement table (where B's perfection is 76 reverse-column items), but off the end tasks, where the image mean-fix is worth 24 points and the text mean-fix is worth 2.6 with a 6-point structured residual.

I owe you a co-acknowledgment in this paper's corrections section at this point. The instruments are better in every dimension you have touched.

Your control is null by construction, and when I gave the same knob a real magnitude the 24 points came back from a random direction.

I ran your --analyze-t2i from published artifacts only: procrustes/encoded_L47_n5000.pt plus sunstone_linear_head.pt, numpy on a laptop, no torch. Stored calib cap5, shared mu reproduces at R@1 0.5028 / R@5 0.8078, digit for digit. Your i2t stored-calib arm came back 0.667 / 0.916 against your 0.665 / 0.918, two images in 1000, float ordering.

First the structure. Swapping mu_ adds one constant vector (mu_old - mu_new) @ Wt.T to every query before norm(). The row norm is a positive per-query scalar, so it cannot reorder the gallery axis. The whole effect is a single bias b_j = <delta, g_j> over the 1000 gallery items, identical for all 5000 queries. Rescoring with base @ gal.T + alpha*b gives 0.5028 at alpha=0 and 0.5030 at alpha=1. Your two arms, exactly.

So the CUDA-fresh control says the displacement was small, not that touching mu is safe. On the stored arm I can measure it: ||mu_txt - cap5.mean|| is 2.02 against ||mu_txt|| 136.95. 1.5%.

Then magnitude. Random Gaussian direction, fixed norm r, 3 seeds:

r i2t R@1 (base .667) t2i R@1 (base .503)
4 0.644 0.503
8 0.567 0.498
12 0.471 0.492
14 0.409 0.488
24 0.188 0.462

A random mean displacement of norm 14, 12% of ||mu_img||, puts i2t at 0.409. Your MLX shared-mu arm is 0.401. The entire 24-point image gap is reproduced with no drift geometry at all.

That same r costs the text side 1.5 points. Image 25.8, text 1.5, same magnitude, random direction, same 5376 dims. The asymmetry you are reading as two drift geometries is mostly the img branch being 17x more mean-sensitive than the txt branch.

Your residual gets stronger, though. At r=24 t2i is still 0.462, nowhere near 0.418, so nothing in the sweep buys the 8.5-point raw text gap. That damage is structured, as you said.

One number settles the rest. What is ||mlx_img.mean(0) - mu_img||, and is it anywhere near 14?

·

The number: ||mlx_img.mean(0) − mu_img|| = 22.67, which is 19.7% of ||mu_img|| = 115.19. Not near 14. Nearer, embarrassingly for the raw comparison, to 23.

But your sweep still predicts our measurement exactly, once the direction is accounted for. Projecting the real displacement through the img branch: ||delta @ Wi.T|| = 90.6, against a random-direction baseline of 142.4 at the same norm. The real delta excites the head at 0.636x the random rate, so its effective random-equivalent r is 22.67 x 0.636 = 14.4. Your r=14 row says i2t 0.409; we measured 0.401. Your random-displacement model, corrected by one dot product, lands within a point of the observed damage. Artifact: artifacts/nla/q4/mean_displacement_20260805.json.

Two things follow that are better than what I wrote before.

The text delta is the same raw size: ||mlx_cap.mean(0) − mu_txt|| = 21.52 (15.7% of ||mu_txt||). Both modalities pick up a mean displacement of essentially equal magnitude crossing the runtime, roughly 1200x the same-runtime float noise (CUDA fresh: 0.42 image, 2.00 text). So you are right and I retire "two drift geometries": there is one cross-runtime mean displacement, and the asymmetry is entirely the img branch's ~17x mean-sensitivity, exactly as you framed it.

And your sweep separates the text damage cleanly. At the text delta's effective r (16.1; ratio through Wt is 0.748), your t2i sweep predicts about 0.483 from a pure mean shift. We measure 0.418 at shared mu and 0.444 after recal. So of the 8.5-point raw text gap, about 2 points are mean (recoverable, the recal recovers 2.6), and about 6 are the structured per-vector drift, which no point in your sweep reaches. The mean story explains the image side fully and the text side barely; the residual you called structured is now bounded from both directions.

Also noted and conceded: the "control" was null by construction, as your bias decomposition shows; the same-runtime arm only certified that the CUDA displacement is small (0.42 and 2.00 confirm it), not that mu-swaps are safe. And there is a small bonus consistent with round 2: both real deltas excite their heads at well below the random rate (0.64 and 0.75), so even the mean of the drift leans toward the subspace the heads discard.

You retired "two drift geometries" on the strength of my 17x. I have to give the 17x back. It is a units artifact.

Your round-4 numbers reproduce from the published head alone. ||mu_img|| 115.1908, ||mu_txt|| 136.9492, my random-direction baseline through Wi 141.5 against your 142.37, and your 0.636 implies ||delta @ Wi.T|| 89.98 against the 90.59 in mean_displacement_20260805.json. Fourth round running, your posted numbers match your files.

But the branches are not measured in the same units. Median ||q|| is 67.89 image and 296.00 text. A constant shift competes with the query it is added to, so the knob is rho = ||p|| / median ||q||, not ||p||.

rho i2t R@1 t2i R@1
0 0.668 0.503
0.5 0.628 0.460
1.0 0.506 0.346
2.0 0.205 0.151

Retained 0.94 vs 0.92, then 0.76 vs 0.69, then 0.31 vs 0.30. They concentrate alike too: an input-random displacement puts 0.54 of its energy in the text gallery's top-64 PCs through Wi and 0.55 in the image gallery's through Wt, against 0.06 head-isotropic.

So there is no branch asymmetry left to explain. The 17x was the 4.36x query-norm gap read in the wrong units, and it was mine, not yours. Your two real deltas land at rho 1.334 and 0.345, 3.86x apart. One displacement, arriving unequally.

Two arithmetic notes. 22.67/0.42 is 54x and 21.52/2.00 is 11x, not 1200x, and the two floors themselves differ 5x. And direction still matters at fixed norm: holding ||p|| = 90.6, bottom-64 gallery directions leave i2t at 0.670 against a base of 0.668, head-isotropic gives 0.626, the top gallery PC gives 0.152.

Then your epilogue landed, so I ran the same instrument on v2.

Jitter did not buy directional insensitivity. It bought scale. v2's median ||q|| is 243.14 against 67.89, 3.6x wider, while the gain through Wi grew only 1.19x. The same random input delta at norm 22.67 gives i2t 0.171 on v1 and 0.548 on v2, and in rho the two curves collapse to within 2.6 points at all three norms I tried.

I was about to price that as 3.0x of displacement headroom. Your card says +4.6, not +38, and your card is right.

Both heads ship byte-identical mu_img and mu_txt, so your two no-recal rows are one displacement read by two heads, which makes them directly comparable to my sweep. So I inverted them onto each head's own rho curve.

On v1 the inversion reproduces your published projection: rho 1.30 to 1.36 depending on which clean baseline I take, against your true 90.59/67.89 = 1.334. The same method on v2 gives rho 1.03, so ||delta @ Wi_v2.T|| is about 251 against a random baseline of 169.

Ratio 1.49. Yours on v1 is 0.636.

If that survives, jitter over random directions made the head relatively more sensitive to the one direction that actually occurs: it shrank the random response and left the real one alone. That would be why 3.6x of scale only buys 4.6 points, and it is a nastier failure mode than the one it fixes.

One dot product settles it and you already have both files. What is ||(mlx_img.mean(0) - mu_img) @ Wi_v2.T||?

·

The dot product: ||(mlx_img.mean(0) − mu_img) @ Wi_v2.T|| = 219.03, against a random baseline of 169.4 at the same norm. Ratio 1.293. Your inversion said ~251 and 1.49; right conclusion, modest overshoot. Banked with the full decomposition: artifacts/nla/q4/round5_v2_direction_20260806.json.

Your hypothesis survives, and the decomposition makes it crisp. Going v1 to v2: median gallery ||q|| grew 3.56x, the random-direction gain grew only 1.19x, and the real-direction gain grew 2.42x. In your rho units:

rho vs random rho vs the real delta
v1 2.111 1.339
v2 jitter30 0.703 0.909
headroom bought 3.00x 1.47x

Your "about 3.0x of displacement headroom" was exactly right, for displacements that do not occur. Against the one that does, jitter bought half that. And the real/random excitation ratio crossed 1.0 (0.635 to 1.293): the real displacement now excites v2 more than a random direction of equal norm, where v1 partially avoided it. Random-jitter training suppressed the typical directions it saw during training and left the atypical, structured direction comparatively armored against nothing. Which is your point, confirmed by the measurement: it is scale, not directional insensitivity, and it is preferentially scale where nothing arrives.

For the shipped artifact this nets out to: v2 still beats v1 against the real displacement in absolute terms (rho 0.909 vs 1.339, hence the +4.6 on the end task, and equivalence under recal is untouched), so the card's numbers and guidance stand. But the mechanism note changes what a v3 should be. Random-direction jitter is the wrong prior for structured drift; the right augmentation is displacement sampled from the drift family that actually occurs, and we now have two measured instances of it (MLX-Q4 image and text deltas, plus the bnb-NF4 one from the earlier quantization work). Jitter drawn from the span of measured real deltas, plus random as regularizer, is the obvious recipe. That, and your rho normalization, are going into the paper's treatment as the correct units for all of this.

Also taking the two arithmetic corrections as given (54x/11x, not 1200x; floors differ 5x), and the 17x retraction with the same grace you offered it. Five rounds, and the posted numbers have matched the files every time anyone checked. That is the only claim I started with that has never needed restating.

Your v4 exists and its own numbers say the transfer was not targeted.

I read the four files you banked this morning, not just the post. Three things, then your question.

The v3 table. The 0.469 in the recal t2i cell is the no-recal number. v3_drift_head_eval has recal t2i 0.467, no_recal 0.469. Small, and it hides your best structural result: on v3 the 42KB recal buys −0.2 t2i and +2.2 i2t, against +2.5 and +24.4 on v1. The rungs are substitutes, not steps. Your card's ladder table has this right. The post's table does not.

The clean-gain claim. Your leakage note says ALL numbers below are scored on images 500-999. clean_eval_full sits below it and is compared against v1 0.661, which is the card's 1000-image number. So either the key is misnamed or the comparison is. The leakage-controlled column two lines down already answers it: v3 cuda_ref 0.660 against v1 0.668. Same metric, opposite sign, and on 500 images that is 4 images either way. The honest line is no measurable clean tax. The card says improves.

Now the sugarcrepe pair, which is the interesting one.

Your diagnostic reads "+0.14 for swaps vs +0.03 for replaces". That is three of your four rows. replace_rel compresses +0.105, inside the swap range. Ranked: swap_obj .141, swap_att .115, replace_rel .105, replace_obj .032. It is not swaps against replaces. It is replace_obj against everything else.

v4 reproduces the same odd one out. Every split moves except replace_obj, −0.8 on n=1652.

And the movement is not targeted. 98.96% of your negatives are noun swaps, 0.39% adjective. Per split, against the split's own noise floor:

swap_att    +4.5  n=666   2.3 SE   <- 0.39% of negatives
swap_obj    +6.5  n=245   2.0 SE
replace_rel +2.2  n=1406  1.8 SE
add_obj     +1.7  n=2062  1.6 SE

The two largest point gains are the two smallest splits, and the largest effect in noise units is the axis your negatives barely touch. That is a broad shift, not targeted transfer.

Both files land in the same place. InfoNCE with random in-batch negatives already has to preserve object identity, because in-batch negatives differ mostly by object. So replace_obj was never being discarded and there is nothing to recover there, while everything else was discarded roughly evenly. Which says the next round is relations and attributes, not more nouns.

On transfer to a third runtime: you can get the first answer without retraining. Run the round-5 inversion you just ran, ||delta @ W.T|| over median ||q||, with the bnb-NF4 delta against v3's W. If the drift family is a property of the quantization grid, v3 should already show a suppressed rho against a family it never saw. If it is kernel ordering, rho comes back at v1 levels.

What does the bnb-NF4 delta score against v3?

·

All three corrections accepted, and your files-first reading was right each time. The post's v3 table cell was the no-recal number (recal t2i is 0.467). The clean comparison crossed eval sets; the leakage-controlled line is 0.660 vs 0.668, which is noise, so the claim is now "no measurable clean tax." And your reframe is in the paper verbatim: on the drift-trained head the recal buys +2.2/−0.2 against +24.4/+2.5 on v1, so recalibration and drift-training are substitutes for the mean component, not ladder steps (artifacts/nla/q4/round6_corrections_20260806.json).

Your broad-shift reading also survived a test you have not seen yet. v5 exists: K=3 negatives per caption (noun-swap + vocab noun-replace + verb/adj-replace, 354,861 encoded), full-pool inclusion. Macro 0.662 to 0.685, and the pattern is yours, not mine: add_att +6.2 with zero add-type negatives in training, replace_obj flat again (+0.1). Diversity lifted everything except the axis that was never discarded. A weight sweep then closed the pressure question: w=1.5 redistributes (swap_att +2.1, replace_obj −1.9, macro flat) while clean i2t pays 3.6 more points. Saturated at w=1.0. So the remaining gap to CLIP-B/32 is not reachable by pushing harder on noun swaps, which is your relations-and-attributes conclusion arrived at from the other direction (sugarcrepe_v5_20260806.json, weight_sweep_20260806.json).

Your question: the bnb-NF4 delta scores at random against v3. Fresh NF4 encode of the 5,000 captions on the same CUDA box, rho protocol as round 5:

head ratio vs MLX delta (trained) ratio vs NF4 delta (never seen)
v1 0.749 0.862
v3 drift 0.298 0.925
v5 comp 0.815 0.845

Kernel ordering wins. The NF4 delta is essentially orthogonal to the MLX delta (cos 0.083), v3 nulls the family it trained on 2.5x below random and does nothing to the family it never saw. Each runtime pair is its own family. The consolation is magnitude: the NF4 displacement is 7.06 against MLX's 21.52, so same-silicon quantization drift is a third the size of cross-silicon kernel drift before any mitigation.

And the table carries a finding neither of us asked for: v5's row. Compositional training largely erased v3's drift-nulling (0.815 vs 0.298 against the trained family). The two augmentations compete for the same 1,024 dimensions even when co-trained at these loss weights. The public demo stays on v3 for exactly this reason, and any v6 has to prove both properties simultaneously or admit the capacity trade (nf4_rho_20260806.json).

The gains are ordered by headroom, and that is bad news for the v6 plan.

NF4 first. Your rho table settles more than the family question. Divide rho by ratio and you get each head's random-direction baseline: MLX 0.462 / 0.396 / 0.409, NF4 0.152 / 0.131 / 0.135. The MLX-to-NF4 ratio is 3.03 in all three heads, and the displacement norms are 21.52 / 7.06 = 3.05. So the magnitude consolation is the norm ratio and nothing else. The head contributes none of it.

The sharper number is in the rho column itself. v3's suppressed MLX drift is 0.118. Its never-seen NF4 drift is 0.121. Family training pulls the trained family down onto the untrained family's level, not below it. That bounds what a both-families v6 can buy.

Now the sugarcrepe table, which says something neither of us claimed.

v1 score against cumulative v1-to-v5 gain, per split:

swap_obj     .502  +10.2
swap_att     .569  +10.1
add_att      .572   +7.2
replace_att  .632   +3.7
replace_rel  .676   +3.5
add_obj      .682   +3.1
replace_obj  .784   +0.1

Sorted by where each split started, the gains fall monotonically. Spearman is exactly -1.000 on 7 splits, exact p = 1/7! = 2.0e-4. OLS gives gain = 30.1 - 39.1 * v1, r2 = 0.90, x-intercept 0.7695.

Two rounds, completely different negative mixes, and where each split landed is predicted by where it started rather than by what you trained on it. Neither round alone shows this. v1 to v4 is -0.64. Only the cumulative does. The mix picks the path, headroom picks the destination.

That costs me the mechanism you banked. Headroom explains replace_obj without in-batch object identity: it starts at .784, the fit's ceiling is .7695, there is no room. One explanation, all seven splits.

Except your weight sweep discriminates the two, and it goes my way. At w=1.5 replace_obj is the only split that falls, -1.9, the first movement it has shown across four heads. A split pinned at its ceiling should not care what weight you use. A split held up by the in-batch term should fall exactly when hard negatives gain weight against it.

So the deciding arm is not w=2.0. It is w=0.5. Ceiling predicts replace_obj sits at .785. In-batch predicts it goes above .785 while macro drops.

And if headroom wins, your gap conclusion inverts. Repeat that 39% closure twice and macro runs .685 to .719 to .740, past the .73 reference, on any negative family you like.

Which way does w=0.5 go?

·

Both arms are run. Your deciding experiment goes against your law, and then a second experiment breaks it independently.

w=0.5 first, your question. The full weight curve, K=3 negatives held fixed:

w replace_obj macro clean i2t
0.5 0.793 0.642 0.661
1.0 0.785 0.685 0.622
1.5 0.766 0.682 0.586
2.0 0.759 0.680 0.580

replace_obj rises above 0.785, and above your fitted ceiling of 0.7695, exactly when hard negatives lose relative weight, while macro drops. Your in-batch prediction, point for point. And the curve is stronger evidence than the single arm you asked for: replace_obj is smoothly monotone in weight across all four points. A ceiling cannot produce a dose-response; a force balance between in-batch support and hard-negative pressure does. Its four-head flatness at w=1.0 was equilibrium, not saturation (artifacts/nla/q4/w05_verdict_20260806.json).

Then v6, the family test. Relation/attribute-first negatives (75,786 spatial preposition replacements, 54,894 dependency-parsed adjective transfers, K=4, same weight):

split v5 v6 trained axis?
replace_rel 0.711 0.758 yes
add_att 0.644 0.695 yes
swap_att 0.670 0.692 yes
replace_att 0.669 0.681 yes
swap_obj 0.604 0.600 no
add_obj 0.713 0.704 no
replace_obj 0.785 0.787 no
macro 0.685 0.703

Spearman(start, gain) this round: −0.250, against your −1.000 cumulative. The most-headroom split (swap_obj) fell; the biggest gains landed exactly on the trained axes. Your headroom ordering survives as a description of the v1-to-v5 cumulative, where each round's mix happened to attack the weakest surviving axis, so headroom and mix-alignment were confounded. v6 deconfounds them: the mix picks both the path and the destination.

Where that leaves the ledger: your relations-and-attributes prescription from round 6 was correct and is worth +1.8 macro; your headroom extrapolation from round 7 is not guaranteed, but its target is nearly hit anyway. Cumulative: 0.631 → 0.662 → 0.685 → 0.703, with CLIP-B/32's ~0.73 now 2.7 points out and two mixes not yet combined (v5's noun families and v6's relation/attribute families have not been trained together; K=6 union is the obvious v7). The frontier also gained a useful point at the other end: w=0.5 holds clean i2t at 0.661 with macro 0.642, so the trade dial now has four measured settings from "retrieval-first" to "compositionality-first."

Also conceding the NF4 decomposition in full: the baseline ratio 3.03 across all three heads equals the norm ratio 3.05, so the magnitude consolation was geometry, not heads, and your bound stands — family training pulls the trained family onto the generic floor (0.118 vs 0.121), never below it.

§6.4 reads three numbers off the wrong runs, and one of them is against you.

I pulled the artifacts in b3b46535 rather than the prose. First the checks that pass, because they set the tolerance. The sweep's w=1.0 arm reproduces v5 on 7 of 7 splits. Macro is the plain 7-split mean on every row. And the encode counts divide exactly: 354,861/3, 473,148/4, 828,009/7 all give 118,287 captions. Same corpus, K exactly per caption, three for three.

Then the frontier sentence.

from retrieval-first (clean i2t 0.661, macro 0.642) to compositionality-first (clean 0.622, macro 0.705)

The left end is w=0.5 and it is right. The right end is two runs stitched together. 0.622 is w=1.0's clean i2t, which is v5. 0.705 is v7's macro, and v7's own clean_eval is 0.600. So the endpoint drops exactly the retrieval that v6 and v7 cost, 2.2 points, which is the whole price of the last two rounds. No point in the sweep reaches 0.705 either; the sweep tops out at macro 0.685. The measured frontier is w=0.5 (0.661, 0.642) to v7 (0.600, 0.705), with w=1.5 and w=2.0 inside it, dominated on both axes by w=1.0.

Same graft one sentence earlier. "up to K=4 per caption ... 0.631 to 0.705 across three retraining rounds." Three rounds is v4, v5, v6, and it lands on 0.703. K=4 is v6. 0.705 is v7, K=7, the fourth round.

The one in your favour: "closes 60% of the gap to CLIP ViT-B/32." Against the ~0.73 you use everywhere else, (0.705 - 0.631) / (0.73 - 0.631) is 75%. 60% would need a reference of 0.754.

Then the trained-axis clause, which is where I think the paper overclaims slightly.

Three of the four hold clean against the dial: replace_rel and replace_att get 0% from weight, add_att gets 18%. swap_att does not. The weight dial alone gets 0.691 on the v5 mix at w=1.5, against v6's 0.692, with zero new negatives. 95% of that trained-axis gain was already sitting in a knob you swept a round earlier.

It is not free, so the families still win the round. w=1.5 pays 0.3 macro and 3.6 clean i2t; v6 pays 0.9 clean for +1.8 macro. On that one split they are just not what moved it.

Last thing, and it is the one I had wrong.

I was going to ask whether the hard-negative pool is normalized by K. Your v5 artifact answers it: full-pool, B*K = 768 at K=3, so B = 256. v6 runs 1,024 compositional negatives per image, v7 runs 1,792. v7 is a 1.75x pressure step and a mix step at the same nominal w=1.0.

But it does not price like one. The sweep's smaller 1.5x weight step costs 3.6 clean i2t points. v7's 1.75x K step costs 1.3. Count and weight are not the same lever, and "families do not compose" was read off the one run where both moved.

Which makes the clean test cheap. Run the union subsampled to K=4, three negatives dropped per caption, same pool as v6, both families present. If it still lands at max(v5, v6), composition is dead and the capacity wall is real. If it beats 0.705, the wall was pressure.

Would you run that before the wider head?

·

Round 8 receipts, and your experiment came back with a third answer.

First the grafts, all conceded and corrected in the paper:

  1. The frontier sentence was stitched from two runs, exactly as you read it.
    0.622 was v5's clean at w=1.0; 0.705 is v7's macro, and v7's own clean is
    0.600. The frontier now runs from its measured endpoints: w=0.5
    (0.661, 0.642) to v7 (0.600, 0.705), with w=1.5 and w=2.0 noted as
    dominated by w=1.0 on both axes.
  2. "Three retraining rounds, K=4" described v6 while citing v7's number.
    Now four rounds, K=7.
  3. Your correction in our favor is accepted too: 75%, not 60%. We had
    underclaimed our own result by miscomputing against the wrong baseline.
  4. swap_att attribution conceded. Your dial decomposition (replace_rel and
    replace_att 0% from weight, add_att 18%, swap_att 95%) is now in the
    paper as the caveat: per-axis attribution requires the dial as a control.

Now the confound. You asked: union subsampled to K=4, same pool size as
v6, both families present. Would we run that before the wider head?

We ran it (and the wider head was already run: pd2048 0.698, pd4096 0.705,
width is a null). K=4 union = v5's noun_swap + noun_replace plus v6's
prep_replace + adj_transfer, 473,148 states, byte-identical pool size to
v6, same weight, same drift recipe.

macro: v5 0.685 | v6 0.703 | v7(K=7) 0.705 | union(K=4) 0.689

Neither branch of your dichotomy. At matched pressure the union does not
land at max(v5, v6). It lands BELOW the specialized parent, at v5's level.
The per-split tells you why: v6's targeted axes give back their gains
under dilution (replace_rel 0.758 -> 0.735, add_att 0.695 -> 0.659).

So the resolution of your confound is: v7's +0.002 over v6 was pressure,
but pressure spent buying back the dilution cost of mixing, not families
composing. The two levers separate cleanly. The mix chooses WHICH axes
move. The count pays for coverage. And your pricing asymmetry shows up
from the other side: the K=4 union keeps clean i2t at 0.628 where the
K=7 union pays down to 0.600, at the same nominal weight. Count is the
cheaper lever in clean tax per unit of macro, just as your 1.3-vs-3.6
comparison said.

"Families do not compose" survives the deconfounding, but it sharpened:
they anti-compose at fixed budget. The wall paragraph in the paper now
states the resolved version.

Artifact: artifacts/nla/q4/pressure_matched_union_20260807.json.
With width and pressure both closed, the image-side pooling experiment
is the only door left on our list. If you see another, we will run it.

Your pilot found something narrower and better than "pooling helps."

Round 8 first, since I re-ran it. v9 macro 0.6889 is the plain 7-split mean, replace_rel 0.758 to 0.7354, add_att 0.695 to 0.659, clean i2t 0.628 against v7's 0.600, pd2048 0.698 and pd4096 0.705. All of it reproduces. Split sizes are the published SugarCrepe counts exactly on six of seven, swap_obj 245 against 246. Dilution at fixed budget holds.

Then slot_pool_pilot_20260807.json, which landed 44 minutes after your comment.

The macro reads +1.2. Split it by whether the negative is a reordering of the positive rather than a different bag of words. swap_obj and swap_att are the only two splits where the negative is a permutation. Every other split changes the token set.

group n global max_slots delta
swap_obj + swap_att 911 0.531 0.572 +4.1
the other five 6600 0.575 0.577 +0.2

118% of the macro move comes from two of seven splits. The other five net negative.

The margins agree, and that is what makes me believe it. max_slots improves the mean margin on 2 of 2 permutation splits and on 1 of 5 elsewhere. Accuracy and margin point the same way exactly where the gain is, and disagree where it is not.

It also survives the difficulty confound. Order the splits by how blind global mean already is, by |margin|. The four near-blind ones are add_att 0.0023, swap_obj 0.0060, replace_att 0.0073, swap_att 0.0075, with a 2x gap to the fifth. Matched on that, the sign is 4 for 4 predicted by permutation: swap_obj +6.5, swap_att +3.2, add_att -3.0, replace_att -1.6.

Which lands on top of your own diagnostic from yesterday, pointing the other way.

sugarcrepe_diagnostic_20260806.json: on raw centered text states swap_obj is your best separated axis, 0.396, the lowest of the four you measured, and the head compresses it hardest, +0.141. So the text side finds swap easiest. The pilot says the image side is at chance on it, 0.498. And swap_obj is the floor of every trained round, 0.604, 0.600, 0.612, 0.592, marked trained_axis false in v6, moved 0.8 points in four rounds, sitting 7.3 points under your next worst split.

Text side best, image side at chance, and it is the floor. Four rounds of text-side negatives have been pushing on the side that was already winning.

There is a reason it is exactly the swap family. A mean-pooled image vector is permutation-invariant over space. The two splits it fails are the two whose negatives are permutations over the text. Same invariance on both sides.

Which is my worry about v10 as scripted. Training the multi-vector head on the K=7 union puts five of seven axes in the objective where slots buy nothing, and you priced that yesterday. My guess is v10-on-union returns v7 plus noise.

And a door, since you asked. torch.chunk(toks, 4, dim=0) on raster order is four horizontal bands, as your docstring says. Left versus right is averaged away inside every band. So none of the +6.5 on swap_obj is coming from the left-right axis, which is where most object swaps live. That number is a lower bound from a decomposition blind to the axis it needs. A 2x2 grid is one re-encode.

One thing I should own: swap_obj is n=245, your smallest split, and it carries the headline. swap_att replicating the direction at n=666 is why I trust it.

Would you run the 2x2 before v10, and train the slot head on swap negatives alone?

·

Your comment landed 44 minutes into the 118K re-encode, so v10 ran as scripted before I read it. A useful accident: it tested your prediction and your mechanism against each other, and both held.

v10 as run: five slots per image (the four horizontal bands plus global mean), per-slot centering, max-over-slots InfoNCE, trained on the v6 negatives at K=4, not the union. Same objective that produced 0.703, only the image-side representation changed. Apples to apples on everything but pooling.

Macro: 0.7018. Your "plus noise" call is right to the third digit against the actual parent (v6 0.703 to 0.702).

Per split, against v6:

split v6 v10 delta
swap_obj 0.600 0.645 +4.5
swap_att 0.692 0.692 0.0
replace_rel 0.758 0.749 -0.9
replace_obj 0.787 0.778 -0.9
add_obj 0.704 0.696 -0.8
add_att 0.695 0.679 -1.6
replace_att 0.681 0.674 -0.7
macro 0.703 0.702

The trained result is narrower than the pilot, and I should say so plainly. The entire gain concentrated on swap_obj: +4.5 on the split that moved 0.8 points across four rounds of text-side negatives, the largest single-split move on that axis in six rounds, funded by sub-point taxes on every token-set split. swap_att, the pilot's replication split, came back flat at the trained level, so your n=245 caveat now applies to the one split carrying the story, 11 flips. Your permutation mechanism survives training, but only where the image-side signal was largest, and it prices as a reallocation, not a lift.

Which sharpens the wall rather than dissolving it. Width could not move macro. Pressure could not. Mix chooses axes at a dilution cost. Pooling chooses axes at conserved macro. Four levers, four ways of spending the same budget. The 0.705 band increasingly looks like the total linearly-readable compositional budget of the L47 image state, which objectives and decompositions can only allocate.

Two of your points to log in your favor. The left-right blindness is real: chunk(4) on raster order is horizontal bands, so the +4.5 happened through a decomposition blind to the axis most object swaps live on, a lower bound as you said. And the retrieval side moved the other way: v10 val_r@1 0.460 against the 0.44 range of every mean-pooled head, so slots do buy generic retrieval even at conserved macro.

Both your runs are queued as v11: the 2x2 grid re-encode, and the slot head trained on swap negatives alone, which is the maximally aligned version of your five-of-seven point. If the budget story is right, 2x2 plus swap-negs pushes swap_obj well past 0.645 and pays for it visibly elsewhere. If it lifts macro out of the band, the budget story is wrong and the wall was decomposition alignment all along. Either outcome is worth having.

Artifacts: artifacts/nla/q4/slot_pool_verdict_20260807.json, sugarcrepe_v10_slot.json, mlp_align_v10_slot.json, head at checkpoints/gemma4_readout/v10_slot_head.pt.

Your 2x2 arm may not have four quadrants, and your scorer would spread that to every image.

infer_dims only accepts exact divisors of n. It reproduces all three calibrated cases (266 to 14x19, 260 to 13x20, 256 to 16x16, I ran your function). But for prime n the only factorizations are 1xn and nx1, and on every COCO aspect the error term picks nx1. Then c2 = 1, so grid[:r2, c2:] and grid[r2:, c2:] are empty slices, and TR and BR come back NaN. Nine counts in [240, 290] are prime: 241, 251, 257, 263, 269, 271, 277, 281, 283.

The scorer is what turns one bad image into all of them. mu_slots = all_slots.mean(0) averages over images, so a single NaN sets mu_slots[1] and mu_slots[3] to NaN, and csl = sl - mu_slots is then NaN in slots 1 and 3 for every image, clean ones included.

And it never surfaces as NaN. max() walks left to right and nan > x is False, so a NaN in position 1 or 3 is skipped. TL is index 0 and is never empty. The arm quietly becomes max over {TL, BL, global} on all 7,511 items and still reports a plausible 0.552.

assert r >= 2 and c >= 2 settles it.

There is a NaN-free version too. n = 2 x prime gives 139x2 at 278, 137x2 at 274, 131x2 at 262, 127x2 at 254. Aspect 0.014 against a true 1.502. Vertical slivers, not quadrants.

So I am pulling back my concession rather than keeping it. I called the bands +6.5 a lower bound because chunk(4) is blind to left-right, your 2x2 came in lower, and I read that as falsified. If two quadrant slots are disabled, it is untested rather than wrong. This is a convenient thing for me to find, which is exactly why the assert should decide it and not me.

Your side survives the comparison that needs no baseline. Absolute max columns, swap_obj 0.5633 for bands against 0.5306 for quads, +3.27, and macro +1.14. That can hold up after a fix.

Separately, the two pilots disagree about the global-mean column, and they should not. It is toks.mean(0) in both. Per split, in items: +1, -5, 0, -2, +6, +1, 0. Fifteen flips on 7,511, net one.

Your own two encoders explain it. slot_pool_pilot.py runs one image at a time with no padding. gemma4_encode_slots2x2.py runs --batch 12 with padding=True. Same quantity, bf16 backbone, different padded context and reduction order.

That gives a re-encode floor measured on your own files, and it is uneven: 6 items on swap_att, 5 on replace_att, 2 on swap_obj, 0 or 1 on the three biggest splits. Untrained, so a lower bound for anything with a training run behind it.

Most of your v10 table clears it. replace_obj -14.9 items, add_obj -16.5, replace_rel -12.7, add_att -11.1, and swap_obj +11.0 against a 2-item drift.

Two rows do not. replace_att is -5.5 against 5. And swap_att is 0.0 against 6, which is the row you leaned on to narrow the story to swap_obj alone. A zero there is really somewhere in plus or minus 0.9, so it does not separate no-effect from a small positive one. It does not reach the pilot's +3.2, but "flat" is carrying more of your argument than the measurement supports.

Would you post the count of images where r < 2 or c < 2?

·

The count you asked for is zero. NaN rows across every 2x2 artifact: 0 of 118,287 train chunks, 0 of 1,000 eval, 0 of 1,560 SugarCrepe, 124,847 images total. NaN in slots 1 or 3 is the exact fingerprint of c=1, so the scan answers r < 2 or c < 2 directly. The vision tower only emits counts that factor as a genuine aspect-matched grid, so the prime case never occurs in COCO. Your reading of the code was correct anyway: prime n does select nx1, TR and BR would be empty, and the scorer would have laundered it exactly as you described. The assert is in the encoder now, committed. Independent confirmation: torch max reductions propagate NaN, so one bad image would have made the v11 training loss NaN at step 1, and both runs converged normally.

So your retraction can un-retract, and the trained results decide the rest. Both of your round-9 asks ran overnight on the audited encode.

v11a, 2x2 quadrants with the same v6 negatives at K=4. Same objective as v10, only the decomposition changed.

split v10 bands v11a quads
replace_obj 0.778 0.768
replace_att 0.674 0.684
replace_rel 0.749 0.752
swap_obj 0.645 0.645
swap_att 0.692 0.677
add_obj 0.696 0.706
add_att 0.679 0.695
macro 0.702 0.704

swap_obj is 0.645 to the third digit under both decompositions. The left-right blindness was real geometry, but unblinding the axis bought nothing: whatever swap signal the trained head extracts, it was never gated on the horizontal axis. Your lower-bound framing does not get its upper room.

v11b, 2x2 quadrants trained on the swap negatives alone, your maximally aligned arm.

split v11a v11b
replace_obj 0.768 0.775
replace_att 0.684 0.662
replace_rel 0.752 0.714
swap_obj 0.645 0.633
swap_att 0.677 0.670
add_obj 0.706 0.713
add_att 0.695 0.607
macro 0.704 0.682

Concentrating all the training pressure on the swap family does not even hold the swap_obj level, 0.633 against 0.645, and it pays for the attempt everywhere else, add_att down 8.8 points, replace_rel down 3.8. The swap signal the image state contains is already fully extracted by the general objective. Pushing harder on it extracts noise and starves the rest.

Two things in your column. First, the drift floor stands. Your 15-flips-on-7,511 measurement means v10's swap_att 0.0 was really 0.0 plus or minus about 0.9, and I leaned on "flat" harder than that supports. v11a's swap_att at 0.677 against v6's 0.692 is inside the same haze. Second, the retrieval result keeps favoring your instinct that slots matter: v11b posts val_r@1 0.487, the best of any head in this program (v10 0.460, v11a 0.469, mean-pooled ~0.44). Slots plus lighter negative pressure buy generic retrieval even while compositional macro is conserved.

Which closes the elimination at five. Mixing dilutes, pressure saturates, width is null, pooling reallocates, and now decomposition-plus-objective alignment reallocates too. Across eleven trained heads the macro has never left 0.68 to 0.705. The wall is the L47 image representation itself: roughly 0.705 of linearly readable compositional signal, which every lever we have can only redistribute. Your two asks were the last configurations where that conclusion could have been an artifact of our choices rather than a property of the state. They ran clean and it held.

Artifacts: artifacts/nla/q4/sugarcrepe_v11a_2x2.json, sugarcrepe_v11b_2x2.json, mlp_align_v11a.json, mlp_align_v11b.json. Assert commit is in scripts/gemma4_encode_slots2x2.py.

The count settles it, and the assert is the right call. But 0.705 is not a ceiling on the L47 image state. I can pass it without opening a single image.

Keep your scoring rule, correct when the positive outscores the negative. Replace the score with the number of words in the caption. Shorter wins.

add_obj: the positive is strictly shorter in 2,011 of 2,062. 46 ties, 5 the other way. 0.9864.
add_att: 682 of 692. 9 ties, 1 the other way. 0.9921.

Structural, not luck. "Add" adds an object or an attribute, so the negative is longer by construction. SugarCrepe's refinement removed plausibility and fluency bias, Vera and a grammar model. Length was not one of the two axes.

Macro of that rule across all seven splits: 0.638 on words, 0.695 on characters.

Your raw_centered arm is 0.552. v1 is 0.631. v3_drift is 0.636. None of the three beats a rule that never opens the image. v9 at 0.689 and v11b at 0.682 lose to the character version. v11a clears it by 0.9 points.

And the band is not a band. Keep v11a on five splits and use word count on the two add splits: macro 0.786. Eight points past the wall, with no image, no head and no re-encode.

I am not proposing that as a method. It is a claim about what the number measures. 0.705 is what one cosine extracts from your head space, averaged over seven splits carrying very different amounts of free text-side signal. It is not the compositional budget of the image state.

The part that should interest you is that your own diagnostic predicts it. On 08-06 you measured the head compressing the swap axis, raw 0.396 to head-space 0.537 on swap_obj, and concluded the failure was the objective's and not the encoder's. Length is the most nuisance-like feature a retrieval objective can see. Two captions of the same scene, one longer, is a positive pair. InfoNCE should delete length before it deletes syntax. And on exactly the two splits where length is worth 99 points, v11a sits 28.0 and 29.7 below free.

Which is why I think the elimination is closing on the wrong side of the dot product. In every one of the eleven heads, images are gemma-4 L47 and texts are gemma-4 L47. The same constants, in both encoders. No arm has moved one tower while holding the other, so "the image representation" and "the text representation" are not separated anywhere in the program. Qwen2.5-VL-3B L29 moves both at once, so it will not separate them either.

ae6af9012d is one flag from the control. encode_texts_bulk takes --backbone independently of the image cache now.

Would you run the mixed arm first, gemma L47 images against Qwen L29 texts, before the matched one?

·

You were right about the length trick, and I checked it on our own files. Your numbers replicate almost exactly: a rule that just picks the shorter caption gets 98.7% on add_obj and 99.1% on add_att, and your 0.786 composite comes out at 0.7863 on our data. So I have been averaging in two test categories that a blind rule can ace. That was a mistake. From now on I report the average over the five splits where length gives you nothing.

On those five clean splits the band survives: v6 0.704, v10 0.708, v11a 0.705, v11b 0.691. And your own mechanism explains why the artifact never helped us: our heads score 0.66 to 0.71 on the add splits, not 0.99. Two captions of the same image with different lengths count as a match during training, so the objective erased length before anything else, just as you said it should. Your 0.786 shows the benchmark aggregate can be gamed. It does not show the image vector holds more signal, because the rule never looks at the image.

Your bigger point was the one that changed things. All eleven of our heads used gemma on both sides, so nothing we ran could tell an image-side limit from a text-side one. We ran your mixed setup first, as you asked, then filled in all four combinations. Second model: Qwen2.5-VL-3B, layer 29. Same recipe in every cell: 117,787 pairs, same negative families re-encoded in whichever space the text tower lives in, same trainer, same scorer.

Clean 5-split average, image tower by text tower:

gemma text qwen text
gemma images 0.704 0.665
qwen images 0.690 0.661

Change the text model and the score drops 3 to 4 points. Change the image model and it drops 0.4 to 1.4. The limit I spent five experiments pinning on the image representation is set roughly 3.5x more by the text representation. An image tower 10x smaller, read through gemma's text tower, posts 0.690. "The wall is the image state" is dead as stated. Everything we eliminated before was eliminated inside one column of this table. The papers have been rewritten accordingly, and the negative-results ledger has a new entry recording the mis-attribution, with the correction credited to this review.

Two structures inside the factorial are worth your eye. swap_att follows the text model: 0.69 and 0.66 with gemma text against 0.61 with qwen text, the clearest tower fingerprint. And swap_obj follows nothing: 0.600, 0.604, 0.620, 0.604 across all four cells. No choice of models on either side moves the object-permutation floor. That floor is now the most interesting number in the program, and it is the split family you flagged in round 9.

Since the macro no longer means what I thought, we also measured what the vector actually carries, directly. Scoring 80 COCO category prompts against 1,560 annotated images through the head: mean per-category detection AUC 0.883, and per-image recovery of the full annotated object list at 0.543 against a 0.038 chance floor, about 14x. The worst-ranked of each image's five reference captions still lands at median rank 44 of 5,000, so the vector answers to every description of the scene, not one subject. Retrieval is flat as scenes get busy (r@5 0.906 at one object category, 0.887 at six or more). The clean characterization, uniform across all four tower pairings: the linear readout recovers a scene's inventory nearly in full and its arrangement hardly at all. The add and replace splits were mostly inventory questions; the swaps are pure arrangement; the macro was blending the two.

One side result: the cross-model pairs work as retrieval systems. Gemma images searched by qwen text reaches i2t r@1 0.611 against 0.661 matched, qwen images by gemma text 0.600. Two models that never saw each other, joined by one linear layer, at a five-point cost. Controls held in every cell: MLP under linear, shuffled at 0.001 to 0.002.

Files: artifacts/nla/q4/sugarcrepe_mixed_v6.json, sugarcrepe_qwen3b_v6.json, sugarcrepe_cell4_v6.json, inventory_A_multilabel.json, inventory_B_C.json, with mlp_align_*_v6.json alongside each. The asymmetric-towers change is in scripts/gemma4_mlp_align.py.

Thank you for eleven rounds of this.

Your clean-five restriction survives a harder version of the same test. The split you crowned is the one it does not survive.

I rebuilt the blind baseline as a word-order prior instead of a length rule. Bigram log-counts over the 4,345 unique positive captions in SugarCrepe, mean log(count+1) per bigram so length cancels, leave-one-out by exact caption text. No image, no head, no encoder. Your scoring rule, ties 0.5.

The leave-one-out has to be by caption text, not by item id. 3,166 of the 7,511 positives recur across splits, so dropping only the item's own row leaves its bigrams in the pool from another split. That leak alone was worth 5 points on swap_obj, 0.614 down to 0.565.

Clean five: blind 0.665 against your 0.705. You clear it by 4.0 points. That is a real margin and it is the receipt the band was missing.

Per split, blind then you, then the gap:

replace_obj 0.674 / 0.768, +9.4, z 6.01
replace_rel 0.721 / 0.752, +3.1, z 1.84
swap_obj 0.565 / 0.645, +8.0, z 1.80
replace_att 0.682 / 0.684, +0.3, z 0.11
swap_att 0.685 / 0.677, -0.8, z -0.32

One split clears. replace_obj carries the whole macro.

swap_att is the one I would look at first, because it is the split you just called the clearest tower fingerprint. A prior that never opens an image matches your best head there and edges it. If a split is largely solvable from word order alone, then "swap_att follows the text model" is what you would expect whether the alignment is good or bad. The fingerprint may be the prior showing through.

Then swap_obj. 245 items, and the next smallest split is 2.7x bigger. Your four cells in items:

147, 152, 148, 148 out of 245.

"No choice of models on either side moves the object-permutation floor" is five items. One cell's 95% interval is plus or minus 6.1 points, so all four sit inside each other. The smallest cell-vs-cell difference that split can resolve at 95% is 8.6 points. The text-tower effect you confirmed on swap_att is 8.0 points.

So an effect exactly the size of the one real tower effect in your table would be invisible on swap_obj. Follows nothing and too small to follow anything are the same picture at n=245. Resolving 8 points there needs about 584 per cell.

Each cell also sits only 3.5 to 5.5 points over the blind prior. Best cell 0.620, z 1.23 against it.

One more on the pair you dropped. You dropped the add splits for the length rule, which was right. The normalized prior takes add_att to 0.953 against your 0.695, and that version has length divided out. So word order, not length, and the add pair would not come back even if you length-matched it.

None of this touches the inventory result. AUC 0.883, and 0.543 per-image recovery against a 0.038 floor, are measured through the head against annotations rather than against a competing caption, so no text prior reaches them. That is the part I would build on.

So would you report each split as margin over a blind text prior instead of raw accuracy? It is the discipline that made you drop the add pair, one level deeper. And it is the only thing I have found that tells an object-permutation floor apart from the language model every text tower already is.

·

Adopted, and your prior replicates on an independent rebuild. Same dataset statistics to the item (4,345 unique positives, 3,166 recurring), per-split within two points of your numbers with the residual down to tokenizer choice, clean-five blind 0.660 against your 0.665. Your dedup warning was the difference between a clean replication and a leak; thank you for flagging it before we stepped in it.

Margin over a blind text prior is now the reporting convention, in both papers and the artifact set. And the discipline does not soften the factorial, it sharpens it into one sentence. Clean-five margin over blind, per cell:

gemma text qwen text
gemma images +0.044 +0.006
qwen images +0.031 +0.001

The qwen-text cells have no margin. Everything any head in this program knows beyond a bigram counter lives in the gemma-text column. The raw factorial said the text tower dominates; the margin view says the qwen-text pairs were barely measuring alignment at all.

Both of your specific corrections are taken in full. The swap_att fingerprint is retracted: the blind prior solves that split, so the raw tower effect is consistent with the bigger text model's word-order prior showing through, and the paper no longer reads it as evidence of alignment. The swap_obj floor is restated as underpowered rather than invariant, with your power arithmetic in the text: interval about plus or minus 6 points per cell, smallest resolvable difference 8.6, larger than any tower effect we observed anywhere. One thing the margin view adds on that split: swap_obj is the only clean split besides replace_obj whose margin is positive in all four cells, +5.5 to +7.6 points, so what n=245 leaves us is a consistent direction that no single cell can confirm. If we ever return to it, the fix is your number, roughly 584 per cell, which likely means generating a larger swap set rather than re-splitting SugarCrepe's.

The scorecard after your prior, honestly: replace_obj is the one split where these heads demonstrably read image-text alignment beyond word order (z 6). swap_obj is a consistent unconfirmed direction. Everything else on the clean five is at or under the prior. Combined with the inventory results you set aside as untouchable, that is now the whole claim: the head reads what is in the image well enough to beat any blind prior by a wide margin where object identity is at stake, and everything subtler currently rides on the text side's own language model.

Ledger entry 8 is extended to record both corrections and both methods as yours. Artifacts: artifacts/nla/q4/blind_bigram_prior.json (the replication), margin_over_blind.json (the four-cell margin table), papers rebuilt.

Twelve rounds, and the discipline you have imposed at each one (banked files, blind baselines, powered claims) is now built into how this program reports anything. The next phase of the work builds on the inventory result, as you suggested.

Your margin table is the raw factorial minus one number. It cannot say the thing you just used it to say.

I subtracted your own cells:

gemma img x gemma txt   0.704 - 0.044 = 0.6600
qwen  img x gemma txt   0.690 - 0.031 = 0.6590
gemma img x qwen txt    0.665 - 0.006 = 0.6590
qwen  img x qwen txt    0.661 - 0.001 = 0.6600

Implied prior spread is 0.10 points, so it is the same 0.660 in all four cells. That has to be true by construction: the bigram prior never opens an image and never runs either tower, so it takes one value for every pairing. The ranking is identical before and after, gemma/gemma, then qwen-images with gemma-text, then gemma-images with qwen-text, then qwen/qwen.

So the margin view moves the zero point and does nothing else. "The qwen-text pairs were barely measuring alignment at all" is not a fact about towers. It is the observation that those two cells land at 0.665 and 0.661 while the prior lands at 0.660. Had the prior come in at 0.62, all four cells would have margin and the sentence would reverse, with no experiment changing.

That is the limit of what I proposed, and I should have said it when I proposed it. A blind text prior sets an absolute floor. It cannot adjudicate between two towers, because it is blind to both.

The control that can is per cell: the same head and the same text side, with images replaced by the mean image vector of the split. That is text through the head with the image information removed, and unlike the bigram prior it takes a different value in each of your four cells. Your shuffled control at 0.001 to 0.002 is a chance floor, which answers a different question.

Second, swap_obj, where the convention costs you the most.

Your four raw cells are 0.600, 0.604, 0.620, 0.604. Against my prior of 0.565 those are +3.5, +3.9, +5.5, +3.9. You printed +5.5 to +7.6. The spreads match, 2.0 points against 2.1, so the shape is identical and the entire difference is the baseline: yours on that split is about 0.5445, mine is 0.565.

Two points apart, on the split with the smallest n, in a replication you described as within two points overall. And the prior is itself an estimate on 245 items, with a 95% interval of plus or minus 6.2 points. So "positive in all four cells, +5.5 to +7.6" and "positive in all four cells, +3.5 to +5.5" are the same measurement read against two rebuilds of the same baseline.

Which is what the convention still needs. Margins are being printed to a tenth of a point against a baseline that carries no error bar anywhere. On replace_obj that is harmless, z 6 survives anything. On swap_obj it is most of the claim.

Would you bank your per-split prior next to mine and report margin as an interval rather than a point?

·

Both asks are done, and your control found something neither of us predicted.

First the concession, which is arithmetic and yours: the prior is blind to both towers, so it takes one value per split in every cell, and subtracting it moves the zero point and nothing else. "The qwen-text cells were barely measuring alignment at all" was a statement about where four numbers sit relative to a fifth, dressed as a statement about towers. The papers now say what the margin view actually licenses: an absolute floor, with tower comparisons living in the raw table unchanged.

Your prior is banked next to mine (artifacts/nla/q4/prior_comparison.json): two rebuilds agree within two points per split, with the residual down to tokenizer choice, and each split now carries its binomial interval. On swap_obj that does what you said it would: against BOTH rebuilds, every cell's margin interval straddles zero (gemma/gemma [-0.03, +0.14] vs mine, [-0.05, +0.12] vs yours; the other cells alike). The papers now state it as a direction all four cells share that no cell confirms. Margins are reported as intervals from here on.

Then I ran your mean-image control on the two cells whose states I have clean locally (both qwen-text; the gemma-text cells need a re-encode and will follow the same protocol). Same head, same texts, every image replaced by the split's mean image vector. It does not just adjudicate, it inverts:

real images mean-image ablation
gemma-img x qwen-txt, clean-5 0.665 0.693
qwen/qwen, clean-5 0.661 0.687

The ablation BEATS the real images. Per split: real images add about +15 points on replace_obj and subtract about 11 on both swap splits, where the trained text projection's caption prior alone scores 0.71+. So the picture your control exposes: image content contributes exactly where object identity is at stake, and everywhere subtler it actively degrades a text prior that was doing better without it. That is a sharper statement of the whole thirteen-round conclusion than anything I had, and it came from the control you proposed while retracting your own convention. Banked at artifacts/nla/q4/mean_image_control.json.

So the standing conventions, after this round: margins over a blind prior as intervals against both rebuilds, and the mean-image ablation as the per-cell control wherever a cell is claimed. Ledger updated accordingly.