[TRTLLM-11513][fix] fix iterator undefined behavior in WindowBlockManager::getFreeBlock offload path#12297
[TRTLLM-11513][fix] fix iterator undefined behavior in WindowBlockManager::getFreeBlock offload path#12297thorjohnsen wants to merge 18 commits intoNVIDIA:mainfrom
Conversation
📝 WalkthroughWalkthroughThe Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
@eopXD could you review this? thanks |
fa19cda to
8d1d0fd
Compare
…fload path claimBlock() reads getCacheLevel(block) which is based on isPrimary(). In the offload path, swapMemoryPoolBlockOffset() was called before claimBlock(), so getCacheLevel() returned the post-swap level while mFreeBlockIterators[id] still pointed into the pre-swap queue. Erasing via a std::list::iterator into a different std::list instance is undefined behaviour (C++17 §26.3.10.4); in practice it silently corrupts mNumFreeBlocksPerLevel and the free-list structure. Fix: claim both blocks before the swap, matching the correct ordering already used by WindowBlockManager::offloadBlock() (line 1121). Signed-off-by: thorjohnsen <41591019+thorjohnsen@users.noreply.github.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
/bot run --disable-fail-fast |
|
PR_Github #39952 [ run ] triggered by Bot. Commit: |
|
/bot kill |
|
PR_Github #39957 [ kill ] triggered by Bot. Commit: |
|
PR_Github #39957 [ kill ] completed with state |
Signed-off-by: thorjohnsen <41591019+thorjohnsen@users.noreply.github.com>
…BlockOffset Signed-off-by: thorjohnsen <41591019+thorjohnsen@users.noreply.github.com>
Signed-off-by: thorjohnsen <41591019+thorjohnsen@users.noreply.github.com>
Signed-off-by: thorjohnsen <41591019+thorjohnsen@users.noreply.github.com>
Signed-off-by: thorjohnsen <41591019+thorjohnsen@users.noreply.github.com>
Signed-off-by: thorjohnsen <41591019+thorjohnsen@users.noreply.github.com>
Signed-off-by: thorjohnsen <41591019+thorjohnsen@users.noreply.github.com>
Signed-off-by: thorjohnsen <41591019+thorjohnsen@users.noreply.github.com>
Signed-off-by: thorjohnsen <41591019+thorjohnsen@users.noreply.github.com>
|
/bot run --disable-fail-fast |
|
PR_Github #39983 [ run ] triggered by Bot. Commit: |
|
PR_Github #39983 [ run ] completed with state
|
Signed-off-by: thorjohnsen <41591019+thorjohnsen@users.noreply.github.com>
Signed-off-by: thorjohnsen <41591019+thorjohnsen@users.noreply.github.com>
|
/bot run --disable-fail-fast |
|
PR_Github #40026 [ run ] triggered by Bot. Commit: |
|
PR_Github #40026 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #40134 [ run ] triggered by Bot. Commit: |
|
PR_Github #40134 [ run ] completed with state |
|
/bot run --disable-fail-fast |
|
PR_Github #40137 [ run ] triggered by Bot. Commit: |
eopXD
left a comment
There was a problem hiding this comment.
Looks good, but some test coverage can help illustrate the change and extra security.
…n LRUEvictionPolicy Add ClaimAfterSwapDoesNotCorruptQueues to LRUPolicyTest to guard against reintroduction of the iterator UB fixed in PR NVIDIA#12297. The test releases a primary and a secondary block into their respective free queues, then calls swapMemoryPoolBlockOffset() on both (flipping isPrimary()) before calling claimBlock(). With the old bare-iterator approach this would erase from the wrong std::list; with the fix (stored cacheLevel tuple) the counts and queue integrity remain correct. Signed-off-by: thorjohnsen <41591019+thorjohnsen@users.noreply.github.com>
|
PR_Github #40137 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
/bot run |
|
PR_Github #40372 [ run ] triggered by Bot. Commit: |
|
PR_Github #40372 [ run ] completed with state
|
|
/bot run |
|
PR_Github #40551 [ run ] triggered by Bot. Commit: |
|
PR_Github #40551 [ run ] completed with state |
|
/bot run |
|
PR_Github #40732 [ run ] triggered by Bot. Commit: |
Summary
WindowBlockManager::getFreeBlock, the KV-cache offload path calledswapMemoryPoolBlockOffset()before callingclaimBlock()on either block. After the swap,getCacheLevel()returns the post-swap cache level, butmFreeBlockIterators[id]still points into the pre-swap queue.claimBlock()then calls.erase()on the wrongstd::list— undefined behaviour per C++17 §26.3.10.4.mNumFreeBlocksPerLeveland the free-list structure inLRUEvictionPolicy, causing block counts to diverge from reality and eventually producing "No free block found" aborts.block(primary) andoffloadBlock(secondary) beforeswapMemoryPoolBlockOffset(), so eachclaimBlock()call sees the cache level that matches the iterator. This is identical to the ordering already used correctly byWindowBlockManager::offloadBlock()(line 1121).Root cause (detailed)
LRUEvictionPolicy::claimBlock()(evictionPolicy.cpp):Both
blockandoffloadBlockare obtained from the eviction policy's free queues and therefore have validmFreeBlockIterators. After the swap the iterator/level mismatch affects both of them.API cleanup
I have also updated the API so that it no longer matters whether claimBlock is called before or after swapMemoryPoolBlockOffset. With the cleaned up API, the above fix is no longer necessary, but I think it is the correct way of doing things. We should claim the blocks before changing their state with swapMemoryPoolBlockOffset.
Test plan
--kv_cache_offload_gpu_to_cpu_fracor equivalent) and run multi-request inference — previously this path would eventually corrupt the free-list counters leading to a crash or incorrect "no free blocks" error.tests/unittest/batch_manager/test_kv_cache_manager.*suite.std::listiterator misuse is reported inLRUEvictionPolicy::claimBlock.🤖 Generated with Claude Code
Summary by CodeRabbit