Skip to content

Commit b2442d8

Browse files
authored
Merge branch 'main' into jwyman/upper-bound
2 parents 2e6cab0 + 8951d7a commit b2442d8

19 files changed

Lines changed: 1154 additions & 412 deletions

File tree

README.md

Lines changed: 72 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ under-development version).
213213

214214
```bash
215215
$ git checkout main
216+
$ mkdir -p build && cd build
216217
```
217218

218219
If building the Java client you must first install Maven and a JDK
@@ -223,60 +224,23 @@ the `default-jdk` package:
223224
$ apt-get install default-jdk maven
224225
```
225226

226-
Building on Windows vs. non-Windows requires different invocations
227-
because Triton on Windows does not yet support all the build options.
228-
229-
#### Non-Windows
230-
231227
Use *cmake* to configure the build. You should adjust the flags depending on
232228
the components of Triton Client you are working and would like to build.
233229

234-
If you are building on a release branch (or on a development branch
235-
that is based off of a release branch), then you must also use
236-
additional cmake arguments to point to that release branch for repos
237-
that the client build depends on. For example, if you are building the
238-
r21.10 client branch then you need to use the following additional
239-
cmake flags:
240-
241-
```
242-
-DTRITON_COMMON_REPO_TAG=r21.10
243-
-DTRITON_THIRD_PARTY_REPO_TAG=r21.10
244-
-DTRITON_CORE_REPO_TAG=r21.10
245-
```
246-
247-
Then use *make* to build the clients and examples.
248-
249-
```
250-
$ make cc-clients python-clients java-clients
251-
```
252-
253-
When the build completes the libraries and examples can be found in
254-
the install directory.
255-
256-
#### Windows
257-
258-
To build the clients you must install an appropriate C++ compiler and
259-
other dependencies required for the build. The easiest way to do this
260-
is to create the [Windows min Docker
261-
image](https://github.com/triton-inference-server/server/blob/main/docs/customization_guide/build.md#windows-10-min-image)
262-
and the perform the build within a container launched from that image.
263-
264-
```
265-
> docker run -it --rm win10-py3-min powershell
266-
```
267-
268-
It is not necessary to use Docker or the win10-py3-min container for
269-
the build, but if you do not you must install the appropriate
270-
dependencies onto your host system.
271-
272-
Next use *cmake* to configure the build. If you are not building
273-
within the win10-py3-min container then you will likely need to adjust
274-
the CMAKE_TOOLCHAIN_FILE location in the following command.
275-
276230
```
277-
$ mkdir build
278-
$ cd build
279-
$ cmake -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_TOOLCHAIN_FILE='/vcpkg/scripts/buildsystems/vcpkg.cmake' -DCMAKE_INSTALL_PREFIX=install -DTRITON_ENABLE_CC_GRPC=ON -DTRITON_ENABLE_PYTHON_GRPC=ON -DTRITON_ENABLE_GPU=OFF -DTRITON_ENABLE_EXAMPLES=ON -DTRITON_ENABLE_TESTS=ON ..
231+
cmake \
232+
-DCMAKE_BUILD_TYPE=Release \
233+
-DCMAKE_INSTALL_PREFIX=$(pwd)/install \
234+
-DTRITON_ENABLE_CC_HTTP=ON \
235+
-DTRITON_ENABLE_CC_GRPC=ON \
236+
-DTRITON_ENABLE_PYTHON_HTTP=ON \
237+
-DTRITON_ENABLE_PYTHON_GRPC=ON \
238+
-DTRITON_ENABLE_JAVA_HTTP=ON \
239+
-DTRITON_ENABLE_EXAMPLES=ON \
240+
-DTRITON_ENABLE_TESTS=ON \
241+
-DTRITON_ENABLE_GPU=ON \
242+
-DTRITON_ENABLE_ZLIB=ON \
243+
..
280244
```
281245

282246
If you are building on a release branch (or on a development branch
@@ -292,11 +256,10 @@ cmake flags:
292256
-DTRITON_CORE_REPO_TAG=r21.10
293257
```
294258

295-
Then use msbuild.exe to build.
259+
Then use *make* to build the clients and examples.
296260

297261
```
298-
$ msbuild.exe cc-clients.vcxproj -p:Configuration=Release -clp:ErrorsOnly
299-
$ msbuild.exe python-clients.vcxproj -p:Configuration=Release -clp:ErrorsOnly
262+
$ make -j$(nproc)/ cc-clients python-clients java-clients
300263
```
301264

302265
When the build completes the libraries and examples can be found in
@@ -515,6 +478,7 @@ examples demonstrate how to infer with AsyncIO.
515478

516479

517480
### Request Cancellation
481+
#### Python gRPC client
518482

519483
Starting from r23.10, triton python gRPC client can issue cancellation
520484
to inflight requests. This can be done by calling `cancel()` on the
@@ -560,6 +524,61 @@ asynchronous iterator returned by `stream_infer()` API.
560524
See more details about these APIs in
561525
[grpc/aio/\__init__.py](src/python/library/tritonclient/grpc/aio/__init__.py).
562526

527+
#### C++ gRPC client
528+
529+
Starting from r26.05, triton python C++ client can issue cancellation to
530+
inflight requests. This can be done by passing an optional
531+
`CallContext** ctx_out` to `AsyncInfer()`; the returned `CallContext` exposes
532+
`Cancel()`.
533+
534+
```cpp
535+
tc::CallContext* ctx = nullptr;
536+
client->AsyncInfer(
537+
callback, options, inputs, outputs,
538+
/*headers=*/{}, GRPC_COMPRESS_NONE, &ctx);
539+
ctx->Cancel();
540+
delete ctx;
541+
```
542+
543+
For batch fan-out via `AsyncInferMulti()`, pass an optional
544+
`std::vector<CallContext*>* ctxs_out`; on return it is sized to `inputs.size()`
545+
with one `CallContext` per leaf request (or `nullptr` for any request that
546+
failed locally before the RPC started). Cancellation is per-request — siblings
547+
continue to completion, and the multi callback still fires exactly once.
548+
549+
```cpp
550+
std::vector<tc::CallContext*> ctxs;
551+
client->AsyncInferMulti(
552+
multi_callback, options, inputs, outputs,
553+
/*headers=*/{}, GRPC_COMPRESS_NONE, &ctxs);
554+
for (auto* ctx : ctxs) {
555+
if (ctx != nullptr) {
556+
ctx->Cancel();
557+
}
558+
}
559+
for (auto* ctx : ctxs) {
560+
delete ctx;
561+
}
562+
```
563+
564+
For streaming requests, pass `true` to `StopStream()`. Cancelling after the
565+
RPC has completed is a safe no-op. The caller owns the returned `CallContext`
566+
and must `delete` it. Call sites that omit `ctx_out` keep the previous,
567+
non-cancellable behavior.
568+
569+
```cpp
570+
client->StartStream(callback, ...);
571+
for (...) {
572+
client->AsyncStreamInfer(options, inputs, outputs);
573+
}
574+
client->StopStream(/*cancel_requests=*/true);
575+
```
576+
577+
See more details about these APIs in
578+
[grpc_client.h](src/c++/library/grpc_client.h).
579+
580+
#### Server-side behavior
581+
563582
See [request_cancellation](https://github.com/triton-inference-server/server/blob/main/docs/user_guide/request_cancellation.md)
564583
in the server user-guide to learn about how this is handled on the
565584
server side.

src/c++/library/grpc_client.cc

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
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
@@ -34,6 +34,7 @@
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)
172173
class 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

13621401
Error
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

Comments
 (0)