Fix reference passing for multi-round runs in bot.py (closes #30, closes #26) - #51
Conversation
Broly Security ScanNote Baseline snapshot is missing for this repo. Broly is running in PR-only fallback mode until the first scheduled baseline completes. This does not block the PR. Note ✅ Clean scan Note Re-scan this PR anytime with
|
c3aa0f1 to
60f7169
Compare
Each round stored references as a flat list, so Dataset.from_dict gave every model a single string (its own prior answer), enumerated character by character (togethercomputer#30). Store the full list per row so each model sees all prior answers, and drop models that failed instead of passing on "None". Also document why references go in the system prompt (togethercomputer#26). Add verify_main.py, an offline check that drives the real bot.main(). Closes togethercomputer#30. Closes togethercomputer#26.
60f7169 to
0c24be5
Compare
|
@Nutlope this is ready for a look when you have time. It fixes the character splitting in #30, where the round loop stored references as a flat list so Dataset.from_dict handed each model a single string. It also answers #26 in a docstring. It adds verify_main.py, an offline check that drives the real bot.main() and needs no API key, since the repo has no offline test today. Happy to split the docstring out if you would rather keep the bugfix alone. |
What was broken (#30)
With
--rounds 2, the first round works, but the second round sends each reference model a system prompt that ends like this:That is a previous answer spelled out one character at a time, which is exactly the paste in #30.
Root cause: at the end of each round,
bot.pystores the collected responses as a flat list of strings:Dataset.from_dictsplits every column across rows, so in the next round each row'sreferencesfield is a single string (that model's own previous answer).generate_with_referencesonly checkslen(references) > 0, which is truthy for a non empty string, soinject_references_to_messagesrunsfor i, reference in enumerate(references)over a string and enumerates its characters into the "Responses from models:" list.There are two defects folded together:
strwhere alist[str]is required, which produces the visible character splitting.generate_for_alpaca_eval.pyandgenerate_for_mt_bench.pypass the fullprev_referenceslist to every proposer each round.The fix
Store one full copy of the response list per dataset row, so each model in a later round receives all n responses from the round before it:
A fresh list per row rather than one list repeated, matching how the
instructioncolumn two lines above is built.Models that failed are now skipped instead of passed on.
generate_togetherreturnsNonewhen a model errors out, and both eval scripts already drop those withif reference is not None. Without this, the fix would put the textNoneinto the system prompt of every other model, silently.bot.pyprints a line saying how many were dropped, since it is interactive and a missing model changes the answer.The
referencescolumn is also initialised and reset as empty lists rather than empty strings. Empty lists skip injection exactly as empty strings did (len(...) == 0), so round 1 is untouched. This is type hygiene, not part of the behaviour fix.Three decisions, each made deliberately:
process_fn. PR fix(bot): fix a bug where references was a string instead of an array #18 (credit to @gligoran for first spotting the string versus list mismatch) wraps the string into a list insideprocess_fn. That stops the character splitting but leaves the second defect in place: each model still only sees its own previous answer, which is not the MoA architecture. Fixing wheredata["references"]is built restores the full fan out.Why the system prompt, not the user prompt (#26)
#26 asked why
moa.pyjoined the proposers' answers into the user message whileinject_references_to_messagesputs them into the system message. That difference was real when #26 was filed, becausemoa.pythen did{"role": "user", "content": ",".join(results)}, and it was unintentional demo drift rather than a design decision. #47 (commit 0bb4bc8) already movedmoa.pyto the system prompt placement, andadvanced-moa.pyuses it too, so the inconsistency is already gone from main. The paper does not pin the chat role of the Table 1 "Aggregate and Synthesize" prompt, so the authoritative reference is the implementation the paper's results were produced with, namelyinject_references_to_messagestogether with the eval scripts. That means the references go in the system prompt while the original user prompt stays in the user turn, which is the+ x_1term of equation 1 in Section 2.2. This PR records that answer in a docstring oninject_references_to_messages, including the list not string contract that #30 tripped over, so the question is answered in the code rather than in a closed issue. No behaviour changes inutils.py.Verification
The repo has a
tests.py, but it calls the live Together and OpenAI APIs and asserts exact model output strings, so it cannot run offline or in CI. There was no offline check of the round loop at all, which is part of why this stayed unnoticed. This PR addsverify_main.py.It calls the real
bot.main()rather than re-implementing the round loop, so a regression fails the check regardless of how the code is written. Four things are replaced, each named in the file's docstring:utils.requests.post(captures the exact payload each proposer would receive, returns a canned answer),bot.generate_together_stream(records the aggregator payload),bot.Prompt.ask(feeds scripted answers, ending withexit), and thenum_procargument toDataset.map(bot.pymaps with one worker per model, and spawned workers would not inherit the patchedrequests.post). Outbound sockets raise, as a hard stop in case any of that is bypassed. No API key is needed and no request leaves the machine.Six scenarios:
rounds=1,rounds=2,rounds=3,rounds=2across two chat turns,rounds=2withmulti_turnoff, androunds=2with one model failing.On current main,
rounds=1passes and the four multi round scenarios fail:The failing model scenario does not report at all on main, it raises first:
After the fix all six pass. Because the check asserts behaviour rather than matching source text, it also catches a weakened fix: forwarding only the first model's answer (
[:1]) fails four scenarios withsaw 1 references, expected 4.rounds=1passing on both sides is the guard that the default path is untouched.Known limitations
--rounds.bot.py's round loop is still a separate copy of the eval scripts'prev_referencesloop rather than shared code. Unifying them felt like too much churn for a bugfix.--rounds 0raisesUnboundLocalError. That is pre existing on main and untouched here, since it is a separate bug from the one --rounds 2 seems broken #30 reports.multi_turnis changed by this diff and is covered by the added check, but it is not reachable from the command line: it has no type annotation, so typer exposes it as a string and--multi-turn Falsepasses the truthy string"False". Also pre existing and left alone.utils.pythat currentblackflags is left untouched to keep the diff minimal. The lines added here areblackclean.verify_main.pyis a standalone script at the repo root, because the repo has no test runner to hook into. Happy to move or reshape it if you would rather it lived elsewhere.Closes #30. Closes #26. Related work: this supersedes the approach in #18 (see above), and #48 covers the same Section 2.2 residual connection from the diagram side.