Repository navigation
Description
Summary
Since 317c1ec ("Preserve compiled storage mutations without cloning backing buffers"), _lower_storage in ignition/preparation/compilation.py skips the flare::storage_call boundary for a complete aten.copy_ when no later node writes the source's storage. It does not check whether the source expression reads the destination's own storage. Two packed-to-logical expansions in the DeepSeek V4.1 path are exactly that shape, and Inductor then recomputes the gather while writing the same buffer, so some logical rows read packed rows that another row has already overwritten.
On DeepSeek-V4.1-Flash-1017, TP4, with speculative decoding (DSpark, adaptive verification), batched requests produce corrupted text and tool calls, including fragments of other concurrent requests' content. The same build serves single requests and requests with speculatation off correctly.
Affected code (unchanged at acbf285)
ignition/preparation/compilation.py:503-506, thecopy_skip condition.ignition/components/vocabulary.py:41setsself.gathered = output[:rows]when the TP shards cover the vocabulary exactly (for example 129280 / 4), and:53runsself.output.copy_(gathered[self.row_indices, :self.output.shape[1]]). The comment there relies on the indexed read materializing first.ignition/recipes/deepseek41/decoder.py:311setsself.head_hidden = scratch.hidden[:head_rows], and:351runss.hidden.copy_(self.head_hidden[samples]).
Generated code
For the logits expansion, Inductor's output computes the gather correctly into a fresh buffer, then recomputes it in place instead of copying that buffer:
buf2 = empty_strided_cuda((32, 129280), (129280, 1), torch.float32)
triton_poi_fused_index_slice_view_0.run(arg0_1, arg1_1, buf2, 4136960, stream=raw_stream0)
triton_poi_fused_copy__index_slice_view_1.run(arg0_1, arg1_1, arg1_1, 4136960, stream=raw_stream0)Reproduction (component level, no model)
A module whose forward writes a packed prefix of a buffer through a mutating custom op, then runs storage.copy_(storage[:physical][indices]) with the packed-to-logical row map that Metadata.set builds (begins[:, None] + arange(width), clamped to physical capacity). Geometry: batch 4, width 8, physical capacity 12, columns 129280 (FP32) or 5120 (BF16), lengths such as (3,3,3,3), (1,7,2,2) and (0,4,7,1). Compiled with compile_forward and with aot_forward, checked against eager over 33 trials each (3 direct calls, 30 CUDA-graph replays):
| Case | Unguarded, RTX PRO 5000 Blackwell (SM120) | Unguarded, RTX 4090 (SM89) | Guarded, both |
|---|---|---|---|
| logits, compile_forward | wrong valid rows in 33/33 trials | 33/33 | 0 wrong |
| logits, aot_forward | 33/33 | 33/33 | 0 wrong |
| hidden, compile_forward | 25/33 | padding rows only | 0 wrong |
| hidden, aot_forward | 23/33 | 31/33 | 0 wrong |
For lengths (3,3,3,3), the third and fourth requests' logical rows carry the second request's packed rows.
Environment: flare-prepare built at a fork of NVIDIA main through 225d466 (no local changes to the files above), torch 2.14.1+cu132, CUDA 13.2.
Proposed fix
Keep the opaque boundary whenever the copy's source reads storage the copy writes:
if (target is torch.ops.aten.copy_.default
and not (accesses[node.args[1]] & mutable_keys)
and not any(last_write.get(key, -1) > positions[node] for key in accesses[node.args[1]])
and all(complete_write(arg) for arg in mutated)):
continueWith this condition, every case above passes. In the full model (stock recipe, TP4 on RTX PRO 6000 Blackwell), a guarded export served with the same runtime passes a concurrent test of tool, reasoning, plain and exact-copy prompts: exact tool arguments, byte-exact copies and no cross-request content.
Prepared with the assistance of AI.
Activity
mark-yong commentedon Nov 3, 2026
Reproduced on one SM120 GPU, think #158 already fixed it.
Using vocabulary projection shape: a pack op writes rows into a storage,
output.copy_(gathered[row_indices, :cols]),torch.compile, four concurrent requests, 33 trials. Same harness against three trees:acbf2858(as filed)acbf2858+ the fix proposed abovecc696a6b)The bad rows are the later requests' logical rows (
[13, 14, 15, 18, ...]) picking up content the copy had already overwritten, and the vulnerable tree does generate atriton_poi_fused_copy__index_slice_view_*kernel that gathers inline and stores into the same buffer. My hidden case corrupted 33/33 rather than the 25/33 reported, so presumably it depends on what the scheduler does that run.Putting a print in the skip condition shows every tree computes the hazard correctly (
disjointis False for the gather source);acbf2858just never consults that clause. The fix proposed above clears the corruption on the vulnerable tree, and #158 lands basically the same clause on the copy_ skip condition (mutable_keys.isdisjoint(accesses[node.args[1]])), so a complete copy whose source reads the region it writes stays an ordered storage call instead of getting folded into the writeback. The #158 description independently walks through this failure, wrong tokens from other requests when two or more requests verify in one round, and it merged Oct 30, two days after this issue was filed.@jwmacd can you rerun against current main? If it's clean for you too I'd close this as fixed by #158.
jwmacd commentedon Nov 4, 2026
Thanks. Confirmed it runs clean against current main.