Skip to content

Commit 5f8881c

Browse files
committed
[TRTLLM-12721][fix] Add gated disagg in-flight cancellation
Signed-off-by: Chien-Chun Hung <2679986+chienchunhung@users.noreply.github.com>
1 parent 87c6b32 commit 5f8881c

18 files changed

Lines changed: 797 additions & 147 deletions

File tree

cpp/include/tensorrt_llm/batch_manager/cacheTransceiver.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,11 @@ class BaseCacheTransceiver
226226
[[nodiscard]] virtual bool checkGenTransferComplete() const = 0;
227227

228228
virtual bool cancelRequest(std::shared_ptr<LlmRequest> llmRequest) = 0;
229+
230+
[[nodiscard]] virtual bool hasPoisonedTransferBuffer() const
231+
{
232+
return false;
233+
}
229234
};
230235

231236
class CacheTransceiver : public BaseCacheTransceiver
@@ -273,6 +278,8 @@ class CacheTransceiver : public BaseCacheTransceiver
273278

274279
virtual bool cancelRequest(std::shared_ptr<LlmRequest> llmRequest) override;
275280

281+
[[nodiscard]] bool hasPoisonedTransferBuffer() const override;
282+
276283
private:
277284
void initializeCommState();
278285

cpp/include/tensorrt_llm/executor/transferAgent.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2+
* Copyright (c) 2025-2026, NVIDIA CORPORATION. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -356,6 +356,13 @@ class TransferStatus
356356
virtual ~TransferStatus() = default;
357357
[[nodiscard]] virtual bool isCompleted() const = 0;
358358
virtual TransferState wait(int64_t timeout_ms = -1) const = 0;
359+
360+
/// Release the backend transfer request handle. A true return means the backend accepted the handle release; it
361+
/// does not prove remote memory quiescence.
362+
[[nodiscard]] virtual bool release()
363+
{
364+
return false;
365+
}
359366
};
360367

361368
struct BaseAgentConfig

cpp/tensorrt_llm/batch_manager/baseTransBuffer.cpp

Lines changed: 141 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,11 +21,29 @@
2121
#include "tensorrt_llm/common/logger.h"
2222
#include "tensorrt_llm/common/opUtils.h"
2323

24+
#include <chrono>
25+
#include <exception>
2426
#include <mutex>
2527

2628
namespace tensorrt_llm::batch_manager
2729
{
2830

31+
namespace
32+
{
33+
34+
char const* bufferKindName(BufferKind kind)
35+
{
36+
switch (kind)
37+
{
38+
case BufferKind::kKV: return "kv";
39+
case BufferKind::kKV_INDEXER: return "kv_indexer";
40+
case BufferKind::kRNN: return "rnn";
41+
}
42+
return "unknown";
43+
}
44+
45+
} // namespace
46+
2947
void BufferIndexHolder::release() noexcept
3048
{
3149
if (!mHeld || mMgr == nullptr)
@@ -50,6 +68,30 @@ void BufferIndexHolder::release() noexcept
5068
mHeld = false;
5169
}
5270

71+
void BufferIndexHolder::poison() noexcept
72+
{
73+
if (!mHeld || mMgr == nullptr)
74+
{
75+
return;
76+
}
77+
try
78+
{
79+
if (mIsRecv)
80+
{
81+
mMgr->poisonBufferIndexForRecv(mIndex);
82+
}
83+
else
84+
{
85+
mMgr->poisonBufferIndexForSend(mIndex);
86+
}
87+
}
88+
catch (...)
89+
{
90+
// noexcept: poison is a fail-closed best effort from exception paths.
91+
}
92+
mHeld = false;
93+
}
94+
5395
BaseTransBufferManager::BaseTransBufferManager(
5496
size_t transferBufferSize, nvinfer1::DataType dataType, std::optional<size_t> maxNumTokens)
5597
: mDataType{dataType}
@@ -78,26 +120,40 @@ BaseTransBufferManager::BaseTransBufferManager(
78120
allocateBuffer();
79121
}
80122

81-
std::optional<int> BaseTransBufferManager::assignBufferIndexForSend()
123+
std::optional<int> BaseTransBufferManager::assignBufferIndexForSend(
124+
std::atomic<bool> const* perRequestCancel, int64_t waitSliceMs)
82125
{
83-
return assignBufferIndex(mConcurrenceSendResource, mSendBufferCount, mOnlyUseDynamicBuffer);
126+
return assignBufferIndex(
127+
mConcurrenceSendResource, mSendBufferCount, mOnlyUseDynamicBuffer, perRequestCancel, waitSliceMs);
84128
}
85129

86130
void BaseTransBufferManager::freeBufferIndexForSend(std::optional<int> bufferId)
87131
{
88132
freeBufferIndex(mConcurrenceSendResource, bufferId, mSendBufferCount, mOnlyUseDynamicBuffer);
89133
}
90134

91-
std::optional<int> BaseTransBufferManager::assignBufferIndexForRecv()
135+
void BaseTransBufferManager::poisonBufferIndexForSend(std::optional<int> bufferId) noexcept
136+
{
137+
poisonBufferIndex(mConcurrenceSendResource, bufferId, mSendBufferCount, mOnlyUseDynamicBuffer, "send");
138+
}
139+
140+
std::optional<int> BaseTransBufferManager::assignBufferIndexForRecv(
141+
std::atomic<bool> const* perRequestCancel, int64_t waitSliceMs)
92142
{
93-
return assignBufferIndex(mConcurrenceRecvResource, mRecvBufferCount, mOnlyUseDynamicBuffer);
143+
return assignBufferIndex(
144+
mConcurrenceRecvResource, mRecvBufferCount, mOnlyUseDynamicBuffer, perRequestCancel, waitSliceMs);
94145
}
95146

96147
void BaseTransBufferManager::freeBufferIndexForRecv(std::optional<int> bufferId)
97148
{
98149
freeBufferIndex(mConcurrenceRecvResource, bufferId, mRecvBufferCount, mOnlyUseDynamicBuffer);
99150
}
100151

152+
void BaseTransBufferManager::poisonBufferIndexForRecv(std::optional<int> bufferId) noexcept
153+
{
154+
poisonBufferIndex(mConcurrenceRecvResource, bufferId, mRecvBufferCount, mOnlyUseDynamicBuffer, "recv");
155+
}
156+
101157
std::tuple<std::vector<runtime::ITensor::SharedPtr>, size_t, bool> BaseTransBufferManager::getOrAllocateSendBuffers(
102158
std::optional<int> bufferId, int targetNum, std::vector<size_t> const& requestedNumberOfElements,
103159
runtime::BufferManager const& bufferManagerToUse)
@@ -249,16 +305,44 @@ void BaseTransBufferManager::allocateBuffer()
249305
}
250306
}
251307

252-
std::optional<int> BaseTransBufferManager::assignBufferIndex(
253-
ConcurrenceResource& resource, size_t bufferCount, bool onlyUseDynamicBuffer)
308+
std::optional<int> BaseTransBufferManager::assignBufferIndex(ConcurrenceResource& resource, size_t bufferCount,
309+
bool onlyUseDynamicBuffer, std::atomic<bool> const* perRequestCancel, int64_t waitSliceMs)
254310
{
311+
auto const isCancelled = [perRequestCancel]()
312+
{ return perRequestCancel != nullptr && perRequestCancel->load(std::memory_order_relaxed); };
313+
if (isCancelled())
314+
{
315+
TLLM_THROW("Cache transfer buffer acquisition cancelled");
316+
}
255317
if (onlyUseDynamicBuffer)
256318
{
319+
TLLM_CHECK_WITH_INFO(!resource.mPoisoned.load(std::memory_order_relaxed),
320+
"Cannot assign dynamic cache transfer buffer kind=%s because the transfer buffer pool is poisoned",
321+
bufferKindName(getBufferKind()));
257322
return std::nullopt;
258323
}
259324
std::unique_lock lk(resource.mBuffersMutex);
260-
resource.mBuffersCV.wait(
261-
lk, [&resource, bufferCount]() { return static_cast<size_t>(resource.mConcurrence) < bufferCount; });
325+
auto const predicate = [&resource, bufferCount]()
326+
{
327+
return resource.mPoisoned.load(std::memory_order_relaxed)
328+
|| static_cast<size_t>(resource.mConcurrence) < bufferCount;
329+
};
330+
auto const slice = std::chrono::milliseconds{waitSliceMs};
331+
while (!predicate())
332+
{
333+
resource.mBuffersCV.wait_for(lk, slice);
334+
if (isCancelled())
335+
{
336+
TLLM_THROW("Cache transfer buffer acquisition cancelled");
337+
}
338+
}
339+
if (isCancelled())
340+
{
341+
TLLM_THROW("Cache transfer buffer acquisition cancelled");
342+
}
343+
TLLM_CHECK_WITH_INFO(!resource.mPoisoned.load(std::memory_order_relaxed),
344+
"Cannot assign cache transfer buffer kind=%s because the transfer buffer pool is poisoned",
345+
bufferKindName(getBufferKind()));
262346
int bufferId = -1;
263347
for (size_t i = 0; i < bufferCount; i++)
264348
{
@@ -288,13 +372,61 @@ void BaseTransBufferManager::freeBufferIndex(
288372
TLLM_CHECK(static_cast<size_t>(bufferId.value()) < bufferCount);
289373
{
290374
std::scoped_lock lk(resource.mBuffersMutex);
375+
if (resource.mBufferIndexFlag[bufferId.value()] == 2)
376+
{
377+
TLLM_LOG_ERROR("Refusing to free poisoned cache transfer buffer kind=%s index=%d",
378+
bufferKindName(getBufferKind()), bufferId.value());
379+
return;
380+
}
291381
resource.mBufferIndexFlag[bufferId.value()] = 0;
292382
}
293383
resource.mConcurrence--;
294384
resource.mBuffersCV.notify_one();
295385
}
296386
}
297387

388+
void BaseTransBufferManager::poisonBufferIndex(ConcurrenceResource& resource, std::optional<int> bufferId,
389+
size_t bufferCount, bool onlyUseDynamicBuffer, char const* direction) noexcept
390+
{
391+
resource.mPoisoned.store(true, std::memory_order_relaxed);
392+
393+
if (onlyUseDynamicBuffer)
394+
{
395+
TLLM_LOG_ERROR("Poisoned dynamic %s cache transfer buffer kind=%s; process restart is required", direction,
396+
bufferKindName(getBufferKind()));
397+
resource.mBuffersCV.notify_all();
398+
return;
399+
}
400+
401+
try
402+
{
403+
if (bufferId.has_value())
404+
{
405+
TLLM_CHECK(static_cast<size_t>(bufferId.value()) < bufferCount);
406+
{
407+
std::scoped_lock lk(resource.mBuffersMutex);
408+
if (resource.mBufferIndexFlag[bufferId.value()] == 1)
409+
{
410+
resource.mBufferIndexFlag[bufferId.value()] = 2;
411+
}
412+
}
413+
}
414+
TLLM_LOG_ERROR("Poisoned %s cache transfer buffer kind=%s index=%d; process restart is required", direction,
415+
bufferKindName(getBufferKind()), bufferId.value_or(-1));
416+
}
417+
catch (std::exception const& e)
418+
{
419+
TLLM_LOG_ERROR("Exception while poisoning %s cache transfer buffer kind=%s index=%d: %s", direction,
420+
bufferKindName(getBufferKind()), bufferId.value_or(-1), e.what());
421+
}
422+
catch (...)
423+
{
424+
TLLM_LOG_ERROR("Unknown exception while poisoning %s cache transfer buffer kind=%s index=%d", direction,
425+
bufferKindName(getBufferKind()), bufferId.value_or(-1));
426+
}
427+
resource.mBuffersCV.notify_all();
428+
}
429+
298430
size_t BaseTransBufferManager::getRecvBufferCount()
299431
{
300432
return mRecvBufferCount;

cpp/tensorrt_llm/batch_manager/baseTransBuffer.h

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
* SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -59,7 +59,7 @@ class BufferIndexHolder
5959
BufferIndexHolder(BaseTransBufferManager& mgr, std::optional<int> index, bool isRecv) noexcept
6060
: mMgr(&mgr)
6161
, mIndex(index)
62-
, mHeld(index.has_value())
62+
, mHeld(true)
6363
, mIsRecv(isRecv)
6464
{
6565
}
@@ -120,13 +120,18 @@ class BufferIndexHolder
120120
/// @brief Release the slot now and disarm the destructor. Safe to call multiple times.
121121
void release() noexcept;
122122

123+
/// @brief Fail-closed release for an exit path where transfer-buffer quiescence is unknown.
124+
void poison() noexcept;
125+
123126
private:
124127
BaseTransBufferManager* mMgr{nullptr};
125128
std::optional<int> mIndex{};
126129
bool mHeld{false};
127130
bool mIsRecv{true};
128131
};
129132

133+
inline constexpr int64_t kBufferAcquireSliceMs = 100;
134+
130135
/// @brief Base class for cache transfer buffer management.
131136
/// Handles buffer pool allocation, index assignment, and slicing.
132137
/// Derived classes provide cache-specific size calculations.
@@ -139,20 +144,28 @@ class BaseTransBufferManager
139144

140145
/// @brief Assign a buffer index for sending.
141146
/// @return Assigned buffer index, or nullopt if using dynamic buffers.
142-
std::optional<int> assignBufferIndexForSend();
147+
std::optional<int> assignBufferIndexForSend(
148+
std::atomic<bool> const* perRequestCancel = nullptr, int64_t waitSliceMs = kBufferAcquireSliceMs);
143149

144150
/// @brief Free a buffer index used for sending.
145151
/// @param bufferId The buffer index to free.
146152
void freeBufferIndexForSend(std::optional<int> bufferId);
147153

154+
/// @brief Poison a send buffer index after an unquiesced transfer exit.
155+
void poisonBufferIndexForSend(std::optional<int> bufferId) noexcept;
156+
148157
/// @brief Assign a buffer index for receiving.
149158
/// @return Assigned buffer index, or nullopt if using dynamic buffers.
150-
std::optional<int> assignBufferIndexForRecv();
159+
std::optional<int> assignBufferIndexForRecv(
160+
std::atomic<bool> const* perRequestCancel = nullptr, int64_t waitSliceMs = kBufferAcquireSliceMs);
151161

152162
/// @brief Free a buffer index used for receiving.
153163
/// @param bufferId The buffer index to free.
154164
void freeBufferIndexForRecv(std::optional<int> bufferId);
155165

166+
/// @brief Poison a receive buffer index after an unquiesced transfer exit.
167+
void poisonBufferIndexForRecv(std::optional<int> bufferId) noexcept;
168+
156169
/// @brief Get or allocate send buffers for cache transfer.
157170
/// @param bufferId The assigned buffer ID.
158171
/// @param targetNum Number of target sequences.
@@ -191,6 +204,12 @@ class BaseTransBufferManager
191204
return mMaxNumTokens;
192205
}
193206

207+
[[nodiscard]] bool hasPoisonedBuffer() const noexcept
208+
{
209+
return mConcurrenceSendResource.mPoisoned.load(std::memory_order_relaxed)
210+
|| mConcurrenceRecvResource.mPoisoned.load(std::memory_order_relaxed);
211+
}
212+
194213
protected:
195214
/// @brief Constructor - derived classes call this after computing buffer sizes.
196215
/// @param transferBufferSize Size of each transfer buffer in bytes.
@@ -206,16 +225,20 @@ class BaseTransBufferManager
206225
std::mutex mBuffersMutex;
207226
std::condition_variable mBuffersCV;
208227
std::atomic<int> mConcurrence{0};
228+
std::atomic<bool> mPoisoned{false};
209229
};
210230

211231
std::tuple<std::vector<runtime::ITensor::SharedPtr>, size_t, bool> getOrAllocateBuffers(std::optional<int> bufferId,
212232
int targetNum, std::vector<size_t> const& requestedNumberOfElements,
213233
runtime::BufferManager const& bufferManagerToUse, ConcurrenceResource& concurrenceResource);
214234

215235
void allocateBuffer();
216-
std::optional<int> assignBufferIndex(ConcurrenceResource& resource, size_t bufferCount, bool onlyUseDynamicBuffer);
236+
std::optional<int> assignBufferIndex(ConcurrenceResource& resource, size_t bufferCount, bool onlyUseDynamicBuffer,
237+
std::atomic<bool> const* perRequestCancel = nullptr, int64_t waitSliceMs = kBufferAcquireSliceMs);
217238
void freeBufferIndex(
218239
ConcurrenceResource& resource, std::optional<int> bufferId, size_t bufferCount, bool onlyUseDynamicBuffer);
240+
void poisonBufferIndex(ConcurrenceResource& resource, std::optional<int> bufferId, size_t bufferCount,
241+
bool onlyUseDynamicBuffer, char const* direction) noexcept;
219242

220243
size_t mPreAllocBufferSize;
221244
size_t mRecvBufferCount;

cpp/tensorrt_llm/batch_manager/cacheFormatter.cpp

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,8 @@ void CacheFormatter::format(tensorrt_llm::batch_manager::TransferSession& sessio
522522
// cache blocks to the corresponding buffer.
523523
// 5. send the buffer to the corresponding target. Ideally, we send only once (one buffer) for each target.
524524

525-
auto cacheBufferId = mCacheTransBufferManager->assignBufferIndexForSend();
525+
auto const* sendCancelFlag = &session.getDataContext().getTransferTerminate();
526+
auto cacheBufferId = mCacheTransBufferManager->assignBufferIndexForSend(sendCancelFlag);
526527
BufferIndexHolder sendHolder(*mCacheTransBufferManager, cacheBufferId, /*isRecv=*/false);
527528
int peerDuplicateHeadFactor = targetInfo.mPeerDupHeadFactor;
528529
auto bufferTargetNum = targetNum / peerDuplicateHeadFactor;
@@ -602,8 +603,19 @@ void CacheFormatter::format(tensorrt_llm::batch_manager::TransferSession& sessio
602603
== inputKvCacheBlocksPerWindow.begin()->second.front()->getDataType());
603604
}
604605

605-
sendAllBuffers(session, deviceId, outputSplitCaches, bufferCoverTargetNum, preAllocSendBuffer, bufferManager,
606-
targetInfo, pickUpConnections);
606+
try
607+
{
608+
sendAllBuffers(session, deviceId, outputSplitCaches, bufferCoverTargetNum, preAllocSendBuffer,
609+
bufferManager, targetInfo, pickUpConnections);
610+
}
611+
catch (...)
612+
{
613+
if (agentConnection != nullptr && common::getEnvDisaggEnableInflightCancel())
614+
{
615+
sendHolder.poison();
616+
}
617+
throw;
618+
}
607619

608620
session.setTime(TransferSession::kTimeTransmissions);
609621

0 commit comments

Comments
 (0)