Settled, and it cost me a headline rather than a metric. Everything below is measured, and the code is in the repo.
Your dtype test, run.
float32 first : 0.08333331346511841
float64 first : 0.0833333358168602
Your five line numbers are exact and your reading of the promotion is exact.
One correction: there are two paths, not three. You have trajectoire_couplage.py:84 as a third because it omits dtype, so the advantage stays float64 and promotes the loss. That is true in numpy and false in torch, where torch.get_default_dtype() is float32, so torch.tensor on a Python float gives a float32 tensor:
torch.tensor(r - b) -> torch.float32, 0.0833333358168602
torch.tensor(r - b, dtype=torch.float32) -> torch.float32, 0.0833333358168602
same dtype and same value: True
The loss stays float32 in both. It does not change your conclusion, since the float32 path is still alone, but that script sits with the other three rather than on its own.
Divergence on the real reward stream, recorded rather than simulated:
seed first disagreement % of first 2000 steps % after max relative gap
0 5 57.7 24.2 5.45e-06
1 5 78.3 29.6 7.96e-06
2 4 79.0 26.1 5.45e-06
Your 5.4e-06 comes out identical. Our percentages differ because your second window is "after the reward saturates" and mine is "after step 2000"; the definitions do not coincide, so I am not claiming you are wrong.
Your diagnosis of the anomaly is right, and it is fully explained. Same seed, same loop, only the advantage line differs:
float32 path 18.62 modes <- the sweep's number
float64 then rounded 11.50 modes <- the trajectory's number
float64 no dtype 11.50 modes <- identical to the previous one
And your point about determinism is the one that should have tipped me off months earlier: two deterministic roundings explain "both are reproducible", multithread nondeterminism never did.
Your second question: yes, and your arithmetic was right to the second decimal. Peak restricted to steps above 90 percent validity, since the raw maximum is 47.5 at step 0 for every run, the untrained network, which has nothing to do with early stopping:
path seed peak at step final gap
float32 0 24.00 5750 18.62 +5.38
float64 0 24.00 5750 11.50 +12.50
You predicted +5.4 from arithmetic alone. Two things you did not ask for. The peak is identical on both paths, same value at the same step, so only the endpoint moves. And the headline does not survive at all: I traced 20 seeds on the canonical path.
mean gap +1.03 modes
median gap +0.00
runs above 1 mode 3 / 20
max gap +8.13
Seventeen runs of twenty gain nothing from stopping early. Your correction was too gentle: it is not +5.4 instead of +12.5, it is zero, and the +12.5 was one seed.
I then wrote up something conditional that I liked โ on float32 all three runs that gain sit in the plural corner, 3 of 8 against 0 of 12 singular, which fits the ceiling story since that corner has twice the height to reach and lose. Then I ran the same twenty seeds on the canonical path and it fell over.
float32 float64 (canonical)
median gap +0.00 +0.03
runs above one mode 3/20 5/20
of which plural 3/8 2/8
of which singular 0/12 3/12
Three of the five are singular on the path I had just declared canonical. The 0-of-12 that carried the whole interpretation was a one-path artifact. What survives on both, and only this: the median is zero and most runs gain nothing. Any reading finer than that did not outlive a change of rounding.
That is the fourth over-reading of my day, and this one is worth separating from the other three. It was not one seed โ it was twenty seeds on one numeric path. The missing control was not more seeds, it was the same measurement on the other path.
Your bug is a better experiment than any of my seeds. Changing the seed changes the initialisation and the trajectory at once, a confound I have carried since the first sweep. Changing the advantage line changes only the second: the initial weights are bit-identical, and the two runs diverge at step 4 or 5, during training. Same starting point, different trajectory. So I ran 70 seeds on each path.
float32 float64
singular / plural 37 / 33 37 / 33
Wilson 95% [0.413,0.641] [0.413,0.641]
p against 1/2 0.7202 0.7202
ceiling violations 0 0
I(det;noun) max 0.0377 0.0158
runs with I > 0.05 0/70 0/70
All 70 of 70 keep the same corner. Zero flips, although the trajectories disagree on 58 to 79 percent of the first two thousand steps. But only 21 of 70 keep the same effective-mode count, correlation 0.68, mean absolute difference 2.87 modes and up to 12.7.
So: the initialisation decides the corner, the trajectory decides how much of it gets filled. Two levels, two causes, separated by a manipulation that touches only one of them, which no seed can do. And the three conclusions the repository rests on are robust to the last bit โ they were not rounding artifacts. Whether that control is standard practice somewhere I have not read, I genuinely do not know, and I am not going to claim novelty from my own ignorance of the literature.
The ceiling is only a weak attractor, incidentally: of the 25 runs sitting exactly on it, 11 reproduce across paths (44 percent) against 22 percent for the others.
Canonical: float64. I expected a trade-off between accuracy and speed and there is none:
float32 path : 19.46 us per call
float64 path : 4.57 us per call (77 percent faster)
The name misleads. Nothing is stored in double, the produced tensor is float32 either way. A Python float is a double, so r - baseline is native and free and only one tensor creation remains, while the other path builds a tensor, dispatches a torch kernel for the tensor-scalar subtraction, and detaches. More operations and one more rounding. In total it is 0.2 percent of a step, so this is not a performance argument, it is that performance does not oppose accuracy here. float32 was never a choice; it was a writing accident in one function, already in the minority six files to five.
entrainer now takes an explicit chemin_avantage, defaulting to float64. The five scripts that went through it have been rerun, 37.8 minutes with bounded parallelism; the six with no sampled training at all are untouched, and those are the ones carrying the product ceiling, the Gibbs optimum, the order-1 marginals and the capacity probe, none of which has an advantage line to change.
What the switch actually moved, archived against rerun, same seed 0 as before:
beta valid% f32 valid% f64 modes f32 modes f64
0.01 99.84 99.99 9.9 18.0
0.02 99.99 99.94 18.6 11.5
0.05 92.65 99.76 23.8 19.9
0.08 94.87 84.11 24.4 26.5
0.12 57.13 55.31 45.9 45.4
0.35 5.27 5.27 43.5 43.5
long grammar, graded reward : 6.4 % -> 15.8 %
The long-grammar number is quoted in the published article and it is now 2.5 times larger. None of this is a surprise, and that is the point: this is a single-seed table, and I had already written in section 4.2 that a single-seed sweep cannot draw a frontier, then used it anyway. The 70-seed conclusions and everything obtained by enumeration are unmoved.
So the practical consequence for the next release is not to refresh those tables with new numbers. It is to replace single-seed tables with multi-seed means and a spread, otherwise the same fragility gets republished with different decimals.
Housekeeping, done. torch.set_num_threads is now in rl_grammaire.py, which fourteen scripts import, with RDTRL_THREADS to override for the large-batch exact-gradient work. It no longer depends on the shell.
And the answer I owed you. Sweep seed 0 emits les 0.544 and des 0.455. The trajectory run emits des 0.912 and les 0.087. Both plural, so it was never a corner disagreement, and "below 12 therefore singular" does not hold: an unbalanced plural product drops under 12 without leaving its corner. You reached that yourself before I sent it, and your reason is the right one, effective modes is 2^H of the joint restricted to the valid set rather than a count of support. You corrected it faster than I delivered it.
Four of my published numbers came apart today: the 2-to-1 branch bias, the all-plural sublanguage, the early-stopping gap, and the corner-conditional version of it I wrote to replace the gap. The first three were one seed. The fourth was twenty seeds and one numeric path, which is the harder lesson, because it looked like the fix rather than the same mistake in better clothes.
All four survived for the same reason, worth saying plainly: I had no reason to rerun a result that did not bother me. The rule is not "replicate", which I already knew. It is replicate what is convenient first, and check the control you just built against the claim you just made with it.
Last thing, and it is yours. I finally ran the reversal test, the one experiment that decides whether the product bound is a law or an accident of my French lexicon.
My first version of it was empty and I nearly ran it. I built a lexicon moving genre-neutrality from the plural determiners to the singular ones: same twenty tokens, same 8,000 space, same 48 valid sentences, same two corners of 24, and it swapped the ceilings and both order-1 marginals cleanly. Too cleanly. The nouns and verbs are already symmetric in number, two per genre and number and three verbs each, so swapping the determiners' number is the sg-pl relabelling and nothing else. Verified on the feature multisets. Seventy seeds would have returned the mirror image by construction and proved only that my code does not branch on the strings "sg" and "pl".
The principle I was missing: a relabelling can permute, but it cannot change a ratio. A perfectly symmetric control is often a perfectly empty one.
So the real test uses three genders. Both corners hold 36 valid sentences, but the singular determiners are genre-neutral so that corner is one product of 2 x 6 x 3 = 36, while the plural ones are marked across three genders so the genre must be fixed: 2 x 2 x 3 = 12. Ratio 3 instead of 2, which no relabelling of a two-gender grammar can produce, since the largest product is an isomorphism invariant.
Ceilings computed by enumeration and committed before the run. Seventy seeds, beta = 0.02, float64:
corner n predicted ceiling max observed violations exactly on it
singular 33 36 36.0 0 2
plural 37 12 12.0 0 7
And the quantitative form is stronger than the violation count:
grammar ceiling ratio observed mean ratio
standard, 2 genders 2.0 1.82
three genders 3.0 3.01
The mean tracks the ratio, not just the ordering. The branch is still a coin at 33/37, p = 0.72, despite both order-1 marginals having flipped, and I(det ; noun) has median 0.0000 with zero of seventy above 0.05 bits.
One sub-prediction of mine was too strong: "effective modes land on integer products" measures 66 percent in the standard grammar and 67 percent here. A stable fraction, so a real fact, but not a rule. The ceiling is a law; the quantisation is not.
Your bound survives a change of grammar, in value and not only in order. That was the one item my own publishability assessment listed as blocking, and it is cleared.
Notebook entries 7.11quinquies through 7.13, and section 1 now dates eight refuted hypotheses.