batching: scope token-flattened rows, and warn on writes that can't be scoped - #722
Open
JadenFiotto-Kaufman wants to merge 1 commit into
Open
JadenFiotto-Kaufman wants to merge 1 commit into
JadenFiotto-Kaufman wants to merge 1 commit into
Conversation
…coped `_narrow_tensor`/`_widen_tensor` only recognized a leading dim equal to the batch size, so anything a model folds tokens into — every transformers MoE block's `(batch*seq, ...)` router and experts, a per-head `(batch*seq*heads, ...)` — went to every invoke whole: a read saw the batch, an in-place edit landed on every invoke, and a replacement was dropped silently. Narrow and widen `k = rows // total` rows per row of batch when the leading dim is a whole multiple, and warn when a write reaches a value neither rule matched, naming the location, its leading dim and the batch size. In-place edits are caught through torch's version counter, since they never come back through `widen`. Also warn when invokes disagree on a forward keyword: the batch is one forward call, so its keywords are batch-wide and the last invoke to pass one wins. Docs: `trace.md`, the `batching.py` module docstring and NNsight.md promised per-invoke scoping of every activation; they now state the rule and how it goes wrong. `invoke-and-batching.md` gains that rule, the forward-keyword one, and the ordering rule for statements written outside the invoke blocks (they run in the collection pass, before the model). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JadenFiotto-Kaufman
added a commit
that referenced
this pull request
Sep 8, 2026
…f it Batching gained the modulo rule and the unscoped-write warnings in #722; the concept page still described the old exact-match rule. State the rule the batcher applies, what a value it cannot scope does to a read and to each kind of write, and keep the shape coincidence as the accepted cost it now is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Items 1, 13 and 14.3 from the stress-sweep decisions.
What was broken
Row scoping was a single shape test (
tensor.shape[0] == self.total). Anythingelse went to every invoke whole, with three silent consequences: a read saw the
whole batch, an in-place edit landed on every invoke, and a replacement was
dropped (
_widen_tensorreturnedfulland discardededited). The layout thathits this in practice is the token-flattened one — every transformers MoE block
reshapes to
(batch*seq, hidden)before its router, somlp.gate.output,mlp.experts.inputsand every.sourceop below the reshape were unscoped. Onhf-internal-testing/tiny-random-Qwen2MoeForCausalLM, two invokes each forcing"their" last token onto an expert both wrote the same rows, and the third,
unedited invoke's logits moved.
Forward keywords on an invoke were merged batch-wide, last writer wins, with
nothing said:
output_hidden_statescame backNonefor the invoke that askedfor it, and a per-invoke
attention_mask/max_new_tokensquietly became thebatch's.
docs/usage/trace.md, thebatching.pymodule docstring and NNsight.md allpromised that each invoke "sees only its own rows of every activation".
What changed
Batcher._narrow_tensor/_widen_tensortake a leading dim that is a wholemultiple of the batch size:
k = rows // totalrows per row of batch, narrowedand spliced at
[start*k, size*k). Fixes the MoE(B*T, D)and per-head(B*T*H, D)cases. Accepted risk, as decided: a leading dim that is a multipleby coincidence (four experts against two invokes) is now sliced rather than
passed through.
dim and the batch size. A replacement is caught in
_widen_tensor(and onlywhen the block actually handed back something different, so round-tripping the
non-batched leaves of a tuple output stays quiet). An in-place edit never comes
back through the batcher at all, so
_narrow_tensorrecords what it servedwhole with torch's
_versioncounter and_report_unscoped— called at the topof
narrow/widen— warns for anything whose version moved. That makes thereport one batcher call late (the next value served to any worker); an edit
after which nothing at all is read is the case it can miss. Reads deliberately
do not warn: from a shape alone, a causal mask is indistinguishable from a
vision tower's patches, and warning on reads would fire constantly.
TransformersModel._batch_forwardwarns when two invokes pass different valuesfor the same forward keyword.
docs/usage/invoke-and-batching.md; the corrected sentence indocs/usage/trace.md, thebatching.pymodule docstring and NNsight.md §4.4 /§7.2; the internals in
docs/developing/batching-internals.md; theforward-keyword rule; and (item 13, docs only) the ordering rule that statements
outside the invoke blocks run in the collection pass, before the model — with
the reduce-after-the-trace remedy, verified here.
Tested
PYTHONPATH=<worktree>/srcagainst the stress env's interpreter, CPU only.tests/test_batching.py:TestFlattenedRows(read, in-place editand replacement on a
(batch*seq, hidden)activation, pure torch),TestUnscopedWrites(in-place warns, replacement warns and is dropped, a readis quiet),
TestMoERouterScoping(the tiny-random Qwen2-MoE regression: aforced route matches the same prompt traced alone, does something, and leaves
the control invoke equal to its solo run),
TestForwardKwargs. All sevenbehavioural ones fail on a
git archive HEADbaseline and pass here.tests/test_batching.py55 passed, 1 skipped. Also green:test_interleaving,test_saving,test_backward,test_language,test_modeling,test_envoy,test_editing,test_source,test_tracing,test_encoder,test_vision,test_chat,test_chunked_tasks,test_multiple_wrappers,test_construction_routing,test_vlm,test_diffusion,test_fragments,test_util,test_memory,test_serialization,test_tensor_parallel_rules,test_deprecations,test_config(~950 tests).tests/vllm/andtests/tp/(no vLLM run, no multi-GPU). ThevLLM batcher inherits
_narrow_tensor, wheretotalis the step's token rows;the modulo branch only engages for a tensor whose leading dim is a multiple of
that, and the warning is a warning.
tracer.cache()(with
include_inputs=True), batchedgeneratewithtracer.iter, andattention-tuple edits produce no warnings.
Deliberately left alone
by luck and does not address the rest; the axis table is the real answer.
(seq, batch, hidden)withseq == total, and a list ofper-sequence tensors: still wrong, still undetectable from a shape. Both are now
written down in the docs.
docs/concepts/batching-and-invokers.md:13and:76still state the old== totalrule — that file belongs to another agent in this batch; it needs thesame correction. Likewise the
nnsight:nnsightskill's batching material,which lives outside this repo.
mine.
🤖 Generated with Claude Code