News
How we found them: an adversarial agent swarm for silent attention bugs
A companion to our last post — not the bug, but the method that surfaced it: a scripted swarm of LLM reviewers split into refuters and reproducers, pointed at our own attention code. What it's good for, and what it doesn't prove.
A companion to the last post. That one was about the bug; this one is about the method that surfaced it — a scripted swarm of LLM reviewers, split into refuters and reproducers, pointed at our own attention code. What it's good for, and — just as important — what it doesn't prove.
The companion question
Our last post described a class of silent attention bug: it returns plausible numbers, doesn't crash, and is invisible to grep and code review. Which raises an obvious question — if the bug is that hard to see, how did we find it? Not by reading. We ran a scripted multi-agent audit over the library. This is the honest account of how that worked, and where it stops working.
Why a swarm, and why for this
Two things make a long-context attention library hard to audit by hand. First, the worst bugs are silent — wrong-but-plausible output, no exception — so a tool that only checks whether the code runs tells you nothing. You need something that reasons about what the code should compute. Second, we had around twenty variants, each re-implementing the same delicate combination step; that's more delicate, duplicated code than careful manual review reliably covers.
In this audit, LLM reviewers earned their keep on that first part — reading for intent (“does this actually combine partial softmaxes with a shared denominator?”) rather than syntax. But a single model turned loose on a large codebase both invents problems that aren't there and misses ones that are. So the design is two ideas: many narrowly-scoped reviewers, and an adversarial check on every candidate defect they report.
The shape of the run
One scripted workflow, four phases, a single run — about four million tokens across roughly 80 scoped agent invocations. They aren't 80 independent researchers: it's the same base model, run in different roles over different slices of the code.
- Orient. One agent maps the package first — the public API, the highest-risk files, and especially the forked and duplicated ones (the
*_fixed.pyvariants, a duplicated communication mixin, deprecated memory pools). Every later agent receives this map, so it can tell whether a buggy file is actually live — exported, built by a factory — or dead code that doesn't matter. - Review — six lenses. Six reviewers, one lens each: attention math and online-softmax accumulation; ring and distributed communication; autograd (does the backward pass differentiate the same function the forward computes?); memory pools and caches; Triton kernels; and sparse patterns plus dead/forked code. Each reads its whole slice of the tree in full — we ran it on a million-token-context model precisely so we could hand each reviewer an entire subsystem in one context instead of preselecting snippets. Each returns structured findings — file, lines, the wrong behavior, the proof, a fix — or an empty list.
- Verify — refuters and reproducers. This is the part that matters. Every finding is then re-checked by two more agents with opposing instructions. One is told: assume this is wrong; find the guard, invariant, or upstream check that makes the code correct; default to “not a bug.” The other is an analytic reproducer, told to try to trigger it: construct concrete tensor shapes, dtype, sequence length versus dilation, world size, and causal flag that would exercise the bug. Not two identical skeptics — a refuter and a reproducer, pulling in opposite directions. Neither one runs the library: both argue from the code, the refuter hunting the guard that makes it safe, the reproducer constructing — on paper — the input that would set it off.
- Vote, then synthesize. Both agree it's real → the finding survives. They split → contested, and labeled as such rather than asserted. Both say not-real → dropped before the report is ever written. A final agent writes up only what survived.
What the vote actually buys you
The value isn't the number of findings — it's that anything both adversaries rejected never reaches you. The reproducer forces “show me the input,” which kills the plausible-sounding bug that's actually guarded upstream. The refuter forces “show me the missing check,” which kills the vague hunch. What's left has been attacked from both sides. The contested cases — where the refuter and the reproducer disagreed — we kept but flagged; several turned out to be real, but narrower than the first pass claimed. Roughly thirty defects survived that gauntlet.
One caveat to state plainly, because the labels invite the wrong reading: this whole stage is analytical. The reproducer specifies an input that should trigger the bug; it doesn't run the code and watch it break. So a surviving finding is analytically supported — two adversaries, reading the same code from opposite directions, both landed on real — not reproduced. (The workflow's status field happens to call these “confirmed”; “survived verification” is the more honest word.) Think of it as three evidence levels: a first-pass candidate, an analytically-supported finding, and — only later — a runtime-reproduced failure. That last level was separate work: a dense-reference oracle and the fix's own test suite, the subject of the last post.
What it caught
The findings were the last post's subject, so only briefly here. The finding that mattered most was the recurring one from that post: at least six separate implementations combining partial softmaxes without a shared denominator. The single most dangerous was a key/value buffer that aliased on the default multi-GPU path, silently corrupting every distributed forward pass. It also caught ordinary loud bugs — a class that wouldn't even construct, a debug method that always threw — the kind you'd trip over eventually anyway. The silent ones are the ones you wouldn't.
What this does not prove
This is the section that keeps the rest honest.
- An audit is a filter, not a proof. A lens finding nothing means that pass didn't see a bug — not that the code is correct. Absence of a finding is not a certificate. The adversarial stage buys precision — what reaches you is likelier to be real; it says nothing about recall, the bugs no lens thought to look for.
- The verifiers aren't independent witnesses. They're the same base model in opposing roles, so they share its blind spots — misread an invariant and the refuter and the reproducer can be wrong about it together. Their agreement lowers the noise; it isn't statistical independence. So a human still corrected severities, merged near-duplicates, and curated the report: the swarm produced raw findings and verdicts, a person decided what shipped.
- Shared context is shared failure. Every reviewer inherits the same orientation map, so if that first pass calls a live file dead — or a dead one live — the mistake rides along to everyone downstream. A higher-assurance run would verify reachability independently instead of trusting the map.
- It's a discovery tool, not a replacement for tests. Once a lens names the invariant you're violating — partials must share one denominator — the durable fix is to encode it as a test, which is what we did. The swarm finds the open seam; a test keeps it shut.
- It's only as good as its lenses. We caught the softmax-combination class because a lens was explicitly hunting online-softmax accumulation. A failure mode nobody wrote a lens for is a failure mode nobody looked for.
- And this is one run on one codebase, tuned to our own failure modes — a recipe we found useful, not a benchmarked method. Adversarial verification and using a model as a judge are well-worn ideas; all we did was wire them for numerical correctness.
If you want to try it
- Give each reviewer a whole subsystem in one context — don't split away the cross-file seams. These bugs live between files: a mask built in one place and applied in another, a buffer sent here and consumed there. Read the files apart and you lose the seam the bug lives in.
- Split verification into refute-versus-reproduce, not N copies of “is this a bug?” Opposing stances catch different failure modes than redundant ones do.
- Drop what both adversaries reject before it reaches a human. That filtering is what makes the output worth reading.
- Keep a person on synthesis and severity. The agents are good at “is this wrong”; they're noisier on “how much does it matter.”
- Point it at the silent bugs. Crashers are far more likely to surface through ordinary tests and CI. The wrong-but-runs bugs are where an audit like this earns its keep.
- Spend it where it pays. A multi-million-token audit isn't a linter for every commit; it's for code where a silent numerical error would be expensive to ship and near-impossible to catch another way. That's where it's worth the cost.