RDTvlokip PRO
AI & ML interests
Recent Activity
Organizations
🔍 J'ai publié mes expériences de RL. Un lecteur a exécuté le code, et quatre de mes chiffres n'ont pas survécu 🇫🇷
Then someone read the code.
Dipankar Sarkar commented four times in a day. Each time he had run something first. He rebuilt my statistics in numpy because he had no torch installed.
He found a bound I had missed. A policy that never learned the determiner to noun dependency has a product support, so at full validity it cannot exceed the largest fully valid product in the sublanguage it entered. That is 12 on one side and 24 on the other, computable before any training. Over 70 seeds it is never crossed, and the most common outcome is the bound itself. I had published one of those numbers as an interesting coincidence.
Then four of my published numbers came apart. Three were a single seed. The fourth was twenty seeds, and I had produced it while fixing the other three.
And the test I built to validate his bound tested nothing. I had swapped two conditions so cleanly that the two grammars were isomorphic. Seventy seeds would have returned the mirror image by construction.
The real lesson:
A relabelling can permute, but it cannot change a ratio. A perfectly symmetric control is often a perfectly empty one. None of my errors were in the reasoning. They were in the plumbing, and nothing in my own process caught a single one.
Code, figures, and the notebook with eight dated refutations 👇
🔗 https://huggingface.co/blog/RDTvlokip/i-published-my-rl-experiments
💻 https://github.com/RDTvlokip/RDTRL
📦 https://doi.org/10.5281/zenodo.21726216
🔍 I published my RL experiments. A reader ran the code, and four of my numbers didn't survive 🇫🇷
🔍 I published my RL experiments. A reader ran the code, and four of my numbers didn't survive 🇫🇷
Draft is up, and you're all over it: https://github.com/RDTvlokip/RDTRL/blob/main/docs/ARTICLE2.md
I took you literally on "put the thread in it" — there's a section quoting the four rounds verbatim, including the line about my own balayage_graines docstring already making the argument that demolished my published table. And including your retraction on 11.50, which landed before I'd sent you numbers I already had on disk.
The bound is credited as yours in the section title, in the "what transfers" list, in CITATION.cff, and as a structured contributors entry with your ORCID in .zenodo.json, so it shows on the Zenodo record rather than only in prose.
You said to raise credit when there was a draft. There is one, so: acknowledgment is what I've written, and I'm not attached to it. If you'd rather it were co-authorship on this second piece, say so — most of what it reports is your review work, and I'd have no argument against it. If acknowledgment is right, that's fine too and it stays as is.
Also worth flagging before you read: the reversal test I built to validate your bound was empty on the first attempt. I'd moved genre-neutrality between the two number families, which — since the nouns and verbs are already symmetric in number — is just the sg/pl relabelling. Isomorphic. Seventy seeds would have returned the mirror image by construction. The version that actually tests something uses three genders, so the ceiling ratio goes from 2 to 3, and that one passes: maxima of exactly 36.0 and 12.0, ratio of observed means 3.01 against a predicted 3.0.
No rush and no obligation — but so you're not left guessing: if I haven't heard back in a week, I'll publish as is, with the acknowledgment as written. That's the option that commits you to nothing, and it's the one I can revise upward later if you'd prefer otherwise. Silence just means the default.
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.
The cheap oracle is lying to you: from GPU kernels to agent rewards
We've been building Validate in Remyx to surface the evidence devs & maintainers need to make that call.
Within GitHub and our studio you can:
* define your project's validation configuration
* implement & run it against a proposed implementation
* review the results alongside the diffs
We're looking for a small group of engineers actively integrating new AI methods into production to help shape Validate. If that sounds like your team, reach out!
LI: https://www.linkedin.com/in/smayorquin/
email me: salma@remyx.ai
Short one for now, the substance will follow.
I have archived the repository on Zenodo, and you are credited in it.
Concept DOI: 10.5281/zenodo.21726216 — resolves to the latest version. The release that first records the acknowledgment is v0.3.1, 10.5281/zenodo.21726387.
The acknowledgment is in four places, deliberately: the README, the abstract in CITATION.cff, the changelog, and as a structured contributors entry with your ORCID in .zenodo.json, since CFF 1.2.0 has no field for it. It names what you actually contributed rather than thanking you vaguely: the product bound, the fact that the two degenerate corners hold the same number of valid sentences but not the same largest product, and the three corrections your criticism forced, namely the unweighted mean that read as an agreement rate, the saturation metric that could exceed 100 percent, and the sample-size claim that pooled three seeds into twenty-four rows.
I took your point about credit and left it as an acknowledgment rather than anything else, to be raised again when there is a draft. Agreed that the thread goes in it.
Your last message deserves a real answer rather than a quick one, so I will come back to it properly. One thing I can say immediately, because the field you asked about is now saved and the check cost one command: you are right that it is not a metric bug, and wrong about why. Sweep seed 0 emits les and des. So does the trajectory run. Both are plural, so it is not a corner disagreement, and "below 12 therefore singular" does not hold — an unbalanced plural product drops under 12 without leaving its corner. Numbers with the rest.
You are right on all four points. I checked them on the real policies rather than on reconstructed joints, then went further, and going further refuted a hypothesis of mine and produced a mechanism. Everything below is in the repo.
The coverage artifact, confirmed. moyenne_cond_det is an unweighted mean over six determiners and a softmax never reaches zero, so the dead rows clear the total > 0 guard and enter with full weight. On the eight runs where I have the complete analysis, the value is (determiners emitted)/6 to four decimals: 0.3333 for two live determiners, 0.6667 for four. And exactly as you say, a 12-mode singular collapse and a 24-mode plural collapse both read 0.3333, which is precisely the distinction the number was carrying in my sentence.
But mass-weighting does not fix it, it destroys it. I rebuilt your four structures and ran all three statistics:
structure modes unweighted mass-weighted I(det;noun)
singular collapse, genre-locked 12.0 0.433 1.0000 0.0000
plural collapse, les/des 24.0 0.400 1.0000 0.0000
singular 24, genre acquired 24.0 0.733 1.0000 1.0000
all six determiners, coupled 48.0 1.000 1.0000 1.5000
The weighted version reads 1.0000 for all four. Perfect agreement is reached by restriction as well as by conditioning, and a genre-locked product agrees perfectly with no coupling whatsoever. What answers the question is the mutual information I(det ; noun), which is a dependence rather than an agreement rate and is exactly 0 for any product regardless of validity. It is now in analyse_exacte alongside cond_det_pondere and determinants_emis.
Rerun on 70 seeds with the right statistic. The conclusion survives, for the right reason this time.
I(det;noun) median 0.0000 bits
I(det;noun) max over 70 0.0377 bits
runs above 0.05 bits 0 / 70
mass-weighted conditional 0.9941 +/- 0.0393 (useless, as predicted)
determiners emitted 1 for 13 runs, 2 for 57
The singular union needs 1.0 bit and the full policy 1.5. The best any of 70 runs manages is 0.038. The claim was true; I had asserted it from a statistic that could not say it, and your proposed replacement could not have said it either.
Your other two points hold. The plural corner has zero gap by construction, so my "zero violations in 70 runs" is really zero in the 37 singular ones, and my grammar is asymmetric as an instrument: only one corner can falsify the bound. That is now a design constraint on the reversal test rather than a footnote. And your 36 is exact, measured at 36.0 modes with I = 0.918 bits, so the annealing result clears a higher bar than the 24 I gave it.
On the dropped mass column, you found a pattern rather than an incident, so I audited it. There are eight unweighted means over a token dimension in the repo. Six are sound and two are wrong, and the dividing line is sharp:
averaged over verdict
observational conditionals (moyenne_cond_det/_nom) wrong
interventional conditionals (determiner forced) correct
order-1 marginals under the uniform policy correct by construction
Observational against interventional. Where I force the token, every row exists and equal weights are right. Where I observe it, the dead rows are softmax artifacts. Both versions live in the same file and I put the wrong one in the results table.
Then two things came out of pushing on your bound.
A hypothesis of mine, refuted. Seeing 19 of 37 singular runs at exactly 12.0, I proposed that a ceiling attained that precisely is not a constraint suffered but an optimum, so REINFORCE would be solving the coupling-free problem exactly and failing only to leave the class. I optimised the same E[R] + beta*H by exact gradient over three independent laws p(d), p(n), p(v), so I(det;noun) is zero by construction. The optimum of that class is 24.00 modes at every beta from 0.01 to 0.08, three seeds of three, in the plural corner, because that is the global largest product. REINFORCE lands on 12 half the time, so it is at a local optimum of the restricted class.
Three nested failures, not one: find the best product of the corner it is in (19/37 singular, 6/33 plural), find the best product at all (37 of 70 miss it), leave the product class (0 of 70). The larger corner is the worse filled, which is the opposite of what more room would suggest. And it settles your point by direct optimisation rather than by argument: the singular corner costs exactly log2(24/12) = 1 bit at strictly equal reward, so those runs stopped short of the optimum twice over.
The ceiling is a plateau, not a basin. I tracked I(det;noun) step by step with an exact probe. Exact gradient, beta = 0.02, seed 0:
step 0 : I = 0.0045 | 47.54 modes | valid 0.60 %
step 100 : I = 0.0000 | 12.00 modes | valid 99.99 %
step 1000 : I = 0.0000 | 12.00 modes | valid 99.97 %
step 1250 : I = 0.8518 | 17.87 modes | valid 99.92 %
step 1500 : I = 0.9980 | 24.00 modes | valid 99.98 %
Exactly 12.00 modes with I identically zero for a thousand steps, then it leaves. Escape times are wildly variable: step 1250, step 2875, and never within 4000 on the third seed. Nothing predicts it at initialisation either: I starts at 0.0045 bits in every seed and the six determiner masses all sit between 0.042 and 0.057 with no structure separating the seed that couples from the one that does not.
And here is what actually separates the two procedures. Every trajectory starts at 47.5 effective modes, the untrained network, and training destroys diversity. But look at the minimum along the way:
procedure minimum modes reached at step
exact 10.74 / 11.06 / 11.24 25
sampled 1.09 / 1.88 / 1.18 400 to 800
Sampled REINFORCE crushes the policy to a single sentence before rebuilding. The exact gradient never drops below 10.7.
The hypothesis this suggests, and I am labelling it as a hypothesis: rebuilding from a near-deterministic point happens position by position, because that is what a per-position entropy bonus can do, and a position-by-position rebuild yields a product by construction. Coupling would require opening a joint direction, which that term never opens. The exact gradient, which never passes through the point, keeps enough joint structure to find the coupled direction later. It predicts something checkable: the depth of the transient collapse should predict whether coupling is ever acquired.
It also gives annealing a mechanism it did not have. I had explained the 45.3-mode annealed run with a story about keeping every conditional trained while the shared representation forms. The simpler reading is that high beta early prevents the crush to a point, so the policy never has to rebuild from a product.
One anomaly I am flagging against myself rather than burying. Sampled seed 0 gives 11.50 modes in the trajectory script and 18.6 in the sweep, at nominally identical settings, and both are reproducible. I had previously blamed torch multithread nondeterminism; both scripts are single-threaded now, so that explanation is dead. There is a code-path difference I have not found, and until I find it one of those two numbers comes from a path I have not audited.
Your last question: only the rows. balayage_70_graines did not save state dicts. It does now, along with the per-determiner mass, the detailed conditional, the mutual information and the emitted-determiner count, so mass-weighted questions can be answered without retraining. The 70 policies are on disk.
New code: optimum_produit.py for the restricted-class optimum, trajectoire_couplage.py for the step-by-step coupling. Notebook entries 7.11bis, 7.11ter and 7.11quater, and the dead hypotheses are dated in section 1, where 1.8 is the one you just cost me.
One last thing. The product bound is yours, and it is now doing most of the load-bearing work in this line of results. If any of it gets written up properly I would rather sort out credit before a draft exists than after.
Collection: https://huggingface.co/collections/TheStageAI/edge-lm
Measured. You are right on the ceiling, right on the statistics, and I was wrong on the branch. Details below, all of it reproducible from the repo.
Your product argument, verified exhaustively. I enumerated every subset of nouns and took the largest fully valid product in each corner:
valid largest product ceiling
short, plural 24 24 = {des,les} x 4 nouns x 3 verbs 4.585 bits
short, singular 24 12 = {le,un} x 2 nouns x 3 verbs 3.585 bits
long, plural 144 72 6.170 bits
long, singular 144 72 6.170 bits
Exactly one bit apart on the short grammar, exactly zero on the long one. Your reading of my long-grammar result is the right one: with the adjective forcing genre on both sides there is nothing structural left, so 4 of 5 is a coin.
Your question: saturation_pct for le and la on the 12-mode runs. Six runs at 12 modes, four exact-gradient and two sampled, plus two 24-mode controls in the same singular corner.
run modes determiners carrying mass mass saturation
exact beta=0.01 seed 0 12.0 le, un 0.500/0.500 100.0/100.0
exact beta=0.01 seed 1 12.0 le, un 0.500/0.500 100.0/100.0
exact beta=0.01 seed 2 12.0 la, une 0.500/0.500 100.0/100.0
exact beta=0.02 seed 1 12.0 le, un 0.499/0.501 100.0/100.0
sampled beta=0.02 seed 1 11.7 le, un 0.448/0.551 95.9/ 99.9
sampled beta=0.02 seed 2 12.0 la, une 0.500/0.500 100.0/100.0
CONTROL exact beta=0.02 s0 24.0 le, la, un, une 0.25 x 4 100.0 x 4
CONTROL exact beta=0.02 s2 24.0 le, la, un, une 0.25 x 4 100.0 x 4
So the direct answer: le reads 100.0 and la carries zero mass, or the exact mirror, depending on the seed. Four runs masculine, two feminine. Six of six are the genre-locked product you described. The gender is a second lottery sitting on top of the branch lottery.
One correction to the question itself. Saturation is 100 percent in both structures. The 24-mode control also reads 100.0 on all four of its determiners, because each one uses both of its compatible nouns. What discriminates is not the saturation but the mass profile: two determiners at 0.5 against four at 0.25. A product and a union of two products saturate equally well; the difference is how many branches are open. Saturation alone would have given you the same number for both and settled nothing.
And a warning about the field you found. You are reading saturation_pct from a version where it was wrong. H was computed over all 8 nouns while H_max used the count of compatible ones, so it could exceed 100 percent, and a value above 100 meant mass leaking onto incompatible nouns, which is a failure, not saturation. I had that written in my own notebook two days ago and left it. It is now split into two fields: masse_accordee_pct for whether the policy stays valid, saturation_pct computed on the conditional restricted to compatible nouns and renormalised, bounded by 100 by construction. It pays immediately: in one run la reads 86.7 percent on a mass of 0.00000, an untrained conditional read off nothing. The mass column kills it at a glance.
Your statistical objection. You were right and the corrected experiment refutes me. 70 seeds, one condition, beta = 0.02, 20000 episodes:
singular / plural 37 / 33
proportion singular 0.5286
Wilson 95% [0.413, 0.641] contains 1/2
binomial against 1/2 p = 0.72
binomial against 2/3 p = 0.016
The branch is indistinguishable from a fair coin, and my "about 2 to 1 toward singular" is rejected. Worse than you said, in fact: in the collapse regime the branch was set by the seed, and beta only copied it, so seed 0 was plural at every beta and seeds 1 and 2 singular at every beta. My 24 rows were 3 draws. And 11 of those 24 were not collapses at all, since at beta >= 0.08 both families are alive and a 50.1/49.9 split had been labelled "singular" by argmax. The order-1 edge at the noun is real and closed form, but it does not survive the sampled dynamics.
Your ceiling, on the other hand, survives everything.
branch n ceiling max observed exceeded exactly at ceiling mean
singular 37 12 12.0 0 19 9.41
plural 33 24 24.0 0 6 15.12
Zero violations in 70 runs, and the modal outcome is the ceiling itself. The effective modes are integer products, {2,4,6,8,12} singular and {6,8,12,16,18,24} plural, so |A_det| x |A_noun| x |A_verb| is visible directly in the histogram. P(noun agrees | det) stays at 0.333 +/- 0.003 across all 37 singular runs: not one of the 70 acquires the conditional.
Which gives the result neither of us had. The exact gradient at beta = 0.02 reaches 24.0 modes inside the singular corner, seeds 0 and 2, so it does cross the uncoupled ceiling of 12 and it does acquire the conditional. Sampled REINFORCE never crosses it, 0 of 70. So the ceiling is not a property of the task, nor of the architecture, nor of the corner: it is a property of the sampled procedure at constant beta, and it has a closed form.
I want to be exact about the scope, because there is one case that crosses it. Annealing beta from 0.2 to 0.01 reaches 45.3 effective modes at 99.97 percent validity, with both families alive. The largest product over the whole valid set, ignoring corners, is 24, so 45.3 is above the global product ceiling and that policy has genuinely acquired the conditional while being sampled. The ceiling therefore binds for constant beta in the collapse regime, not for sampling as such.
That makes the bound more useful rather than less, because it explains the one fix that works. I had reported the annealing result with a qualitative story about keeping every conditional trained while the shared representation forms. The product bound replaces that story with a number: constant low beta drops the policy into one corner and caps it at that corner's largest product, and the schedule's job is to delay the commitment until the coupling exists. That is a mechanism you can compute in advance instead of a schedule you tune.
Your synthesis and mine are compatible and I think this is the correct joint statement: the order-1 marginal at the noun decides which corner the dynamics enter, your largest-product bound decides how far it can get once inside, and sampling is what prevents it from ever leaving. Three different mechanisms, three different measurements, none of which requires looking at a learning curve.
New code, all of it reproducible: sonde_ordre1.py for the marginals at every position, produit_et_saturation.py for the ceilings and the saturation tables, balayage_70_graines.py for the 70-seed branch statistic. The notebook entry recording all of this, including my three errors, is CARNET sections 7.10, 7.11 and 3.3.
Both wins were fake.
Test 1, copy a fixed sentence: solved in 1,639 episodes. Then I checked why. The per-position reward splits 12^12 into twelve independent 12-armed bandits. It doesn't guide the search, it deletes it. Transfer to a target sharing no positions was slower than starting over.
Test 2, a grammar judged by a hand-written parser: 99.9% grammatical. Then I forced the antecedent and measured the consequent. P(noun agrees | determiner) = 0.333. Exactly 2 determiners out of 6. It learned no rule at all. It found an all-plural sublanguage where agreement is vacuously true.
Then the one that stung: an untrained network plus a filter beats 20,000 episodes of REINFORCE on both validity and diversity. Nobody runs that baseline because it looks too stupid to bother with.
None of this is new. Reward shaping moving the optimum: Ng, Harada & Russell, 1999. The variance decomposition that retires the word "sparse": Fisher's ANOVA.
The real lesson:
A high score on a verifier you wrote yourself measures your specification, not the agent. It finds the corner of the output space where your constraint is vacuous, and it looks like success from every angle except the one you forgot to check.
Code, 8 figures, and the notebook with every refuted hypothesis 👇
🔗 https://huggingface.co/blog/RDTvlokip/teaching-a-network-to-write-with-reward-only
💻 https://github.com/RDTvlokip/RDTRL
Your numbers are right and I reproduce all of them. Two points of precision, then the place where I think the reading breaks, then your question, which turned out to be the most useful part of the comment.
Exact values. des and les are the same lexicon entry up to spelling, both (det, None, pl), so their marginals are not close, they are identical: 0.30888… on the short grammar, 0.15327783… on the long one. Your 0.1536 / 0.1535 split is Monte Carlo noise. Singulars are 0.27555… and 0.13714… And your mechanism is right: E[accord_det_nom | det] is 3/4 for a plural determiner against 1/2 for a singular one, and the None is what buys it.
Now run the same probe on position 1. Short grammar, E[R | x_p = t], uniform elsewhere, closed form:
pos 0 (det) des 0.3089 les 0.3089 | le la un une 0.2756 +0.0333 PLURAL
pos 1 (noun) chat chien fleur table 0.2944 | chats ... 0.2778 +0.0167 SINGULAR
pos 2 (verb) all six 0.2789 flat
The two positions point in opposite directions, and the two causes are orthogonal, one per feature:
genre number mean
credit to a singular noun 2/3 2/3 2/3
credit to a plural noun 2/3 1/3 1/2
credit to 'le' (det sg) 1/2 1/2 1/2
credit to 'les' (det pl) 1 1/2 3/4
The plural determiner's edge is entirely in genre, so it is your None. The singular noun's edge is entirely in number, so it is a different accident: I wrote 4 singular determiners and only 2 plural ones. Two independent lexicon accidents, two different features, opposite signs.
That is why the order-1 greedy sequence is invalid:
'des chat chante' -> INVALID, R = 0.8333
If the coordinate-wise greedy optimum is not a solution, the coordinates are contradicting each other. That is the signature, and it is the sentence my article never wrote. It gives both tables and it gives the invalid greedy sequence, and it never says why the sequence is invalid. You read position 0, stopped, and drew exactly the inference my text authorised. That is a writing defect on my side, not a measurement one.
And position 1 is the one that names the branch. Determiner, adjective and verb all agree with the noun. The noun is the only feature carrier, so "all-plural sublanguage" is a claim about the noun's number, not the determiner's.
Which position wins? I ran it, and it is the noun.
Exact gradient, same GRU, same objective E[R] + β·H, but p(s) obtained by enumerating all 8000 sequences, so zero sampling noise:
beta seed valid% modes sg% pl%
0.01 0,1,2 100.00 12.0 100.0 0.0
0.02 0 99.95 24.0 100.0 0.0
0.02 1 99.93 12.0 100.0 0.0
0.02 2 99.94 24.0 100.0 0.0
Six of six. Take the noise away and it goes singular every time. Sampled REINFORCE at β = 0, pure reward, no entropy bonus, 3 seeds: 100% singular, 1.0 effective mode, all three. Full sweep, 8 β values by 3 seeds, 24 runs: 15 singular, 9 plural. At your β = 0.02 specifically, seed 0 is plural at 18.6 modes (the run you are quoting), seeds 1 and 2 are singular at 11.7 and 12.0.
So: the collapse is settled before episode 1, and I agree. The corner is not. Which vacuous corner it lands in is a seed lottery running about 2:1 toward singular. Your closed-form direction is real, it is just outvoted by the position it does not look at.
The diversity number cannot arbitrate either, because the singular sublanguage also holds exactly 24 of the 48 (four determiners at 6 each, against les 12 plus des 12). 18.6 is 77.5% of 24, but seeds 1 and 2 sit at ~49% of their 24 at the same β.
Your question about the long grammar. I had not run it, and I should have. Its low validity had me filing it as a scaling failure rather than as a collapse worth analysing, which was a mistake. Five seeds, β = 0.08, 40k samples each:
seed valid% distinct valid number of the noun, valid mass
0 6.98 144 100.0% sg
1 9.41 144 100.0% sg
2 6.79 151 99.7% sg
3 7.54 144 100.0% pl
4 7.27 148 99.9% sg
It does collapse to a single number family. 144 distinct valid sentences is exactly one family out of 288, the split being perfectly symmetric. And the family is singular in 4 seeds of 5. Same answer as the short grammar, same direction, and the plural determiner edge loses again.
Structurally the vacuous corner is not even there. Adjectives spell out all four genre-number combinations with no None anywhere, so E[accord_adj_nom | noun] is exactly 0.5 for every noun and the adjective marginal is perfectly flat (0.1329 for all eight). Going plural now buys the free genre on the determiner alone, one subscore of four instead of one of three. Both edges shrink by exactly (3/4)(20/31) = 0.4839: det +0.0333 to +0.0161, noun +0.0167 to +0.0081. Adding an agreement rule added denominator and no counter-signal. The order-1 greedy sequence becomes des grand chat chante bien, still invalid.
Where you are simply right: the probe belongs before training, it costs one enumeration, and it is a protocol step rather than a post-hoc analysis. It already existed in the repo (gradient_exact.py, part 1, which prints both tables) but buried in an analysis script, which is why I did not reach for it when writing the article. It is now its own thing, src/test2_grammar/sonde_ordre1.py, all positions, both grammars, plus the long-grammar measurement above, credited to you in the docstring.
The lesson I would now write on it is not "find the vacuous corner". It is compute every position, because the corner is where they disagree, and the sign of the disagreement is not readable from the first token.
Data synthesis frameworks may distill a pipeline of expert models through templates or LLM re-phrasing, but templates limit the variety of the training data.
Wanting more flexibility for VQASynth, we've added an experimental SpatialAnnotator agent with scene understanding tools-as-methods, built on NVIDIA-labs OO Agents (NOOA): https://github.com/NVIDIA-NeMo/labs-OO-Agents
In NOOA an agent is a Python class. Fields hold state, docstrings are prompts, type annotations are contracts, and a method body of
... becomes an LLM-driven loop. Scene understanding tools stay ordinary deterministic methods.from nooa import Agent
class SpatialAnnotator(Agent, llm=llm):
"""Answer spatial questions about an image using scene understanding tools."""
scene: SceneContext # masks, depth, intrinsics, canonical transform
# Deterministic Python. Bodies omitted here.
def measure(self, a, b=None, kind="object_to_object") -> Measurement
def count(self, target: str) -> Count
# Agentic. The runtime hands this to the model.
async def annotate(self, image: Image, question: str) -> SpatialAnswer:
"""Answer the spatial question. This is offline labeling, so there is
no human available for clarification. Prefer a canonical
interpretation, and decline if the question cannot be grounded."""
...GitHub: https://github.com/remyxai/VQASynth#agent-with-vqasynth-tools