Fix negative offsets in GPUBiasingMultiModel.remove_model - #16236
Conversation
|
Adding the local verification, since CI on NVIDIA runners needs a maintainer's I ran the assertions from the new test against the unpatched and patched code on both devices. The parametrization matters: the defect fires only when the removed model sits at arena offset 0, which is exactly what the analysis predicts, so removals from the middle and back pass either way. Unpatched: Patched, CPU ( Both device paths, including the survivor-scores-unchanged assertion, which is the one that would catch a fix that corrected the offsets while breaking the compaction. Separately, on the same hardware with a standalone reproducer: negative offsets go from 126 of 128 to 0, a surviving model's Happy to adjust the test's shape or placement if you would rather it lived somewhere else. |
Removing a boosting model leaves every unused reserved slot holding a negative
offset when the removed model started at arena offset 0.
remove_model clears the departing model's own offsets to 0 and then applies the
shift to the whole table with the predicate `offset < start_state`. Unused slots
also hold 0, and when the removed model started at 0 that predicate is false for
them, so each one is decremented to -num_states / -num_arcs. With the default
reserve of 128 slots, removing the first of three registered models leaves 126
slots negative:
offsets before removal : [0, 6, 11, 0, 0, ...]
offsets after removal : [-6, 0, 5, -6, -6, ...]
The root cause is that 0 means two different things in this table: a valid arena
start address, and an empty slot that has never held a model. The shift cannot
distinguish them, so it transforms slots that should not take part in it.
This is currently latent. The Triton kernel returns early for a negative
model_id, so the -1 sentinel never dereferences its slot, and callers gate on
model2active, so no configuration I could construct produces a wrong score.
Surviving models score bit-identically across a removal both before and after
this change. But the invariant that an offset is a non-negative index into the
arena is violated for almost every slot, and neither the kernel nor
_advance_pytorch re-checks model2active before turning an offset into a pointer,
so a stale or out-of-range positive id would find a negative base.
The fix restricts the shift to ACTIVE models and clears the removed model's own
offsets last. model2active[model_id] is already False at that point, so the
removed model is excluded from the mask. Using an explicit active mask rather
than only reordering the two statements makes the intent readable: inactive
slots are not participants in the relocation transform.
Adds a parametrized regression test covering removal from the front, middle and
back of the arena, asserting that every offset stays non-negative, that inactive
slots carry a canonical empty descriptor, that active extents stay inside the
arena, and that a surviving model's scores are unchanged by the removal. The
existing test only checked the survivor's shifted offset, which is why this was
not caught.
Verified on an NVIDIA RTX A6000 with Triton: negative offsets go from 126 to 0,
surviving models remain bit-identical, slot reuse after removal still works, and
the Triton and non-Triton paths continue to agree.
Signed-off-by: Zaheer Sheriff K <zaheersheriff.k@gmail.com>
353d190 to
0e38652
Compare
What
GPUBiasingMultiModel.remove_modelleaves almost every unused reserved slot holding a negative offset whenever the removed model started at arena offset 0.remove_modelclears the departing model's own offsets to0, then applies the shift across the whole table with the predicateoffset < start_state. Unused slots also hold0. When the removed model started at0, that predicate is false for them, so each one is decremented to-num_states/-num_arcs.With the default reserve of 128 slots, registering three boosting models and removing the first gives:
The underlying cause is that
0carries two meanings in this table: a valid arena start address, and a slot that has never held a model. The shift cannot tell them apart, so it transforms slots that should not take part in it.Impact, stated honestly
This is latent, not an active miscompute. I tried to make it produce a wrong answer and could not:
model_id < 0, so the-1sentinel never dereferences its slot;What is broken is the invariant that an offset is a non-negative index into the arena. Neither the Triton kernel nor
_advance_pytorchre-checksmodel2activebefore turning an offset into a base pointer, so a stale or out-of-range positive id would find a negative base rather than failing closed. It is a hazard for the next consumer that does not gate, and it costs one masked subtraction to remove.The change
Restrict the shift to active models, and clear the removed model's own offsets last.
model2active[model_id]is alreadyFalseat that point, so the removed model is excluded from the mask.Using an explicit active mask rather than only reordering the two statements makes the intent readable: inactive slots are not participants in the relocation transform.
Test
Extends
test_add_then_remove_modelto assert that every offset stays non-negative and that inactive slots carry a canonical empty descriptor.Adds
test_offsets_stay_non_negative_for_any_removal_position, parametrized over removal from the front, middle and back of the arena, asserting:advance()scores are unchanged by an unrelated removal.The existing test checked only the survivor's shifted offset, which is why this was not caught.
Verification
Reproduced and verified on an NVIDIA RTX A6000 with Triton, using a standalone script that builds boosting trees from raw token id lists so it needs no checkpoint, tokenizer or audio. After the change: negative offsets go from 126 to 0, surviving models remain bit-identical, slot reuse after removal still works, and the Triton and non-Triton paths continue to agree.
Two related things, deliberately not in this PR
Raising them here only so they are on the record; each deserves its own thread.
add_modelfiresreallocation_callbackswhen the arena grows, andremove_modelfires nothing, although both move data that a captured CUDA graph addresses. I could not demonstrate a wrong result from this, and the kernel loads the offsets through a pointer at run time, which suggests a replay sees the committed values. I would rather ask whether that is the intended contract than assert a bug.INIT_NUM_MODELS,INIT_NUM_ARCSandINIT_NUM_STATESare fixed and growth doubles, with no configurable maximum and noreserve(). A server that registers a phrase list per session cannot guarantee that no reallocation, and therefore no CUDA-graph reset, happens mid-stream for the other sessions in the batch. A capacity knob would let deployments trade memory for predictability.