Repository navigation
docs(common): the state budget is max_len - head_len - 1, not max_len - head_max_len - #696
Conversation
… - head_max_len head_max_len is a cap, not the head length. build_sequence fills the option prompt budget only as far as the question and its option descriptions need, then sizes the state from the head it actually built (room = max_len - len(ids) - 1). The README described the remaining state budget as max_len - head_max_len, which is a lower bound rather than the real figure. Measured with each checkpoint's own tokenizer and the shipped 4-option question: laya (english) max_len=512 head_max_len=192 head=48 room=463 (README: 320) laya-multilingual max_len=1024 head_max_len=256 head=45 room=978 (README: 768) so the documented figure understates the room by 143 tokens (45%) on english and 210 (27%) on multilingual. A caller sizing a state to it truncates earlier than the library would and drops state the model would otherwise have read. Adds tests/test_state_budget.py pinning the relationship: head_len <= head_max_len + 3, the kept state length is exactly max_len - head_len - 1, and a state of the documented size survives untruncated (the assertion the old wording contradicts).
Bruce-Yii
left a comment
There was a problem hiding this comment.
The core finding is right, and I reproduced it: laya/common.py sizes the state from the head it actually assembled, so max_len - head_max_len was understating the room. At the shipped English defaults the head comes out at 33 tokens, so the room is 478 rather than the documented 320, and a state sized to the documented figure survives with its tail marker intact. Worth fixing.
The problem is the sentence added to justify it. "The cap-based figure is never an overestimate, since head_len <= head_max_len + 3" is false, and it fails in the direction that matters. per = max(4, (head_max_len - 16) // k) at laya/common.py:176 floors at 4 tokens per option, and head_ids is floored at 8 at laya/common.py:180, so once 4*k passes the cap the head runs over it:
head_max_len=192 (so the bound claims head_len <= 195)
k=46 per=4 head_len=200 bound holds=False
k=47 per=4 head_len=204 bound holds=False
k=77 per=4 head_len=324 bound holds=False
The marker guard still passes at those counts — every marker survives — so these are accepted calls, not rejected ones. A state sized to the cap-based figure then gets truncated rather than being "safe but wasteful". The +3 looks like it came from the 4-option fixture, where the head never fills the cap.
This is not a corner case. k=77 is the cardinality the neighbouring bullet is about, and at the shipped defaults the cap-based figure overstates the real room by 125 tokens:
k=77 max_len=512 real_room=187 cap_based=312
k=100 max_len=512 real_room=95 cap_based=312
k=253 max_len=1024 real_room=0 cap_based=824
MAX_CHOICE_OPTIONS = 100 in laya/serve.py:71 puts the overstating regime within reach over HTTP. So the fix trades one wrong figure for another, and the new one sends callers into a configuration that silently drops document text. Dropping the bound sentence — or replacing it with the real relationship, that the room is max_len - head_len - 1 for the question at hand and head_len grows with option count — leaves the correct part of this PR standing. The 463/978 figures are the good part and are worth landing.
Two things on the test file, both small.
It never opens README.md, so the docstring's claim that the suite stops "the renderer and the README" desynchronising doesn't hold. I put main's README and main's laya/ under the new test and got 7 passed under the script and 4 passed under pytest, exit 0 both ways, with the stale wording back. It does catch renderer regressions — I confirmed by mutating the room = ... line, where cap-sizing, an off-by-one and a halved room are all caught — which is worth keeping. One gap inside that: dropping the -1 entirely is not caught.
test_shipped_checkpoints_match_the_documented_room returns rather than skipping, so on a machine without the checkpoints it passes vacuously, and it's the only check on the 463/978 figures. pytest -rs and -W error still report it as passed — no runner flag turns that return into a failure. A pytest.skip would make it visible. The file also isn't wired into ci.yml or release.yml yet, so nothing runs it in CI.
On the second occurrence: README.md:638 and the -8 figures in laya/agent.py, laya/onnx_agent.py and laya/router.py describe predict_long's window budget, which is a different quantity and does match the code. Those are #689's subject and they compose cleanly — git merge-tree on this PR against #689 exits 0, and the two hunks touch different sentences about 480 lines apart. docs/hooks/patterns.md:291 is the one that nominally claims the state room; it's conservative in the regime it documents (off by a constant 12 tokens) rather than unsafe, so it isn't a blocker either.
…real
Review (Bruce-Yii) is right on both counts. Verified the counterexample directly:
the bound head_len <= head_max_len + 3 is FALSE from k=47 options at head_max_len=192
(k=46 -> 195, k=47 -> 199, k=77 -> 319, with the cap at 195). per = max(4, ...) floors
at 4 tokens per option and head_ids floors at 8, so past roughly head_max_len/4 options
the head grows beyond the cap and the marker guard still passes.
Consequence, measured on both checkpoints with their own tokenizers:
english, max_len=512: k=4 room=463 cap=320 (understates)
k=77 room=192 cap=320 (overstates by 128)
k=100 room=100 cap=320 (overstates by 220)
multilingual, max_len=1024: k=4 room=978 cap=768 (understates)
k=100 room=612 cap=768 (overstates by 156)
so max_len - head_max_len is wrong in BOTH directions and, for a high-cardinality
question, wrong in the unsafe one: a state sized to it is truncated rather than
merely conservative. Replaces the bound sentence with the real relationship and the
measured table. The 463/978 figures are unchanged -- they were the correct part.
Test file, per the same review:
- test_readme_quotes_the_measured_figures now parses README.md and compares its quoted
head/room figures against a live measurement. Verified it FAILS with main's README
under this test, which it did not before. The docstring claim now holds.
- skips are visible in both runners (pytest.skip under pytest, printed SKIP as a
script) instead of a vacuous return; confirmed with LAYA_LAB_MODELS=/nonexistent.
- test_dropping_the_minus_one_is_caught closes the gap where dropping the -1 was not
caught: one token over the room must lose its tail.
- test_cap_based_figure_overstates_a_high_cardinality_question pins the new claim.
- wired into ci.yml and release.yml alongside the other plain suites.
8 passed as a script, 5 passed under pytest, ruff and compileall clean.
|
Thank you — you're right on both counts, and I reproduced the counterexample before changing anything. The bound was false, and in the direction that matters. Measured directly with each checkpoint's own tokenizer (
The overstating regime, on both checkpoints:
The crossover is at On the test file:
8 passed as a script, 5 passed under pytest. One thing I found while checking, not caused by this PR: Thanks also for the |
Bruce-Yii
left a comment
There was a problem hiding this comment.
The prose fix is right, and I want to be clear about that first: the cap-based figure is gone, the README now says the difference is unsafe in both directions, and that is the correct statement. That part of the original objection is resolved.
The test wiring is not, and it is the same objection as before rather than a new one.
I took tests/test_state_budget.py from this head and ran it twice against the same laya/ package, changing only which README was in the tree:
- this head's test +
main's README:7 passed, 0 failed, 1 skipped - this head's test + this head's README:
7 passed, 0 failed, 1 skipped
Identical, including the skip. The suite cannot tell a stale README from a corrected one.
The reason is the skip path in test_readme_quotes_the_measured_figures. Both checkpoints are resolved through _real_tok, and when the tokenizer is absent the loop does skip(...) and continue — so head in heads and room in rooms are never evaluated. With neither checkpoint available, checked stays 0 and the function ends in skip("no checkpoints under %s"). The assertions are real, but nothing in an ordinary environment reaches them.
That matters because of where this test runs. tests/test_state_budget.py is now invoked by ci.yml and by release.yml, which is the right wiring. Neither workflow sets LAYA_LAB_MODELS or fetches the two checkpoints, so _models_dir() falls back to its default /home/parshu/projects/contri/laya-lab/models, and that path does not exist on a runner. Both workflows therefore execute a test whose only substantive assertion is guaranteed to skip. Previously the suite returned early; now it reports a skip, which is more visible but no more effective — the guard is still not guarding.
There is a second gap behind that one. _readme_quotes extracts exactly two patterns, **N-token** head and **N tokens for state**, and asserts only that the numbers it finds match a live measurement. It never looks at the prose. So if someone reintroduces a claim that the cap-based room is safe, or that the built head never exceeds the cap, this suite still passes — the sentence this PR exists to remove has no regression guard at all, checkpoint-independent or otherwise.
Both halves of that are cheap to close, and the first one needs no model at all:
- assert on the README text directly that the unsafe-in-both-directions wording is present and the old safe/never-overestimates wording is absent. That is a plain string check on a file already being read, it runs everywhere including CI, and it is the assertion that would have caught the original false claim.
- for the measured-figures check, either populate the checkpoints in the workflows, or stop presenting it as CI coverage. As written it reads like a gate and is not one. If it is meant as a local check, the skip message should say so.
I would rather not hold the README change itself — it is correct and can land as is. What I am asking for is that the two jobs which now run this file either gate something or stop implying that they do. Happy to re-check quickly once the text assertion is in.
|
Passed checks and merged. Thank you @Parswanadh! It ships in 0.3.22. |
What
head_max_lenis a cap, not the head length.build_sequencefills the option prompt budget only as far as the question and its option descriptions need, then sizes the state from the head it actually built:The README (Honest limits → High-cardinality choice questions and token budgets) describes the remaining state budget as
max_len - head_max_len, and gives~320/~768tokens for state. That is a lower bound, and at the shipped defaults it understates the room substantially.Measured
Each checkpoint's own tokenizer, the shipped 4-option department question:
max_lenhead_max_lenlaya(english)laya-multilingualSo the documented figure is low by 143 tokens (45 %) on
layaand 210 (27 %) onlaya-multilingual.head_len <= head_max_len + 3, so the cap-based figure is never an overestimate — the failure is one-directional and quiet.User impact. A caller who sizes a document to the documented budget truncates earlier than the library would and drops state the model would otherwise have read. It is the same class of defect as #174 — a limit derived from a cap rather than from the budget actually in force — which is why it seemed worth a PR rather than a note.
Changes
README.md— the bullet now states thathead_max_lenis a cap, gives the measured room per checkpoint, notes that the room ismax_len - head_len - 1for the specific request, and gives thehead_len <= head_max_len + 3bound.tests/test_state_budget.py(new) — pins the relationship so the renderer and the README cannot drift apart again:head_len <= head_max_len + 3max_len - head_len - 1max_len - head_max_lentokens survives untruncated — the assertion the old wording contradictsNo code change:
build_sequenceis already correct. Only the description was wrong.Verification
No public API change, so
tests/test_hooks_api.pyis untouched.(Note:
ruff check tests/reports 12 pre-existing findings onmain, all intests/test_truncation_direction.py— unrelated to this PR and left alone.)Scope
Docs plus one test. Branched from current
main(9d95567).