1- // Copyright 2020-2023 , NVIDIA CORPORATION & AFFILIATES. All rights reserved.
1+ // Copyright 2020-2026 , NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22//
33// Redistribution and use in source and binary forms, with or without
44// modification, are permitted provided that the following conditions
3434#include < future>
3535#include < iostream>
3636#include < mutex>
37+ #include < new>
3738#include < sstream>
3839#include < string>
3940
@@ -172,16 +173,19 @@ SetTimeout(const uint64_t& client_timeout_ms, grpc::ClientContext* context)
172173class GrpcInferRequest : public InferRequest {
173174 public:
174175 GrpcInferRequest (InferenceServerClient::OnCompleteFn callback = nullptr )
175- : InferRequest(callback), grpc_status_(),
176+ : InferRequest(callback),
177+ grpc_context_ (std::make_shared<grpc::ClientContext>()), grpc_status_(),
176178 grpc_response_(std::make_shared<inference::ModelInferResponse>())
177179 {
178180 }
179181
180182 friend InferenceServerGrpcClient;
181183
182184 private:
183- // Variables for GRPC call
184- grpc::ClientContext grpc_context_;
185+ // Variables for GRPC call. The ClientContext is held by shared_ptr so that
186+ // a CallContext returned to the caller can co-own it and safely call
187+ // TryCancel() at any point during (or after) the RPC's lifetime.
188+ std::shared_ptr<grpc::ClientContext> grpc_context_;
185189 grpc::Status grpc_status_;
186190 std::shared_ptr<inference::ModelInferResponse> grpc_response_;
187191};
@@ -447,6 +451,17 @@ InferResultGrpc::InferResultGrpc(
447451
448452// ==============================================================================
449453
454+ Error
455+ CallContext::Cancel ()
456+ {
457+ if (grpc_context_) {
458+ grpc_context_->TryCancel ();
459+ }
460+ return Error::Success;
461+ }
462+
463+ // ==============================================================================
464+
450465// Advanced users can generically pass any channel arguments
451466// through the channel_args parameter, including KeepAlive options.
452467// Channel arguments provided by the user are at the user's own risk and
@@ -1154,7 +1169,8 @@ InferenceServerGrpcClient::AsyncInfer(
11541169 OnCompleteFn callback, const InferOptions& options,
11551170 const std::vector<InferInput*>& inputs,
11561171 const std::vector<const InferRequestedOutput*>& outputs,
1157- const Headers& headers, grpc_compression_algorithm compression_algorithm)
1172+ const Headers& headers, grpc_compression_algorithm compression_algorithm,
1173+ CallContext** ctx_out)
11581174{
11591175 if (callback == nullptr ) {
11601176 return Error (
@@ -1170,13 +1186,14 @@ InferenceServerGrpcClient::AsyncInfer(
11701186 async_request->Timer ().CaptureTimestamp (RequestTimers::Kind::REQUEST_START );
11711187 async_request->Timer ().CaptureTimestamp (RequestTimers::Kind::SEND_START );
11721188 for (const auto & it : headers) {
1173- async_request->grpc_context_ . AddMetadata (it.first , it.second );
1189+ async_request->grpc_context_ -> AddMetadata (it.first , it.second );
11741190 }
11751191
11761192 if (options.client_timeout_ != 0 ) {
1177- SetTimeout (options.client_timeout_ , &( async_request->grpc_context_ ));
1193+ SetTimeout (options.client_timeout_ , async_request->grpc_context_ . get ( ));
11781194 }
1179- async_request->grpc_context_ .set_compression_algorithm (compression_algorithm);
1195+ async_request->grpc_context_ ->set_compression_algorithm (
1196+ compression_algorithm);
11801197
11811198 Error err = PreRunProcessing (options, inputs, outputs);
11821199 if (!err.IsOk ()) {
@@ -1189,7 +1206,7 @@ InferenceServerGrpcClient::AsyncInfer(
11891206 std::unique_ptr<
11901207 grpc::ClientAsyncResponseReader<inference::ModelInferResponse>>
11911208 rpc (stub_->PrepareAsyncModelInfer (
1192- & async_request->grpc_context_ , infer_request_,
1209+ async_request->grpc_context_ . get () , infer_request_,
11931210 &async_request_completion_queue_));
11941211
11951212 rpc->StartCall ();
@@ -1198,6 +1215,13 @@ InferenceServerGrpcClient::AsyncInfer(
11981215 async_request->grpc_response_ .get (), &async_request->grpc_status_ ,
11991216 (void *)async_request);
12001217
1218+ // Hand the caller a cancellation handle co-owning the same ClientContext.
1219+ // Placed after StartCall/Finish so *ctx_out is only set on success; the
1220+ // earlier validation failure paths leave *ctx_out unchanged (nullptr).
1221+ if (ctx_out != nullptr ) {
1222+ *ctx_out = new CallContext (async_request->grpc_context_ );
1223+ }
1224+
12011225 if (verbose_) {
12021226 std::cout << " Sent request" ;
12031227 if (options.request_id_ .size () != 0 ) {
@@ -1255,7 +1279,8 @@ InferenceServerGrpcClient::AsyncInferMulti(
12551279 OnMultiCompleteFn callback, const std::vector<InferOptions>& options,
12561280 const std::vector<std::vector<InferInput*>>& inputs,
12571281 const std::vector<std::vector<const InferRequestedOutput*>>& outputs,
1258- const Headers& headers, grpc_compression_algorithm compression_algorithm)
1282+ const Headers& headers, grpc_compression_algorithm compression_algorithm,
1283+ std::vector<CallContext*>* ctxs_out)
12591284{
12601285 // Sanity check
12611286 if ((inputs.size () != options.size ()) && (options.size () != 1 )) {
@@ -1284,6 +1309,13 @@ InferenceServerGrpcClient::AsyncInferMulti(
12841309 new std::atomic<size_t >(inputs.size ()));
12851310 std::shared_ptr<std::vector<InferResult*>> responses (
12861311 new std::vector<InferResult*>(inputs.size ()));
1312+
1313+ // Pre-size so AsyncInfer() can write slot i directly; slots stay
1314+ // nullptr for requests that fail before the RPC starts.
1315+ if (ctxs_out != nullptr ) {
1316+ ctxs_out->assign (inputs.size (), nullptr );
1317+ }
1318+
12871319 for (int64_t i = 0 ; i < (int64_t )inputs.size (); ++i) {
12881320 const auto & request_options = options[std::min (max_option_idx, i)];
12891321 const auto & request_output = (max_output_idx == -1 )
@@ -1301,9 +1333,10 @@ InferenceServerGrpcClient::AsyncInferMulti(
13011333 }
13021334 };
13031335
1336+ CallContext** ctx = (ctxs_out != nullptr ) ? &(*ctxs_out)[i] : nullptr ;
13041337 auto err = AsyncInfer (
13051338 cb, request_options, inputs[i], request_output, headers,
1306- compression_algorithm);
1339+ compression_algorithm, ctx );
13071340 if (!err.IsOk ()) {
13081341 // Create response with error as other requests may be sent and their
13091342 // responses may not be accessed outside the callback.
@@ -1336,6 +1369,12 @@ InferenceServerGrpcClient::StartStream(
13361369 " Callback function must be provided along with StartStream() call." );
13371370 }
13381371
1372+ // Reset grpc_context_ in place: a previous StopStream(cancel_requests=
1373+ // true) leaves it cancelled, and ClientContext deletes operator= so we
1374+ // can't just reassign it.
1375+ grpc_context_.~ClientContext ();
1376+ new (&grpc_context_) grpc::ClientContext ();
1377+
13391378 stream_callback_ = callback;
13401379 enable_stream_stats_ = enable_stats;
13411380
@@ -1360,12 +1399,18 @@ InferenceServerGrpcClient::StartStream(
13601399}
13611400
13621401Error
1363- InferenceServerGrpcClient::StopStream ()
1402+ InferenceServerGrpcClient::StopStream (bool cancel_requests )
13641403{
13651404 if (stream_worker_.joinable ()) {
1366- grpc_stream_->WritesDone ();
1367- // The reader thread will drain the stream properly
1405+ if (cancel_requests) {
1406+ stream_cancel_requested_.store (true , std::memory_order_release);
1407+ grpc_context_.TryCancel ();
1408+ } else {
1409+ grpc_stream_->WritesDone ();
1410+ }
1411+ // The reader thread drains the stream and completes the RPC (Finish).
13681412 stream_worker_.join ();
1413+ stream_cancel_requested_.store (false , std::memory_order_release);
13691414 if (verbose_) {
13701415 std::cout << " Stopped stream..." << std::endl;
13711416 }
@@ -1603,7 +1648,14 @@ InferenceServerGrpcClient::AsyncTransfer()
16031648 InferResult* async_result;
16041649 Error err;
16051650 if (!async_request->grpc_status_ .ok ()) {
1606- err = Error (async_request->grpc_status_ .error_message ());
1651+ // Map gRPC CANCELLED to the same message the streaming-cancel path
1652+ // emits, matching tritonclient.grpc._utils.get_cancelled_error().
1653+ if (async_request->grpc_status_ .error_code () ==
1654+ grpc::StatusCode::CANCELLED ) {
1655+ err = Error (" Locally cancelled by application!" );
1656+ } else {
1657+ err = Error (async_request->grpc_status_ .error_message ());
1658+ }
16071659 }
16081660 async_request->Timer ().CaptureTimestamp (RequestTimers::Kind::RECV_START );
16091661 InferResultGrpc::Create (
@@ -1669,6 +1721,16 @@ InferenceServerGrpcClient::AsyncStreamTransfer()
16691721 stream_callback_ (stream_result);
16701722 response = std::make_shared<inference::ModelStreamInferResponse>();
16711723 }
1724+ // After StopStream(cancel_requests=true), Read() drains without a status,
1725+ // so synthesize one final callback carrying the Python-parity message.
1726+ if (stream_cancel_requested_.load (std::memory_order_acquire) && !exiting_) {
1727+ InferResult* cancel_result = nullptr ;
1728+ auto cancel_response =
1729+ std::make_shared<inference::ModelStreamInferResponse>();
1730+ cancel_response->set_error_message (" Locally cancelled by application!" );
1731+ InferResultGrpc::Create (&cancel_result, cancel_response);
1732+ stream_callback_ (cancel_result);
1733+ }
16721734 grpc_stream_->Finish ();
16731735}
16741736
0 commit comments